##// END OF EJS Templates
templateContext: allow for flexible way to communicate additional info for client code from controllers
ergo -
r394:c560756c default
parent child Browse files
Show More
@@ -1,846 +1,849 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2016 RhodeCode GmbH
3 # Copyright (C) 2012-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24
24
25 import formencode
25 import formencode
26 import logging
26 import logging
27
27
28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
28 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
29 from pylons import request, tmpl_context as c, url
29 from pylons import request, tmpl_context as c, url
30 from pylons.controllers.util import redirect
30 from pylons.controllers.util import redirect
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from sqlalchemy.sql import func
32 from sqlalchemy.sql import func
33 from sqlalchemy.sql.expression import or_
33 from sqlalchemy.sql.expression import or_
34
34
35 from rhodecode.lib import auth, diffs, helpers as h
35 from rhodecode.lib import auth, diffs, helpers as h
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.base import (
37 from rhodecode.lib.base import (
38 BaseRepoController, render, vcs_operation_context)
38 BaseRepoController, render, vcs_operation_context)
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
40 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
41 HasAcceptedRepoType, XHRRequired)
41 HasAcceptedRepoType, XHRRequired)
42 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils import jsonify
43 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
43 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
46 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError)
47 from rhodecode.lib.diffs import LimitedDiffContainer
47 from rhodecode.lib.diffs import LimitedDiffContainer
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import ChangesetCommentsModel
49 from rhodecode.model.comment import ChangesetCommentsModel
50 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
50 from rhodecode.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
51 Repository
51 Repository
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel
54 from rhodecode.model.pull_request import PullRequestModel
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 class PullrequestsController(BaseRepoController):
59 class PullrequestsController(BaseRepoController):
60 def __before__(self):
60 def __before__(self):
61 super(PullrequestsController, self).__before__()
61 super(PullrequestsController, self).__before__()
62
62
63 def _load_compare_data(self, pull_request, enable_comments=True):
63 def _load_compare_data(self, pull_request, enable_comments=True):
64 """
64 """
65 Load context data needed for generating compare diff
65 Load context data needed for generating compare diff
66
66
67 :param pull_request: object related to the request
67 :param pull_request: object related to the request
68 :param enable_comments: flag to determine if comments are included
68 :param enable_comments: flag to determine if comments are included
69 """
69 """
70 source_repo = pull_request.source_repo
70 source_repo = pull_request.source_repo
71 source_ref_id = pull_request.source_ref_parts.commit_id
71 source_ref_id = pull_request.source_ref_parts.commit_id
72
72
73 target_repo = pull_request.target_repo
73 target_repo = pull_request.target_repo
74 target_ref_id = pull_request.target_ref_parts.commit_id
74 target_ref_id = pull_request.target_ref_parts.commit_id
75
75
76 # despite opening commits for bookmarks/branches/tags, we always
76 # despite opening commits for bookmarks/branches/tags, we always
77 # convert this to rev to prevent changes after bookmark or branch change
77 # convert this to rev to prevent changes after bookmark or branch change
78 c.source_ref_type = 'rev'
78 c.source_ref_type = 'rev'
79 c.source_ref = source_ref_id
79 c.source_ref = source_ref_id
80
80
81 c.target_ref_type = 'rev'
81 c.target_ref_type = 'rev'
82 c.target_ref = target_ref_id
82 c.target_ref = target_ref_id
83
83
84 c.source_repo = source_repo
84 c.source_repo = source_repo
85 c.target_repo = target_repo
85 c.target_repo = target_repo
86
86
87 c.fulldiff = bool(request.GET.get('fulldiff'))
87 c.fulldiff = bool(request.GET.get('fulldiff'))
88
88
89 # diff_limit is the old behavior, will cut off the whole diff
89 # diff_limit is the old behavior, will cut off the whole diff
90 # if the limit is applied otherwise will just hide the
90 # if the limit is applied otherwise will just hide the
91 # big files from the front-end
91 # big files from the front-end
92 diff_limit = self.cut_off_limit_diff
92 diff_limit = self.cut_off_limit_diff
93 file_limit = self.cut_off_limit_file
93 file_limit = self.cut_off_limit_file
94
94
95 pre_load = ["author", "branch", "date", "message"]
95 pre_load = ["author", "branch", "date", "message"]
96
96
97 c.commit_ranges = []
97 c.commit_ranges = []
98 source_commit = EmptyCommit()
98 source_commit = EmptyCommit()
99 target_commit = EmptyCommit()
99 target_commit = EmptyCommit()
100 c.missing_requirements = False
100 c.missing_requirements = False
101 try:
101 try:
102 c.commit_ranges = [
102 c.commit_ranges = [
103 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
103 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
104 for rev in pull_request.revisions]
104 for rev in pull_request.revisions]
105
105
106 c.statuses = source_repo.statuses(
106 c.statuses = source_repo.statuses(
107 [x.raw_id for x in c.commit_ranges])
107 [x.raw_id for x in c.commit_ranges])
108
108
109 target_commit = source_repo.get_commit(
109 target_commit = source_repo.get_commit(
110 commit_id=safe_str(target_ref_id))
110 commit_id=safe_str(target_ref_id))
111 source_commit = source_repo.get_commit(
111 source_commit = source_repo.get_commit(
112 commit_id=safe_str(source_ref_id))
112 commit_id=safe_str(source_ref_id))
113 except RepositoryRequirementError:
113 except RepositoryRequirementError:
114 c.missing_requirements = True
114 c.missing_requirements = True
115
115
116 c.missing_commits = False
116 c.missing_commits = False
117 if (c.missing_requirements or
117 if (c.missing_requirements or
118 isinstance(source_commit, EmptyCommit) or
118 isinstance(source_commit, EmptyCommit) or
119 source_commit == target_commit):
119 source_commit == target_commit):
120 _parsed = []
120 _parsed = []
121 c.missing_commits = True
121 c.missing_commits = True
122 else:
122 else:
123 vcs_diff = PullRequestModel().get_diff(pull_request)
123 vcs_diff = PullRequestModel().get_diff(pull_request)
124 diff_processor = diffs.DiffProcessor(
124 diff_processor = diffs.DiffProcessor(
125 vcs_diff, format='gitdiff', diff_limit=diff_limit,
125 vcs_diff, format='gitdiff', diff_limit=diff_limit,
126 file_limit=file_limit, show_full_diff=c.fulldiff)
126 file_limit=file_limit, show_full_diff=c.fulldiff)
127 _parsed = diff_processor.prepare()
127 _parsed = diff_processor.prepare()
128
128
129 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
129 c.limited_diff = isinstance(_parsed, LimitedDiffContainer)
130
130
131 c.files = []
131 c.files = []
132 c.changes = {}
132 c.changes = {}
133 c.lines_added = 0
133 c.lines_added = 0
134 c.lines_deleted = 0
134 c.lines_deleted = 0
135 c.included_files = []
135 c.included_files = []
136 c.deleted_files = []
136 c.deleted_files = []
137
137
138 for f in _parsed:
138 for f in _parsed:
139 st = f['stats']
139 st = f['stats']
140 c.lines_added += st['added']
140 c.lines_added += st['added']
141 c.lines_deleted += st['deleted']
141 c.lines_deleted += st['deleted']
142
142
143 fid = h.FID('', f['filename'])
143 fid = h.FID('', f['filename'])
144 c.files.append([fid, f['operation'], f['filename'], f['stats']])
144 c.files.append([fid, f['operation'], f['filename'], f['stats']])
145 c.included_files.append(f['filename'])
145 c.included_files.append(f['filename'])
146 html_diff = diff_processor.as_html(enable_comments=enable_comments,
146 html_diff = diff_processor.as_html(enable_comments=enable_comments,
147 parsed_lines=[f])
147 parsed_lines=[f])
148 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
148 c.changes[fid] = [f['operation'], f['filename'], html_diff, f]
149
149
150 def _extract_ordering(self, request):
150 def _extract_ordering(self, request):
151 column_index = safe_int(request.GET.get('order[0][column]'))
151 column_index = safe_int(request.GET.get('order[0][column]'))
152 order_dir = request.GET.get('order[0][dir]', 'desc')
152 order_dir = request.GET.get('order[0][dir]', 'desc')
153 order_by = request.GET.get(
153 order_by = request.GET.get(
154 'columns[%s][data][sort]' % column_index, 'name_raw')
154 'columns[%s][data][sort]' % column_index, 'name_raw')
155 return order_by, order_dir
155 return order_by, order_dir
156
156
157 @LoginRequired()
157 @LoginRequired()
158 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
158 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
159 'repository.admin')
159 'repository.admin')
160 @HasAcceptedRepoType('git', 'hg')
160 @HasAcceptedRepoType('git', 'hg')
161 def show_all(self, repo_name):
161 def show_all(self, repo_name):
162 # filter types
162 # filter types
163 c.active = 'open'
163 c.active = 'open'
164 c.source = str2bool(request.GET.get('source'))
164 c.source = str2bool(request.GET.get('source'))
165 c.closed = str2bool(request.GET.get('closed'))
165 c.closed = str2bool(request.GET.get('closed'))
166 c.my = str2bool(request.GET.get('my'))
166 c.my = str2bool(request.GET.get('my'))
167 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
167 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
168 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
168 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
169 c.repo_name = repo_name
169 c.repo_name = repo_name
170
170
171 opened_by = None
171 opened_by = None
172 if c.my:
172 if c.my:
173 c.active = 'my'
173 c.active = 'my'
174 opened_by = [c.rhodecode_user.user_id]
174 opened_by = [c.rhodecode_user.user_id]
175
175
176 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
176 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
177 if c.closed:
177 if c.closed:
178 c.active = 'closed'
178 c.active = 'closed'
179 statuses = [PullRequest.STATUS_CLOSED]
179 statuses = [PullRequest.STATUS_CLOSED]
180
180
181 if c.awaiting_review and not c.source:
181 if c.awaiting_review and not c.source:
182 c.active = 'awaiting'
182 c.active = 'awaiting'
183 if c.source and not c.awaiting_review:
183 if c.source and not c.awaiting_review:
184 c.active = 'source'
184 c.active = 'source'
185 if c.awaiting_my_review:
185 if c.awaiting_my_review:
186 c.active = 'awaiting_my'
186 c.active = 'awaiting_my'
187
187
188 data = self._get_pull_requests_list(
188 data = self._get_pull_requests_list(
189 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
189 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
190 if not request.is_xhr:
190 if not request.is_xhr:
191 c.data = json.dumps(data['data'])
191 c.data = json.dumps(data['data'])
192 c.records_total = data['recordsTotal']
192 c.records_total = data['recordsTotal']
193 return render('/pullrequests/pullrequests.html')
193 return render('/pullrequests/pullrequests.html')
194 else:
194 else:
195 return json.dumps(data)
195 return json.dumps(data)
196
196
197 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
197 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
198 # pagination
198 # pagination
199 start = safe_int(request.GET.get('start'), 0)
199 start = safe_int(request.GET.get('start'), 0)
200 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
200 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
201 order_by, order_dir = self._extract_ordering(request)
201 order_by, order_dir = self._extract_ordering(request)
202
202
203 if c.awaiting_review:
203 if c.awaiting_review:
204 pull_requests = PullRequestModel().get_awaiting_review(
204 pull_requests = PullRequestModel().get_awaiting_review(
205 repo_name, source=c.source, opened_by=opened_by,
205 repo_name, source=c.source, opened_by=opened_by,
206 statuses=statuses, offset=start, length=length,
206 statuses=statuses, offset=start, length=length,
207 order_by=order_by, order_dir=order_dir)
207 order_by=order_by, order_dir=order_dir)
208 pull_requests_total_count = PullRequestModel(
208 pull_requests_total_count = PullRequestModel(
209 ).count_awaiting_review(
209 ).count_awaiting_review(
210 repo_name, source=c.source, statuses=statuses,
210 repo_name, source=c.source, statuses=statuses,
211 opened_by=opened_by)
211 opened_by=opened_by)
212 elif c.awaiting_my_review:
212 elif c.awaiting_my_review:
213 pull_requests = PullRequestModel().get_awaiting_my_review(
213 pull_requests = PullRequestModel().get_awaiting_my_review(
214 repo_name, source=c.source, opened_by=opened_by,
214 repo_name, source=c.source, opened_by=opened_by,
215 user_id=c.rhodecode_user.user_id, statuses=statuses,
215 user_id=c.rhodecode_user.user_id, statuses=statuses,
216 offset=start, length=length, order_by=order_by,
216 offset=start, length=length, order_by=order_by,
217 order_dir=order_dir)
217 order_dir=order_dir)
218 pull_requests_total_count = PullRequestModel(
218 pull_requests_total_count = PullRequestModel(
219 ).count_awaiting_my_review(
219 ).count_awaiting_my_review(
220 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
220 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
221 statuses=statuses, opened_by=opened_by)
221 statuses=statuses, opened_by=opened_by)
222 else:
222 else:
223 pull_requests = PullRequestModel().get_all(
223 pull_requests = PullRequestModel().get_all(
224 repo_name, source=c.source, opened_by=opened_by,
224 repo_name, source=c.source, opened_by=opened_by,
225 statuses=statuses, offset=start, length=length,
225 statuses=statuses, offset=start, length=length,
226 order_by=order_by, order_dir=order_dir)
226 order_by=order_by, order_dir=order_dir)
227 pull_requests_total_count = PullRequestModel().count_all(
227 pull_requests_total_count = PullRequestModel().count_all(
228 repo_name, source=c.source, statuses=statuses,
228 repo_name, source=c.source, statuses=statuses,
229 opened_by=opened_by)
229 opened_by=opened_by)
230
230
231 from rhodecode.lib.utils import PartialRenderer
231 from rhodecode.lib.utils import PartialRenderer
232 _render = PartialRenderer('data_table/_dt_elements.html')
232 _render = PartialRenderer('data_table/_dt_elements.html')
233 data = []
233 data = []
234 for pr in pull_requests:
234 for pr in pull_requests:
235 comments = ChangesetCommentsModel().get_all_comments(
235 comments = ChangesetCommentsModel().get_all_comments(
236 c.rhodecode_db_repo.repo_id, pull_request=pr)
236 c.rhodecode_db_repo.repo_id, pull_request=pr)
237
237
238 data.append({
238 data.append({
239 'name': _render('pullrequest_name',
239 'name': _render('pullrequest_name',
240 pr.pull_request_id, pr.target_repo.repo_name),
240 pr.pull_request_id, pr.target_repo.repo_name),
241 'name_raw': pr.pull_request_id,
241 'name_raw': pr.pull_request_id,
242 'status': _render('pullrequest_status',
242 'status': _render('pullrequest_status',
243 pr.calculated_review_status()),
243 pr.calculated_review_status()),
244 'title': _render(
244 'title': _render(
245 'pullrequest_title', pr.title, pr.description),
245 'pullrequest_title', pr.title, pr.description),
246 'description': h.escape(pr.description),
246 'description': h.escape(pr.description),
247 'updated_on': _render('pullrequest_updated_on',
247 'updated_on': _render('pullrequest_updated_on',
248 h.datetime_to_time(pr.updated_on)),
248 h.datetime_to_time(pr.updated_on)),
249 'updated_on_raw': h.datetime_to_time(pr.updated_on),
249 'updated_on_raw': h.datetime_to_time(pr.updated_on),
250 'created_on': _render('pullrequest_updated_on',
250 'created_on': _render('pullrequest_updated_on',
251 h.datetime_to_time(pr.created_on)),
251 h.datetime_to_time(pr.created_on)),
252 'created_on_raw': h.datetime_to_time(pr.created_on),
252 'created_on_raw': h.datetime_to_time(pr.created_on),
253 'author': _render('pullrequest_author',
253 'author': _render('pullrequest_author',
254 pr.author.full_contact, ),
254 pr.author.full_contact, ),
255 'author_raw': pr.author.full_name,
255 'author_raw': pr.author.full_name,
256 'comments': _render('pullrequest_comments', len(comments)),
256 'comments': _render('pullrequest_comments', len(comments)),
257 'comments_raw': len(comments),
257 'comments_raw': len(comments),
258 'closed': pr.is_closed(),
258 'closed': pr.is_closed(),
259 })
259 })
260 # json used to render the grid
260 # json used to render the grid
261 data = ({
261 data = ({
262 'data': data,
262 'data': data,
263 'recordsTotal': pull_requests_total_count,
263 'recordsTotal': pull_requests_total_count,
264 'recordsFiltered': pull_requests_total_count,
264 'recordsFiltered': pull_requests_total_count,
265 })
265 })
266 return data
266 return data
267
267
268 @LoginRequired()
268 @LoginRequired()
269 @NotAnonymous()
269 @NotAnonymous()
270 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
270 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
271 'repository.admin')
271 'repository.admin')
272 @HasAcceptedRepoType('git', 'hg')
272 @HasAcceptedRepoType('git', 'hg')
273 def index(self):
273 def index(self):
274 source_repo = c.rhodecode_db_repo
274 source_repo = c.rhodecode_db_repo
275
275
276 try:
276 try:
277 source_repo.scm_instance().get_commit()
277 source_repo.scm_instance().get_commit()
278 except EmptyRepositoryError:
278 except EmptyRepositoryError:
279 h.flash(h.literal(_('There are no commits yet')),
279 h.flash(h.literal(_('There are no commits yet')),
280 category='warning')
280 category='warning')
281 redirect(url('summary_home', repo_name=source_repo.repo_name))
281 redirect(url('summary_home', repo_name=source_repo.repo_name))
282
282
283 commit_id = request.GET.get('commit')
283 commit_id = request.GET.get('commit')
284 branch_ref = request.GET.get('branch')
284 branch_ref = request.GET.get('branch')
285 bookmark_ref = request.GET.get('bookmark')
285 bookmark_ref = request.GET.get('bookmark')
286
286
287 try:
287 try:
288 source_repo_data = PullRequestModel().generate_repo_data(
288 source_repo_data = PullRequestModel().generate_repo_data(
289 source_repo, commit_id=commit_id,
289 source_repo, commit_id=commit_id,
290 branch=branch_ref, bookmark=bookmark_ref)
290 branch=branch_ref, bookmark=bookmark_ref)
291 except CommitDoesNotExistError as e:
291 except CommitDoesNotExistError as e:
292 log.exception(e)
292 log.exception(e)
293 h.flash(_('Commit does not exist'), 'error')
293 h.flash(_('Commit does not exist'), 'error')
294 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
294 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
295
295
296 default_target_repo = source_repo
296 default_target_repo = source_repo
297 if (source_repo.parent and
297 if (source_repo.parent and
298 not source_repo.parent.scm_instance().is_empty()):
298 not source_repo.parent.scm_instance().is_empty()):
299 # change default if we have a parent repo
299 # change default if we have a parent repo
300 default_target_repo = source_repo.parent
300 default_target_repo = source_repo.parent
301
301
302 target_repo_data = PullRequestModel().generate_repo_data(
302 target_repo_data = PullRequestModel().generate_repo_data(
303 default_target_repo)
303 default_target_repo)
304
304
305 selected_source_ref = source_repo_data['refs']['selected_ref']
305 selected_source_ref = source_repo_data['refs']['selected_ref']
306
306
307 title_source_ref = selected_source_ref.split(':', 2)[1]
307 title_source_ref = selected_source_ref.split(':', 2)[1]
308 c.default_title = PullRequestModel().generate_pullrequest_title(
308 c.default_title = PullRequestModel().generate_pullrequest_title(
309 source=source_repo.repo_name,
309 source=source_repo.repo_name,
310 source_ref=title_source_ref,
310 source_ref=title_source_ref,
311 target=default_target_repo.repo_name
311 target=default_target_repo.repo_name
312 )
312 )
313
313
314 c.default_repo_data = {
314 c.default_repo_data = {
315 'source_repo_name': source_repo.repo_name,
315 'source_repo_name': source_repo.repo_name,
316 'source_refs_json': json.dumps(source_repo_data),
316 'source_refs_json': json.dumps(source_repo_data),
317 'target_repo_name': default_target_repo.repo_name,
317 'target_repo_name': default_target_repo.repo_name,
318 'target_refs_json': json.dumps(target_repo_data),
318 'target_refs_json': json.dumps(target_repo_data),
319 }
319 }
320 c.default_source_ref = selected_source_ref
320 c.default_source_ref = selected_source_ref
321
321
322 return render('/pullrequests/pullrequest.html')
322 return render('/pullrequests/pullrequest.html')
323
323
324 @LoginRequired()
324 @LoginRequired()
325 @NotAnonymous()
325 @NotAnonymous()
326 @XHRRequired()
326 @XHRRequired()
327 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
327 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
328 'repository.admin')
328 'repository.admin')
329 @jsonify
329 @jsonify
330 def get_repo_refs(self, repo_name, target_repo_name):
330 def get_repo_refs(self, repo_name, target_repo_name):
331 repo = Repository.get_by_repo_name(target_repo_name)
331 repo = Repository.get_by_repo_name(target_repo_name)
332 if not repo:
332 if not repo:
333 raise HTTPNotFound
333 raise HTTPNotFound
334 return PullRequestModel().generate_repo_data(repo)
334 return PullRequestModel().generate_repo_data(repo)
335
335
336 @LoginRequired()
336 @LoginRequired()
337 @NotAnonymous()
337 @NotAnonymous()
338 @XHRRequired()
338 @XHRRequired()
339 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
339 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 'repository.admin')
340 'repository.admin')
341 @jsonify
341 @jsonify
342 def get_repo_destinations(self, repo_name):
342 def get_repo_destinations(self, repo_name):
343 repo = Repository.get_by_repo_name(repo_name)
343 repo = Repository.get_by_repo_name(repo_name)
344 if not repo:
344 if not repo:
345 raise HTTPNotFound
345 raise HTTPNotFound
346 filter_query = request.GET.get('query')
346 filter_query = request.GET.get('query')
347
347
348 query = Repository.query() \
348 query = Repository.query() \
349 .order_by(func.length(Repository.repo_name)) \
349 .order_by(func.length(Repository.repo_name)) \
350 .filter(or_(
350 .filter(or_(
351 Repository.repo_name == repo.repo_name,
351 Repository.repo_name == repo.repo_name,
352 Repository.fork_id == repo.repo_id))
352 Repository.fork_id == repo.repo_id))
353
353
354 if filter_query:
354 if filter_query:
355 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
355 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
356 query = query.filter(
356 query = query.filter(
357 Repository.repo_name.ilike(ilike_expression))
357 Repository.repo_name.ilike(ilike_expression))
358
358
359 add_parent = False
359 add_parent = False
360 if repo.parent:
360 if repo.parent:
361 if filter_query in repo.parent.repo_name:
361 if filter_query in repo.parent.repo_name:
362 if not repo.parent.scm_instance().is_empty():
362 if not repo.parent.scm_instance().is_empty():
363 add_parent = True
363 add_parent = True
364
364
365 limit = 20 - 1 if add_parent else 20
365 limit = 20 - 1 if add_parent else 20
366 all_repos = query.limit(limit).all()
366 all_repos = query.limit(limit).all()
367 if add_parent:
367 if add_parent:
368 all_repos += [repo.parent]
368 all_repos += [repo.parent]
369
369
370 repos = []
370 repos = []
371 for obj in self.scm_model.get_repos(all_repos):
371 for obj in self.scm_model.get_repos(all_repos):
372 repos.append({
372 repos.append({
373 'id': obj['name'],
373 'id': obj['name'],
374 'text': obj['name'],
374 'text': obj['name'],
375 'type': 'repo',
375 'type': 'repo',
376 'obj': obj['dbrepo']
376 'obj': obj['dbrepo']
377 })
377 })
378
378
379 data = {
379 data = {
380 'more': False,
380 'more': False,
381 'results': [{
381 'results': [{
382 'text': _('Repositories'),
382 'text': _('Repositories'),
383 'children': repos
383 'children': repos
384 }] if repos else []
384 }] if repos else []
385 }
385 }
386 return data
386 return data
387
387
388 @LoginRequired()
388 @LoginRequired()
389 @NotAnonymous()
389 @NotAnonymous()
390 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
390 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
391 'repository.admin')
391 'repository.admin')
392 @HasAcceptedRepoType('git', 'hg')
392 @HasAcceptedRepoType('git', 'hg')
393 @auth.CSRFRequired()
393 @auth.CSRFRequired()
394 def create(self, repo_name):
394 def create(self, repo_name):
395 repo = Repository.get_by_repo_name(repo_name)
395 repo = Repository.get_by_repo_name(repo_name)
396 if not repo:
396 if not repo:
397 raise HTTPNotFound
397 raise HTTPNotFound
398
398
399 try:
399 try:
400 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
400 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
401 except formencode.Invalid as errors:
401 except formencode.Invalid as errors:
402 if errors.error_dict.get('revisions'):
402 if errors.error_dict.get('revisions'):
403 msg = 'Revisions: %s' % errors.error_dict['revisions']
403 msg = 'Revisions: %s' % errors.error_dict['revisions']
404 elif errors.error_dict.get('pullrequest_title'):
404 elif errors.error_dict.get('pullrequest_title'):
405 msg = _('Pull request requires a title with min. 3 chars')
405 msg = _('Pull request requires a title with min. 3 chars')
406 else:
406 else:
407 msg = _('Error creating pull request: {}').format(errors)
407 msg = _('Error creating pull request: {}').format(errors)
408 log.exception(msg)
408 log.exception(msg)
409 h.flash(msg, 'error')
409 h.flash(msg, 'error')
410
410
411 # would rather just go back to form ...
411 # would rather just go back to form ...
412 return redirect(url('pullrequest_home', repo_name=repo_name))
412 return redirect(url('pullrequest_home', repo_name=repo_name))
413
413
414 source_repo = _form['source_repo']
414 source_repo = _form['source_repo']
415 source_ref = _form['source_ref']
415 source_ref = _form['source_ref']
416 target_repo = _form['target_repo']
416 target_repo = _form['target_repo']
417 target_ref = _form['target_ref']
417 target_ref = _form['target_ref']
418 commit_ids = _form['revisions'][::-1]
418 commit_ids = _form['revisions'][::-1]
419 reviewers = _form['review_members']
419 reviewers = _form['review_members']
420
420
421 # find the ancestor for this pr
421 # find the ancestor for this pr
422 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
422 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
423 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
423 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
424
424
425 source_scm = source_db_repo.scm_instance()
425 source_scm = source_db_repo.scm_instance()
426 target_scm = target_db_repo.scm_instance()
426 target_scm = target_db_repo.scm_instance()
427
427
428 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
428 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
429 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
429 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
430
430
431 ancestor = source_scm.get_common_ancestor(
431 ancestor = source_scm.get_common_ancestor(
432 source_commit.raw_id, target_commit.raw_id, target_scm)
432 source_commit.raw_id, target_commit.raw_id, target_scm)
433
433
434 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
434 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
435 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
435 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
436
436
437 pullrequest_title = _form['pullrequest_title']
437 pullrequest_title = _form['pullrequest_title']
438 title_source_ref = source_ref.split(':', 2)[1]
438 title_source_ref = source_ref.split(':', 2)[1]
439 if not pullrequest_title:
439 if not pullrequest_title:
440 pullrequest_title = PullRequestModel().generate_pullrequest_title(
440 pullrequest_title = PullRequestModel().generate_pullrequest_title(
441 source=source_repo,
441 source=source_repo,
442 source_ref=title_source_ref,
442 source_ref=title_source_ref,
443 target=target_repo
443 target=target_repo
444 )
444 )
445
445
446 description = _form['pullrequest_desc']
446 description = _form['pullrequest_desc']
447 try:
447 try:
448 pull_request = PullRequestModel().create(
448 pull_request = PullRequestModel().create(
449 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
449 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
450 target_ref, commit_ids, reviewers, pullrequest_title,
450 target_ref, commit_ids, reviewers, pullrequest_title,
451 description
451 description
452 )
452 )
453 Session().commit()
453 Session().commit()
454 h.flash(_('Successfully opened new pull request'),
454 h.flash(_('Successfully opened new pull request'),
455 category='success')
455 category='success')
456 except Exception as e:
456 except Exception as e:
457 msg = _('Error occurred during sending pull request')
457 msg = _('Error occurred during sending pull request')
458 log.exception(msg)
458 log.exception(msg)
459 h.flash(msg, category='error')
459 h.flash(msg, category='error')
460 return redirect(url('pullrequest_home', repo_name=repo_name))
460 return redirect(url('pullrequest_home', repo_name=repo_name))
461
461
462 return redirect(url('pullrequest_show', repo_name=target_repo,
462 return redirect(url('pullrequest_show', repo_name=target_repo,
463 pull_request_id=pull_request.pull_request_id))
463 pull_request_id=pull_request.pull_request_id))
464
464
465 @LoginRequired()
465 @LoginRequired()
466 @NotAnonymous()
466 @NotAnonymous()
467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
467 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
468 'repository.admin')
468 'repository.admin')
469 @auth.CSRFRequired()
469 @auth.CSRFRequired()
470 @jsonify
470 @jsonify
471 def update(self, repo_name, pull_request_id):
471 def update(self, repo_name, pull_request_id):
472 pull_request_id = safe_int(pull_request_id)
472 pull_request_id = safe_int(pull_request_id)
473 pull_request = PullRequest.get_or_404(pull_request_id)
473 pull_request = PullRequest.get_or_404(pull_request_id)
474 # only owner or admin can update it
474 # only owner or admin can update it
475 allowed_to_update = PullRequestModel().check_user_update(
475 allowed_to_update = PullRequestModel().check_user_update(
476 pull_request, c.rhodecode_user)
476 pull_request, c.rhodecode_user)
477 if allowed_to_update:
477 if allowed_to_update:
478 if 'reviewers_ids' in request.POST:
478 if 'reviewers_ids' in request.POST:
479 self._update_reviewers(pull_request_id)
479 self._update_reviewers(pull_request_id)
480 elif str2bool(request.POST.get('update_commits', 'false')):
480 elif str2bool(request.POST.get('update_commits', 'false')):
481 self._update_commits(pull_request)
481 self._update_commits(pull_request)
482 elif str2bool(request.POST.get('close_pull_request', 'false')):
482 elif str2bool(request.POST.get('close_pull_request', 'false')):
483 self._reject_close(pull_request)
483 self._reject_close(pull_request)
484 elif str2bool(request.POST.get('edit_pull_request', 'false')):
484 elif str2bool(request.POST.get('edit_pull_request', 'false')):
485 self._edit_pull_request(pull_request)
485 self._edit_pull_request(pull_request)
486 else:
486 else:
487 raise HTTPBadRequest()
487 raise HTTPBadRequest()
488 return True
488 return True
489 raise HTTPForbidden()
489 raise HTTPForbidden()
490
490
491 def _edit_pull_request(self, pull_request):
491 def _edit_pull_request(self, pull_request):
492 try:
492 try:
493 PullRequestModel().edit(
493 PullRequestModel().edit(
494 pull_request, request.POST.get('title'),
494 pull_request, request.POST.get('title'),
495 request.POST.get('description'))
495 request.POST.get('description'))
496 except ValueError:
496 except ValueError:
497 msg = _(u'Cannot update closed pull requests.')
497 msg = _(u'Cannot update closed pull requests.')
498 h.flash(msg, category='error')
498 h.flash(msg, category='error')
499 return
499 return
500 else:
500 else:
501 Session().commit()
501 Session().commit()
502
502
503 msg = _(u'Pull request title & description updated.')
503 msg = _(u'Pull request title & description updated.')
504 h.flash(msg, category='success')
504 h.flash(msg, category='success')
505 return
505 return
506
506
507 def _update_commits(self, pull_request):
507 def _update_commits(self, pull_request):
508 try:
508 try:
509 if PullRequestModel().has_valid_update_type(pull_request):
509 if PullRequestModel().has_valid_update_type(pull_request):
510 updated_version, changes = PullRequestModel().update_commits(
510 updated_version, changes = PullRequestModel().update_commits(
511 pull_request)
511 pull_request)
512 if updated_version:
512 if updated_version:
513 msg = _(
513 msg = _(
514 u'Pull request updated to "{source_commit_id}" with '
514 u'Pull request updated to "{source_commit_id}" with '
515 u'{count_added} added, {count_removed} removed '
515 u'{count_added} added, {count_removed} removed '
516 u'commits.'
516 u'commits.'
517 ).format(
517 ).format(
518 source_commit_id=pull_request.source_ref_parts.commit_id,
518 source_commit_id=pull_request.source_ref_parts.commit_id,
519 count_added=len(changes.added),
519 count_added=len(changes.added),
520 count_removed=len(changes.removed))
520 count_removed=len(changes.removed))
521 h.flash(msg, category='success')
521 h.flash(msg, category='success')
522 else:
522 else:
523 h.flash(_("Nothing changed in pull request."),
523 h.flash(_("Nothing changed in pull request."),
524 category='warning')
524 category='warning')
525 else:
525 else:
526 msg = _(
526 msg = _(
527 u"Skipping update of pull request due to reference "
527 u"Skipping update of pull request due to reference "
528 u"type: {reference_type}"
528 u"type: {reference_type}"
529 ).format(reference_type=pull_request.source_ref_parts.type)
529 ).format(reference_type=pull_request.source_ref_parts.type)
530 h.flash(msg, category='warning')
530 h.flash(msg, category='warning')
531 except CommitDoesNotExistError:
531 except CommitDoesNotExistError:
532 h.flash(
532 h.flash(
533 _(u'Update failed due to missing commits.'), category='error')
533 _(u'Update failed due to missing commits.'), category='error')
534
534
535 @auth.CSRFRequired()
535 @auth.CSRFRequired()
536 @LoginRequired()
536 @LoginRequired()
537 @NotAnonymous()
537 @NotAnonymous()
538 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
538 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
539 'repository.admin')
539 'repository.admin')
540 def merge(self, repo_name, pull_request_id):
540 def merge(self, repo_name, pull_request_id):
541 """
541 """
542 POST /{repo_name}/pull-request/{pull_request_id}
542 POST /{repo_name}/pull-request/{pull_request_id}
543
543
544 Merge will perform a server-side merge of the specified
544 Merge will perform a server-side merge of the specified
545 pull request, if the pull request is approved and mergeable.
545 pull request, if the pull request is approved and mergeable.
546 After succesfull merging, the pull request is automatically
546 After succesfull merging, the pull request is automatically
547 closed, with a relevant comment.
547 closed, with a relevant comment.
548 """
548 """
549 pull_request_id = safe_int(pull_request_id)
549 pull_request_id = safe_int(pull_request_id)
550 pull_request = PullRequest.get_or_404(pull_request_id)
550 pull_request = PullRequest.get_or_404(pull_request_id)
551 user = c.rhodecode_user
551 user = c.rhodecode_user
552
552
553 if self._meets_merge_pre_conditions(pull_request, user):
553 if self._meets_merge_pre_conditions(pull_request, user):
554 log.debug("Pre-conditions checked, trying to merge.")
554 log.debug("Pre-conditions checked, trying to merge.")
555 extras = vcs_operation_context(
555 extras = vcs_operation_context(
556 request.environ, repo_name=pull_request.target_repo.repo_name,
556 request.environ, repo_name=pull_request.target_repo.repo_name,
557 username=user.username, action='push',
557 username=user.username, action='push',
558 scm=pull_request.target_repo.repo_type)
558 scm=pull_request.target_repo.repo_type)
559 self._merge_pull_request(pull_request, user, extras)
559 self._merge_pull_request(pull_request, user, extras)
560
560
561 return redirect(url(
561 return redirect(url(
562 'pullrequest_show',
562 'pullrequest_show',
563 repo_name=pull_request.target_repo.repo_name,
563 repo_name=pull_request.target_repo.repo_name,
564 pull_request_id=pull_request.pull_request_id))
564 pull_request_id=pull_request.pull_request_id))
565
565
566 def _meets_merge_pre_conditions(self, pull_request, user):
566 def _meets_merge_pre_conditions(self, pull_request, user):
567 if not PullRequestModel().check_user_merge(pull_request, user):
567 if not PullRequestModel().check_user_merge(pull_request, user):
568 raise HTTPForbidden()
568 raise HTTPForbidden()
569
569
570 merge_status, msg = PullRequestModel().merge_status(pull_request)
570 merge_status, msg = PullRequestModel().merge_status(pull_request)
571 if not merge_status:
571 if not merge_status:
572 log.debug("Cannot merge, not mergeable.")
572 log.debug("Cannot merge, not mergeable.")
573 h.flash(msg, category='error')
573 h.flash(msg, category='error')
574 return False
574 return False
575
575
576 if (pull_request.calculated_review_status()
576 if (pull_request.calculated_review_status()
577 is not ChangesetStatus.STATUS_APPROVED):
577 is not ChangesetStatus.STATUS_APPROVED):
578 log.debug("Cannot merge, approval is pending.")
578 log.debug("Cannot merge, approval is pending.")
579 msg = _('Pull request reviewer approval is pending.')
579 msg = _('Pull request reviewer approval is pending.')
580 h.flash(msg, category='error')
580 h.flash(msg, category='error')
581 return False
581 return False
582 return True
582 return True
583
583
584 def _merge_pull_request(self, pull_request, user, extras):
584 def _merge_pull_request(self, pull_request, user, extras):
585 merge_resp = PullRequestModel().merge(
585 merge_resp = PullRequestModel().merge(
586 pull_request, user, extras=extras)
586 pull_request, user, extras=extras)
587
587
588 if merge_resp.executed:
588 if merge_resp.executed:
589 log.debug("The merge was successful, closing the pull request.")
589 log.debug("The merge was successful, closing the pull request.")
590 PullRequestModel().close_pull_request(
590 PullRequestModel().close_pull_request(
591 pull_request.pull_request_id, user)
591 pull_request.pull_request_id, user)
592 Session().commit()
592 Session().commit()
593 msg = _('Pull request was successfully merged and closed.')
593 msg = _('Pull request was successfully merged and closed.')
594 h.flash(msg, category='success')
594 h.flash(msg, category='success')
595 else:
595 else:
596 log.debug(
596 log.debug(
597 "The merge was not successful. Merge response: %s",
597 "The merge was not successful. Merge response: %s",
598 merge_resp)
598 merge_resp)
599 msg = PullRequestModel().merge_status_message(
599 msg = PullRequestModel().merge_status_message(
600 merge_resp.failure_reason)
600 merge_resp.failure_reason)
601 h.flash(msg, category='error')
601 h.flash(msg, category='error')
602
602
603 def _update_reviewers(self, pull_request_id):
603 def _update_reviewers(self, pull_request_id):
604 reviewers_ids = map(int, filter(
604 reviewers_ids = map(int, filter(
605 lambda v: v not in [None, ''],
605 lambda v: v not in [None, ''],
606 request.POST.get('reviewers_ids', '').split(',')))
606 request.POST.get('reviewers_ids', '').split(',')))
607 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
607 PullRequestModel().update_reviewers(pull_request_id, reviewers_ids)
608 Session().commit()
608 Session().commit()
609
609
610 def _reject_close(self, pull_request):
610 def _reject_close(self, pull_request):
611 if pull_request.is_closed():
611 if pull_request.is_closed():
612 raise HTTPForbidden()
612 raise HTTPForbidden()
613
613
614 PullRequestModel().close_pull_request_with_comment(
614 PullRequestModel().close_pull_request_with_comment(
615 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
615 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
616 Session().commit()
616 Session().commit()
617
617
618 @LoginRequired()
618 @LoginRequired()
619 @NotAnonymous()
619 @NotAnonymous()
620 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
620 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
621 'repository.admin')
621 'repository.admin')
622 @auth.CSRFRequired()
622 @auth.CSRFRequired()
623 @jsonify
623 @jsonify
624 def delete(self, repo_name, pull_request_id):
624 def delete(self, repo_name, pull_request_id):
625 pull_request_id = safe_int(pull_request_id)
625 pull_request_id = safe_int(pull_request_id)
626 pull_request = PullRequest.get_or_404(pull_request_id)
626 pull_request = PullRequest.get_or_404(pull_request_id)
627 # only owner can delete it !
627 # only owner can delete it !
628 if pull_request.author.user_id == c.rhodecode_user.user_id:
628 if pull_request.author.user_id == c.rhodecode_user.user_id:
629 PullRequestModel().delete(pull_request)
629 PullRequestModel().delete(pull_request)
630 Session().commit()
630 Session().commit()
631 h.flash(_('Successfully deleted pull request'),
631 h.flash(_('Successfully deleted pull request'),
632 category='success')
632 category='success')
633 return redirect(url('my_account_pullrequests'))
633 return redirect(url('my_account_pullrequests'))
634 raise HTTPForbidden()
634 raise HTTPForbidden()
635
635
636 @LoginRequired()
636 @LoginRequired()
637 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
637 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
638 'repository.admin')
638 'repository.admin')
639 def show(self, repo_name, pull_request_id):
639 def show(self, repo_name, pull_request_id):
640 pull_request_id = safe_int(pull_request_id)
640 pull_request_id = safe_int(pull_request_id)
641 c.pull_request = PullRequest.get_or_404(pull_request_id)
641 c.pull_request = PullRequest.get_or_404(pull_request_id)
642
642
643 if hasattr(c, 'pylons_dispatch_info'):
644 c.pylons_dispatch_info['extra']['pull_request'] = pull_request_id
645
643 # pull_requests repo_name we opened it against
646 # pull_requests repo_name we opened it against
644 # ie. target_repo must match
647 # ie. target_repo must match
645 if repo_name != c.pull_request.target_repo.repo_name:
648 if repo_name != c.pull_request.target_repo.repo_name:
646 raise HTTPNotFound
649 raise HTTPNotFound
647
650
648 c.allowed_to_change_status = PullRequestModel(). \
651 c.allowed_to_change_status = PullRequestModel(). \
649 check_user_change_status(c.pull_request, c.rhodecode_user)
652 check_user_change_status(c.pull_request, c.rhodecode_user)
650 c.allowed_to_update = PullRequestModel().check_user_update(
653 c.allowed_to_update = PullRequestModel().check_user_update(
651 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
654 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
652 c.allowed_to_merge = PullRequestModel().check_user_merge(
655 c.allowed_to_merge = PullRequestModel().check_user_merge(
653 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
656 c.pull_request, c.rhodecode_user) and not c.pull_request.is_closed()
654
657
655 cc_model = ChangesetCommentsModel()
658 cc_model = ChangesetCommentsModel()
656
659
657 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
660 c.pull_request_reviewers = c.pull_request.reviewers_statuses()
658
661
659 c.pull_request_review_status = c.pull_request.calculated_review_status()
662 c.pull_request_review_status = c.pull_request.calculated_review_status()
660 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
663 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
661 c.pull_request)
664 c.pull_request)
662 c.approval_msg = None
665 c.approval_msg = None
663 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
666 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
664 c.approval_msg = _('Reviewer approval is pending.')
667 c.approval_msg = _('Reviewer approval is pending.')
665 c.pr_merge_status = False
668 c.pr_merge_status = False
666 # load compare data into template context
669 # load compare data into template context
667 enable_comments = not c.pull_request.is_closed()
670 enable_comments = not c.pull_request.is_closed()
668 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
671 self._load_compare_data(c.pull_request, enable_comments=enable_comments)
669
672
670 # this is a hack to properly display links, when creating PR, the
673 # this is a hack to properly display links, when creating PR, the
671 # compare view and others uses different notation, and
674 # compare view and others uses different notation, and
672 # compare_commits.html renders links based on the target_repo.
675 # compare_commits.html renders links based on the target_repo.
673 # We need to swap that here to generate it properly on the html side
676 # We need to swap that here to generate it properly on the html side
674 c.target_repo = c.source_repo
677 c.target_repo = c.source_repo
675
678
676 # inline comments
679 # inline comments
677 c.inline_cnt = 0
680 c.inline_cnt = 0
678 c.inline_comments = cc_model.get_inline_comments(
681 c.inline_comments = cc_model.get_inline_comments(
679 c.rhodecode_db_repo.repo_id,
682 c.rhodecode_db_repo.repo_id,
680 pull_request=pull_request_id).items()
683 pull_request=pull_request_id).items()
681 # count inline comments
684 # count inline comments
682 for __, lines in c.inline_comments:
685 for __, lines in c.inline_comments:
683 for comments in lines.values():
686 for comments in lines.values():
684 c.inline_cnt += len(comments)
687 c.inline_cnt += len(comments)
685
688
686 # outdated comments
689 # outdated comments
687 c.outdated_cnt = 0
690 c.outdated_cnt = 0
688 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
691 if ChangesetCommentsModel.use_outdated_comments(c.pull_request):
689 c.outdated_comments = cc_model.get_outdated_comments(
692 c.outdated_comments = cc_model.get_outdated_comments(
690 c.rhodecode_db_repo.repo_id,
693 c.rhodecode_db_repo.repo_id,
691 pull_request=c.pull_request)
694 pull_request=c.pull_request)
692 # Count outdated comments and check for deleted files
695 # Count outdated comments and check for deleted files
693 for file_name, lines in c.outdated_comments.iteritems():
696 for file_name, lines in c.outdated_comments.iteritems():
694 for comments in lines.values():
697 for comments in lines.values():
695 c.outdated_cnt += len(comments)
698 c.outdated_cnt += len(comments)
696 if file_name not in c.included_files:
699 if file_name not in c.included_files:
697 c.deleted_files.append(file_name)
700 c.deleted_files.append(file_name)
698 else:
701 else:
699 c.outdated_comments = {}
702 c.outdated_comments = {}
700
703
701 # comments
704 # comments
702 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
705 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
703 pull_request=pull_request_id)
706 pull_request=pull_request_id)
704
707
705 if c.allowed_to_update:
708 if c.allowed_to_update:
706 force_close = ('forced_closed', _('Close Pull Request'))
709 force_close = ('forced_closed', _('Close Pull Request'))
707 statuses = ChangesetStatus.STATUSES + [force_close]
710 statuses = ChangesetStatus.STATUSES + [force_close]
708 else:
711 else:
709 statuses = ChangesetStatus.STATUSES
712 statuses = ChangesetStatus.STATUSES
710 c.commit_statuses = statuses
713 c.commit_statuses = statuses
711
714
712 c.ancestor = None # TODO: add ancestor here
715 c.ancestor = None # TODO: add ancestor here
713
716
714 return render('/pullrequests/pullrequest_show.html')
717 return render('/pullrequests/pullrequest_show.html')
715
718
716 @LoginRequired()
719 @LoginRequired()
717 @NotAnonymous()
720 @NotAnonymous()
718 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
721 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
719 'repository.admin')
722 'repository.admin')
720 @auth.CSRFRequired()
723 @auth.CSRFRequired()
721 @jsonify
724 @jsonify
722 def comment(self, repo_name, pull_request_id):
725 def comment(self, repo_name, pull_request_id):
723 pull_request_id = safe_int(pull_request_id)
726 pull_request_id = safe_int(pull_request_id)
724 pull_request = PullRequest.get_or_404(pull_request_id)
727 pull_request = PullRequest.get_or_404(pull_request_id)
725 if pull_request.is_closed():
728 if pull_request.is_closed():
726 raise HTTPForbidden()
729 raise HTTPForbidden()
727
730
728 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
731 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
729 # as a changeset status, still we want to send it in one value.
732 # as a changeset status, still we want to send it in one value.
730 status = request.POST.get('changeset_status', None)
733 status = request.POST.get('changeset_status', None)
731 text = request.POST.get('text')
734 text = request.POST.get('text')
732 if status and '_closed' in status:
735 if status and '_closed' in status:
733 close_pr = True
736 close_pr = True
734 status = status.replace('_closed', '')
737 status = status.replace('_closed', '')
735 else:
738 else:
736 close_pr = False
739 close_pr = False
737
740
738 forced = (status == 'forced')
741 forced = (status == 'forced')
739 if forced:
742 if forced:
740 status = 'rejected'
743 status = 'rejected'
741
744
742 allowed_to_change_status = PullRequestModel().check_user_change_status(
745 allowed_to_change_status = PullRequestModel().check_user_change_status(
743 pull_request, c.rhodecode_user)
746 pull_request, c.rhodecode_user)
744
747
745 if status and allowed_to_change_status:
748 if status and allowed_to_change_status:
746 message = (_('Status change %(transition_icon)s %(status)s')
749 message = (_('Status change %(transition_icon)s %(status)s')
747 % {'transition_icon': '>',
750 % {'transition_icon': '>',
748 'status': ChangesetStatus.get_status_lbl(status)})
751 'status': ChangesetStatus.get_status_lbl(status)})
749 if close_pr:
752 if close_pr:
750 message = _('Closing with') + ' ' + message
753 message = _('Closing with') + ' ' + message
751 text = text or message
754 text = text or message
752 comm = ChangesetCommentsModel().create(
755 comm = ChangesetCommentsModel().create(
753 text=text,
756 text=text,
754 repo=c.rhodecode_db_repo.repo_id,
757 repo=c.rhodecode_db_repo.repo_id,
755 user=c.rhodecode_user.user_id,
758 user=c.rhodecode_user.user_id,
756 pull_request=pull_request_id,
759 pull_request=pull_request_id,
757 f_path=request.POST.get('f_path'),
760 f_path=request.POST.get('f_path'),
758 line_no=request.POST.get('line'),
761 line_no=request.POST.get('line'),
759 status_change=(ChangesetStatus.get_status_lbl(status)
762 status_change=(ChangesetStatus.get_status_lbl(status)
760 if status and allowed_to_change_status else None),
763 if status and allowed_to_change_status else None),
761 closing_pr=close_pr
764 closing_pr=close_pr
762 )
765 )
763
766
764 if allowed_to_change_status:
767 if allowed_to_change_status:
765 old_calculated_status = pull_request.calculated_review_status()
768 old_calculated_status = pull_request.calculated_review_status()
766 # get status if set !
769 # get status if set !
767 if status:
770 if status:
768 ChangesetStatusModel().set_status(
771 ChangesetStatusModel().set_status(
769 c.rhodecode_db_repo.repo_id,
772 c.rhodecode_db_repo.repo_id,
770 status,
773 status,
771 c.rhodecode_user.user_id,
774 c.rhodecode_user.user_id,
772 comm,
775 comm,
773 pull_request=pull_request_id
776 pull_request=pull_request_id
774 )
777 )
775
778
776 Session().flush()
779 Session().flush()
777 # we now calculate the status of pull request, and based on that
780 # we now calculate the status of pull request, and based on that
778 # calculation we set the commits status
781 # calculation we set the commits status
779 calculated_status = pull_request.calculated_review_status()
782 calculated_status = pull_request.calculated_review_status()
780 if old_calculated_status != calculated_status:
783 if old_calculated_status != calculated_status:
781 PullRequestModel()._trigger_pull_request_hook(
784 PullRequestModel()._trigger_pull_request_hook(
782 pull_request, c.rhodecode_user, 'review_status_change')
785 pull_request, c.rhodecode_user, 'review_status_change')
783
786
784 calculated_status_lbl = ChangesetStatus.get_status_lbl(
787 calculated_status_lbl = ChangesetStatus.get_status_lbl(
785 calculated_status)
788 calculated_status)
786
789
787 if close_pr:
790 if close_pr:
788 status_completed = (
791 status_completed = (
789 calculated_status in [ChangesetStatus.STATUS_APPROVED,
792 calculated_status in [ChangesetStatus.STATUS_APPROVED,
790 ChangesetStatus.STATUS_REJECTED])
793 ChangesetStatus.STATUS_REJECTED])
791 if forced or status_completed:
794 if forced or status_completed:
792 PullRequestModel().close_pull_request(
795 PullRequestModel().close_pull_request(
793 pull_request_id, c.rhodecode_user)
796 pull_request_id, c.rhodecode_user)
794 else:
797 else:
795 h.flash(_('Closing pull request on other statuses than '
798 h.flash(_('Closing pull request on other statuses than '
796 'rejected or approved is forbidden. '
799 'rejected or approved is forbidden. '
797 'Calculated status from all reviewers '
800 'Calculated status from all reviewers '
798 'is currently: %s') % calculated_status_lbl,
801 'is currently: %s') % calculated_status_lbl,
799 category='warning')
802 category='warning')
800
803
801 Session().commit()
804 Session().commit()
802
805
803 if not request.is_xhr:
806 if not request.is_xhr:
804 return redirect(h.url('pullrequest_show', repo_name=repo_name,
807 return redirect(h.url('pullrequest_show', repo_name=repo_name,
805 pull_request_id=pull_request_id))
808 pull_request_id=pull_request_id))
806
809
807 data = {
810 data = {
808 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
811 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
809 }
812 }
810 if comm:
813 if comm:
811 c.co = comm
814 c.co = comm
812 data.update(comm.get_dict())
815 data.update(comm.get_dict())
813 data.update({'rendered_text':
816 data.update({'rendered_text':
814 render('changeset/changeset_comment_block.html')})
817 render('changeset/changeset_comment_block.html')})
815
818
816 return data
819 return data
817
820
818 @LoginRequired()
821 @LoginRequired()
819 @NotAnonymous()
822 @NotAnonymous()
820 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
823 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
821 'repository.admin')
824 'repository.admin')
822 @auth.CSRFRequired()
825 @auth.CSRFRequired()
823 @jsonify
826 @jsonify
824 def delete_comment(self, repo_name, comment_id):
827 def delete_comment(self, repo_name, comment_id):
825 return self._delete_comment(comment_id)
828 return self._delete_comment(comment_id)
826
829
827 def _delete_comment(self, comment_id):
830 def _delete_comment(self, comment_id):
828 comment_id = safe_int(comment_id)
831 comment_id = safe_int(comment_id)
829 co = ChangesetComment.get_or_404(comment_id)
832 co = ChangesetComment.get_or_404(comment_id)
830 if co.pull_request.is_closed():
833 if co.pull_request.is_closed():
831 # don't allow deleting comments on closed pull request
834 # don't allow deleting comments on closed pull request
832 raise HTTPForbidden()
835 raise HTTPForbidden()
833
836
834 is_owner = co.author.user_id == c.rhodecode_user.user_id
837 is_owner = co.author.user_id == c.rhodecode_user.user_id
835 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
838 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
836 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
839 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
837 old_calculated_status = co.pull_request.calculated_review_status()
840 old_calculated_status = co.pull_request.calculated_review_status()
838 ChangesetCommentsModel().delete(comment=co)
841 ChangesetCommentsModel().delete(comment=co)
839 Session().commit()
842 Session().commit()
840 calculated_status = co.pull_request.calculated_review_status()
843 calculated_status = co.pull_request.calculated_review_status()
841 if old_calculated_status != calculated_status:
844 if old_calculated_status != calculated_status:
842 PullRequestModel()._trigger_pull_request_hook(
845 PullRequestModel()._trigger_pull_request_hook(
843 co.pull_request, c.rhodecode_user, 'review_status_change')
846 co.pull_request, c.rhodecode_user, 'review_status_change')
844 return True
847 return True
845 else:
848 else:
846 raise HTTPForbidden()
849 raise HTTPForbidden()
@@ -1,551 +1,557 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 The base Controller API
22 The base Controller API
23 Provides the BaseController class for subclassing. And usage in different
23 Provides the BaseController class for subclassing. And usage in different
24 controllers
24 controllers
25 """
25 """
26
26
27 import logging
27 import logging
28 import socket
28 import socket
29
29
30 import ipaddress
30 import ipaddress
31
31
32 from paste.auth.basic import AuthBasicAuthenticator
32 from paste.auth.basic import AuthBasicAuthenticator
33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
33 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
34 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
35 from pylons import config, tmpl_context as c, request, session, url
35 from pylons import config, tmpl_context as c, request, session, url
36 from pylons.controllers import WSGIController
36 from pylons.controllers import WSGIController
37 from pylons.controllers.util import redirect
37 from pylons.controllers.util import redirect
38 from pylons.i18n import translation
38 from pylons.i18n import translation
39 # marcink: don't remove this import
39 # marcink: don't remove this import
40 from pylons.templating import render_mako as render # noqa
40 from pylons.templating import render_mako as render # noqa
41 from pylons.i18n.translation import _
41 from pylons.i18n.translation import _
42 from webob.exc import HTTPFound
42 from webob.exc import HTTPFound
43
43
44
44
45 import rhodecode
45 import rhodecode
46 from rhodecode.authentication.base import VCS_TYPE
46 from rhodecode.authentication.base import VCS_TYPE
47 from rhodecode.lib import auth, utils2
47 from rhodecode.lib import auth, utils2
48 from rhodecode.lib import helpers as h
48 from rhodecode.lib import helpers as h
49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
49 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
50 from rhodecode.lib.exceptions import UserCreationError
50 from rhodecode.lib.exceptions import UserCreationError
51 from rhodecode.lib.utils import (
51 from rhodecode.lib.utils import (
52 get_repo_slug, set_rhodecode_config, password_changed,
52 get_repo_slug, set_rhodecode_config, password_changed,
53 get_enabled_hook_classes)
53 get_enabled_hook_classes)
54 from rhodecode.lib.utils2 import (
54 from rhodecode.lib.utils2 import (
55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
55 str2bool, safe_unicode, AttributeDict, safe_int, md5, aslist)
56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
56 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
57 from rhodecode.model import meta
57 from rhodecode.model import meta
58 from rhodecode.model.db import Repository, User
58 from rhodecode.model.db import Repository, User
59 from rhodecode.model.notification import NotificationModel
59 from rhodecode.model.notification import NotificationModel
60 from rhodecode.model.scm import ScmModel
60 from rhodecode.model.scm import ScmModel
61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
61 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
62
62
63
63
64 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
65
65
66
66
67 def _filter_proxy(ip):
67 def _filter_proxy(ip):
68 """
68 """
69 Passed in IP addresses in HEADERS can be in a special format of multiple
69 Passed in IP addresses in HEADERS can be in a special format of multiple
70 ips. Those comma separated IPs are passed from various proxies in the
70 ips. Those comma separated IPs are passed from various proxies in the
71 chain of request processing. The left-most being the original client.
71 chain of request processing. The left-most being the original client.
72 We only care about the first IP which came from the org. client.
72 We only care about the first IP which came from the org. client.
73
73
74 :param ip: ip string from headers
74 :param ip: ip string from headers
75 """
75 """
76 if ',' in ip:
76 if ',' in ip:
77 _ips = ip.split(',')
77 _ips = ip.split(',')
78 _first_ip = _ips[0].strip()
78 _first_ip = _ips[0].strip()
79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
79 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
80 return _first_ip
80 return _first_ip
81 return ip
81 return ip
82
82
83
83
84 def _filter_port(ip):
84 def _filter_port(ip):
85 """
85 """
86 Removes a port from ip, there are 4 main cases to handle here.
86 Removes a port from ip, there are 4 main cases to handle here.
87 - ipv4 eg. 127.0.0.1
87 - ipv4 eg. 127.0.0.1
88 - ipv6 eg. ::1
88 - ipv6 eg. ::1
89 - ipv4+port eg. 127.0.0.1:8080
89 - ipv4+port eg. 127.0.0.1:8080
90 - ipv6+port eg. [::1]:8080
90 - ipv6+port eg. [::1]:8080
91
91
92 :param ip:
92 :param ip:
93 """
93 """
94 def is_ipv6(ip_addr):
94 def is_ipv6(ip_addr):
95 if hasattr(socket, 'inet_pton'):
95 if hasattr(socket, 'inet_pton'):
96 try:
96 try:
97 socket.inet_pton(socket.AF_INET6, ip_addr)
97 socket.inet_pton(socket.AF_INET6, ip_addr)
98 except socket.error:
98 except socket.error:
99 return False
99 return False
100 else:
100 else:
101 # fallback to ipaddress
101 # fallback to ipaddress
102 try:
102 try:
103 ipaddress.IPv6Address(ip_addr)
103 ipaddress.IPv6Address(ip_addr)
104 except Exception:
104 except Exception:
105 return False
105 return False
106 return True
106 return True
107
107
108 if ':' not in ip: # must be ipv4 pure ip
108 if ':' not in ip: # must be ipv4 pure ip
109 return ip
109 return ip
110
110
111 if '[' in ip and ']' in ip: # ipv6 with port
111 if '[' in ip and ']' in ip: # ipv6 with port
112 return ip.split(']')[0][1:].lower()
112 return ip.split(']')[0][1:].lower()
113
113
114 # must be ipv6 or ipv4 with port
114 # must be ipv6 or ipv4 with port
115 if is_ipv6(ip):
115 if is_ipv6(ip):
116 return ip
116 return ip
117 else:
117 else:
118 ip, _port = ip.split(':')[:2] # means ipv4+port
118 ip, _port = ip.split(':')[:2] # means ipv4+port
119 return ip
119 return ip
120
120
121
121
122 def get_ip_addr(environ):
122 def get_ip_addr(environ):
123 proxy_key = 'HTTP_X_REAL_IP'
123 proxy_key = 'HTTP_X_REAL_IP'
124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
124 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
125 def_key = 'REMOTE_ADDR'
125 def_key = 'REMOTE_ADDR'
126 _filters = lambda x: _filter_port(_filter_proxy(x))
126 _filters = lambda x: _filter_port(_filter_proxy(x))
127
127
128 ip = environ.get(proxy_key)
128 ip = environ.get(proxy_key)
129 if ip:
129 if ip:
130 return _filters(ip)
130 return _filters(ip)
131
131
132 ip = environ.get(proxy_key2)
132 ip = environ.get(proxy_key2)
133 if ip:
133 if ip:
134 return _filters(ip)
134 return _filters(ip)
135
135
136 ip = environ.get(def_key, '0.0.0.0')
136 ip = environ.get(def_key, '0.0.0.0')
137 return _filters(ip)
137 return _filters(ip)
138
138
139
139
140 def get_server_ip_addr(environ, log_errors=True):
140 def get_server_ip_addr(environ, log_errors=True):
141 hostname = environ.get('SERVER_NAME')
141 hostname = environ.get('SERVER_NAME')
142 try:
142 try:
143 return socket.gethostbyname(hostname)
143 return socket.gethostbyname(hostname)
144 except Exception as e:
144 except Exception as e:
145 if log_errors:
145 if log_errors:
146 # in some cases this lookup is not possible, and we don't want to
146 # in some cases this lookup is not possible, and we don't want to
147 # make it an exception in logs
147 # make it an exception in logs
148 log.exception('Could not retrieve server ip address: %s', e)
148 log.exception('Could not retrieve server ip address: %s', e)
149 return hostname
149 return hostname
150
150
151
151
152 def get_server_port(environ):
152 def get_server_port(environ):
153 return environ.get('SERVER_PORT')
153 return environ.get('SERVER_PORT')
154
154
155
155
156 def get_access_path(environ):
156 def get_access_path(environ):
157 path = environ.get('PATH_INFO')
157 path = environ.get('PATH_INFO')
158 org_req = environ.get('pylons.original_request')
158 org_req = environ.get('pylons.original_request')
159 if org_req:
159 if org_req:
160 path = org_req.environ.get('PATH_INFO')
160 path = org_req.environ.get('PATH_INFO')
161 return path
161 return path
162
162
163
163
164 def vcs_operation_context(
164 def vcs_operation_context(
165 environ, repo_name, username, action, scm, check_locking=True):
165 environ, repo_name, username, action, scm, check_locking=True):
166 """
166 """
167 Generate the context for a vcs operation, e.g. push or pull.
167 Generate the context for a vcs operation, e.g. push or pull.
168
168
169 This context is passed over the layers so that hooks triggered by the
169 This context is passed over the layers so that hooks triggered by the
170 vcs operation know details like the user, the user's IP address etc.
170 vcs operation know details like the user, the user's IP address etc.
171
171
172 :param check_locking: Allows to switch of the computation of the locking
172 :param check_locking: Allows to switch of the computation of the locking
173 data. This serves mainly the need of the simplevcs middleware to be
173 data. This serves mainly the need of the simplevcs middleware to be
174 able to disable this for certain operations.
174 able to disable this for certain operations.
175
175
176 """
176 """
177 # Tri-state value: False: unlock, None: nothing, True: lock
177 # Tri-state value: False: unlock, None: nothing, True: lock
178 make_lock = None
178 make_lock = None
179 locked_by = [None, None, None]
179 locked_by = [None, None, None]
180 is_anonymous = username == User.DEFAULT_USER
180 is_anonymous = username == User.DEFAULT_USER
181 if not is_anonymous and check_locking:
181 if not is_anonymous and check_locking:
182 log.debug('Checking locking on repository "%s"', repo_name)
182 log.debug('Checking locking on repository "%s"', repo_name)
183 user = User.get_by_username(username)
183 user = User.get_by_username(username)
184 repo = Repository.get_by_repo_name(repo_name)
184 repo = Repository.get_by_repo_name(repo_name)
185 make_lock, __, locked_by = repo.get_locking_state(
185 make_lock, __, locked_by = repo.get_locking_state(
186 action, user.user_id)
186 action, user.user_id)
187
187
188 settings_model = VcsSettingsModel(repo=repo_name)
188 settings_model = VcsSettingsModel(repo=repo_name)
189 ui_settings = settings_model.get_ui_settings()
189 ui_settings = settings_model.get_ui_settings()
190
190
191 extras = {
191 extras = {
192 'ip': get_ip_addr(environ),
192 'ip': get_ip_addr(environ),
193 'username': username,
193 'username': username,
194 'action': action,
194 'action': action,
195 'repository': repo_name,
195 'repository': repo_name,
196 'scm': scm,
196 'scm': scm,
197 'config': rhodecode.CONFIG['__file__'],
197 'config': rhodecode.CONFIG['__file__'],
198 'make_lock': make_lock,
198 'make_lock': make_lock,
199 'locked_by': locked_by,
199 'locked_by': locked_by,
200 'server_url': utils2.get_server_url(environ),
200 'server_url': utils2.get_server_url(environ),
201 'hooks': get_enabled_hook_classes(ui_settings),
201 'hooks': get_enabled_hook_classes(ui_settings),
202 }
202 }
203 return extras
203 return extras
204
204
205
205
206 class BasicAuth(AuthBasicAuthenticator):
206 class BasicAuth(AuthBasicAuthenticator):
207
207
208 def __init__(self, realm, authfunc, auth_http_code=None,
208 def __init__(self, realm, authfunc, auth_http_code=None,
209 initial_call_detection=False):
209 initial_call_detection=False):
210 self.realm = realm
210 self.realm = realm
211 self.initial_call = initial_call_detection
211 self.initial_call = initial_call_detection
212 self.authfunc = authfunc
212 self.authfunc = authfunc
213 self._rc_auth_http_code = auth_http_code
213 self._rc_auth_http_code = auth_http_code
214
214
215 def _get_response_from_code(self, http_code):
215 def _get_response_from_code(self, http_code):
216 try:
216 try:
217 return get_exception(safe_int(http_code))
217 return get_exception(safe_int(http_code))
218 except Exception:
218 except Exception:
219 log.exception('Failed to fetch response for code %s' % http_code)
219 log.exception('Failed to fetch response for code %s' % http_code)
220 return HTTPForbidden
220 return HTTPForbidden
221
221
222 def build_authentication(self):
222 def build_authentication(self):
223 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
223 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
224 if self._rc_auth_http_code and not self.initial_call:
224 if self._rc_auth_http_code and not self.initial_call:
225 # return alternative HTTP code if alternative http return code
225 # return alternative HTTP code if alternative http return code
226 # is specified in RhodeCode config, but ONLY if it's not the
226 # is specified in RhodeCode config, but ONLY if it's not the
227 # FIRST call
227 # FIRST call
228 custom_response_klass = self._get_response_from_code(
228 custom_response_klass = self._get_response_from_code(
229 self._rc_auth_http_code)
229 self._rc_auth_http_code)
230 return custom_response_klass(headers=head)
230 return custom_response_klass(headers=head)
231 return HTTPUnauthorized(headers=head)
231 return HTTPUnauthorized(headers=head)
232
232
233 def authenticate(self, environ):
233 def authenticate(self, environ):
234 authorization = AUTHORIZATION(environ)
234 authorization = AUTHORIZATION(environ)
235 if not authorization:
235 if not authorization:
236 return self.build_authentication()
236 return self.build_authentication()
237 (authmeth, auth) = authorization.split(' ', 1)
237 (authmeth, auth) = authorization.split(' ', 1)
238 if 'basic' != authmeth.lower():
238 if 'basic' != authmeth.lower():
239 return self.build_authentication()
239 return self.build_authentication()
240 auth = auth.strip().decode('base64')
240 auth = auth.strip().decode('base64')
241 _parts = auth.split(':', 1)
241 _parts = auth.split(':', 1)
242 if len(_parts) == 2:
242 if len(_parts) == 2:
243 username, password = _parts
243 username, password = _parts
244 if self.authfunc(
244 if self.authfunc(
245 username, password, environ, VCS_TYPE):
245 username, password, environ, VCS_TYPE):
246 return username
246 return username
247 if username and password:
247 if username and password:
248 # we mark that we actually executed authentication once, at
248 # we mark that we actually executed authentication once, at
249 # that point we can use the alternative auth code
249 # that point we can use the alternative auth code
250 self.initial_call = False
250 self.initial_call = False
251
251
252 return self.build_authentication()
252 return self.build_authentication()
253
253
254 __call__ = authenticate
254 __call__ = authenticate
255
255
256
256
257 def attach_context_attributes(context):
257 def attach_context_attributes(context):
258 rc_config = SettingsModel().get_all_settings(cache=True)
258 rc_config = SettingsModel().get_all_settings(cache=True)
259
259
260 context.rhodecode_version = rhodecode.__version__
260 context.rhodecode_version = rhodecode.__version__
261 context.rhodecode_edition = config.get('rhodecode.edition')
261 context.rhodecode_edition = config.get('rhodecode.edition')
262 # unique secret + version does not leak the version but keep consistency
262 # unique secret + version does not leak the version but keep consistency
263 context.rhodecode_version_hash = md5(
263 context.rhodecode_version_hash = md5(
264 config.get('beaker.session.secret', '') +
264 config.get('beaker.session.secret', '') +
265 rhodecode.__version__)[:8]
265 rhodecode.__version__)[:8]
266
266
267 # Default language set for the incoming request
267 # Default language set for the incoming request
268 context.language = translation.get_lang()[0]
268 context.language = translation.get_lang()[0]
269
269
270 # Visual options
270 # Visual options
271 context.visual = AttributeDict({})
271 context.visual = AttributeDict({})
272
272
273 # DB store
273 # DB store
274 context.visual.show_public_icon = str2bool(
274 context.visual.show_public_icon = str2bool(
275 rc_config.get('rhodecode_show_public_icon'))
275 rc_config.get('rhodecode_show_public_icon'))
276 context.visual.show_private_icon = str2bool(
276 context.visual.show_private_icon = str2bool(
277 rc_config.get('rhodecode_show_private_icon'))
277 rc_config.get('rhodecode_show_private_icon'))
278 context.visual.stylify_metatags = str2bool(
278 context.visual.stylify_metatags = str2bool(
279 rc_config.get('rhodecode_stylify_metatags'))
279 rc_config.get('rhodecode_stylify_metatags'))
280 context.visual.dashboard_items = safe_int(
280 context.visual.dashboard_items = safe_int(
281 rc_config.get('rhodecode_dashboard_items', 100))
281 rc_config.get('rhodecode_dashboard_items', 100))
282 context.visual.admin_grid_items = safe_int(
282 context.visual.admin_grid_items = safe_int(
283 rc_config.get('rhodecode_admin_grid_items', 100))
283 rc_config.get('rhodecode_admin_grid_items', 100))
284 context.visual.repository_fields = str2bool(
284 context.visual.repository_fields = str2bool(
285 rc_config.get('rhodecode_repository_fields'))
285 rc_config.get('rhodecode_repository_fields'))
286 context.visual.show_version = str2bool(
286 context.visual.show_version = str2bool(
287 rc_config.get('rhodecode_show_version'))
287 rc_config.get('rhodecode_show_version'))
288 context.visual.use_gravatar = str2bool(
288 context.visual.use_gravatar = str2bool(
289 rc_config.get('rhodecode_use_gravatar'))
289 rc_config.get('rhodecode_use_gravatar'))
290 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
290 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
291 context.visual.default_renderer = rc_config.get(
291 context.visual.default_renderer = rc_config.get(
292 'rhodecode_markup_renderer', 'rst')
292 'rhodecode_markup_renderer', 'rst')
293 context.visual.rhodecode_support_url = \
293 context.visual.rhodecode_support_url = \
294 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
294 rc_config.get('rhodecode_support_url') or url('rhodecode_support')
295
295
296 context.pre_code = rc_config.get('rhodecode_pre_code')
296 context.pre_code = rc_config.get('rhodecode_pre_code')
297 context.post_code = rc_config.get('rhodecode_post_code')
297 context.post_code = rc_config.get('rhodecode_post_code')
298 context.rhodecode_name = rc_config.get('rhodecode_title')
298 context.rhodecode_name = rc_config.get('rhodecode_title')
299 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
299 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
300 # if we have specified default_encoding in the request, it has more
300 # if we have specified default_encoding in the request, it has more
301 # priority
301 # priority
302 if request.GET.get('default_encoding'):
302 if request.GET.get('default_encoding'):
303 context.default_encodings.insert(0, request.GET.get('default_encoding'))
303 context.default_encodings.insert(0, request.GET.get('default_encoding'))
304 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
304 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
305
305
306 # INI stored
306 # INI stored
307 context.labs_active = str2bool(
307 context.labs_active = str2bool(
308 config.get('labs_settings_active', 'false'))
308 config.get('labs_settings_active', 'false'))
309 context.visual.allow_repo_location_change = str2bool(
309 context.visual.allow_repo_location_change = str2bool(
310 config.get('allow_repo_location_change', True))
310 config.get('allow_repo_location_change', True))
311 context.visual.allow_custom_hooks_settings = str2bool(
311 context.visual.allow_custom_hooks_settings = str2bool(
312 config.get('allow_custom_hooks_settings', True))
312 config.get('allow_custom_hooks_settings', True))
313 context.debug_style = str2bool(config.get('debug_style', False))
313 context.debug_style = str2bool(config.get('debug_style', False))
314
314
315 context.rhodecode_instanceid = config.get('instance_id')
315 context.rhodecode_instanceid = config.get('instance_id')
316
316
317 # AppEnlight
317 # AppEnlight
318 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
318 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
319 context.appenlight_api_public_key = config.get(
319 context.appenlight_api_public_key = config.get(
320 'appenlight.api_public_key', '')
320 'appenlight.api_public_key', '')
321 context.appenlight_server_url = config.get('appenlight.server_url', '')
321 context.appenlight_server_url = config.get('appenlight.server_url', '')
322
322
323 # END CONFIG VARS
323 # END CONFIG VARS
324
324
325 # TODO: This dosn't work when called from pylons compatibility tween.
325 # TODO: This dosn't work when called from pylons compatibility tween.
326 # Fix this and remove it from base controller.
326 # Fix this and remove it from base controller.
327 # context.repo_name = get_repo_slug(request) # can be empty
327 # context.repo_name = get_repo_slug(request) # can be empty
328
328
329 context.csrf_token = auth.get_csrf_token()
329 context.csrf_token = auth.get_csrf_token()
330 context.backends = rhodecode.BACKENDS.keys()
330 context.backends = rhodecode.BACKENDS.keys()
331 context.backends.sort()
331 context.backends.sort()
332 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
332 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(
333 context.rhodecode_user.user_id)
333 context.rhodecode_user.user_id)
334
334
335
335
336 def get_auth_user(environ):
336 def get_auth_user(environ):
337 ip_addr = get_ip_addr(environ)
337 ip_addr = get_ip_addr(environ)
338 # make sure that we update permissions each time we call controller
338 # make sure that we update permissions each time we call controller
339 _auth_token = (request.GET.get('auth_token', '') or
339 _auth_token = (request.GET.get('auth_token', '') or
340 request.GET.get('api_key', ''))
340 request.GET.get('api_key', ''))
341
341
342 if _auth_token:
342 if _auth_token:
343 # when using API_KEY we are sure user exists.
343 # when using API_KEY we are sure user exists.
344 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
344 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
345 authenticated = False
345 authenticated = False
346 else:
346 else:
347 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
347 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
348 try:
348 try:
349 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
349 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
350 ip_addr=ip_addr)
350 ip_addr=ip_addr)
351 except UserCreationError as e:
351 except UserCreationError as e:
352 h.flash(e, 'error')
352 h.flash(e, 'error')
353 # container auth or other auth functions that create users
353 # container auth or other auth functions that create users
354 # on the fly can throw this exception signaling that there's
354 # on the fly can throw this exception signaling that there's
355 # issue with user creation, explanation should be provided
355 # issue with user creation, explanation should be provided
356 # in Exception itself. We then create a simple blank
356 # in Exception itself. We then create a simple blank
357 # AuthUser
357 # AuthUser
358 auth_user = AuthUser(ip_addr=ip_addr)
358 auth_user = AuthUser(ip_addr=ip_addr)
359
359
360 if password_changed(auth_user, session):
360 if password_changed(auth_user, session):
361 session.invalidate()
361 session.invalidate()
362 cookie_store = CookieStoreWrapper(
362 cookie_store = CookieStoreWrapper(
363 session.get('rhodecode_user'))
363 session.get('rhodecode_user'))
364 auth_user = AuthUser(ip_addr=ip_addr)
364 auth_user = AuthUser(ip_addr=ip_addr)
365
365
366 authenticated = cookie_store.get('is_authenticated')
366 authenticated = cookie_store.get('is_authenticated')
367
367
368 if not auth_user.is_authenticated and auth_user.is_user_object:
368 if not auth_user.is_authenticated and auth_user.is_user_object:
369 # user is not authenticated and not empty
369 # user is not authenticated and not empty
370 auth_user.set_authenticated(authenticated)
370 auth_user.set_authenticated(authenticated)
371
371
372 return auth_user
372 return auth_user
373
373
374
374
375 class BaseController(WSGIController):
375 class BaseController(WSGIController):
376
376
377 def __before__(self):
377 def __before__(self):
378 """
378 """
379 __before__ is called before controller methods and after __call__
379 __before__ is called before controller methods and after __call__
380 """
380 """
381 # on each call propagate settings calls into global settings.
381 # on each call propagate settings calls into global settings.
382 set_rhodecode_config(config)
382 set_rhodecode_config(config)
383 attach_context_attributes(c)
383 attach_context_attributes(c)
384
384
385 # TODO: Remove this when fixed in attach_context_attributes()
385 # TODO: Remove this when fixed in attach_context_attributes()
386 c.repo_name = get_repo_slug(request) # can be empty
386 c.repo_name = get_repo_slug(request) # can be empty
387
387
388 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
388 self.cut_off_limit_diff = safe_int(config.get('cut_off_limit_diff'))
389 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
389 self.cut_off_limit_file = safe_int(config.get('cut_off_limit_file'))
390 self.sa = meta.Session
390 self.sa = meta.Session
391 self.scm_model = ScmModel(self.sa)
391 self.scm_model = ScmModel(self.sa)
392
392
393 default_lang = c.language
393 default_lang = c.language
394 user_lang = c.language
394 user_lang = c.language
395 try:
395 try:
396 user_obj = self._rhodecode_user.get_instance()
396 user_obj = self._rhodecode_user.get_instance()
397 if user_obj:
397 if user_obj:
398 user_lang = user_obj.user_data.get('language')
398 user_lang = user_obj.user_data.get('language')
399 except Exception:
399 except Exception:
400 log.exception('Failed to fetch user language for user %s',
400 log.exception('Failed to fetch user language for user %s',
401 self._rhodecode_user)
401 self._rhodecode_user)
402
402
403 if user_lang and user_lang != default_lang:
403 if user_lang and user_lang != default_lang:
404 log.debug('set language to %s for user %s', user_lang,
404 log.debug('set language to %s for user %s', user_lang,
405 self._rhodecode_user)
405 self._rhodecode_user)
406 translation.set_lang(user_lang)
406 translation.set_lang(user_lang)
407
407
408 def _dispatch_redirect(self, with_url, environ, start_response):
408 def _dispatch_redirect(self, with_url, environ, start_response):
409 resp = HTTPFound(with_url)
409 resp = HTTPFound(with_url)
410 environ['SCRIPT_NAME'] = '' # handle prefix middleware
410 environ['SCRIPT_NAME'] = '' # handle prefix middleware
411 environ['PATH_INFO'] = with_url
411 environ['PATH_INFO'] = with_url
412 return resp(environ, start_response)
412 return resp(environ, start_response)
413
413
414 def __call__(self, environ, start_response):
414 def __call__(self, environ, start_response):
415 """Invoke the Controller"""
415 """Invoke the Controller"""
416 # WSGIController.__call__ dispatches to the Controller method
416 # WSGIController.__call__ dispatches to the Controller method
417 # the request is routed to. This routing information is
417 # the request is routed to. This routing information is
418 # available in environ['pylons.routes_dict']
418 # available in environ['pylons.routes_dict']
419 from rhodecode.lib import helpers as h
419 from rhodecode.lib import helpers as h
420
420
421 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
421 # Provide the Pylons context to Pyramid's debugtoolbar if it asks
422 if environ.get('debugtoolbar.wants_pylons_context', False):
422 if environ.get('debugtoolbar.wants_pylons_context', False):
423 environ['debugtoolbar.pylons_context'] = c._current_obj()
423 environ['debugtoolbar.pylons_context'] = c._current_obj()
424
424
425 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
425 _route_name = '.'.join([environ['pylons.routes_dict']['controller'],
426 environ['pylons.routes_dict']['action']])
426 environ['pylons.routes_dict']['action']])
427
427
428 c.pylons_dispatch_info = {
429 'controller': environ['pylons.routes_dict']['controller'],
430 'action': environ['pylons.routes_dict']['action'],
431 'extra': {'plugins': {}}
432 }
433
428 self.rc_config = SettingsModel().get_all_settings(cache=True)
434 self.rc_config = SettingsModel().get_all_settings(cache=True)
429 self.ip_addr = get_ip_addr(environ)
435 self.ip_addr = get_ip_addr(environ)
430
436
431 # The rhodecode auth user is looked up and passed through the
437 # The rhodecode auth user is looked up and passed through the
432 # environ by the pylons compatibility tween in pyramid.
438 # environ by the pylons compatibility tween in pyramid.
433 # So we can just grab it from there.
439 # So we can just grab it from there.
434 auth_user = environ['rc_auth_user']
440 auth_user = environ['rc_auth_user']
435
441
436 # set globals for auth user
442 # set globals for auth user
437 request.user = auth_user
443 request.user = auth_user
438 c.rhodecode_user = self._rhodecode_user = auth_user
444 c.rhodecode_user = self._rhodecode_user = auth_user
439
445
440 log.info('IP: %s User: %s accessed %s [%s]' % (
446 log.info('IP: %s User: %s accessed %s [%s]' % (
441 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
447 self.ip_addr, auth_user, safe_unicode(get_access_path(environ)),
442 _route_name)
448 _route_name)
443 )
449 )
444
450
445 # TODO: Maybe this should be move to pyramid to cover all views.
451 # TODO: Maybe this should be move to pyramid to cover all views.
446 # check user attributes for password change flag
452 # check user attributes for password change flag
447 user_obj = auth_user.get_instance()
453 user_obj = auth_user.get_instance()
448 if user_obj and user_obj.user_data.get('force_password_change'):
454 if user_obj and user_obj.user_data.get('force_password_change'):
449 h.flash('You are required to change your password', 'warning',
455 h.flash('You are required to change your password', 'warning',
450 ignore_duplicate=True)
456 ignore_duplicate=True)
451
457
452 skip_user_check_urls = [
458 skip_user_check_urls = [
453 'error.document', 'login.logout', 'login.index',
459 'error.document', 'login.logout', 'login.index',
454 'admin/my_account.my_account_password',
460 'admin/my_account.my_account_password',
455 'admin/my_account.my_account_password_update'
461 'admin/my_account.my_account_password_update'
456 ]
462 ]
457 if _route_name not in skip_user_check_urls:
463 if _route_name not in skip_user_check_urls:
458 return self._dispatch_redirect(
464 return self._dispatch_redirect(
459 url('my_account_password'), environ, start_response)
465 url('my_account_password'), environ, start_response)
460
466
461 return WSGIController.__call__(self, environ, start_response)
467 return WSGIController.__call__(self, environ, start_response)
462
468
463
469
464 class BaseRepoController(BaseController):
470 class BaseRepoController(BaseController):
465 """
471 """
466 Base class for controllers responsible for loading all needed data for
472 Base class for controllers responsible for loading all needed data for
467 repository loaded items are
473 repository loaded items are
468
474
469 c.rhodecode_repo: instance of scm repository
475 c.rhodecode_repo: instance of scm repository
470 c.rhodecode_db_repo: instance of db
476 c.rhodecode_db_repo: instance of db
471 c.repository_requirements_missing: shows that repository specific data
477 c.repository_requirements_missing: shows that repository specific data
472 could not be displayed due to the missing requirements
478 could not be displayed due to the missing requirements
473 c.repository_pull_requests: show number of open pull requests
479 c.repository_pull_requests: show number of open pull requests
474 """
480 """
475
481
476 def __before__(self):
482 def __before__(self):
477 super(BaseRepoController, self).__before__()
483 super(BaseRepoController, self).__before__()
478 if c.repo_name: # extracted from routes
484 if c.repo_name: # extracted from routes
479 db_repo = Repository.get_by_repo_name(c.repo_name)
485 db_repo = Repository.get_by_repo_name(c.repo_name)
480 if not db_repo:
486 if not db_repo:
481 return
487 return
482
488
483 log.debug(
489 log.debug(
484 'Found repository in database %s with state `%s`',
490 'Found repository in database %s with state `%s`',
485 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
491 safe_unicode(db_repo), safe_unicode(db_repo.repo_state))
486 route = getattr(request.environ.get('routes.route'), 'name', '')
492 route = getattr(request.environ.get('routes.route'), 'name', '')
487
493
488 # allow to delete repos that are somehow damages in filesystem
494 # allow to delete repos that are somehow damages in filesystem
489 if route in ['delete_repo']:
495 if route in ['delete_repo']:
490 return
496 return
491
497
492 if db_repo.repo_state in [Repository.STATE_PENDING]:
498 if db_repo.repo_state in [Repository.STATE_PENDING]:
493 if route in ['repo_creating_home']:
499 if route in ['repo_creating_home']:
494 return
500 return
495 check_url = url('repo_creating_home', repo_name=c.repo_name)
501 check_url = url('repo_creating_home', repo_name=c.repo_name)
496 return redirect(check_url)
502 return redirect(check_url)
497
503
498 self.rhodecode_db_repo = db_repo
504 self.rhodecode_db_repo = db_repo
499
505
500 missing_requirements = False
506 missing_requirements = False
501 try:
507 try:
502 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
508 self.rhodecode_repo = self.rhodecode_db_repo.scm_instance()
503 except RepositoryRequirementError as e:
509 except RepositoryRequirementError as e:
504 missing_requirements = True
510 missing_requirements = True
505 self._handle_missing_requirements(e)
511 self._handle_missing_requirements(e)
506
512
507 if self.rhodecode_repo is None and not missing_requirements:
513 if self.rhodecode_repo is None and not missing_requirements:
508 log.error('%s this repository is present in database but it '
514 log.error('%s this repository is present in database but it '
509 'cannot be created as an scm instance', c.repo_name)
515 'cannot be created as an scm instance', c.repo_name)
510
516
511 h.flash(_(
517 h.flash(_(
512 "The repository at %(repo_name)s cannot be located.") %
518 "The repository at %(repo_name)s cannot be located.") %
513 {'repo_name': c.repo_name},
519 {'repo_name': c.repo_name},
514 category='error', ignore_duplicate=True)
520 category='error', ignore_duplicate=True)
515 redirect(url('home'))
521 redirect(url('home'))
516
522
517 # update last change according to VCS data
523 # update last change according to VCS data
518 if not missing_requirements:
524 if not missing_requirements:
519 commit = db_repo.get_commit(
525 commit = db_repo.get_commit(
520 pre_load=["author", "date", "message", "parents"])
526 pre_load=["author", "date", "message", "parents"])
521 db_repo.update_commit_cache(commit)
527 db_repo.update_commit_cache(commit)
522
528
523 # Prepare context
529 # Prepare context
524 c.rhodecode_db_repo = db_repo
530 c.rhodecode_db_repo = db_repo
525 c.rhodecode_repo = self.rhodecode_repo
531 c.rhodecode_repo = self.rhodecode_repo
526 c.repository_requirements_missing = missing_requirements
532 c.repository_requirements_missing = missing_requirements
527
533
528 self._update_global_counters(self.scm_model, db_repo)
534 self._update_global_counters(self.scm_model, db_repo)
529
535
530 def _update_global_counters(self, scm_model, db_repo):
536 def _update_global_counters(self, scm_model, db_repo):
531 """
537 """
532 Base variables that are exposed to every page of repository
538 Base variables that are exposed to every page of repository
533 """
539 """
534 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
540 c.repository_pull_requests = scm_model.get_pull_requests(db_repo)
535
541
536 def _handle_missing_requirements(self, error):
542 def _handle_missing_requirements(self, error):
537 self.rhodecode_repo = None
543 self.rhodecode_repo = None
538 log.error(
544 log.error(
539 'Requirements are missing for repository %s: %s',
545 'Requirements are missing for repository %s: %s',
540 c.repo_name, error.message)
546 c.repo_name, error.message)
541
547
542 summary_url = url('summary_home', repo_name=c.repo_name)
548 summary_url = url('summary_home', repo_name=c.repo_name)
543 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
549 statistics_url = url('edit_repo_statistics', repo_name=c.repo_name)
544 settings_update_url = url('repo', repo_name=c.repo_name)
550 settings_update_url = url('repo', repo_name=c.repo_name)
545 path = request.path
551 path = request.path
546 should_redirect = (
552 should_redirect = (
547 path not in (summary_url, settings_update_url)
553 path not in (summary_url, settings_update_url)
548 and '/settings' not in path or path == statistics_url
554 and '/settings' not in path or path == statistics_url
549 )
555 )
550 if should_redirect:
556 if should_redirect:
551 redirect(summary_url)
557 redirect(summary_url)
@@ -1,1897 +1,1898 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
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
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
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/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27
27
28 import random
28 import random
29 import hashlib
29 import hashlib
30 import StringIO
30 import StringIO
31 import urllib
31 import urllib
32 import math
32 import math
33 import logging
33 import logging
34 import re
34 import re
35 import urlparse
35 import urlparse
36 import time
36 import time
37 import string
37 import string
38 import hashlib
38 import hashlib
39 import pygments
39 import pygments
40
40
41 from datetime import datetime
41 from datetime import datetime
42 from functools import partial
42 from functools import partial
43 from pygments.formatters.html import HtmlFormatter
43 from pygments.formatters.html import HtmlFormatter
44 from pygments import highlight as code_highlight
44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
47 from pylons import url
47 from pylons import url
48 from pylons.i18n.translation import _, ungettext
48 from pylons.i18n.translation import _, ungettext
49 from pyramid.threadlocal import get_current_request
49 from pyramid.threadlocal import get_current_request
50
50
51 from webhelpers.html import literal, HTML, escape
51 from webhelpers.html import literal, HTML, escape
52 from webhelpers.html.tools import *
52 from webhelpers.html.tools import *
53 from webhelpers.html.builder import make_tag
53 from webhelpers.html.builder import make_tag
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
54 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
55 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
56 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
57 submit, text, password, textarea, title, ul, xml_declaration, radio
57 submit, text, password, textarea, title, ul, xml_declaration, radio
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
58 from webhelpers.html.tools import auto_link, button_to, highlight, \
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
59 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
60 from webhelpers.pylonslib import Flash as _Flash
60 from webhelpers.pylonslib import Flash as _Flash
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
61 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
62 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
63 replace_whitespace, urlify, truncate, wrap_paragraphs
63 replace_whitespace, urlify, truncate, wrap_paragraphs
64 from webhelpers.date import time_ago_in_words
64 from webhelpers.date import time_ago_in_words
65 from webhelpers.paginate import Page as _Page
65 from webhelpers.paginate import Page as _Page
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
66 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
67 convert_boolean_attrs, NotGiven, _make_safe_id_component
68 from webhelpers2.number import format_byte_size
68 from webhelpers2.number import format_byte_size
69
69
70 from rhodecode.lib.annotate import annotate_highlight
70 from rhodecode.lib.annotate import annotate_highlight
71 from rhodecode.lib.action_parser import action_parser
71 from rhodecode.lib.action_parser import action_parser
72 from rhodecode.lib.ext_json import json
72 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
73 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
73 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
74 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
74 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
75 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
75 AttributeDict, safe_int, md5, md5_safe
76 AttributeDict, safe_int, md5, md5_safe
76 from rhodecode.lib.markup_renderer import MarkupRenderer
77 from rhodecode.lib.markup_renderer import MarkupRenderer
77 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
78 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
78 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
79 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
79 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
80 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
80 from rhodecode.model.changeset_status import ChangesetStatusModel
81 from rhodecode.model.changeset_status import ChangesetStatusModel
81 from rhodecode.model.db import Permission, User, Repository
82 from rhodecode.model.db import Permission, User, Repository
82 from rhodecode.model.repo_group import RepoGroupModel
83 from rhodecode.model.repo_group import RepoGroupModel
83 from rhodecode.model.settings import IssueTrackerSettingsModel
84 from rhodecode.model.settings import IssueTrackerSettingsModel
84
85
85 log = logging.getLogger(__name__)
86 log = logging.getLogger(__name__)
86
87
87 DEFAULT_USER = User.DEFAULT_USER
88 DEFAULT_USER = User.DEFAULT_USER
88 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
89 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
89
90
90
91
91 def html_escape(text, html_escape_table=None):
92 def html_escape(text, html_escape_table=None):
92 """Produce entities within text."""
93 """Produce entities within text."""
93 if not html_escape_table:
94 if not html_escape_table:
94 html_escape_table = {
95 html_escape_table = {
95 "&": "&amp;",
96 "&": "&amp;",
96 '"': "&quot;",
97 '"': "&quot;",
97 "'": "&apos;",
98 "'": "&apos;",
98 ">": "&gt;",
99 ">": "&gt;",
99 "<": "&lt;",
100 "<": "&lt;",
100 }
101 }
101 return "".join(html_escape_table.get(c, c) for c in text)
102 return "".join(html_escape_table.get(c, c) for c in text)
102
103
103
104
104 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
105 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
105 """
106 """
106 Truncate string ``s`` at the first occurrence of ``sub``.
107 Truncate string ``s`` at the first occurrence of ``sub``.
107
108
108 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
109 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
109 """
110 """
110 suffix_if_chopped = suffix_if_chopped or ''
111 suffix_if_chopped = suffix_if_chopped or ''
111 pos = s.find(sub)
112 pos = s.find(sub)
112 if pos == -1:
113 if pos == -1:
113 return s
114 return s
114
115
115 if inclusive:
116 if inclusive:
116 pos += len(sub)
117 pos += len(sub)
117
118
118 chopped = s[:pos]
119 chopped = s[:pos]
119 left = s[pos:].strip()
120 left = s[pos:].strip()
120
121
121 if left and suffix_if_chopped:
122 if left and suffix_if_chopped:
122 chopped += suffix_if_chopped
123 chopped += suffix_if_chopped
123
124
124 return chopped
125 return chopped
125
126
126
127
127 def shorter(text, size=20):
128 def shorter(text, size=20):
128 postfix = '...'
129 postfix = '...'
129 if len(text) > size:
130 if len(text) > size:
130 return text[:size - len(postfix)] + postfix
131 return text[:size - len(postfix)] + postfix
131 return text
132 return text
132
133
133
134
134 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
135 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
135 """
136 """
136 Reset button
137 Reset button
137 """
138 """
138 _set_input_attrs(attrs, type, name, value)
139 _set_input_attrs(attrs, type, name, value)
139 _set_id_attr(attrs, id, name)
140 _set_id_attr(attrs, id, name)
140 convert_boolean_attrs(attrs, ["disabled"])
141 convert_boolean_attrs(attrs, ["disabled"])
141 return HTML.input(**attrs)
142 return HTML.input(**attrs)
142
143
143 reset = _reset
144 reset = _reset
144 safeid = _make_safe_id_component
145 safeid = _make_safe_id_component
145
146
146
147
147 def branding(name, length=40):
148 def branding(name, length=40):
148 return truncate(name, length, indicator="")
149 return truncate(name, length, indicator="")
149
150
150
151
151 def FID(raw_id, path):
152 def FID(raw_id, path):
152 """
153 """
153 Creates a unique ID for filenode based on it's hash of path and commit
154 Creates a unique ID for filenode based on it's hash of path and commit
154 it's safe to use in urls
155 it's safe to use in urls
155
156
156 :param raw_id:
157 :param raw_id:
157 :param path:
158 :param path:
158 """
159 """
159
160
160 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
161 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
161
162
162
163
163 class _GetError(object):
164 class _GetError(object):
164 """Get error from form_errors, and represent it as span wrapped error
165 """Get error from form_errors, and represent it as span wrapped error
165 message
166 message
166
167
167 :param field_name: field to fetch errors for
168 :param field_name: field to fetch errors for
168 :param form_errors: form errors dict
169 :param form_errors: form errors dict
169 """
170 """
170
171
171 def __call__(self, field_name, form_errors):
172 def __call__(self, field_name, form_errors):
172 tmpl = """<span class="error_msg">%s</span>"""
173 tmpl = """<span class="error_msg">%s</span>"""
173 if form_errors and field_name in form_errors:
174 if form_errors and field_name in form_errors:
174 return literal(tmpl % form_errors.get(field_name))
175 return literal(tmpl % form_errors.get(field_name))
175
176
176 get_error = _GetError()
177 get_error = _GetError()
177
178
178
179
179 class _ToolTip(object):
180 class _ToolTip(object):
180
181
181 def __call__(self, tooltip_title, trim_at=50):
182 def __call__(self, tooltip_title, trim_at=50):
182 """
183 """
183 Special function just to wrap our text into nice formatted
184 Special function just to wrap our text into nice formatted
184 autowrapped text
185 autowrapped text
185
186
186 :param tooltip_title:
187 :param tooltip_title:
187 """
188 """
188 tooltip_title = escape(tooltip_title)
189 tooltip_title = escape(tooltip_title)
189 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
190 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
190 return tooltip_title
191 return tooltip_title
191 tooltip = _ToolTip()
192 tooltip = _ToolTip()
192
193
193
194
194 def files_breadcrumbs(repo_name, commit_id, file_path):
195 def files_breadcrumbs(repo_name, commit_id, file_path):
195 if isinstance(file_path, str):
196 if isinstance(file_path, str):
196 file_path = safe_unicode(file_path)
197 file_path = safe_unicode(file_path)
197
198
198 # TODO: johbo: Is this always a url like path, or is this operating
199 # TODO: johbo: Is this always a url like path, or is this operating
199 # system dependent?
200 # system dependent?
200 path_segments = file_path.split('/')
201 path_segments = file_path.split('/')
201
202
202 repo_name_html = escape(repo_name)
203 repo_name_html = escape(repo_name)
203 if len(path_segments) == 1 and path_segments[0] == '':
204 if len(path_segments) == 1 and path_segments[0] == '':
204 url_segments = [repo_name_html]
205 url_segments = [repo_name_html]
205 else:
206 else:
206 url_segments = [
207 url_segments = [
207 link_to(
208 link_to(
208 repo_name_html,
209 repo_name_html,
209 url('files_home',
210 url('files_home',
210 repo_name=repo_name,
211 repo_name=repo_name,
211 revision=commit_id,
212 revision=commit_id,
212 f_path=''),
213 f_path=''),
213 class_='pjax-link')]
214 class_='pjax-link')]
214
215
215 last_cnt = len(path_segments) - 1
216 last_cnt = len(path_segments) - 1
216 for cnt, segment in enumerate(path_segments):
217 for cnt, segment in enumerate(path_segments):
217 if not segment:
218 if not segment:
218 continue
219 continue
219 segment_html = escape(segment)
220 segment_html = escape(segment)
220
221
221 if cnt != last_cnt:
222 if cnt != last_cnt:
222 url_segments.append(
223 url_segments.append(
223 link_to(
224 link_to(
224 segment_html,
225 segment_html,
225 url('files_home',
226 url('files_home',
226 repo_name=repo_name,
227 repo_name=repo_name,
227 revision=commit_id,
228 revision=commit_id,
228 f_path='/'.join(path_segments[:cnt + 1])),
229 f_path='/'.join(path_segments[:cnt + 1])),
229 class_='pjax-link'))
230 class_='pjax-link'))
230 else:
231 else:
231 url_segments.append(segment_html)
232 url_segments.append(segment_html)
232
233
233 return literal('/'.join(url_segments))
234 return literal('/'.join(url_segments))
234
235
235
236
236 class CodeHtmlFormatter(HtmlFormatter):
237 class CodeHtmlFormatter(HtmlFormatter):
237 """
238 """
238 My code Html Formatter for source codes
239 My code Html Formatter for source codes
239 """
240 """
240
241
241 def wrap(self, source, outfile):
242 def wrap(self, source, outfile):
242 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
243 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
243
244
244 def _wrap_code(self, source):
245 def _wrap_code(self, source):
245 for cnt, it in enumerate(source):
246 for cnt, it in enumerate(source):
246 i, t = it
247 i, t = it
247 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
248 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
248 yield i, t
249 yield i, t
249
250
250 def _wrap_tablelinenos(self, inner):
251 def _wrap_tablelinenos(self, inner):
251 dummyoutfile = StringIO.StringIO()
252 dummyoutfile = StringIO.StringIO()
252 lncount = 0
253 lncount = 0
253 for t, line in inner:
254 for t, line in inner:
254 if t:
255 if t:
255 lncount += 1
256 lncount += 1
256 dummyoutfile.write(line)
257 dummyoutfile.write(line)
257
258
258 fl = self.linenostart
259 fl = self.linenostart
259 mw = len(str(lncount + fl - 1))
260 mw = len(str(lncount + fl - 1))
260 sp = self.linenospecial
261 sp = self.linenospecial
261 st = self.linenostep
262 st = self.linenostep
262 la = self.lineanchors
263 la = self.lineanchors
263 aln = self.anchorlinenos
264 aln = self.anchorlinenos
264 nocls = self.noclasses
265 nocls = self.noclasses
265 if sp:
266 if sp:
266 lines = []
267 lines = []
267
268
268 for i in range(fl, fl + lncount):
269 for i in range(fl, fl + lncount):
269 if i % st == 0:
270 if i % st == 0:
270 if i % sp == 0:
271 if i % sp == 0:
271 if aln:
272 if aln:
272 lines.append('<a href="#%s%d" class="special">%*d</a>' %
273 lines.append('<a href="#%s%d" class="special">%*d</a>' %
273 (la, i, mw, i))
274 (la, i, mw, i))
274 else:
275 else:
275 lines.append('<span class="special">%*d</span>' % (mw, i))
276 lines.append('<span class="special">%*d</span>' % (mw, i))
276 else:
277 else:
277 if aln:
278 if aln:
278 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
279 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
279 else:
280 else:
280 lines.append('%*d' % (mw, i))
281 lines.append('%*d' % (mw, i))
281 else:
282 else:
282 lines.append('')
283 lines.append('')
283 ls = '\n'.join(lines)
284 ls = '\n'.join(lines)
284 else:
285 else:
285 lines = []
286 lines = []
286 for i in range(fl, fl + lncount):
287 for i in range(fl, fl + lncount):
287 if i % st == 0:
288 if i % st == 0:
288 if aln:
289 if aln:
289 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
290 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
290 else:
291 else:
291 lines.append('%*d' % (mw, i))
292 lines.append('%*d' % (mw, i))
292 else:
293 else:
293 lines.append('')
294 lines.append('')
294 ls = '\n'.join(lines)
295 ls = '\n'.join(lines)
295
296
296 # in case you wonder about the seemingly redundant <div> here: since the
297 # in case you wonder about the seemingly redundant <div> here: since the
297 # content in the other cell also is wrapped in a div, some browsers in
298 # content in the other cell also is wrapped in a div, some browsers in
298 # some configurations seem to mess up the formatting...
299 # some configurations seem to mess up the formatting...
299 if nocls:
300 if nocls:
300 yield 0, ('<table class="%stable">' % self.cssclass +
301 yield 0, ('<table class="%stable">' % self.cssclass +
301 '<tr><td><div class="linenodiv" '
302 '<tr><td><div class="linenodiv" '
302 'style="background-color: #f0f0f0; padding-right: 10px">'
303 'style="background-color: #f0f0f0; padding-right: 10px">'
303 '<pre style="line-height: 125%">' +
304 '<pre style="line-height: 125%">' +
304 ls + '</pre></div></td><td id="hlcode" class="code">')
305 ls + '</pre></div></td><td id="hlcode" class="code">')
305 else:
306 else:
306 yield 0, ('<table class="%stable">' % self.cssclass +
307 yield 0, ('<table class="%stable">' % self.cssclass +
307 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
308 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
308 ls + '</pre></div></td><td id="hlcode" class="code">')
309 ls + '</pre></div></td><td id="hlcode" class="code">')
309 yield 0, dummyoutfile.getvalue()
310 yield 0, dummyoutfile.getvalue()
310 yield 0, '</td></tr></table>'
311 yield 0, '</td></tr></table>'
311
312
312
313
313 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
314 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
314 def __init__(self, **kw):
315 def __init__(self, **kw):
315 # only show these line numbers if set
316 # only show these line numbers if set
316 self.only_lines = kw.pop('only_line_numbers', [])
317 self.only_lines = kw.pop('only_line_numbers', [])
317 self.query_terms = kw.pop('query_terms', [])
318 self.query_terms = kw.pop('query_terms', [])
318 self.max_lines = kw.pop('max_lines', 5)
319 self.max_lines = kw.pop('max_lines', 5)
319 self.line_context = kw.pop('line_context', 3)
320 self.line_context = kw.pop('line_context', 3)
320 self.url = kw.pop('url', None)
321 self.url = kw.pop('url', None)
321
322
322 super(CodeHtmlFormatter, self).__init__(**kw)
323 super(CodeHtmlFormatter, self).__init__(**kw)
323
324
324 def _wrap_code(self, source):
325 def _wrap_code(self, source):
325 for cnt, it in enumerate(source):
326 for cnt, it in enumerate(source):
326 i, t = it
327 i, t = it
327 t = '<pre>%s</pre>' % t
328 t = '<pre>%s</pre>' % t
328 yield i, t
329 yield i, t
329
330
330 def _wrap_tablelinenos(self, inner):
331 def _wrap_tablelinenos(self, inner):
331 yield 0, '<table class="code-highlight %stable">' % self.cssclass
332 yield 0, '<table class="code-highlight %stable">' % self.cssclass
332
333
333 last_shown_line_number = 0
334 last_shown_line_number = 0
334 current_line_number = 1
335 current_line_number = 1
335
336
336 for t, line in inner:
337 for t, line in inner:
337 if not t:
338 if not t:
338 yield t, line
339 yield t, line
339 continue
340 continue
340
341
341 if current_line_number in self.only_lines:
342 if current_line_number in self.only_lines:
342 if last_shown_line_number + 1 != current_line_number:
343 if last_shown_line_number + 1 != current_line_number:
343 yield 0, '<tr>'
344 yield 0, '<tr>'
344 yield 0, '<td class="line">...</td>'
345 yield 0, '<td class="line">...</td>'
345 yield 0, '<td id="hlcode" class="code"></td>'
346 yield 0, '<td id="hlcode" class="code"></td>'
346 yield 0, '</tr>'
347 yield 0, '</tr>'
347
348
348 yield 0, '<tr>'
349 yield 0, '<tr>'
349 if self.url:
350 if self.url:
350 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
351 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
351 self.url, current_line_number, current_line_number)
352 self.url, current_line_number, current_line_number)
352 else:
353 else:
353 yield 0, '<td class="line"><a href="">%i</a></td>' % (
354 yield 0, '<td class="line"><a href="">%i</a></td>' % (
354 current_line_number)
355 current_line_number)
355 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
356 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
356 yield 0, '</tr>'
357 yield 0, '</tr>'
357
358
358 last_shown_line_number = current_line_number
359 last_shown_line_number = current_line_number
359
360
360 current_line_number += 1
361 current_line_number += 1
361
362
362
363
363 yield 0, '</table>'
364 yield 0, '</table>'
364
365
365
366
366 def extract_phrases(text_query):
367 def extract_phrases(text_query):
367 """
368 """
368 Extracts phrases from search term string making sure phrases
369 Extracts phrases from search term string making sure phrases
369 contained in double quotes are kept together - and discarding empty values
370 contained in double quotes are kept together - and discarding empty values
370 or fully whitespace values eg.
371 or fully whitespace values eg.
371
372
372 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
373 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
373
374
374 """
375 """
375
376
376 in_phrase = False
377 in_phrase = False
377 buf = ''
378 buf = ''
378 phrases = []
379 phrases = []
379 for char in text_query:
380 for char in text_query:
380 if in_phrase:
381 if in_phrase:
381 if char == '"': # end phrase
382 if char == '"': # end phrase
382 phrases.append(buf)
383 phrases.append(buf)
383 buf = ''
384 buf = ''
384 in_phrase = False
385 in_phrase = False
385 continue
386 continue
386 else:
387 else:
387 buf += char
388 buf += char
388 continue
389 continue
389 else:
390 else:
390 if char == '"': # start phrase
391 if char == '"': # start phrase
391 in_phrase = True
392 in_phrase = True
392 phrases.append(buf)
393 phrases.append(buf)
393 buf = ''
394 buf = ''
394 continue
395 continue
395 elif char == ' ':
396 elif char == ' ':
396 phrases.append(buf)
397 phrases.append(buf)
397 buf = ''
398 buf = ''
398 continue
399 continue
399 else:
400 else:
400 buf += char
401 buf += char
401
402
402 phrases.append(buf)
403 phrases.append(buf)
403 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
404 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
404 return phrases
405 return phrases
405
406
406
407
407 def get_matching_offsets(text, phrases):
408 def get_matching_offsets(text, phrases):
408 """
409 """
409 Returns a list of string offsets in `text` that the list of `terms` match
410 Returns a list of string offsets in `text` that the list of `terms` match
410
411
411 >>> get_matching_offsets('some text here', ['some', 'here'])
412 >>> get_matching_offsets('some text here', ['some', 'here'])
412 [(0, 4), (10, 14)]
413 [(0, 4), (10, 14)]
413
414
414 """
415 """
415 offsets = []
416 offsets = []
416 for phrase in phrases:
417 for phrase in phrases:
417 for match in re.finditer(phrase, text):
418 for match in re.finditer(phrase, text):
418 offsets.append((match.start(), match.end()))
419 offsets.append((match.start(), match.end()))
419
420
420 return offsets
421 return offsets
421
422
422
423
423 def normalize_text_for_matching(x):
424 def normalize_text_for_matching(x):
424 """
425 """
425 Replaces all non alnum characters to spaces and lower cases the string,
426 Replaces all non alnum characters to spaces and lower cases the string,
426 useful for comparing two text strings without punctuation
427 useful for comparing two text strings without punctuation
427 """
428 """
428 return re.sub(r'[^\w]', ' ', x.lower())
429 return re.sub(r'[^\w]', ' ', x.lower())
429
430
430
431
431 def get_matching_line_offsets(lines, terms):
432 def get_matching_line_offsets(lines, terms):
432 """ Return a set of `lines` indices (starting from 1) matching a
433 """ Return a set of `lines` indices (starting from 1) matching a
433 text search query, along with `context` lines above/below matching lines
434 text search query, along with `context` lines above/below matching lines
434
435
435 :param lines: list of strings representing lines
436 :param lines: list of strings representing lines
436 :param terms: search term string to match in lines eg. 'some text'
437 :param terms: search term string to match in lines eg. 'some text'
437 :param context: number of lines above/below a matching line to add to result
438 :param context: number of lines above/below a matching line to add to result
438 :param max_lines: cut off for lines of interest
439 :param max_lines: cut off for lines of interest
439 eg.
440 eg.
440
441
441 text = '''
442 text = '''
442 words words words
443 words words words
443 words words words
444 words words words
444 some text some
445 some text some
445 words words words
446 words words words
446 words words words
447 words words words
447 text here what
448 text here what
448 '''
449 '''
449 get_matching_line_offsets(text, 'text', context=1)
450 get_matching_line_offsets(text, 'text', context=1)
450 {3: [(5, 9)], 6: [(0, 4)]]
451 {3: [(5, 9)], 6: [(0, 4)]]
451
452
452 """
453 """
453 matching_lines = {}
454 matching_lines = {}
454 phrases = [normalize_text_for_matching(phrase)
455 phrases = [normalize_text_for_matching(phrase)
455 for phrase in extract_phrases(terms)]
456 for phrase in extract_phrases(terms)]
456
457
457 for line_index, line in enumerate(lines, start=1):
458 for line_index, line in enumerate(lines, start=1):
458 match_offsets = get_matching_offsets(
459 match_offsets = get_matching_offsets(
459 normalize_text_for_matching(line), phrases)
460 normalize_text_for_matching(line), phrases)
460 if match_offsets:
461 if match_offsets:
461 matching_lines[line_index] = match_offsets
462 matching_lines[line_index] = match_offsets
462
463
463 return matching_lines
464 return matching_lines
464
465
465
466
466 def get_lexer_safe(mimetype=None, filepath=None):
467 def get_lexer_safe(mimetype=None, filepath=None):
467 """
468 """
468 Tries to return a relevant pygments lexer using mimetype/filepath name,
469 Tries to return a relevant pygments lexer using mimetype/filepath name,
469 defaulting to plain text if none could be found
470 defaulting to plain text if none could be found
470 """
471 """
471 lexer = None
472 lexer = None
472 try:
473 try:
473 if mimetype:
474 if mimetype:
474 lexer = get_lexer_for_mimetype(mimetype)
475 lexer = get_lexer_for_mimetype(mimetype)
475 if not lexer:
476 if not lexer:
476 lexer = get_lexer_for_filename(filepath)
477 lexer = get_lexer_for_filename(filepath)
477 except pygments.util.ClassNotFound:
478 except pygments.util.ClassNotFound:
478 pass
479 pass
479
480
480 if not lexer:
481 if not lexer:
481 lexer = get_lexer_by_name('text')
482 lexer = get_lexer_by_name('text')
482
483
483 return lexer
484 return lexer
484
485
485
486
486 def pygmentize(filenode, **kwargs):
487 def pygmentize(filenode, **kwargs):
487 """
488 """
488 pygmentize function using pygments
489 pygmentize function using pygments
489
490
490 :param filenode:
491 :param filenode:
491 """
492 """
492 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
493 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
493 return literal(code_highlight(filenode.content, lexer,
494 return literal(code_highlight(filenode.content, lexer,
494 CodeHtmlFormatter(**kwargs)))
495 CodeHtmlFormatter(**kwargs)))
495
496
496
497
497 def pygmentize_annotation(repo_name, filenode, **kwargs):
498 def pygmentize_annotation(repo_name, filenode, **kwargs):
498 """
499 """
499 pygmentize function for annotation
500 pygmentize function for annotation
500
501
501 :param filenode:
502 :param filenode:
502 """
503 """
503
504
504 color_dict = {}
505 color_dict = {}
505
506
506 def gen_color(n=10000):
507 def gen_color(n=10000):
507 """generator for getting n of evenly distributed colors using
508 """generator for getting n of evenly distributed colors using
508 hsv color and golden ratio. It always return same order of colors
509 hsv color and golden ratio. It always return same order of colors
509
510
510 :returns: RGB tuple
511 :returns: RGB tuple
511 """
512 """
512
513
513 def hsv_to_rgb(h, s, v):
514 def hsv_to_rgb(h, s, v):
514 if s == 0.0:
515 if s == 0.0:
515 return v, v, v
516 return v, v, v
516 i = int(h * 6.0) # XXX assume int() truncates!
517 i = int(h * 6.0) # XXX assume int() truncates!
517 f = (h * 6.0) - i
518 f = (h * 6.0) - i
518 p = v * (1.0 - s)
519 p = v * (1.0 - s)
519 q = v * (1.0 - s * f)
520 q = v * (1.0 - s * f)
520 t = v * (1.0 - s * (1.0 - f))
521 t = v * (1.0 - s * (1.0 - f))
521 i = i % 6
522 i = i % 6
522 if i == 0:
523 if i == 0:
523 return v, t, p
524 return v, t, p
524 if i == 1:
525 if i == 1:
525 return q, v, p
526 return q, v, p
526 if i == 2:
527 if i == 2:
527 return p, v, t
528 return p, v, t
528 if i == 3:
529 if i == 3:
529 return p, q, v
530 return p, q, v
530 if i == 4:
531 if i == 4:
531 return t, p, v
532 return t, p, v
532 if i == 5:
533 if i == 5:
533 return v, p, q
534 return v, p, q
534
535
535 golden_ratio = 0.618033988749895
536 golden_ratio = 0.618033988749895
536 h = 0.22717784590367374
537 h = 0.22717784590367374
537
538
538 for _ in xrange(n):
539 for _ in xrange(n):
539 h += golden_ratio
540 h += golden_ratio
540 h %= 1
541 h %= 1
541 HSV_tuple = [h, 0.95, 0.95]
542 HSV_tuple = [h, 0.95, 0.95]
542 RGB_tuple = hsv_to_rgb(*HSV_tuple)
543 RGB_tuple = hsv_to_rgb(*HSV_tuple)
543 yield map(lambda x: str(int(x * 256)), RGB_tuple)
544 yield map(lambda x: str(int(x * 256)), RGB_tuple)
544
545
545 cgenerator = gen_color()
546 cgenerator = gen_color()
546
547
547 def get_color_string(commit_id):
548 def get_color_string(commit_id):
548 if commit_id in color_dict:
549 if commit_id in color_dict:
549 col = color_dict[commit_id]
550 col = color_dict[commit_id]
550 else:
551 else:
551 col = color_dict[commit_id] = cgenerator.next()
552 col = color_dict[commit_id] = cgenerator.next()
552 return "color: rgb(%s)! important;" % (', '.join(col))
553 return "color: rgb(%s)! important;" % (', '.join(col))
553
554
554 def url_func(repo_name):
555 def url_func(repo_name):
555
556
556 def _url_func(commit):
557 def _url_func(commit):
557 author = commit.author
558 author = commit.author
558 date = commit.date
559 date = commit.date
559 message = tooltip(commit.message)
560 message = tooltip(commit.message)
560
561
561 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
562 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
562 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
563 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
563 "</b> %s<br/></div>")
564 "</b> %s<br/></div>")
564
565
565 tooltip_html = tooltip_html % (author, date, message)
566 tooltip_html = tooltip_html % (author, date, message)
566 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
567 lnk_format = '%5s:%s' % ('r%s' % commit.idx, commit.short_id)
567 uri = link_to(
568 uri = link_to(
568 lnk_format,
569 lnk_format,
569 url('changeset_home', repo_name=repo_name,
570 url('changeset_home', repo_name=repo_name,
570 revision=commit.raw_id),
571 revision=commit.raw_id),
571 style=get_color_string(commit.raw_id),
572 style=get_color_string(commit.raw_id),
572 class_='tooltip',
573 class_='tooltip',
573 title=tooltip_html
574 title=tooltip_html
574 )
575 )
575
576
576 uri += '\n'
577 uri += '\n'
577 return uri
578 return uri
578 return _url_func
579 return _url_func
579
580
580 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
581 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
581
582
582
583
583 def is_following_repo(repo_name, user_id):
584 def is_following_repo(repo_name, user_id):
584 from rhodecode.model.scm import ScmModel
585 from rhodecode.model.scm import ScmModel
585 return ScmModel().is_following_repo(repo_name, user_id)
586 return ScmModel().is_following_repo(repo_name, user_id)
586
587
587
588
588 class _Message(object):
589 class _Message(object):
589 """A message returned by ``Flash.pop_messages()``.
590 """A message returned by ``Flash.pop_messages()``.
590
591
591 Converting the message to a string returns the message text. Instances
592 Converting the message to a string returns the message text. Instances
592 also have the following attributes:
593 also have the following attributes:
593
594
594 * ``message``: the message text.
595 * ``message``: the message text.
595 * ``category``: the category specified when the message was created.
596 * ``category``: the category specified when the message was created.
596 """
597 """
597
598
598 def __init__(self, category, message):
599 def __init__(self, category, message):
599 self.category = category
600 self.category = category
600 self.message = message
601 self.message = message
601
602
602 def __str__(self):
603 def __str__(self):
603 return self.message
604 return self.message
604
605
605 __unicode__ = __str__
606 __unicode__ = __str__
606
607
607 def __html__(self):
608 def __html__(self):
608 return escape(safe_unicode(self.message))
609 return escape(safe_unicode(self.message))
609
610
610
611
611 class Flash(_Flash):
612 class Flash(_Flash):
612
613
613 def pop_messages(self):
614 def pop_messages(self):
614 """Return all accumulated messages and delete them from the session.
615 """Return all accumulated messages and delete them from the session.
615
616
616 The return value is a list of ``Message`` objects.
617 The return value is a list of ``Message`` objects.
617 """
618 """
618 from pylons import session
619 from pylons import session
619
620
620 messages = []
621 messages = []
621
622
622 # Pop the 'old' pylons flash messages. They are tuples of the form
623 # Pop the 'old' pylons flash messages. They are tuples of the form
623 # (category, message)
624 # (category, message)
624 for cat, msg in session.pop(self.session_key, []):
625 for cat, msg in session.pop(self.session_key, []):
625 messages.append(_Message(cat, msg))
626 messages.append(_Message(cat, msg))
626
627
627 # Pop the 'new' pyramid flash messages for each category as list
628 # Pop the 'new' pyramid flash messages for each category as list
628 # of strings.
629 # of strings.
629 for cat in self.categories:
630 for cat in self.categories:
630 for msg in session.pop_flash(queue=cat):
631 for msg in session.pop_flash(queue=cat):
631 messages.append(_Message(cat, msg))
632 messages.append(_Message(cat, msg))
632 # Map messages from the default queue to the 'notice' category.
633 # Map messages from the default queue to the 'notice' category.
633 for msg in session.pop_flash():
634 for msg in session.pop_flash():
634 messages.append(_Message('notice', msg))
635 messages.append(_Message('notice', msg))
635
636
636 session.save()
637 session.save()
637 return messages
638 return messages
638
639
639 flash = Flash()
640 flash = Flash()
640
641
641 #==============================================================================
642 #==============================================================================
642 # SCM FILTERS available via h.
643 # SCM FILTERS available via h.
643 #==============================================================================
644 #==============================================================================
644 from rhodecode.lib.vcs.utils import author_name, author_email
645 from rhodecode.lib.vcs.utils import author_name, author_email
645 from rhodecode.lib.utils2 import credentials_filter, age as _age
646 from rhodecode.lib.utils2 import credentials_filter, age as _age
646 from rhodecode.model.db import User, ChangesetStatus
647 from rhodecode.model.db import User, ChangesetStatus
647
648
648 age = _age
649 age = _age
649 capitalize = lambda x: x.capitalize()
650 capitalize = lambda x: x.capitalize()
650 email = author_email
651 email = author_email
651 short_id = lambda x: x[:12]
652 short_id = lambda x: x[:12]
652 hide_credentials = lambda x: ''.join(credentials_filter(x))
653 hide_credentials = lambda x: ''.join(credentials_filter(x))
653
654
654
655
655 def age_component(datetime_iso, value=None, time_is_local=False):
656 def age_component(datetime_iso, value=None, time_is_local=False):
656 title = value or format_date(datetime_iso)
657 title = value or format_date(datetime_iso)
657
658
658 # detect if we have a timezone info, otherwise, add it
659 # detect if we have a timezone info, otherwise, add it
659 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
660 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
660 tzinfo = '+00:00'
661 tzinfo = '+00:00'
661
662
662 if time_is_local:
663 if time_is_local:
663 tzinfo = time.strftime("+%H:%M",
664 tzinfo = time.strftime("+%H:%M",
664 time.gmtime(
665 time.gmtime(
665 (datetime.now() - datetime.utcnow()).seconds + 1
666 (datetime.now() - datetime.utcnow()).seconds + 1
666 )
667 )
667 )
668 )
668
669
669 return literal(
670 return literal(
670 '<time class="timeago tooltip" '
671 '<time class="timeago tooltip" '
671 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
672 'title="{1}" datetime="{0}{2}">{1}</time>'.format(
672 datetime_iso, title, tzinfo))
673 datetime_iso, title, tzinfo))
673
674
674
675
675 def _shorten_commit_id(commit_id):
676 def _shorten_commit_id(commit_id):
676 from rhodecode import CONFIG
677 from rhodecode import CONFIG
677 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
678 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
678 return commit_id[:def_len]
679 return commit_id[:def_len]
679
680
680
681
681 def show_id(commit):
682 def show_id(commit):
682 """
683 """
683 Configurable function that shows ID
684 Configurable function that shows ID
684 by default it's r123:fffeeefffeee
685 by default it's r123:fffeeefffeee
685
686
686 :param commit: commit instance
687 :param commit: commit instance
687 """
688 """
688 from rhodecode import CONFIG
689 from rhodecode import CONFIG
689 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
690 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
690
691
691 raw_id = _shorten_commit_id(commit.raw_id)
692 raw_id = _shorten_commit_id(commit.raw_id)
692 if show_idx:
693 if show_idx:
693 return 'r%s:%s' % (commit.idx, raw_id)
694 return 'r%s:%s' % (commit.idx, raw_id)
694 else:
695 else:
695 return '%s' % (raw_id, )
696 return '%s' % (raw_id, )
696
697
697
698
698 def format_date(date):
699 def format_date(date):
699 """
700 """
700 use a standardized formatting for dates used in RhodeCode
701 use a standardized formatting for dates used in RhodeCode
701
702
702 :param date: date/datetime object
703 :param date: date/datetime object
703 :return: formatted date
704 :return: formatted date
704 """
705 """
705
706
706 if date:
707 if date:
707 _fmt = "%a, %d %b %Y %H:%M:%S"
708 _fmt = "%a, %d %b %Y %H:%M:%S"
708 return safe_unicode(date.strftime(_fmt))
709 return safe_unicode(date.strftime(_fmt))
709
710
710 return u""
711 return u""
711
712
712
713
713 class _RepoChecker(object):
714 class _RepoChecker(object):
714
715
715 def __init__(self, backend_alias):
716 def __init__(self, backend_alias):
716 self._backend_alias = backend_alias
717 self._backend_alias = backend_alias
717
718
718 def __call__(self, repository):
719 def __call__(self, repository):
719 if hasattr(repository, 'alias'):
720 if hasattr(repository, 'alias'):
720 _type = repository.alias
721 _type = repository.alias
721 elif hasattr(repository, 'repo_type'):
722 elif hasattr(repository, 'repo_type'):
722 _type = repository.repo_type
723 _type = repository.repo_type
723 else:
724 else:
724 _type = repository
725 _type = repository
725 return _type == self._backend_alias
726 return _type == self._backend_alias
726
727
727 is_git = _RepoChecker('git')
728 is_git = _RepoChecker('git')
728 is_hg = _RepoChecker('hg')
729 is_hg = _RepoChecker('hg')
729 is_svn = _RepoChecker('svn')
730 is_svn = _RepoChecker('svn')
730
731
731
732
732 def get_repo_type_by_name(repo_name):
733 def get_repo_type_by_name(repo_name):
733 repo = Repository.get_by_repo_name(repo_name)
734 repo = Repository.get_by_repo_name(repo_name)
734 return repo.repo_type
735 return repo.repo_type
735
736
736
737
737 def is_svn_without_proxy(repository):
738 def is_svn_without_proxy(repository):
738 from rhodecode import CONFIG
739 from rhodecode import CONFIG
739 if is_svn(repository):
740 if is_svn(repository):
740 if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
741 if not CONFIG.get('rhodecode_proxy_subversion_http_requests', False):
741 return True
742 return True
742 return False
743 return False
743
744
744
745
745 def discover_user(author):
746 def discover_user(author):
746 """
747 """
747 Tries to discover RhodeCode User based on the autho string. Author string
748 Tries to discover RhodeCode User based on the autho string. Author string
748 is typically `FirstName LastName <email@address.com>`
749 is typically `FirstName LastName <email@address.com>`
749 """
750 """
750
751
751 # if author is already an instance use it for extraction
752 # if author is already an instance use it for extraction
752 if isinstance(author, User):
753 if isinstance(author, User):
753 return author
754 return author
754
755
755 # Valid email in the attribute passed, see if they're in the system
756 # Valid email in the attribute passed, see if they're in the system
756 _email = author_email(author)
757 _email = author_email(author)
757 if _email != '':
758 if _email != '':
758 user = User.get_by_email(_email, case_insensitive=True, cache=True)
759 user = User.get_by_email(_email, case_insensitive=True, cache=True)
759 if user is not None:
760 if user is not None:
760 return user
761 return user
761
762
762 # Maybe it's a username, we try to extract it and fetch by username ?
763 # Maybe it's a username, we try to extract it and fetch by username ?
763 _author = author_name(author)
764 _author = author_name(author)
764 user = User.get_by_username(_author, case_insensitive=True, cache=True)
765 user = User.get_by_username(_author, case_insensitive=True, cache=True)
765 if user is not None:
766 if user is not None:
766 return user
767 return user
767
768
768 return None
769 return None
769
770
770
771
771 def email_or_none(author):
772 def email_or_none(author):
772 # extract email from the commit string
773 # extract email from the commit string
773 _email = author_email(author)
774 _email = author_email(author)
774 if _email != '':
775 if _email != '':
775 # check it against RhodeCode database, and use the MAIN email for this
776 # check it against RhodeCode database, and use the MAIN email for this
776 # user
777 # user
777 user = User.get_by_email(_email, case_insensitive=True, cache=True)
778 user = User.get_by_email(_email, case_insensitive=True, cache=True)
778 if user is not None:
779 if user is not None:
779 return user.email
780 return user.email
780 return _email
781 return _email
781
782
782 # See if it contains a username we can get an email from
783 # See if it contains a username we can get an email from
783 user = User.get_by_username(author_name(author), case_insensitive=True,
784 user = User.get_by_username(author_name(author), case_insensitive=True,
784 cache=True)
785 cache=True)
785 if user is not None:
786 if user is not None:
786 return user.email
787 return user.email
787
788
788 # No valid email, not a valid user in the system, none!
789 # No valid email, not a valid user in the system, none!
789 return None
790 return None
790
791
791
792
792 def link_to_user(author, length=0, **kwargs):
793 def link_to_user(author, length=0, **kwargs):
793 user = discover_user(author)
794 user = discover_user(author)
794 # user can be None, but if we have it already it means we can re-use it
795 # user can be None, but if we have it already it means we can re-use it
795 # in the person() function, so we save 1 intensive-query
796 # in the person() function, so we save 1 intensive-query
796 if user:
797 if user:
797 author = user
798 author = user
798
799
799 display_person = person(author, 'username_or_name_or_email')
800 display_person = person(author, 'username_or_name_or_email')
800 if length:
801 if length:
801 display_person = shorter(display_person, length)
802 display_person = shorter(display_person, length)
802
803
803 if user:
804 if user:
804 return link_to(
805 return link_to(
805 escape(display_person),
806 escape(display_person),
806 url('user_profile', username=user.username),
807 url('user_profile', username=user.username),
807 **kwargs)
808 **kwargs)
808 else:
809 else:
809 return escape(display_person)
810 return escape(display_person)
810
811
811
812
812 def person(author, show_attr="username_and_name"):
813 def person(author, show_attr="username_and_name"):
813 user = discover_user(author)
814 user = discover_user(author)
814 if user:
815 if user:
815 return getattr(user, show_attr)
816 return getattr(user, show_attr)
816 else:
817 else:
817 _author = author_name(author)
818 _author = author_name(author)
818 _email = email(author)
819 _email = email(author)
819 return _author or _email
820 return _author or _email
820
821
821
822
822 def person_by_id(id_, show_attr="username_and_name"):
823 def person_by_id(id_, show_attr="username_and_name"):
823 # attr to return from fetched user
824 # attr to return from fetched user
824 person_getter = lambda usr: getattr(usr, show_attr)
825 person_getter = lambda usr: getattr(usr, show_attr)
825
826
826 #maybe it's an ID ?
827 #maybe it's an ID ?
827 if str(id_).isdigit() or isinstance(id_, int):
828 if str(id_).isdigit() or isinstance(id_, int):
828 id_ = int(id_)
829 id_ = int(id_)
829 user = User.get(id_)
830 user = User.get(id_)
830 if user is not None:
831 if user is not None:
831 return person_getter(user)
832 return person_getter(user)
832 return id_
833 return id_
833
834
834
835
835 def gravatar_with_user(author, show_disabled=False):
836 def gravatar_with_user(author, show_disabled=False):
836 from rhodecode.lib.utils import PartialRenderer
837 from rhodecode.lib.utils import PartialRenderer
837 _render = PartialRenderer('base/base.html')
838 _render = PartialRenderer('base/base.html')
838 return _render('gravatar_with_user', author, show_disabled=show_disabled)
839 return _render('gravatar_with_user', author, show_disabled=show_disabled)
839
840
840
841
841 def desc_stylize(value):
842 def desc_stylize(value):
842 """
843 """
843 converts tags from value into html equivalent
844 converts tags from value into html equivalent
844
845
845 :param value:
846 :param value:
846 """
847 """
847 if not value:
848 if not value:
848 return ''
849 return ''
849
850
850 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
851 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
851 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
852 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
852 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
853 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
853 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
854 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
854 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
855 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
855 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
856 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
856 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
857 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
857 '<div class="metatag" tag="lang">\\2</div>', value)
858 '<div class="metatag" tag="lang">\\2</div>', value)
858 value = re.sub(r'\[([a-z]+)\]',
859 value = re.sub(r'\[([a-z]+)\]',
859 '<div class="metatag" tag="\\1">\\1</div>', value)
860 '<div class="metatag" tag="\\1">\\1</div>', value)
860
861
861 return value
862 return value
862
863
863
864
864 def escaped_stylize(value):
865 def escaped_stylize(value):
865 """
866 """
866 converts tags from value into html equivalent, but escaping its value first
867 converts tags from value into html equivalent, but escaping its value first
867 """
868 """
868 if not value:
869 if not value:
869 return ''
870 return ''
870
871
871 # Using default webhelper escape method, but has to force it as a
872 # Using default webhelper escape method, but has to force it as a
872 # plain unicode instead of a markup tag to be used in regex expressions
873 # plain unicode instead of a markup tag to be used in regex expressions
873 value = unicode(escape(safe_unicode(value)))
874 value = unicode(escape(safe_unicode(value)))
874
875
875 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
876 value = re.sub(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
876 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
877 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
877 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
878 value = re.sub(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]',
878 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
879 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
879 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
880 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]',
880 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
881 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
881 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
882 value = re.sub(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+]*)\]',
882 '<div class="metatag" tag="lang">\\2</div>', value)
883 '<div class="metatag" tag="lang">\\2</div>', value)
883 value = re.sub(r'\[([a-z]+)\]',
884 value = re.sub(r'\[([a-z]+)\]',
884 '<div class="metatag" tag="\\1">\\1</div>', value)
885 '<div class="metatag" tag="\\1">\\1</div>', value)
885
886
886 return value
887 return value
887
888
888
889
889 def bool2icon(value):
890 def bool2icon(value):
890 """
891 """
891 Returns boolean value of a given value, represented as html element with
892 Returns boolean value of a given value, represented as html element with
892 classes that will represent icons
893 classes that will represent icons
893
894
894 :param value: given value to convert to html node
895 :param value: given value to convert to html node
895 """
896 """
896
897
897 if value: # does bool conversion
898 if value: # does bool conversion
898 return HTML.tag('i', class_="icon-true")
899 return HTML.tag('i', class_="icon-true")
899 else: # not true as bool
900 else: # not true as bool
900 return HTML.tag('i', class_="icon-false")
901 return HTML.tag('i', class_="icon-false")
901
902
902
903
903 #==============================================================================
904 #==============================================================================
904 # PERMS
905 # PERMS
905 #==============================================================================
906 #==============================================================================
906 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
907 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
907 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
908 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
908 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
909 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token
909
910
910
911
911 #==============================================================================
912 #==============================================================================
912 # GRAVATAR URL
913 # GRAVATAR URL
913 #==============================================================================
914 #==============================================================================
914 class InitialsGravatar(object):
915 class InitialsGravatar(object):
915 def __init__(self, email_address, first_name, last_name, size=30,
916 def __init__(self, email_address, first_name, last_name, size=30,
916 background=None, text_color='#fff'):
917 background=None, text_color='#fff'):
917 self.size = size
918 self.size = size
918 self.first_name = first_name
919 self.first_name = first_name
919 self.last_name = last_name
920 self.last_name = last_name
920 self.email_address = email_address
921 self.email_address = email_address
921 self.background = background or self.str2color(email_address)
922 self.background = background or self.str2color(email_address)
922 self.text_color = text_color
923 self.text_color = text_color
923
924
924 def get_color_bank(self):
925 def get_color_bank(self):
925 """
926 """
926 returns a predefined list of colors that gravatars can use.
927 returns a predefined list of colors that gravatars can use.
927 Those are randomized distinct colors that guarantee readability and
928 Those are randomized distinct colors that guarantee readability and
928 uniqueness.
929 uniqueness.
929
930
930 generated with: http://phrogz.net/css/distinct-colors.html
931 generated with: http://phrogz.net/css/distinct-colors.html
931 """
932 """
932 return [
933 return [
933 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
934 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
934 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
935 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
935 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
936 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
936 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
937 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
937 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
938 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
938 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
939 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
939 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
940 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
940 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
941 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
941 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
942 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
942 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
943 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
943 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
944 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
944 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
945 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
945 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
946 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
946 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
947 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
947 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
948 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
948 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
949 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
949 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
950 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
950 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
951 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
951 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
952 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
952 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
953 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
953 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
954 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
954 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
955 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
955 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
956 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
956 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
957 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
957 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
958 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
958 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
959 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
959 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
960 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
960 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
961 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
961 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
962 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
962 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
963 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
963 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
964 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
964 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
965 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
965 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
966 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
966 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
967 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
967 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
968 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
968 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
969 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
969 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
970 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
970 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
971 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
971 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
972 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
972 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
973 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
973 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
974 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
974 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
975 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
975 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
976 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
976 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
977 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
977 '#4f8c46', '#368dd9', '#5c0073'
978 '#4f8c46', '#368dd9', '#5c0073'
978 ]
979 ]
979
980
980 def rgb_to_hex_color(self, rgb_tuple):
981 def rgb_to_hex_color(self, rgb_tuple):
981 """
982 """
982 Converts an rgb_tuple passed to an hex color.
983 Converts an rgb_tuple passed to an hex color.
983
984
984 :param rgb_tuple: tuple with 3 ints represents rgb color space
985 :param rgb_tuple: tuple with 3 ints represents rgb color space
985 """
986 """
986 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
987 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
987
988
988 def email_to_int_list(self, email_str):
989 def email_to_int_list(self, email_str):
989 """
990 """
990 Get every byte of the hex digest value of email and turn it to integer.
991 Get every byte of the hex digest value of email and turn it to integer.
991 It's going to be always between 0-255
992 It's going to be always between 0-255
992 """
993 """
993 digest = md5_safe(email_str.lower())
994 digest = md5_safe(email_str.lower())
994 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
995 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
995
996
996 def pick_color_bank_index(self, email_str, color_bank):
997 def pick_color_bank_index(self, email_str, color_bank):
997 return self.email_to_int_list(email_str)[0] % len(color_bank)
998 return self.email_to_int_list(email_str)[0] % len(color_bank)
998
999
999 def str2color(self, email_str):
1000 def str2color(self, email_str):
1000 """
1001 """
1001 Tries to map in a stable algorithm an email to color
1002 Tries to map in a stable algorithm an email to color
1002
1003
1003 :param email_str:
1004 :param email_str:
1004 """
1005 """
1005 color_bank = self.get_color_bank()
1006 color_bank = self.get_color_bank()
1006 # pick position (module it's length so we always find it in the
1007 # pick position (module it's length so we always find it in the
1007 # bank even if it's smaller than 256 values
1008 # bank even if it's smaller than 256 values
1008 pos = self.pick_color_bank_index(email_str, color_bank)
1009 pos = self.pick_color_bank_index(email_str, color_bank)
1009 return color_bank[pos]
1010 return color_bank[pos]
1010
1011
1011 def normalize_email(self, email_address):
1012 def normalize_email(self, email_address):
1012 import unicodedata
1013 import unicodedata
1013 # default host used to fill in the fake/missing email
1014 # default host used to fill in the fake/missing email
1014 default_host = u'localhost'
1015 default_host = u'localhost'
1015
1016
1016 if not email_address:
1017 if not email_address:
1017 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1018 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1018
1019
1019 email_address = safe_unicode(email_address)
1020 email_address = safe_unicode(email_address)
1020
1021
1021 if u'@' not in email_address:
1022 if u'@' not in email_address:
1022 email_address = u'%s@%s' % (email_address, default_host)
1023 email_address = u'%s@%s' % (email_address, default_host)
1023
1024
1024 if email_address.endswith(u'@'):
1025 if email_address.endswith(u'@'):
1025 email_address = u'%s%s' % (email_address, default_host)
1026 email_address = u'%s%s' % (email_address, default_host)
1026
1027
1027 email_address = unicodedata.normalize('NFKD', email_address)\
1028 email_address = unicodedata.normalize('NFKD', email_address)\
1028 .encode('ascii', 'ignore')
1029 .encode('ascii', 'ignore')
1029 return email_address
1030 return email_address
1030
1031
1031 def get_initials(self):
1032 def get_initials(self):
1032 """
1033 """
1033 Returns 2 letter initials calculated based on the input.
1034 Returns 2 letter initials calculated based on the input.
1034 The algorithm picks first given email address, and takes first letter
1035 The algorithm picks first given email address, and takes first letter
1035 of part before @, and then the first letter of server name. In case
1036 of part before @, and then the first letter of server name. In case
1036 the part before @ is in a format of `somestring.somestring2` it replaces
1037 the part before @ is in a format of `somestring.somestring2` it replaces
1037 the server letter with first letter of somestring2
1038 the server letter with first letter of somestring2
1038
1039
1039 In case function was initialized with both first and lastname, this
1040 In case function was initialized with both first and lastname, this
1040 overrides the extraction from email by first letter of the first and
1041 overrides the extraction from email by first letter of the first and
1041 last name. We add special logic to that functionality, In case Full name
1042 last name. We add special logic to that functionality, In case Full name
1042 is compound, like Guido Von Rossum, we use last part of the last name
1043 is compound, like Guido Von Rossum, we use last part of the last name
1043 (Von Rossum) picking `R`.
1044 (Von Rossum) picking `R`.
1044
1045
1045 Function also normalizes the non-ascii characters to they ascii
1046 Function also normalizes the non-ascii characters to they ascii
1046 representation, eg Δ„ => A
1047 representation, eg Δ„ => A
1047 """
1048 """
1048 import unicodedata
1049 import unicodedata
1049 # replace non-ascii to ascii
1050 # replace non-ascii to ascii
1050 first_name = unicodedata.normalize(
1051 first_name = unicodedata.normalize(
1051 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1052 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1052 last_name = unicodedata.normalize(
1053 last_name = unicodedata.normalize(
1053 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1054 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1054
1055
1055 # do NFKD encoding, and also make sure email has proper format
1056 # do NFKD encoding, and also make sure email has proper format
1056 email_address = self.normalize_email(self.email_address)
1057 email_address = self.normalize_email(self.email_address)
1057
1058
1058 # first push the email initials
1059 # first push the email initials
1059 prefix, server = email_address.split('@', 1)
1060 prefix, server = email_address.split('@', 1)
1060
1061
1061 # check if prefix is maybe a 'firstname.lastname' syntax
1062 # check if prefix is maybe a 'firstname.lastname' syntax
1062 _dot_split = prefix.rsplit('.', 1)
1063 _dot_split = prefix.rsplit('.', 1)
1063 if len(_dot_split) == 2:
1064 if len(_dot_split) == 2:
1064 initials = [_dot_split[0][0], _dot_split[1][0]]
1065 initials = [_dot_split[0][0], _dot_split[1][0]]
1065 else:
1066 else:
1066 initials = [prefix[0], server[0]]
1067 initials = [prefix[0], server[0]]
1067
1068
1068 # then try to replace either firtname or lastname
1069 # then try to replace either firtname or lastname
1069 fn_letter = (first_name or " ")[0].strip()
1070 fn_letter = (first_name or " ")[0].strip()
1070 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1071 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1071
1072
1072 if fn_letter:
1073 if fn_letter:
1073 initials[0] = fn_letter
1074 initials[0] = fn_letter
1074
1075
1075 if ln_letter:
1076 if ln_letter:
1076 initials[1] = ln_letter
1077 initials[1] = ln_letter
1077
1078
1078 return ''.join(initials).upper()
1079 return ''.join(initials).upper()
1079
1080
1080 def get_img_data_by_type(self, font_family, img_type):
1081 def get_img_data_by_type(self, font_family, img_type):
1081 default_user = """
1082 default_user = """
1082 <svg xmlns="http://www.w3.org/2000/svg"
1083 <svg xmlns="http://www.w3.org/2000/svg"
1083 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1084 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1084 viewBox="-15 -10 439.165 429.164"
1085 viewBox="-15 -10 439.165 429.164"
1085
1086
1086 xml:space="preserve"
1087 xml:space="preserve"
1087 style="background:{background};" >
1088 style="background:{background};" >
1088
1089
1089 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1090 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1090 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1091 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1091 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1092 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1092 168.596,153.916,216.671,
1093 168.596,153.916,216.671,
1093 204.583,216.671z" fill="{text_color}"/>
1094 204.583,216.671z" fill="{text_color}"/>
1094 <path d="M407.164,374.717L360.88,
1095 <path d="M407.164,374.717L360.88,
1095 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1096 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1096 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1097 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1097 15.366-44.203,23.488-69.076,23.488c-24.877,
1098 15.366-44.203,23.488-69.076,23.488c-24.877,
1098 0-48.762-8.122-69.078-23.488
1099 0-48.762-8.122-69.078-23.488
1099 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1100 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1100 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1101 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1101 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1102 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1102 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1103 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1103 19.402-10.527 C409.699,390.129,
1104 19.402-10.527 C409.699,390.129,
1104 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1105 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1105 </svg>""".format(
1106 </svg>""".format(
1106 size=self.size,
1107 size=self.size,
1107 background='#979797', # @grey4
1108 background='#979797', # @grey4
1108 text_color=self.text_color,
1109 text_color=self.text_color,
1109 font_family=font_family)
1110 font_family=font_family)
1110
1111
1111 return {
1112 return {
1112 "default_user": default_user
1113 "default_user": default_user
1113 }[img_type]
1114 }[img_type]
1114
1115
1115 def get_img_data(self, svg_type=None):
1116 def get_img_data(self, svg_type=None):
1116 """
1117 """
1117 generates the svg metadata for image
1118 generates the svg metadata for image
1118 """
1119 """
1119
1120
1120 font_family = ','.join([
1121 font_family = ','.join([
1121 'proximanovaregular',
1122 'proximanovaregular',
1122 'Proxima Nova Regular',
1123 'Proxima Nova Regular',
1123 'Proxima Nova',
1124 'Proxima Nova',
1124 'Arial',
1125 'Arial',
1125 'Lucida Grande',
1126 'Lucida Grande',
1126 'sans-serif'
1127 'sans-serif'
1127 ])
1128 ])
1128 if svg_type:
1129 if svg_type:
1129 return self.get_img_data_by_type(font_family, svg_type)
1130 return self.get_img_data_by_type(font_family, svg_type)
1130
1131
1131 initials = self.get_initials()
1132 initials = self.get_initials()
1132 img_data = """
1133 img_data = """
1133 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1134 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1134 width="{size}" height="{size}"
1135 width="{size}" height="{size}"
1135 style="width: 100%; height: 100%; background-color: {background}"
1136 style="width: 100%; height: 100%; background-color: {background}"
1136 viewBox="0 0 {size} {size}">
1137 viewBox="0 0 {size} {size}">
1137 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1138 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1138 pointer-events="auto" fill="{text_color}"
1139 pointer-events="auto" fill="{text_color}"
1139 font-family="{font_family}"
1140 font-family="{font_family}"
1140 style="font-weight: 400; font-size: {f_size}px;">{text}
1141 style="font-weight: 400; font-size: {f_size}px;">{text}
1141 </text>
1142 </text>
1142 </svg>""".format(
1143 </svg>""".format(
1143 size=self.size,
1144 size=self.size,
1144 f_size=self.size/1.85, # scale the text inside the box nicely
1145 f_size=self.size/1.85, # scale the text inside the box nicely
1145 background=self.background,
1146 background=self.background,
1146 text_color=self.text_color,
1147 text_color=self.text_color,
1147 text=initials.upper(),
1148 text=initials.upper(),
1148 font_family=font_family)
1149 font_family=font_family)
1149
1150
1150 return img_data
1151 return img_data
1151
1152
1152 def generate_svg(self, svg_type=None):
1153 def generate_svg(self, svg_type=None):
1153 img_data = self.get_img_data(svg_type)
1154 img_data = self.get_img_data(svg_type)
1154 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1155 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1155
1156
1156
1157
1157 def initials_gravatar(email_address, first_name, last_name, size=30):
1158 def initials_gravatar(email_address, first_name, last_name, size=30):
1158 svg_type = None
1159 svg_type = None
1159 if email_address == User.DEFAULT_USER_EMAIL:
1160 if email_address == User.DEFAULT_USER_EMAIL:
1160 svg_type = 'default_user'
1161 svg_type = 'default_user'
1161 klass = InitialsGravatar(email_address, first_name, last_name, size)
1162 klass = InitialsGravatar(email_address, first_name, last_name, size)
1162 return klass.generate_svg(svg_type=svg_type)
1163 return klass.generate_svg(svg_type=svg_type)
1163
1164
1164
1165
1165 def gravatar_url(email_address, size=30):
1166 def gravatar_url(email_address, size=30):
1166 # doh, we need to re-import those to mock it later
1167 # doh, we need to re-import those to mock it later
1167 from pylons import tmpl_context as c
1168 from pylons import tmpl_context as c
1168
1169
1169 _use_gravatar = c.visual.use_gravatar
1170 _use_gravatar = c.visual.use_gravatar
1170 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1171 _gravatar_url = c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL
1171
1172
1172 email_address = email_address or User.DEFAULT_USER_EMAIL
1173 email_address = email_address or User.DEFAULT_USER_EMAIL
1173 if isinstance(email_address, unicode):
1174 if isinstance(email_address, unicode):
1174 # hashlib crashes on unicode items
1175 # hashlib crashes on unicode items
1175 email_address = safe_str(email_address)
1176 email_address = safe_str(email_address)
1176
1177
1177 # empty email or default user
1178 # empty email or default user
1178 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1179 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1179 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1180 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1180
1181
1181 if _use_gravatar:
1182 if _use_gravatar:
1182 # TODO: Disuse pyramid thread locals. Think about another solution to
1183 # TODO: Disuse pyramid thread locals. Think about another solution to
1183 # get the host and schema here.
1184 # get the host and schema here.
1184 request = get_current_request()
1185 request = get_current_request()
1185 tmpl = safe_str(_gravatar_url)
1186 tmpl = safe_str(_gravatar_url)
1186 tmpl = tmpl.replace('{email}', email_address)\
1187 tmpl = tmpl.replace('{email}', email_address)\
1187 .replace('{md5email}', md5_safe(email_address.lower())) \
1188 .replace('{md5email}', md5_safe(email_address.lower())) \
1188 .replace('{netloc}', request.host)\
1189 .replace('{netloc}', request.host)\
1189 .replace('{scheme}', request.scheme)\
1190 .replace('{scheme}', request.scheme)\
1190 .replace('{size}', safe_str(size))
1191 .replace('{size}', safe_str(size))
1191 return tmpl
1192 return tmpl
1192 else:
1193 else:
1193 return initials_gravatar(email_address, '', '', size=size)
1194 return initials_gravatar(email_address, '', '', size=size)
1194
1195
1195
1196
1196 class Page(_Page):
1197 class Page(_Page):
1197 """
1198 """
1198 Custom pager to match rendering style with paginator
1199 Custom pager to match rendering style with paginator
1199 """
1200 """
1200
1201
1201 def _get_pos(self, cur_page, max_page, items):
1202 def _get_pos(self, cur_page, max_page, items):
1202 edge = (items / 2) + 1
1203 edge = (items / 2) + 1
1203 if (cur_page <= edge):
1204 if (cur_page <= edge):
1204 radius = max(items / 2, items - cur_page)
1205 radius = max(items / 2, items - cur_page)
1205 elif (max_page - cur_page) < edge:
1206 elif (max_page - cur_page) < edge:
1206 radius = (items - 1) - (max_page - cur_page)
1207 radius = (items - 1) - (max_page - cur_page)
1207 else:
1208 else:
1208 radius = items / 2
1209 radius = items / 2
1209
1210
1210 left = max(1, (cur_page - (radius)))
1211 left = max(1, (cur_page - (radius)))
1211 right = min(max_page, cur_page + (radius))
1212 right = min(max_page, cur_page + (radius))
1212 return left, cur_page, right
1213 return left, cur_page, right
1213
1214
1214 def _range(self, regexp_match):
1215 def _range(self, regexp_match):
1215 """
1216 """
1216 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1217 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1217
1218
1218 Arguments:
1219 Arguments:
1219
1220
1220 regexp_match
1221 regexp_match
1221 A "re" (regular expressions) match object containing the
1222 A "re" (regular expressions) match object containing the
1222 radius of linked pages around the current page in
1223 radius of linked pages around the current page in
1223 regexp_match.group(1) as a string
1224 regexp_match.group(1) as a string
1224
1225
1225 This function is supposed to be called as a callable in
1226 This function is supposed to be called as a callable in
1226 re.sub.
1227 re.sub.
1227
1228
1228 """
1229 """
1229 radius = int(regexp_match.group(1))
1230 radius = int(regexp_match.group(1))
1230
1231
1231 # Compute the first and last page number within the radius
1232 # Compute the first and last page number within the radius
1232 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1233 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1233 # -> leftmost_page = 5
1234 # -> leftmost_page = 5
1234 # -> rightmost_page = 9
1235 # -> rightmost_page = 9
1235 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1236 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1236 self.last_page,
1237 self.last_page,
1237 (radius * 2) + 1)
1238 (radius * 2) + 1)
1238 nav_items = []
1239 nav_items = []
1239
1240
1240 # Create a link to the first page (unless we are on the first page
1241 # Create a link to the first page (unless we are on the first page
1241 # or there would be no need to insert '..' spacers)
1242 # or there would be no need to insert '..' spacers)
1242 if self.page != self.first_page and self.first_page < leftmost_page:
1243 if self.page != self.first_page and self.first_page < leftmost_page:
1243 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1244 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1244
1245
1245 # Insert dots if there are pages between the first page
1246 # Insert dots if there are pages between the first page
1246 # and the currently displayed page range
1247 # and the currently displayed page range
1247 if leftmost_page - self.first_page > 1:
1248 if leftmost_page - self.first_page > 1:
1248 # Wrap in a SPAN tag if nolink_attr is set
1249 # Wrap in a SPAN tag if nolink_attr is set
1249 text = '..'
1250 text = '..'
1250 if self.dotdot_attr:
1251 if self.dotdot_attr:
1251 text = HTML.span(c=text, **self.dotdot_attr)
1252 text = HTML.span(c=text, **self.dotdot_attr)
1252 nav_items.append(text)
1253 nav_items.append(text)
1253
1254
1254 for thispage in xrange(leftmost_page, rightmost_page + 1):
1255 for thispage in xrange(leftmost_page, rightmost_page + 1):
1255 # Hilight the current page number and do not use a link
1256 # Hilight the current page number and do not use a link
1256 if thispage == self.page:
1257 if thispage == self.page:
1257 text = '%s' % (thispage,)
1258 text = '%s' % (thispage,)
1258 # Wrap in a SPAN tag if nolink_attr is set
1259 # Wrap in a SPAN tag if nolink_attr is set
1259 if self.curpage_attr:
1260 if self.curpage_attr:
1260 text = HTML.span(c=text, **self.curpage_attr)
1261 text = HTML.span(c=text, **self.curpage_attr)
1261 nav_items.append(text)
1262 nav_items.append(text)
1262 # Otherwise create just a link to that page
1263 # Otherwise create just a link to that page
1263 else:
1264 else:
1264 text = '%s' % (thispage,)
1265 text = '%s' % (thispage,)
1265 nav_items.append(self._pagerlink(thispage, text))
1266 nav_items.append(self._pagerlink(thispage, text))
1266
1267
1267 # Insert dots if there are pages between the displayed
1268 # Insert dots if there are pages between the displayed
1268 # page numbers and the end of the page range
1269 # page numbers and the end of the page range
1269 if self.last_page - rightmost_page > 1:
1270 if self.last_page - rightmost_page > 1:
1270 text = '..'
1271 text = '..'
1271 # Wrap in a SPAN tag if nolink_attr is set
1272 # Wrap in a SPAN tag if nolink_attr is set
1272 if self.dotdot_attr:
1273 if self.dotdot_attr:
1273 text = HTML.span(c=text, **self.dotdot_attr)
1274 text = HTML.span(c=text, **self.dotdot_attr)
1274 nav_items.append(text)
1275 nav_items.append(text)
1275
1276
1276 # Create a link to the very last page (unless we are on the last
1277 # Create a link to the very last page (unless we are on the last
1277 # page or there would be no need to insert '..' spacers)
1278 # page or there would be no need to insert '..' spacers)
1278 if self.page != self.last_page and rightmost_page < self.last_page:
1279 if self.page != self.last_page and rightmost_page < self.last_page:
1279 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1280 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1280
1281
1281 ## prerender links
1282 ## prerender links
1282 #_page_link = url.current()
1283 #_page_link = url.current()
1283 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1284 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1284 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1285 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1285 return self.separator.join(nav_items)
1286 return self.separator.join(nav_items)
1286
1287
1287 def pager(self, format='~2~', page_param='page', partial_param='partial',
1288 def pager(self, format='~2~', page_param='page', partial_param='partial',
1288 show_if_single_page=False, separator=' ', onclick=None,
1289 show_if_single_page=False, separator=' ', onclick=None,
1289 symbol_first='<<', symbol_last='>>',
1290 symbol_first='<<', symbol_last='>>',
1290 symbol_previous='<', symbol_next='>',
1291 symbol_previous='<', symbol_next='>',
1291 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1292 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1292 curpage_attr={'class': 'pager_curpage'},
1293 curpage_attr={'class': 'pager_curpage'},
1293 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1294 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1294
1295
1295 self.curpage_attr = curpage_attr
1296 self.curpage_attr = curpage_attr
1296 self.separator = separator
1297 self.separator = separator
1297 self.pager_kwargs = kwargs
1298 self.pager_kwargs = kwargs
1298 self.page_param = page_param
1299 self.page_param = page_param
1299 self.partial_param = partial_param
1300 self.partial_param = partial_param
1300 self.onclick = onclick
1301 self.onclick = onclick
1301 self.link_attr = link_attr
1302 self.link_attr = link_attr
1302 self.dotdot_attr = dotdot_attr
1303 self.dotdot_attr = dotdot_attr
1303
1304
1304 # Don't show navigator if there is no more than one page
1305 # Don't show navigator if there is no more than one page
1305 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1306 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1306 return ''
1307 return ''
1307
1308
1308 from string import Template
1309 from string import Template
1309 # Replace ~...~ in token format by range of pages
1310 # Replace ~...~ in token format by range of pages
1310 result = re.sub(r'~(\d+)~', self._range, format)
1311 result = re.sub(r'~(\d+)~', self._range, format)
1311
1312
1312 # Interpolate '%' variables
1313 # Interpolate '%' variables
1313 result = Template(result).safe_substitute({
1314 result = Template(result).safe_substitute({
1314 'first_page': self.first_page,
1315 'first_page': self.first_page,
1315 'last_page': self.last_page,
1316 'last_page': self.last_page,
1316 'page': self.page,
1317 'page': self.page,
1317 'page_count': self.page_count,
1318 'page_count': self.page_count,
1318 'items_per_page': self.items_per_page,
1319 'items_per_page': self.items_per_page,
1319 'first_item': self.first_item,
1320 'first_item': self.first_item,
1320 'last_item': self.last_item,
1321 'last_item': self.last_item,
1321 'item_count': self.item_count,
1322 'item_count': self.item_count,
1322 'link_first': self.page > self.first_page and \
1323 'link_first': self.page > self.first_page and \
1323 self._pagerlink(self.first_page, symbol_first) or '',
1324 self._pagerlink(self.first_page, symbol_first) or '',
1324 'link_last': self.page < self.last_page and \
1325 'link_last': self.page < self.last_page and \
1325 self._pagerlink(self.last_page, symbol_last) or '',
1326 self._pagerlink(self.last_page, symbol_last) or '',
1326 'link_previous': self.previous_page and \
1327 'link_previous': self.previous_page and \
1327 self._pagerlink(self.previous_page, symbol_previous) \
1328 self._pagerlink(self.previous_page, symbol_previous) \
1328 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1329 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1329 'link_next': self.next_page and \
1330 'link_next': self.next_page and \
1330 self._pagerlink(self.next_page, symbol_next) \
1331 self._pagerlink(self.next_page, symbol_next) \
1331 or HTML.span(symbol_next, class_="pg-next disabled")
1332 or HTML.span(symbol_next, class_="pg-next disabled")
1332 })
1333 })
1333
1334
1334 return literal(result)
1335 return literal(result)
1335
1336
1336
1337
1337 #==============================================================================
1338 #==============================================================================
1338 # REPO PAGER, PAGER FOR REPOSITORY
1339 # REPO PAGER, PAGER FOR REPOSITORY
1339 #==============================================================================
1340 #==============================================================================
1340 class RepoPage(Page):
1341 class RepoPage(Page):
1341
1342
1342 def __init__(self, collection, page=1, items_per_page=20,
1343 def __init__(self, collection, page=1, items_per_page=20,
1343 item_count=None, url=None, **kwargs):
1344 item_count=None, url=None, **kwargs):
1344
1345
1345 """Create a "RepoPage" instance. special pager for paging
1346 """Create a "RepoPage" instance. special pager for paging
1346 repository
1347 repository
1347 """
1348 """
1348 self._url_generator = url
1349 self._url_generator = url
1349
1350
1350 # Safe the kwargs class-wide so they can be used in the pager() method
1351 # Safe the kwargs class-wide so they can be used in the pager() method
1351 self.kwargs = kwargs
1352 self.kwargs = kwargs
1352
1353
1353 # Save a reference to the collection
1354 # Save a reference to the collection
1354 self.original_collection = collection
1355 self.original_collection = collection
1355
1356
1356 self.collection = collection
1357 self.collection = collection
1357
1358
1358 # The self.page is the number of the current page.
1359 # The self.page is the number of the current page.
1359 # The first page has the number 1!
1360 # The first page has the number 1!
1360 try:
1361 try:
1361 self.page = int(page) # make it int() if we get it as a string
1362 self.page = int(page) # make it int() if we get it as a string
1362 except (ValueError, TypeError):
1363 except (ValueError, TypeError):
1363 self.page = 1
1364 self.page = 1
1364
1365
1365 self.items_per_page = items_per_page
1366 self.items_per_page = items_per_page
1366
1367
1367 # Unless the user tells us how many items the collections has
1368 # Unless the user tells us how many items the collections has
1368 # we calculate that ourselves.
1369 # we calculate that ourselves.
1369 if item_count is not None:
1370 if item_count is not None:
1370 self.item_count = item_count
1371 self.item_count = item_count
1371 else:
1372 else:
1372 self.item_count = len(self.collection)
1373 self.item_count = len(self.collection)
1373
1374
1374 # Compute the number of the first and last available page
1375 # Compute the number of the first and last available page
1375 if self.item_count > 0:
1376 if self.item_count > 0:
1376 self.first_page = 1
1377 self.first_page = 1
1377 self.page_count = int(math.ceil(float(self.item_count) /
1378 self.page_count = int(math.ceil(float(self.item_count) /
1378 self.items_per_page))
1379 self.items_per_page))
1379 self.last_page = self.first_page + self.page_count - 1
1380 self.last_page = self.first_page + self.page_count - 1
1380
1381
1381 # Make sure that the requested page number is the range of
1382 # Make sure that the requested page number is the range of
1382 # valid pages
1383 # valid pages
1383 if self.page > self.last_page:
1384 if self.page > self.last_page:
1384 self.page = self.last_page
1385 self.page = self.last_page
1385 elif self.page < self.first_page:
1386 elif self.page < self.first_page:
1386 self.page = self.first_page
1387 self.page = self.first_page
1387
1388
1388 # Note: the number of items on this page can be less than
1389 # Note: the number of items on this page can be less than
1389 # items_per_page if the last page is not full
1390 # items_per_page if the last page is not full
1390 self.first_item = max(0, (self.item_count) - (self.page *
1391 self.first_item = max(0, (self.item_count) - (self.page *
1391 items_per_page))
1392 items_per_page))
1392 self.last_item = ((self.item_count - 1) - items_per_page *
1393 self.last_item = ((self.item_count - 1) - items_per_page *
1393 (self.page - 1))
1394 (self.page - 1))
1394
1395
1395 self.items = list(self.collection[self.first_item:self.last_item + 1])
1396 self.items = list(self.collection[self.first_item:self.last_item + 1])
1396
1397
1397 # Links to previous and next page
1398 # Links to previous and next page
1398 if self.page > self.first_page:
1399 if self.page > self.first_page:
1399 self.previous_page = self.page - 1
1400 self.previous_page = self.page - 1
1400 else:
1401 else:
1401 self.previous_page = None
1402 self.previous_page = None
1402
1403
1403 if self.page < self.last_page:
1404 if self.page < self.last_page:
1404 self.next_page = self.page + 1
1405 self.next_page = self.page + 1
1405 else:
1406 else:
1406 self.next_page = None
1407 self.next_page = None
1407
1408
1408 # No items available
1409 # No items available
1409 else:
1410 else:
1410 self.first_page = None
1411 self.first_page = None
1411 self.page_count = 0
1412 self.page_count = 0
1412 self.last_page = None
1413 self.last_page = None
1413 self.first_item = None
1414 self.first_item = None
1414 self.last_item = None
1415 self.last_item = None
1415 self.previous_page = None
1416 self.previous_page = None
1416 self.next_page = None
1417 self.next_page = None
1417 self.items = []
1418 self.items = []
1418
1419
1419 # This is a subclass of the 'list' type. Initialise the list now.
1420 # This is a subclass of the 'list' type. Initialise the list now.
1420 list.__init__(self, reversed(self.items))
1421 list.__init__(self, reversed(self.items))
1421
1422
1422
1423
1423 def changed_tooltip(nodes):
1424 def changed_tooltip(nodes):
1424 """
1425 """
1425 Generates a html string for changed nodes in commit page.
1426 Generates a html string for changed nodes in commit page.
1426 It limits the output to 30 entries
1427 It limits the output to 30 entries
1427
1428
1428 :param nodes: LazyNodesGenerator
1429 :param nodes: LazyNodesGenerator
1429 """
1430 """
1430 if nodes:
1431 if nodes:
1431 pref = ': <br/> '
1432 pref = ': <br/> '
1432 suf = ''
1433 suf = ''
1433 if len(nodes) > 30:
1434 if len(nodes) > 30:
1434 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1435 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1435 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1436 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1436 for x in nodes[:30]]) + suf)
1437 for x in nodes[:30]]) + suf)
1437 else:
1438 else:
1438 return ': ' + _('No Files')
1439 return ': ' + _('No Files')
1439
1440
1440
1441
1441 def breadcrumb_repo_link(repo):
1442 def breadcrumb_repo_link(repo):
1442 """
1443 """
1443 Makes a breadcrumbs path link to repo
1444 Makes a breadcrumbs path link to repo
1444
1445
1445 ex::
1446 ex::
1446 group >> subgroup >> repo
1447 group >> subgroup >> repo
1447
1448
1448 :param repo: a Repository instance
1449 :param repo: a Repository instance
1449 """
1450 """
1450
1451
1451 path = [
1452 path = [
1452 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1453 link_to(group.name, url('repo_group_home', group_name=group.group_name))
1453 for group in repo.groups_with_parents
1454 for group in repo.groups_with_parents
1454 ] + [
1455 ] + [
1455 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1456 link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name))
1456 ]
1457 ]
1457
1458
1458 return literal(' &raquo; '.join(path))
1459 return literal(' &raquo; '.join(path))
1459
1460
1460
1461
1461 def format_byte_size_binary(file_size):
1462 def format_byte_size_binary(file_size):
1462 """
1463 """
1463 Formats file/folder sizes to standard.
1464 Formats file/folder sizes to standard.
1464 """
1465 """
1465 formatted_size = format_byte_size(file_size, binary=True)
1466 formatted_size = format_byte_size(file_size, binary=True)
1466 return formatted_size
1467 return formatted_size
1467
1468
1468
1469
1469 def fancy_file_stats(stats):
1470 def fancy_file_stats(stats):
1470 """
1471 """
1471 Displays a fancy two colored bar for number of added/deleted
1472 Displays a fancy two colored bar for number of added/deleted
1472 lines of code on file
1473 lines of code on file
1473
1474
1474 :param stats: two element list of added/deleted lines of code
1475 :param stats: two element list of added/deleted lines of code
1475 """
1476 """
1476 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1477 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1477 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1478 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1478
1479
1479 def cgen(l_type, a_v, d_v):
1480 def cgen(l_type, a_v, d_v):
1480 mapping = {'tr': 'top-right-rounded-corner-mid',
1481 mapping = {'tr': 'top-right-rounded-corner-mid',
1481 'tl': 'top-left-rounded-corner-mid',
1482 'tl': 'top-left-rounded-corner-mid',
1482 'br': 'bottom-right-rounded-corner-mid',
1483 'br': 'bottom-right-rounded-corner-mid',
1483 'bl': 'bottom-left-rounded-corner-mid'}
1484 'bl': 'bottom-left-rounded-corner-mid'}
1484 map_getter = lambda x: mapping[x]
1485 map_getter = lambda x: mapping[x]
1485
1486
1486 if l_type == 'a' and d_v:
1487 if l_type == 'a' and d_v:
1487 #case when added and deleted are present
1488 #case when added and deleted are present
1488 return ' '.join(map(map_getter, ['tl', 'bl']))
1489 return ' '.join(map(map_getter, ['tl', 'bl']))
1489
1490
1490 if l_type == 'a' and not d_v:
1491 if l_type == 'a' and not d_v:
1491 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1492 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1492
1493
1493 if l_type == 'd' and a_v:
1494 if l_type == 'd' and a_v:
1494 return ' '.join(map(map_getter, ['tr', 'br']))
1495 return ' '.join(map(map_getter, ['tr', 'br']))
1495
1496
1496 if l_type == 'd' and not a_v:
1497 if l_type == 'd' and not a_v:
1497 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1498 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1498
1499
1499 a, d = stats['added'], stats['deleted']
1500 a, d = stats['added'], stats['deleted']
1500 width = 100
1501 width = 100
1501
1502
1502 if stats['binary']: # binary operations like chmod/rename etc
1503 if stats['binary']: # binary operations like chmod/rename etc
1503 lbl = []
1504 lbl = []
1504 bin_op = 0 # undefined
1505 bin_op = 0 # undefined
1505
1506
1506 # prefix with bin for binary files
1507 # prefix with bin for binary files
1507 if BIN_FILENODE in stats['ops']:
1508 if BIN_FILENODE in stats['ops']:
1508 lbl += ['bin']
1509 lbl += ['bin']
1509
1510
1510 if NEW_FILENODE in stats['ops']:
1511 if NEW_FILENODE in stats['ops']:
1511 lbl += [_('new file')]
1512 lbl += [_('new file')]
1512 bin_op = NEW_FILENODE
1513 bin_op = NEW_FILENODE
1513 elif MOD_FILENODE in stats['ops']:
1514 elif MOD_FILENODE in stats['ops']:
1514 lbl += [_('mod')]
1515 lbl += [_('mod')]
1515 bin_op = MOD_FILENODE
1516 bin_op = MOD_FILENODE
1516 elif DEL_FILENODE in stats['ops']:
1517 elif DEL_FILENODE in stats['ops']:
1517 lbl += [_('del')]
1518 lbl += [_('del')]
1518 bin_op = DEL_FILENODE
1519 bin_op = DEL_FILENODE
1519 elif RENAMED_FILENODE in stats['ops']:
1520 elif RENAMED_FILENODE in stats['ops']:
1520 lbl += [_('rename')]
1521 lbl += [_('rename')]
1521 bin_op = RENAMED_FILENODE
1522 bin_op = RENAMED_FILENODE
1522
1523
1523 # chmod can go with other operations, so we add a + to lbl if needed
1524 # chmod can go with other operations, so we add a + to lbl if needed
1524 if CHMOD_FILENODE in stats['ops']:
1525 if CHMOD_FILENODE in stats['ops']:
1525 lbl += [_('chmod')]
1526 lbl += [_('chmod')]
1526 if bin_op == 0:
1527 if bin_op == 0:
1527 bin_op = CHMOD_FILENODE
1528 bin_op = CHMOD_FILENODE
1528
1529
1529 lbl = '+'.join(lbl)
1530 lbl = '+'.join(lbl)
1530 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1531 b_a = '<div class="bin bin%s %s" style="width:100%%">%s</div>' \
1531 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1532 % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1532 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1533 b_d = '<div class="bin bin1" style="width:0%%"></div>'
1533 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1534 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1534
1535
1535 t = stats['added'] + stats['deleted']
1536 t = stats['added'] + stats['deleted']
1536 unit = float(width) / (t or 1)
1537 unit = float(width) / (t or 1)
1537
1538
1538 # needs > 9% of width to be visible or 0 to be hidden
1539 # needs > 9% of width to be visible or 0 to be hidden
1539 a_p = max(9, unit * a) if a > 0 else 0
1540 a_p = max(9, unit * a) if a > 0 else 0
1540 d_p = max(9, unit * d) if d > 0 else 0
1541 d_p = max(9, unit * d) if d > 0 else 0
1541 p_sum = a_p + d_p
1542 p_sum = a_p + d_p
1542
1543
1543 if p_sum > width:
1544 if p_sum > width:
1544 #adjust the percentage to be == 100% since we adjusted to 9
1545 #adjust the percentage to be == 100% since we adjusted to 9
1545 if a_p > d_p:
1546 if a_p > d_p:
1546 a_p = a_p - (p_sum - width)
1547 a_p = a_p - (p_sum - width)
1547 else:
1548 else:
1548 d_p = d_p - (p_sum - width)
1549 d_p = d_p - (p_sum - width)
1549
1550
1550 a_v = a if a > 0 else ''
1551 a_v = a if a > 0 else ''
1551 d_v = d if d > 0 else ''
1552 d_v = d if d > 0 else ''
1552
1553
1553 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1554 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1554 cgen('a', a_v, d_v), a_p, a_v
1555 cgen('a', a_v, d_v), a_p, a_v
1555 )
1556 )
1556 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1557 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1557 cgen('d', a_v, d_v), d_p, d_v
1558 cgen('d', a_v, d_v), d_p, d_v
1558 )
1559 )
1559 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1560 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1560
1561
1561
1562
1562 def urlify_text(text_, safe=True):
1563 def urlify_text(text_, safe=True):
1563 """
1564 """
1564 Extrac urls from text and make html links out of them
1565 Extrac urls from text and make html links out of them
1565
1566
1566 :param text_:
1567 :param text_:
1567 """
1568 """
1568
1569
1569 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1570 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1570 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1571 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1571
1572
1572 def url_func(match_obj):
1573 def url_func(match_obj):
1573 url_full = match_obj.groups()[0]
1574 url_full = match_obj.groups()[0]
1574 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1575 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1575 _newtext = url_pat.sub(url_func, text_)
1576 _newtext = url_pat.sub(url_func, text_)
1576 if safe:
1577 if safe:
1577 return literal(_newtext)
1578 return literal(_newtext)
1578 return _newtext
1579 return _newtext
1579
1580
1580
1581
1581 def urlify_commits(text_, repository):
1582 def urlify_commits(text_, repository):
1582 """
1583 """
1583 Extract commit ids from text and make link from them
1584 Extract commit ids from text and make link from them
1584
1585
1585 :param text_:
1586 :param text_:
1586 :param repository: repo name to build the URL with
1587 :param repository: repo name to build the URL with
1587 """
1588 """
1588 from pylons import url # doh, we need to re-import url to mock it later
1589 from pylons import url # doh, we need to re-import url to mock it later
1589 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1590 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1590
1591
1591 def url_func(match_obj):
1592 def url_func(match_obj):
1592 commit_id = match_obj.groups()[1]
1593 commit_id = match_obj.groups()[1]
1593 pref = match_obj.groups()[0]
1594 pref = match_obj.groups()[0]
1594 suf = match_obj.groups()[2]
1595 suf = match_obj.groups()[2]
1595
1596
1596 tmpl = (
1597 tmpl = (
1597 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1598 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1598 '%(commit_id)s</a>%(suf)s'
1599 '%(commit_id)s</a>%(suf)s'
1599 )
1600 )
1600 return tmpl % {
1601 return tmpl % {
1601 'pref': pref,
1602 'pref': pref,
1602 'cls': 'revision-link',
1603 'cls': 'revision-link',
1603 'url': url('changeset_home', repo_name=repository,
1604 'url': url('changeset_home', repo_name=repository,
1604 revision=commit_id),
1605 revision=commit_id),
1605 'commit_id': commit_id,
1606 'commit_id': commit_id,
1606 'suf': suf
1607 'suf': suf
1607 }
1608 }
1608
1609
1609 newtext = URL_PAT.sub(url_func, text_)
1610 newtext = URL_PAT.sub(url_func, text_)
1610
1611
1611 return newtext
1612 return newtext
1612
1613
1613
1614
1614 def _process_url_func(match_obj, repo_name, uid, entry):
1615 def _process_url_func(match_obj, repo_name, uid, entry):
1615 pref = ''
1616 pref = ''
1616 if match_obj.group().startswith(' '):
1617 if match_obj.group().startswith(' '):
1617 pref = ' '
1618 pref = ' '
1618
1619
1619 issue_id = ''.join(match_obj.groups())
1620 issue_id = ''.join(match_obj.groups())
1620 tmpl = (
1621 tmpl = (
1621 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1622 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1622 '%(issue-prefix)s%(id-repr)s'
1623 '%(issue-prefix)s%(id-repr)s'
1623 '</a>')
1624 '</a>')
1624
1625
1625 (repo_name_cleaned,
1626 (repo_name_cleaned,
1626 parent_group_name) = RepoGroupModel().\
1627 parent_group_name) = RepoGroupModel().\
1627 _get_group_name_and_parent(repo_name)
1628 _get_group_name_and_parent(repo_name)
1628
1629
1629 # variables replacement
1630 # variables replacement
1630 named_vars = {
1631 named_vars = {
1631 'id': issue_id,
1632 'id': issue_id,
1632 'repo': repo_name,
1633 'repo': repo_name,
1633 'repo_name': repo_name_cleaned,
1634 'repo_name': repo_name_cleaned,
1634 'group_name': parent_group_name
1635 'group_name': parent_group_name
1635 }
1636 }
1636 # named regex variables
1637 # named regex variables
1637 named_vars.update(match_obj.groupdict())
1638 named_vars.update(match_obj.groupdict())
1638 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1639 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1639
1640
1640 return tmpl % {
1641 return tmpl % {
1641 'pref': pref,
1642 'pref': pref,
1642 'cls': 'issue-tracker-link',
1643 'cls': 'issue-tracker-link',
1643 'url': _url,
1644 'url': _url,
1644 'id-repr': issue_id,
1645 'id-repr': issue_id,
1645 'issue-prefix': entry['pref'],
1646 'issue-prefix': entry['pref'],
1646 'serv': entry['url'],
1647 'serv': entry['url'],
1647 }
1648 }
1648
1649
1649
1650
1650 def process_patterns(text_string, repo_name, config):
1651 def process_patterns(text_string, repo_name, config):
1651 repo = None
1652 repo = None
1652 if repo_name:
1653 if repo_name:
1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1654 # Retrieving repo_name to avoid invalid repo_name to explode on
1654 # IssueTrackerSettingsModel but still passing invalid name further down
1655 # IssueTrackerSettingsModel but still passing invalid name further down
1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1656 repo = Repository.get_by_repo_name(repo_name, cache=True)
1656
1657
1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1658 settings_model = IssueTrackerSettingsModel(repo=repo)
1658 active_entries = settings_model.get_settings(cache=True)
1659 active_entries = settings_model.get_settings(cache=True)
1659
1660
1660 newtext = text_string
1661 newtext = text_string
1661 for uid, entry in active_entries.items():
1662 for uid, entry in active_entries.items():
1662 url_func = partial(
1663 url_func = partial(
1663 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1664 _process_url_func, repo_name=repo_name, entry=entry, uid=uid)
1664
1665
1665 log.debug('found issue tracker entry with uid %s' % (uid,))
1666 log.debug('found issue tracker entry with uid %s' % (uid,))
1666
1667
1667 if not (entry['pat'] and entry['url']):
1668 if not (entry['pat'] and entry['url']):
1668 log.debug('skipping due to missing data')
1669 log.debug('skipping due to missing data')
1669 continue
1670 continue
1670
1671
1671 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1672 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1672 % (uid, entry['pat'], entry['url'], entry['pref']))
1673 % (uid, entry['pat'], entry['url'], entry['pref']))
1673
1674
1674 try:
1675 try:
1675 pattern = re.compile(r'%s' % entry['pat'])
1676 pattern = re.compile(r'%s' % entry['pat'])
1676 except re.error:
1677 except re.error:
1677 log.exception(
1678 log.exception(
1678 'issue tracker pattern: `%s` failed to compile',
1679 'issue tracker pattern: `%s` failed to compile',
1679 entry['pat'])
1680 entry['pat'])
1680 continue
1681 continue
1681
1682
1682 newtext = pattern.sub(url_func, newtext)
1683 newtext = pattern.sub(url_func, newtext)
1683 log.debug('processed prefix:uid `%s`' % (uid,))
1684 log.debug('processed prefix:uid `%s`' % (uid,))
1684
1685
1685 return newtext
1686 return newtext
1686
1687
1687
1688
1688 def urlify_commit_message(commit_text, repository=None):
1689 def urlify_commit_message(commit_text, repository=None):
1689 """
1690 """
1690 Parses given text message and makes proper links.
1691 Parses given text message and makes proper links.
1691 issues are linked to given issue-server, and rest is a commit link
1692 issues are linked to given issue-server, and rest is a commit link
1692
1693
1693 :param commit_text:
1694 :param commit_text:
1694 :param repository:
1695 :param repository:
1695 """
1696 """
1696 from pylons import url # doh, we need to re-import url to mock it later
1697 from pylons import url # doh, we need to re-import url to mock it later
1697 from rhodecode import CONFIG
1698 from rhodecode import CONFIG
1698
1699
1699 def escaper(string):
1700 def escaper(string):
1700 return string.replace('<', '&lt;').replace('>', '&gt;')
1701 return string.replace('<', '&lt;').replace('>', '&gt;')
1701
1702
1702 newtext = escaper(commit_text)
1703 newtext = escaper(commit_text)
1703 # urlify commits - extract commit ids and make link out of them, if we have
1704 # urlify commits - extract commit ids and make link out of them, if we have
1704 # the scope of repository present.
1705 # the scope of repository present.
1705 if repository:
1706 if repository:
1706 newtext = urlify_commits(newtext, repository)
1707 newtext = urlify_commits(newtext, repository)
1707
1708
1708 # extract http/https links and make them real urls
1709 # extract http/https links and make them real urls
1709 newtext = urlify_text(newtext, safe=False)
1710 newtext = urlify_text(newtext, safe=False)
1710
1711
1711 # process issue tracker patterns
1712 # process issue tracker patterns
1712 newtext = process_patterns(newtext, repository or '', CONFIG)
1713 newtext = process_patterns(newtext, repository or '', CONFIG)
1713
1714
1714 return literal(newtext)
1715 return literal(newtext)
1715
1716
1716
1717
1717 def rst(source, mentions=False):
1718 def rst(source, mentions=False):
1718 return literal('<div class="rst-block">%s</div>' %
1719 return literal('<div class="rst-block">%s</div>' %
1719 MarkupRenderer.rst(source, mentions=mentions))
1720 MarkupRenderer.rst(source, mentions=mentions))
1720
1721
1721
1722
1722 def markdown(source, mentions=False):
1723 def markdown(source, mentions=False):
1723 return literal('<div class="markdown-block">%s</div>' %
1724 return literal('<div class="markdown-block">%s</div>' %
1724 MarkupRenderer.markdown(source, flavored=True,
1725 MarkupRenderer.markdown(source, flavored=True,
1725 mentions=mentions))
1726 mentions=mentions))
1726
1727
1727 def renderer_from_filename(filename, exclude=None):
1728 def renderer_from_filename(filename, exclude=None):
1728 from rhodecode.config.conf import MARKDOWN_EXTS, RST_EXTS
1729 from rhodecode.config.conf import MARKDOWN_EXTS, RST_EXTS
1729
1730
1730 def _filter(elements):
1731 def _filter(elements):
1731 if isinstance(exclude, (list, tuple)):
1732 if isinstance(exclude, (list, tuple)):
1732 return [x for x in elements if x not in exclude]
1733 return [x for x in elements if x not in exclude]
1733 return elements
1734 return elements
1734
1735
1735 if filename.endswith(tuple(_filter([x[0] for x in MARKDOWN_EXTS if x[0]]))):
1736 if filename.endswith(tuple(_filter([x[0] for x in MARKDOWN_EXTS if x[0]]))):
1736 return 'markdown'
1737 return 'markdown'
1737 if filename.endswith(tuple(_filter([x[0] for x in RST_EXTS if x[0]]))):
1738 if filename.endswith(tuple(_filter([x[0] for x in RST_EXTS if x[0]]))):
1738 return 'rst'
1739 return 'rst'
1739
1740
1740
1741
1741 def render(source, renderer='rst', mentions=False):
1742 def render(source, renderer='rst', mentions=False):
1742 if renderer == 'rst':
1743 if renderer == 'rst':
1743 return rst(source, mentions=mentions)
1744 return rst(source, mentions=mentions)
1744 if renderer == 'markdown':
1745 if renderer == 'markdown':
1745 return markdown(source, mentions=mentions)
1746 return markdown(source, mentions=mentions)
1746
1747
1747
1748
1748 def commit_status(repo, commit_id):
1749 def commit_status(repo, commit_id):
1749 return ChangesetStatusModel().get_status(repo, commit_id)
1750 return ChangesetStatusModel().get_status(repo, commit_id)
1750
1751
1751
1752
1752 def commit_status_lbl(commit_status):
1753 def commit_status_lbl(commit_status):
1753 return dict(ChangesetStatus.STATUSES).get(commit_status)
1754 return dict(ChangesetStatus.STATUSES).get(commit_status)
1754
1755
1755
1756
1756 def commit_time(repo_name, commit_id):
1757 def commit_time(repo_name, commit_id):
1757 repo = Repository.get_by_repo_name(repo_name)
1758 repo = Repository.get_by_repo_name(repo_name)
1758 commit = repo.get_commit(commit_id=commit_id)
1759 commit = repo.get_commit(commit_id=commit_id)
1759 return commit.date
1760 return commit.date
1760
1761
1761
1762
1762 def get_permission_name(key):
1763 def get_permission_name(key):
1763 return dict(Permission.PERMS).get(key)
1764 return dict(Permission.PERMS).get(key)
1764
1765
1765
1766
1766 def journal_filter_help():
1767 def journal_filter_help():
1767 return _(
1768 return _(
1768 'Example filter terms:\n' +
1769 'Example filter terms:\n' +
1769 ' repository:vcs\n' +
1770 ' repository:vcs\n' +
1770 ' username:marcin\n' +
1771 ' username:marcin\n' +
1771 ' action:*push*\n' +
1772 ' action:*push*\n' +
1772 ' ip:127.0.0.1\n' +
1773 ' ip:127.0.0.1\n' +
1773 ' date:20120101\n' +
1774 ' date:20120101\n' +
1774 ' date:[20120101100000 TO 20120102]\n' +
1775 ' date:[20120101100000 TO 20120102]\n' +
1775 '\n' +
1776 '\n' +
1776 'Generate wildcards using \'*\' character:\n' +
1777 'Generate wildcards using \'*\' character:\n' +
1777 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1778 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1778 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1779 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1779 '\n' +
1780 '\n' +
1780 'Optional AND / OR operators in queries\n' +
1781 'Optional AND / OR operators in queries\n' +
1781 ' "repository:vcs OR repository:test"\n' +
1782 ' "repository:vcs OR repository:test"\n' +
1782 ' "username:test AND repository:test*"\n'
1783 ' "username:test AND repository:test*"\n'
1783 )
1784 )
1784
1785
1785
1786
1786 def not_mapped_error(repo_name):
1787 def not_mapped_error(repo_name):
1787 flash(_('%s repository is not mapped to db perhaps'
1788 flash(_('%s repository is not mapped to db perhaps'
1788 ' it was created or renamed from the filesystem'
1789 ' it was created or renamed from the filesystem'
1789 ' please run the application again'
1790 ' please run the application again'
1790 ' in order to rescan repositories') % repo_name, category='error')
1791 ' in order to rescan repositories') % repo_name, category='error')
1791
1792
1792
1793
1793 def ip_range(ip_addr):
1794 def ip_range(ip_addr):
1794 from rhodecode.model.db import UserIpMap
1795 from rhodecode.model.db import UserIpMap
1795 s, e = UserIpMap._get_ip_range(ip_addr)
1796 s, e = UserIpMap._get_ip_range(ip_addr)
1796 return '%s - %s' % (s, e)
1797 return '%s - %s' % (s, e)
1797
1798
1798
1799
1799 def form(url, method='post', needs_csrf_token=True, **attrs):
1800 def form(url, method='post', needs_csrf_token=True, **attrs):
1800 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1801 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1801 if method.lower() != 'get' and needs_csrf_token:
1802 if method.lower() != 'get' and needs_csrf_token:
1802 raise Exception(
1803 raise Exception(
1803 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1804 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1804 'CSRF token. If the endpoint does not require such token you can ' +
1805 'CSRF token. If the endpoint does not require such token you can ' +
1805 'explicitly set the parameter needs_csrf_token to false.')
1806 'explicitly set the parameter needs_csrf_token to false.')
1806
1807
1807 return wh_form(url, method=method, **attrs)
1808 return wh_form(url, method=method, **attrs)
1808
1809
1809
1810
1810 def secure_form(url, method="POST", multipart=False, **attrs):
1811 def secure_form(url, method="POST", multipart=False, **attrs):
1811 """Start a form tag that points the action to an url. This
1812 """Start a form tag that points the action to an url. This
1812 form tag will also include the hidden field containing
1813 form tag will also include the hidden field containing
1813 the auth token.
1814 the auth token.
1814
1815
1815 The url options should be given either as a string, or as a
1816 The url options should be given either as a string, or as a
1816 ``url()`` function. The method for the form defaults to POST.
1817 ``url()`` function. The method for the form defaults to POST.
1817
1818
1818 Options:
1819 Options:
1819
1820
1820 ``multipart``
1821 ``multipart``
1821 If set to True, the enctype is set to "multipart/form-data".
1822 If set to True, the enctype is set to "multipart/form-data".
1822 ``method``
1823 ``method``
1823 The method to use when submitting the form, usually either
1824 The method to use when submitting the form, usually either
1824 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1825 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1825 hidden input with name _method is added to simulate the verb
1826 hidden input with name _method is added to simulate the verb
1826 over POST.
1827 over POST.
1827
1828
1828 """
1829 """
1829 from webhelpers.pylonslib.secure_form import insecure_form
1830 from webhelpers.pylonslib.secure_form import insecure_form
1830 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1831 from rhodecode.lib.auth import get_csrf_token, csrf_token_key
1831 form = insecure_form(url, method, multipart, **attrs)
1832 form = insecure_form(url, method, multipart, **attrs)
1832 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1833 token = HTML.div(hidden(csrf_token_key, get_csrf_token()), style="display: none;")
1833 return literal("%s\n%s" % (form, token))
1834 return literal("%s\n%s" % (form, token))
1834
1835
1835 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1836 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1836 select_html = select(name, selected, options, **attrs)
1837 select_html = select(name, selected, options, **attrs)
1837 select2 = """
1838 select2 = """
1838 <script>
1839 <script>
1839 $(document).ready(function() {
1840 $(document).ready(function() {
1840 $('#%s').select2({
1841 $('#%s').select2({
1841 containerCssClass: 'drop-menu',
1842 containerCssClass: 'drop-menu',
1842 dropdownCssClass: 'drop-menu-dropdown',
1843 dropdownCssClass: 'drop-menu-dropdown',
1843 dropdownAutoWidth: true%s
1844 dropdownAutoWidth: true%s
1844 });
1845 });
1845 });
1846 });
1846 </script>
1847 </script>
1847 """
1848 """
1848 filter_option = """,
1849 filter_option = """,
1849 minimumResultsForSearch: -1
1850 minimumResultsForSearch: -1
1850 """
1851 """
1851 input_id = attrs.get('id') or name
1852 input_id = attrs.get('id') or name
1852 filter_enabled = "" if enable_filter else filter_option
1853 filter_enabled = "" if enable_filter else filter_option
1853 select_script = literal(select2 % (input_id, filter_enabled))
1854 select_script = literal(select2 % (input_id, filter_enabled))
1854
1855
1855 return literal(select_html+select_script)
1856 return literal(select_html+select_script)
1856
1857
1857
1858
1858 def get_visual_attr(tmpl_context_var, attr_name):
1859 def get_visual_attr(tmpl_context_var, attr_name):
1859 """
1860 """
1860 A safe way to get a variable from visual variable of template context
1861 A safe way to get a variable from visual variable of template context
1861
1862
1862 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1863 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1863 :param attr_name: name of the attribute we fetch from the c.visual
1864 :param attr_name: name of the attribute we fetch from the c.visual
1864 """
1865 """
1865 visual = getattr(tmpl_context_var, 'visual', None)
1866 visual = getattr(tmpl_context_var, 'visual', None)
1866 if not visual:
1867 if not visual:
1867 return
1868 return
1868 else:
1869 else:
1869 return getattr(visual, attr_name, None)
1870 return getattr(visual, attr_name, None)
1870
1871
1871
1872
1872 def get_last_path_part(file_node):
1873 def get_last_path_part(file_node):
1873 if not file_node.path:
1874 if not file_node.path:
1874 return u''
1875 return u''
1875
1876
1876 path = safe_unicode(file_node.path.split('/')[-1])
1877 path = safe_unicode(file_node.path.split('/')[-1])
1877 return u'../' + path
1878 return u'../' + path
1878
1879
1879
1880
1880 def route_path(*args, **kwds):
1881 def route_path(*args, **kwds):
1881 """
1882 """
1882 Wrapper around pyramids `route_path` function. It is used to generate
1883 Wrapper around pyramids `route_path` function. It is used to generate
1883 URLs from within pylons views or templates. This will be removed when
1884 URLs from within pylons views or templates. This will be removed when
1884 pyramid migration if finished.
1885 pyramid migration if finished.
1885 """
1886 """
1886 req = get_current_request()
1887 req = get_current_request()
1887 return req.route_path(*args, **kwds)
1888 return req.route_path(*args, **kwds)
1888
1889
1889
1890
1890 def resource_path(*args, **kwds):
1891 def resource_path(*args, **kwds):
1891 """
1892 """
1892 Wrapper around pyramids `route_path` function. It is used to generate
1893 Wrapper around pyramids `route_path` function. It is used to generate
1893 URLs from within pylons views or templates. This will be removed when
1894 URLs from within pylons views or templates. This will be removed when
1894 pyramid migration if finished.
1895 pyramid migration if finished.
1895 """
1896 """
1896 req = get_current_request()
1897 req = get_current_request()
1897 return req.resource_path(*args, **kwds)
1898 return req.resource_path(*args, **kwds)
@@ -1,170 +1,171 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <!DOCTYPE html>
2 <!DOCTYPE html>
3
3
4 <%def name="get_template_context()" filter="n, trim">{
4 <%def name="get_template_context()" filter="n, trim">{
5
5
6 ## repo data
6 ## repo data
7 repo_name: "${getattr(c, 'repo_name', '')}",
7 repo_name: "${getattr(c, 'repo_name', '')}",
8 % if hasattr(c, 'rhodecode_db_repo'):
8 % if hasattr(c, 'rhodecode_db_repo'):
9 repo_type: "${c.rhodecode_db_repo.repo_type}",
9 repo_type: "${c.rhodecode_db_repo.repo_type}",
10 repo_landing_commit: "${c.rhodecode_db_repo.landing_rev[1]}",
10 repo_landing_commit: "${c.rhodecode_db_repo.landing_rev[1]}",
11 % else:
11 % else:
12 repo_type: null,
12 repo_type: null,
13 repo_landing_commit: null,
13 repo_landing_commit: null,
14 % endif
14 % endif
15
15
16 ## user data
16 ## user data
17 % if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
17 % if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 rhodecode_user: {
18 rhodecode_user: {
19 username: "${c.rhodecode_user.username}",
19 username: "${c.rhodecode_user.username}",
20 email: "${c.rhodecode_user.email}",
20 email: "${c.rhodecode_user.email}",
21 },
21 },
22 % else:
22 % else:
23 rhodecode_user: {
23 rhodecode_user: {
24 username: null,
24 username: null,
25 email: null,
25 email: null,
26 },
26 },
27 % endif
27 % endif
28
28
29 ## visual settings
29 ## visual settings
30 visual: {
30 visual: {
31 default_renderer: "${h.get_visual_attr(c, 'default_renderer')}"
31 default_renderer: "${h.get_visual_attr(c, 'default_renderer')}"
32 },
32 },
33
33
34 ## current commit context, filled inside templates that expose that
34 ## current commit context, filled inside templates that expose that
35 commit_data: {
35 commit_data: {
36 commit_id: null,
36 commit_id: null,
37 },
37 },
38
38
39 ## current pr context, filled inside templates that expose that
39 ## current pr context, filled inside templates that expose that
40 pull_request_data: {
40 pull_request_data: {
41 pull_request_id: null,
41 pull_request_id: null,
42 },
42 },
43
43
44 ## timeago settings, can be overwritten by custom user settings later
44 ## timeago settings, can be overwritten by custom user settings later
45 timeago: {
45 timeago: {
46 refresh_time: ${120 * 1000},
46 refresh_time: ${120 * 1000},
47 cutoff_limit: ${1000*60*60*24*7}
47 cutoff_limit: ${1000*60*60*24*7}
48 }
48 },
49 dispatch_info: ${h.json.dumps(getattr(c, 'pylons_dispatch_info', {}))|n}
49 }
50 }
50
51
51 </%def>
52 </%def>
52
53
53 <html xmlns="http://www.w3.org/1999/xhtml">
54 <html xmlns="http://www.w3.org/1999/xhtml">
54 <head>
55 <head>
55 <title>${self.title()}</title>
56 <title>${self.title()}</title>
56 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
57 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
57 <%def name="robots()">
58 <%def name="robots()">
58 <meta name="robots" content="index, nofollow"/>
59 <meta name="robots" content="index, nofollow"/>
59 </%def>
60 </%def>
60 ${self.robots()}
61 ${self.robots()}
61 <link rel="icon" href="${h.url('/images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
62 <link rel="icon" href="${h.url('/images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
62
63
63 ## CSS definitions
64 ## CSS definitions
64 <%def name="css()">
65 <%def name="css()">
65 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
66 <link rel="stylesheet" type="text/css" href="${h.url('/css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
66 <!--[if lt IE 9]>
67 <!--[if lt IE 9]>
67 <link rel="stylesheet" type="text/css" href="${h.url('/css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
68 <link rel="stylesheet" type="text/css" href="${h.url('/css/ie.css', ver=c.rhodecode_version_hash)}" media="screen"/>
68 <![endif]-->
69 <![endif]-->
69 ## EXTRA FOR CSS
70 ## EXTRA FOR CSS
70 ${self.css_extra()}
71 ${self.css_extra()}
71 </%def>
72 </%def>
72 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
73 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
73 <%def name="css_extra()">
74 <%def name="css_extra()">
74 </%def>
75 </%def>
75
76
76 ${self.css()}
77 ${self.css()}
77
78
78 ## JAVASCRIPT
79 ## JAVASCRIPT
79 <%def name="js()">
80 <%def name="js()">
80 <script src="${h.url('/js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
81 <script src="${h.url('/js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
81 <script type="text/javascript">
82 <script type="text/javascript">
82 // register templateContext to pass template variables to JS
83 // register templateContext to pass template variables to JS
83 var templateContext = ${get_template_context()};
84 var templateContext = ${get_template_context()};
84
85
85 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
86 var REPO_NAME = "${getattr(c, 'repo_name', '')}";
86 %if hasattr(c, 'rhodecode_db_repo'):
87 %if hasattr(c, 'rhodecode_db_repo'):
87 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
88 var REPO_LANDING_REV = '${c.rhodecode_db_repo.landing_rev[1]}';
88 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
89 var REPO_TYPE = '${c.rhodecode_db_repo.repo_type}';
89 %else:
90 %else:
90 var REPO_LANDING_REV = '';
91 var REPO_LANDING_REV = '';
91 var REPO_TYPE = '';
92 var REPO_TYPE = '';
92 %endif
93 %endif
93 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
94 var APPLICATION_URL = "${h.url('home').rstrip('/')}";
94 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
95 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
95 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
96 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
96 % if getattr(c, 'rhodecode_user', None):
97 % if getattr(c, 'rhodecode_user', None):
97 var USER = {name:'${c.rhodecode_user.username}'};
98 var USER = {name:'${c.rhodecode_user.username}'};
98 % else:
99 % else:
99 var USER = {name:null};
100 var USER = {name:null};
100 % endif
101 % endif
101
102
102 var APPENLIGHT = {
103 var APPENLIGHT = {
103 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
104 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
104 key: '${getattr(c, "appenlight_api_public_key", "")}',
105 key: '${getattr(c, "appenlight_api_public_key", "")}',
105 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
106 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
106 requestInfo: {
107 requestInfo: {
107 % if getattr(c, 'rhodecode_user', None):
108 % if getattr(c, 'rhodecode_user', None):
108 ip: '${c.rhodecode_user.ip_addr}',
109 ip: '${c.rhodecode_user.ip_addr}',
109 username: '${c.rhodecode_user.username}'
110 username: '${c.rhodecode_user.username}'
110 % endif
111 % endif
111 }
112 }
112 };
113 };
113 </script>
114 </script>
114
115
115 <!--[if lt IE 9]>
116 <!--[if lt IE 9]>
116 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
117 <script language="javascript" type="text/javascript" src="${h.url('/js/excanvas.min.js')}"></script>
117 <![endif]-->
118 <![endif]-->
118 <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
119 <script language="javascript" type="text/javascript" src="${h.url('/js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
119 <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
120 <script language="javascript" type="text/javascript" src="${h.url('/js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
120 <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
121 <script>CodeMirror.modeURL = "${h.url('/js/mode/%N/%N.js')}";</script>
121
122
122 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
123 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
123 ${self.js_extra()}
124 ${self.js_extra()}
124
125
125 <script type="text/javascript">
126 <script type="text/javascript">
126 $(document).ready(function(){
127 $(document).ready(function(){
127 show_more_event();
128 show_more_event();
128 timeagoActivate();
129 timeagoActivate();
129 })
130 })
130 </script>
131 </script>
131
132
132 </%def>
133 </%def>
133
134
134 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
135 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
135 <%def name="js_extra()"></%def>
136 <%def name="js_extra()"></%def>
136 ${self.js()}
137 ${self.js()}
137
138
138 <%def name="head_extra()"></%def>
139 <%def name="head_extra()"></%def>
139 ${self.head_extra()}
140 ${self.head_extra()}
140
141
141 <%include file="/base/plugins_base.html"/>
142 <%include file="/base/plugins_base.html"/>
142
143
143 ## extra stuff
144 ## extra stuff
144 %if c.pre_code:
145 %if c.pre_code:
145 ${c.pre_code|n}
146 ${c.pre_code|n}
146 %endif
147 %endif
147 </head>
148 </head>
148 <body id="body">
149 <body id="body">
149 <noscript>
150 <noscript>
150 <div class="noscript-error">
151 <div class="noscript-error">
151 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
152 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
152 </div>
153 </div>
153 </noscript>
154 </noscript>
154 ## IE hacks
155 ## IE hacks
155 <!--[if IE 7]>
156 <!--[if IE 7]>
156 <script>$(document.body).addClass('ie7')</script>
157 <script>$(document.body).addClass('ie7')</script>
157 <![endif]-->
158 <![endif]-->
158 <!--[if IE 8]>
159 <!--[if IE 8]>
159 <script>$(document.body).addClass('ie8')</script>
160 <script>$(document.body).addClass('ie8')</script>
160 <![endif]-->
161 <![endif]-->
161 <!--[if IE 9]>
162 <!--[if IE 9]>
162 <script>$(document.body).addClass('ie9')</script>
163 <script>$(document.body).addClass('ie9')</script>
163 <![endif]-->
164 <![endif]-->
164
165
165 ${next.body()}
166 ${next.body()}
166 %if c.post_code:
167 %if c.post_code:
167 ${c.post_code|n}
168 ${c.post_code|n}
168 %endif
169 %endif
169 </body>
170 </body>
170 </html>
171 </html>
General Comments 0
You need to be logged in to leave comments. Login now