##// END OF EJS Templates
json: fixed calls to json after orjson implementation
super-admin -
r4974:37813a48 default
parent child Browse files
Show More
@@ -1,1322 +1,1321 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23 import formencode
24 import formencode.htmlfill
24 import formencode.htmlfill
25
25
26 from pyramid.httpexceptions import HTTPFound
26 from pyramid.httpexceptions import HTTPFound
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode import events
30 from rhodecode import events
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
33 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
34 from rhodecode.authentication.plugins import auth_rhodecode
34 from rhodecode.authentication.plugins import auth_rhodecode
35 from rhodecode.events import trigger
35 from rhodecode.events import trigger
36 from rhodecode.model.db import true, UserNotice
36 from rhodecode.model.db import true, UserNotice
37
37
38 from rhodecode.lib import audit_logger, rc_cache, auth
38 from rhodecode.lib import audit_logger, rc_cache, auth
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
40 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
41 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
42 UserOwnsArtifactsException, DefaultUserException)
42 UserOwnsArtifactsException, DefaultUserException)
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib import ext_json
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 from rhodecode.lib import helpers as h
46 from rhodecode.lib import helpers as h
47 from rhodecode.lib.helpers import SqlPage
47 from rhodecode.lib.helpers import SqlPage
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 from rhodecode.model.auth_token import AuthTokenModel
49 from rhodecode.model.auth_token import AuthTokenModel
50 from rhodecode.model.forms import (
50 from rhodecode.model.forms import (
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 UserExtraEmailForm, UserExtraIpForm)
52 UserExtraEmailForm, UserExtraIpForm)
53 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
54 from rhodecode.model.repo_group import RepoGroupModel
54 from rhodecode.model.repo_group import RepoGroupModel
55 from rhodecode.model.ssh_key import SshKeyModel
55 from rhodecode.model.ssh_key import SshKeyModel
56 from rhodecode.model.user import UserModel
56 from rhodecode.model.user import UserModel
57 from rhodecode.model.user_group import UserGroupModel
57 from rhodecode.model.user_group import UserGroupModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 UserApiKeys, UserSshKeys, RepoGroup)
60 UserApiKeys, UserSshKeys, RepoGroup)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65
65
66 class AdminUsersView(BaseAppView, DataGridAppView):
66 class AdminUsersView(BaseAppView, DataGridAppView):
67
67
68 def load_default_context(self):
68 def load_default_context(self):
69 c = self._get_local_tmpl_context()
69 c = self._get_local_tmpl_context()
70 return c
70 return c
71
71
72 @LoginRequired()
72 @LoginRequired()
73 @HasPermissionAllDecorator('hg.admin')
73 @HasPermissionAllDecorator('hg.admin')
74 def users_list(self):
74 def users_list(self):
75 c = self.load_default_context()
75 c = self.load_default_context()
76 return self._get_template_context(c)
76 return self._get_template_context(c)
77
77
78 @LoginRequired()
78 @LoginRequired()
79 @HasPermissionAllDecorator('hg.admin')
79 @HasPermissionAllDecorator('hg.admin')
80 def users_list_data(self):
80 def users_list_data(self):
81 self.load_default_context()
81 self.load_default_context()
82 column_map = {
82 column_map = {
83 'first_name': 'name',
83 'first_name': 'name',
84 'last_name': 'lastname',
84 'last_name': 'lastname',
85 }
85 }
86 draw, start, limit = self._extract_chunk(self.request)
86 draw, start, limit = self._extract_chunk(self.request)
87 search_q, order_by, order_dir = self._extract_ordering(
87 search_q, order_by, order_dir = self._extract_ordering(
88 self.request, column_map=column_map)
88 self.request, column_map=column_map)
89 _render = self.request.get_partial_renderer(
89 _render = self.request.get_partial_renderer(
90 'rhodecode:templates/data_table/_dt_elements.mako')
90 'rhodecode:templates/data_table/_dt_elements.mako')
91
91
92 def user_actions(user_id, username):
92 def user_actions(user_id, username):
93 return _render("user_actions", user_id, username)
93 return _render("user_actions", user_id, username)
94
94
95 users_data_total_count = User.query()\
95 users_data_total_count = User.query()\
96 .filter(User.username != User.DEFAULT_USER) \
96 .filter(User.username != User.DEFAULT_USER) \
97 .count()
97 .count()
98
98
99 users_data_total_inactive_count = User.query()\
99 users_data_total_inactive_count = User.query()\
100 .filter(User.username != User.DEFAULT_USER) \
100 .filter(User.username != User.DEFAULT_USER) \
101 .filter(User.active != true())\
101 .filter(User.active != true())\
102 .count()
102 .count()
103
103
104 # json generate
104 # json generate
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105 base_q = User.query().filter(User.username != User.DEFAULT_USER)
106 base_inactive_q = base_q.filter(User.active != true())
106 base_inactive_q = base_q.filter(User.active != true())
107
107
108 if search_q:
108 if search_q:
109 like_expression = u'%{}%'.format(safe_unicode(search_q))
109 like_expression = u'%{}%'.format(safe_unicode(search_q))
110 base_q = base_q.filter(or_(
110 base_q = base_q.filter(or_(
111 User.username.ilike(like_expression),
111 User.username.ilike(like_expression),
112 User._email.ilike(like_expression),
112 User._email.ilike(like_expression),
113 User.name.ilike(like_expression),
113 User.name.ilike(like_expression),
114 User.lastname.ilike(like_expression),
114 User.lastname.ilike(like_expression),
115 ))
115 ))
116 base_inactive_q = base_q.filter(User.active != true())
116 base_inactive_q = base_q.filter(User.active != true())
117
117
118 users_data_total_filtered_count = base_q.count()
118 users_data_total_filtered_count = base_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
119 users_data_total_filtered_inactive_count = base_inactive_q.count()
120
120
121 sort_col = getattr(User, order_by, None)
121 sort_col = getattr(User, order_by, None)
122 if sort_col:
122 if sort_col:
123 if order_dir == 'asc':
123 if order_dir == 'asc':
124 # handle null values properly to order by NULL last
124 # handle null values properly to order by NULL last
125 if order_by in ['last_activity']:
125 if order_by in ['last_activity']:
126 sort_col = coalesce(sort_col, datetime.date.max)
126 sort_col = coalesce(sort_col, datetime.date.max)
127 sort_col = sort_col.asc()
127 sort_col = sort_col.asc()
128 else:
128 else:
129 # handle null values properly to order by NULL last
129 # handle null values properly to order by NULL last
130 if order_by in ['last_activity']:
130 if order_by in ['last_activity']:
131 sort_col = coalesce(sort_col, datetime.date.min)
131 sort_col = coalesce(sort_col, datetime.date.min)
132 sort_col = sort_col.desc()
132 sort_col = sort_col.desc()
133
133
134 base_q = base_q.order_by(sort_col)
134 base_q = base_q.order_by(sort_col)
135 base_q = base_q.offset(start).limit(limit)
135 base_q = base_q.offset(start).limit(limit)
136
136
137 users_list = base_q.all()
137 users_list = base_q.all()
138
138
139 users_data = []
139 users_data = []
140 for user in users_list:
140 for user in users_list:
141 users_data.append({
141 users_data.append({
142 "username": h.gravatar_with_user(self.request, user.username),
142 "username": h.gravatar_with_user(self.request, user.username),
143 "email": user.email,
143 "email": user.email,
144 "first_name": user.first_name,
144 "first_name": user.first_name,
145 "last_name": user.last_name,
145 "last_name": user.last_name,
146 "last_login": h.format_date(user.last_login),
146 "last_login": h.format_date(user.last_login),
147 "last_activity": h.format_date(user.last_activity),
147 "last_activity": h.format_date(user.last_activity),
148 "active": h.bool2icon(user.active),
148 "active": h.bool2icon(user.active),
149 "active_raw": user.active,
149 "active_raw": user.active,
150 "admin": h.bool2icon(user.admin),
150 "admin": h.bool2icon(user.admin),
151 "extern_type": user.extern_type,
151 "extern_type": user.extern_type,
152 "extern_name": user.extern_name,
152 "extern_name": user.extern_name,
153 "action": user_actions(user.user_id, user.username),
153 "action": user_actions(user.user_id, user.username),
154 })
154 })
155 data = ({
155 data = ({
156 'draw': draw,
156 'draw': draw,
157 'data': users_data,
157 'data': users_data,
158 'recordsTotal': users_data_total_count,
158 'recordsTotal': users_data_total_count,
159 'recordsFiltered': users_data_total_filtered_count,
159 'recordsFiltered': users_data_total_filtered_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
160 'recordsTotalInactive': users_data_total_inactive_count,
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
161 'recordsFilteredInactive': users_data_total_filtered_inactive_count
162 })
162 })
163
163
164 return data
164 return data
165
165
166 def _set_personal_repo_group_template_vars(self, c_obj):
166 def _set_personal_repo_group_template_vars(self, c_obj):
167 DummyUser = AttributeDict({
167 DummyUser = AttributeDict({
168 'username': '${username}',
168 'username': '${username}',
169 'user_id': '${user_id}',
169 'user_id': '${user_id}',
170 })
170 })
171 c_obj.default_create_repo_group = RepoGroupModel() \
171 c_obj.default_create_repo_group = RepoGroupModel() \
172 .get_default_create_personal_repo_group()
172 .get_default_create_personal_repo_group()
173 c_obj.personal_repo_group_name = RepoGroupModel() \
173 c_obj.personal_repo_group_name = RepoGroupModel() \
174 .get_personal_group_name(DummyUser)
174 .get_personal_group_name(DummyUser)
175
175
176 @LoginRequired()
176 @LoginRequired()
177 @HasPermissionAllDecorator('hg.admin')
177 @HasPermissionAllDecorator('hg.admin')
178 def users_new(self):
178 def users_new(self):
179 _ = self.request.translate
179 _ = self.request.translate
180 c = self.load_default_context()
180 c = self.load_default_context()
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
181 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
182 self._set_personal_repo_group_template_vars(c)
182 self._set_personal_repo_group_template_vars(c)
183 return self._get_template_context(c)
183 return self._get_template_context(c)
184
184
185 @LoginRequired()
185 @LoginRequired()
186 @HasPermissionAllDecorator('hg.admin')
186 @HasPermissionAllDecorator('hg.admin')
187 @CSRFRequired()
187 @CSRFRequired()
188 def users_create(self):
188 def users_create(self):
189 _ = self.request.translate
189 _ = self.request.translate
190 c = self.load_default_context()
190 c = self.load_default_context()
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 user_model = UserModel()
192 user_model = UserModel()
193 user_form = UserForm(self.request.translate)()
193 user_form = UserForm(self.request.translate)()
194 try:
194 try:
195 form_result = user_form.to_python(dict(self.request.POST))
195 form_result = user_form.to_python(dict(self.request.POST))
196 user = user_model.create(form_result)
196 user = user_model.create(form_result)
197 Session().flush()
197 Session().flush()
198 creation_data = user.get_api_data()
198 creation_data = user.get_api_data()
199 username = form_result['username']
199 username = form_result['username']
200
200
201 audit_logger.store_web(
201 audit_logger.store_web(
202 'user.create', action_data={'data': creation_data},
202 'user.create', action_data={'data': creation_data},
203 user=c.rhodecode_user)
203 user=c.rhodecode_user)
204
204
205 user_link = h.link_to(
205 user_link = h.link_to(
206 h.escape(username),
206 h.escape(username),
207 h.route_path('user_edit', user_id=user.user_id))
207 h.route_path('user_edit', user_id=user.user_id))
208 h.flash(h.literal(_('Created user %(user_link)s')
208 h.flash(h.literal(_('Created user %(user_link)s')
209 % {'user_link': user_link}), category='success')
209 % {'user_link': user_link}), category='success')
210 Session().commit()
210 Session().commit()
211 except formencode.Invalid as errors:
211 except formencode.Invalid as errors:
212 self._set_personal_repo_group_template_vars(c)
212 self._set_personal_repo_group_template_vars(c)
213 data = render(
213 data = render(
214 'rhodecode:templates/admin/users/user_add.mako',
214 'rhodecode:templates/admin/users/user_add.mako',
215 self._get_template_context(c), self.request)
215 self._get_template_context(c), self.request)
216 html = formencode.htmlfill.render(
216 html = formencode.htmlfill.render(
217 data,
217 data,
218 defaults=errors.value,
218 defaults=errors.value,
219 errors=errors.error_dict or {},
219 errors=errors.error_dict or {},
220 prefix_error=False,
220 prefix_error=False,
221 encoding="UTF-8",
221 encoding="UTF-8",
222 force_defaults=False
222 force_defaults=False
223 )
223 )
224 return Response(html)
224 return Response(html)
225 except UserCreationError as e:
225 except UserCreationError as e:
226 h.flash(e, 'error')
226 h.flash(e, 'error')
227 except Exception:
227 except Exception:
228 log.exception("Exception creation of user")
228 log.exception("Exception creation of user")
229 h.flash(_('Error occurred during creation of user %s')
229 h.flash(_('Error occurred during creation of user %s')
230 % self.request.POST.get('username'), category='error')
230 % self.request.POST.get('username'), category='error')
231 raise HTTPFound(h.route_path('users'))
231 raise HTTPFound(h.route_path('users'))
232
232
233
233
234 class UsersView(UserAppView):
234 class UsersView(UserAppView):
235 ALLOW_SCOPED_TOKENS = False
235 ALLOW_SCOPED_TOKENS = False
236 """
236 """
237 This view has alternative version inside EE, if modified please take a look
237 This view has alternative version inside EE, if modified please take a look
238 in there as well.
238 in there as well.
239 """
239 """
240
240
241 def get_auth_plugins(self):
241 def get_auth_plugins(self):
242 valid_plugins = []
242 valid_plugins = []
243 authn_registry = get_authn_registry(self.request.registry)
243 authn_registry = get_authn_registry(self.request.registry)
244 for plugin in authn_registry.get_plugins_for_authentication():
244 for plugin in authn_registry.get_plugins_for_authentication():
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
245 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
246 valid_plugins.append(plugin)
246 valid_plugins.append(plugin)
247 elif plugin.name == 'rhodecode':
247 elif plugin.name == 'rhodecode':
248 valid_plugins.append(plugin)
248 valid_plugins.append(plugin)
249
249
250 # extend our choices if user has set a bound plugin which isn't enabled at the
250 # extend our choices if user has set a bound plugin which isn't enabled at the
251 # moment
251 # moment
252 extern_type = self.db_user.extern_type
252 extern_type = self.db_user.extern_type
253 if extern_type not in [x.uid for x in valid_plugins]:
253 if extern_type not in [x.uid for x in valid_plugins]:
254 try:
254 try:
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
255 plugin = authn_registry.get_plugin_by_uid(extern_type)
256 if plugin:
256 if plugin:
257 valid_plugins.append(plugin)
257 valid_plugins.append(plugin)
258
258
259 except Exception:
259 except Exception:
260 log.exception(
260 log.exception(
261 'Could not extend user plugins with `{}`'.format(extern_type))
261 'Could not extend user plugins with `{}`'.format(extern_type))
262 return valid_plugins
262 return valid_plugins
263
263
264 def load_default_context(self):
264 def load_default_context(self):
265 req = self.request
265 req = self.request
266
266
267 c = self._get_local_tmpl_context()
267 c = self._get_local_tmpl_context()
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
268 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
269 c.allowed_languages = [
269 c.allowed_languages = [
270 ('en', 'English (en)'),
270 ('en', 'English (en)'),
271 ('de', 'German (de)'),
271 ('de', 'German (de)'),
272 ('fr', 'French (fr)'),
272 ('fr', 'French (fr)'),
273 ('it', 'Italian (it)'),
273 ('it', 'Italian (it)'),
274 ('ja', 'Japanese (ja)'),
274 ('ja', 'Japanese (ja)'),
275 ('pl', 'Polish (pl)'),
275 ('pl', 'Polish (pl)'),
276 ('pt', 'Portuguese (pt)'),
276 ('pt', 'Portuguese (pt)'),
277 ('ru', 'Russian (ru)'),
277 ('ru', 'Russian (ru)'),
278 ('zh', 'Chinese (zh)'),
278 ('zh', 'Chinese (zh)'),
279 ]
279 ]
280
280
281 c.allowed_extern_types = [
281 c.allowed_extern_types = [
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
282 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
283 ]
283 ]
284 perms = req.registry.settings.get('available_permissions')
284 perms = req.registry.settings.get('available_permissions')
285 if not perms:
285 if not perms:
286 # inject info about available permissions
286 # inject info about available permissions
287 auth.set_available_permissions(req.registry.settings)
287 auth.set_available_permissions(req.registry.settings)
288
288
289 c.available_permissions = req.registry.settings['available_permissions']
289 c.available_permissions = req.registry.settings['available_permissions']
290 PermissionModel().set_global_permission_choices(
290 PermissionModel().set_global_permission_choices(
291 c, gettext_translator=req.translate)
291 c, gettext_translator=req.translate)
292
292
293 return c
293 return c
294
294
295 @LoginRequired()
295 @LoginRequired()
296 @HasPermissionAllDecorator('hg.admin')
296 @HasPermissionAllDecorator('hg.admin')
297 @CSRFRequired()
297 @CSRFRequired()
298 def user_update(self):
298 def user_update(self):
299 _ = self.request.translate
299 _ = self.request.translate
300 c = self.load_default_context()
300 c = self.load_default_context()
301
301
302 user_id = self.db_user_id
302 user_id = self.db_user_id
303 c.user = self.db_user
303 c.user = self.db_user
304
304
305 c.active = 'profile'
305 c.active = 'profile'
306 c.extern_type = c.user.extern_type
306 c.extern_type = c.user.extern_type
307 c.extern_name = c.user.extern_name
307 c.extern_name = c.user.extern_name
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
309 available_languages = [x[0] for x in c.allowed_languages]
309 available_languages = [x[0] for x in c.allowed_languages]
310 _form = UserForm(self.request.translate, edit=True,
310 _form = UserForm(self.request.translate, edit=True,
311 available_languages=available_languages,
311 available_languages=available_languages,
312 old_data={'user_id': user_id,
312 old_data={'user_id': user_id,
313 'email': c.user.email})()
313 'email': c.user.email})()
314
314
315 c.edit_mode = self.request.POST.get('edit') == '1'
315 c.edit_mode = self.request.POST.get('edit') == '1'
316 form_result = {}
316 form_result = {}
317 old_values = c.user.get_api_data()
317 old_values = c.user.get_api_data()
318 try:
318 try:
319 form_result = _form.to_python(dict(self.request.POST))
319 form_result = _form.to_python(dict(self.request.POST))
320 skip_attrs = ['extern_name']
320 skip_attrs = ['extern_name']
321 # TODO: plugin should define if username can be updated
321 # TODO: plugin should define if username can be updated
322
322
323 if c.extern_type != "rhodecode" and not c.edit_mode:
323 if c.extern_type != "rhodecode" and not c.edit_mode:
324 # forbid updating username for external accounts
324 # forbid updating username for external accounts
325 skip_attrs.append('username')
325 skip_attrs.append('username')
326
326
327 UserModel().update_user(
327 UserModel().update_user(
328 user_id, skip_attrs=skip_attrs, **form_result)
328 user_id, skip_attrs=skip_attrs, **form_result)
329
329
330 audit_logger.store_web(
330 audit_logger.store_web(
331 'user.edit', action_data={'old_data': old_values},
331 'user.edit', action_data={'old_data': old_values},
332 user=c.rhodecode_user)
332 user=c.rhodecode_user)
333
333
334 Session().commit()
334 Session().commit()
335 h.flash(_('User updated successfully'), category='success')
335 h.flash(_('User updated successfully'), category='success')
336 except formencode.Invalid as errors:
336 except formencode.Invalid as errors:
337 data = render(
337 data = render(
338 'rhodecode:templates/admin/users/user_edit.mako',
338 'rhodecode:templates/admin/users/user_edit.mako',
339 self._get_template_context(c), self.request)
339 self._get_template_context(c), self.request)
340 html = formencode.htmlfill.render(
340 html = formencode.htmlfill.render(
341 data,
341 data,
342 defaults=errors.value,
342 defaults=errors.value,
343 errors=errors.error_dict or {},
343 errors=errors.error_dict or {},
344 prefix_error=False,
344 prefix_error=False,
345 encoding="UTF-8",
345 encoding="UTF-8",
346 force_defaults=False
346 force_defaults=False
347 )
347 )
348 return Response(html)
348 return Response(html)
349 except UserCreationError as e:
349 except UserCreationError as e:
350 h.flash(e, 'error')
350 h.flash(e, 'error')
351 except Exception:
351 except Exception:
352 log.exception("Exception updating user")
352 log.exception("Exception updating user")
353 h.flash(_('Error occurred during update of user %s')
353 h.flash(_('Error occurred during update of user %s')
354 % form_result.get('username'), category='error')
354 % form_result.get('username'), category='error')
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
356
356
357 @LoginRequired()
357 @LoginRequired()
358 @HasPermissionAllDecorator('hg.admin')
358 @HasPermissionAllDecorator('hg.admin')
359 @CSRFRequired()
359 @CSRFRequired()
360 def user_delete(self):
360 def user_delete(self):
361 _ = self.request.translate
361 _ = self.request.translate
362 c = self.load_default_context()
362 c = self.load_default_context()
363 c.user = self.db_user
363 c.user = self.db_user
364
364
365 _repos = c.user.repositories
365 _repos = c.user.repositories
366 _repo_groups = c.user.repository_groups
366 _repo_groups = c.user.repository_groups
367 _user_groups = c.user.user_groups
367 _user_groups = c.user.user_groups
368 _pull_requests = c.user.user_pull_requests
368 _pull_requests = c.user.user_pull_requests
369 _artifacts = c.user.artifacts
369 _artifacts = c.user.artifacts
370
370
371 handle_repos = None
371 handle_repos = None
372 handle_repo_groups = None
372 handle_repo_groups = None
373 handle_user_groups = None
373 handle_user_groups = None
374 handle_pull_requests = None
374 handle_pull_requests = None
375 handle_artifacts = None
375 handle_artifacts = None
376
376
377 # calls for flash of handle based on handle case detach or delete
377 # calls for flash of handle based on handle case detach or delete
378 def set_handle_flash_repos():
378 def set_handle_flash_repos():
379 handle = handle_repos
379 handle = handle_repos
380 if handle == 'detach':
380 if handle == 'detach':
381 h.flash(_('Detached %s repositories') % len(_repos),
381 h.flash(_('Detached %s repositories') % len(_repos),
382 category='success')
382 category='success')
383 elif handle == 'delete':
383 elif handle == 'delete':
384 h.flash(_('Deleted %s repositories') % len(_repos),
384 h.flash(_('Deleted %s repositories') % len(_repos),
385 category='success')
385 category='success')
386
386
387 def set_handle_flash_repo_groups():
387 def set_handle_flash_repo_groups():
388 handle = handle_repo_groups
388 handle = handle_repo_groups
389 if handle == 'detach':
389 if handle == 'detach':
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
391 category='success')
391 category='success')
392 elif handle == 'delete':
392 elif handle == 'delete':
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
394 category='success')
394 category='success')
395
395
396 def set_handle_flash_user_groups():
396 def set_handle_flash_user_groups():
397 handle = handle_user_groups
397 handle = handle_user_groups
398 if handle == 'detach':
398 if handle == 'detach':
399 h.flash(_('Detached %s user groups') % len(_user_groups),
399 h.flash(_('Detached %s user groups') % len(_user_groups),
400 category='success')
400 category='success')
401 elif handle == 'delete':
401 elif handle == 'delete':
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
403 category='success')
403 category='success')
404
404
405 def set_handle_flash_pull_requests():
405 def set_handle_flash_pull_requests():
406 handle = handle_pull_requests
406 handle = handle_pull_requests
407 if handle == 'detach':
407 if handle == 'detach':
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
409 category='success')
409 category='success')
410 elif handle == 'delete':
410 elif handle == 'delete':
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
412 category='success')
412 category='success')
413
413
414 def set_handle_flash_artifacts():
414 def set_handle_flash_artifacts():
415 handle = handle_artifacts
415 handle = handle_artifacts
416 if handle == 'detach':
416 if handle == 'detach':
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
418 category='success')
418 category='success')
419 elif handle == 'delete':
419 elif handle == 'delete':
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
421 category='success')
421 category='success')
422
422
423 handle_user = User.get_first_super_admin()
423 handle_user = User.get_first_super_admin()
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
424 handle_user_id = safe_int(self.request.POST.get('detach_user_id'))
425 if handle_user_id:
425 if handle_user_id:
426 # NOTE(marcink): we get new owner for objects...
426 # NOTE(marcink): we get new owner for objects...
427 handle_user = User.get_or_404(handle_user_id)
427 handle_user = User.get_or_404(handle_user_id)
428
428
429 if _repos and self.request.POST.get('user_repos'):
429 if _repos and self.request.POST.get('user_repos'):
430 handle_repos = self.request.POST['user_repos']
430 handle_repos = self.request.POST['user_repos']
431
431
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
433 handle_repo_groups = self.request.POST['user_repo_groups']
433 handle_repo_groups = self.request.POST['user_repo_groups']
434
434
435 if _user_groups and self.request.POST.get('user_user_groups'):
435 if _user_groups and self.request.POST.get('user_user_groups'):
436 handle_user_groups = self.request.POST['user_user_groups']
436 handle_user_groups = self.request.POST['user_user_groups']
437
437
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
439 handle_pull_requests = self.request.POST['user_pull_requests']
439 handle_pull_requests = self.request.POST['user_pull_requests']
440
440
441 if _artifacts and self.request.POST.get('user_artifacts'):
441 if _artifacts and self.request.POST.get('user_artifacts'):
442 handle_artifacts = self.request.POST['user_artifacts']
442 handle_artifacts = self.request.POST['user_artifacts']
443
443
444 old_values = c.user.get_api_data()
444 old_values = c.user.get_api_data()
445
445
446 try:
446 try:
447
447
448 UserModel().delete(
448 UserModel().delete(
449 c.user,
449 c.user,
450 handle_repos=handle_repos,
450 handle_repos=handle_repos,
451 handle_repo_groups=handle_repo_groups,
451 handle_repo_groups=handle_repo_groups,
452 handle_user_groups=handle_user_groups,
452 handle_user_groups=handle_user_groups,
453 handle_pull_requests=handle_pull_requests,
453 handle_pull_requests=handle_pull_requests,
454 handle_artifacts=handle_artifacts,
454 handle_artifacts=handle_artifacts,
455 handle_new_owner=handle_user
455 handle_new_owner=handle_user
456 )
456 )
457
457
458 audit_logger.store_web(
458 audit_logger.store_web(
459 'user.delete', action_data={'old_data': old_values},
459 'user.delete', action_data={'old_data': old_values},
460 user=c.rhodecode_user)
460 user=c.rhodecode_user)
461
461
462 Session().commit()
462 Session().commit()
463 set_handle_flash_repos()
463 set_handle_flash_repos()
464 set_handle_flash_repo_groups()
464 set_handle_flash_repo_groups()
465 set_handle_flash_user_groups()
465 set_handle_flash_user_groups()
466 set_handle_flash_pull_requests()
466 set_handle_flash_pull_requests()
467 set_handle_flash_artifacts()
467 set_handle_flash_artifacts()
468 username = h.escape(old_values['username'])
468 username = h.escape(old_values['username'])
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
472 UserOwnsArtifactsException, DefaultUserException) as e:
472 UserOwnsArtifactsException, DefaultUserException) as e:
473 h.flash(e, category='warning')
473 h.flash(e, category='warning')
474 except Exception:
474 except Exception:
475 log.exception("Exception during deletion of user")
475 log.exception("Exception during deletion of user")
476 h.flash(_('An error occurred during deletion of user'),
476 h.flash(_('An error occurred during deletion of user'),
477 category='error')
477 category='error')
478 raise HTTPFound(h.route_path('users'))
478 raise HTTPFound(h.route_path('users'))
479
479
480 @LoginRequired()
480 @LoginRequired()
481 @HasPermissionAllDecorator('hg.admin')
481 @HasPermissionAllDecorator('hg.admin')
482 def user_edit(self):
482 def user_edit(self):
483 _ = self.request.translate
483 _ = self.request.translate
484 c = self.load_default_context()
484 c = self.load_default_context()
485 c.user = self.db_user
485 c.user = self.db_user
486
486
487 c.active = 'profile'
487 c.active = 'profile'
488 c.extern_type = c.user.extern_type
488 c.extern_type = c.user.extern_type
489 c.extern_name = c.user.extern_name
489 c.extern_name = c.user.extern_name
490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
490 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
491 c.edit_mode = self.request.GET.get('edit') == '1'
491 c.edit_mode = self.request.GET.get('edit') == '1'
492
492
493 defaults = c.user.get_dict()
493 defaults = c.user.get_dict()
494 defaults.update({'language': c.user.user_data.get('language')})
494 defaults.update({'language': c.user.user_data.get('language')})
495
495
496 data = render(
496 data = render(
497 'rhodecode:templates/admin/users/user_edit.mako',
497 'rhodecode:templates/admin/users/user_edit.mako',
498 self._get_template_context(c), self.request)
498 self._get_template_context(c), self.request)
499 html = formencode.htmlfill.render(
499 html = formencode.htmlfill.render(
500 data,
500 data,
501 defaults=defaults,
501 defaults=defaults,
502 encoding="UTF-8",
502 encoding="UTF-8",
503 force_defaults=False
503 force_defaults=False
504 )
504 )
505 return Response(html)
505 return Response(html)
506
506
507 @LoginRequired()
507 @LoginRequired()
508 @HasPermissionAllDecorator('hg.admin')
508 @HasPermissionAllDecorator('hg.admin')
509 def user_edit_advanced(self):
509 def user_edit_advanced(self):
510 _ = self.request.translate
510 _ = self.request.translate
511 c = self.load_default_context()
511 c = self.load_default_context()
512
512
513 user_id = self.db_user_id
513 user_id = self.db_user_id
514 c.user = self.db_user
514 c.user = self.db_user
515
515
516 c.detach_user = User.get_first_super_admin()
516 c.detach_user = User.get_first_super_admin()
517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
517 detach_user_id = safe_int(self.request.GET.get('detach_user_id'))
518 if detach_user_id:
518 if detach_user_id:
519 c.detach_user = User.get_or_404(detach_user_id)
519 c.detach_user = User.get_or_404(detach_user_id)
520
520
521 c.active = 'advanced'
521 c.active = 'advanced'
522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
522 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
523 c.personal_repo_group_name = RepoGroupModel()\
523 c.personal_repo_group_name = RepoGroupModel()\
524 .get_personal_group_name(c.user)
524 .get_personal_group_name(c.user)
525
525
526 c.user_to_review_rules = sorted(
526 c.user_to_review_rules = sorted(
527 (x.user for x in c.user.user_review_rules),
527 (x.user for x in c.user.user_review_rules),
528 key=lambda u: u.username.lower())
528 key=lambda u: u.username.lower())
529
529
530 defaults = c.user.get_dict()
530 defaults = c.user.get_dict()
531
531
532 # Interim workaround if the user participated on any pull requests as a
532 # Interim workaround if the user participated on any pull requests as a
533 # reviewer.
533 # reviewer.
534 has_review = len(c.user.reviewer_pull_requests)
534 has_review = len(c.user.reviewer_pull_requests)
535 c.can_delete_user = not has_review
535 c.can_delete_user = not has_review
536 c.can_delete_user_message = ''
536 c.can_delete_user_message = ''
537 inactive_link = h.link_to(
537 inactive_link = h.link_to(
538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
538 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
539 if has_review == 1:
539 if has_review == 1:
540 c.can_delete_user_message = h.literal(_(
540 c.can_delete_user_message = h.literal(_(
541 'The user participates as reviewer in {} pull request and '
541 'The user participates as reviewer in {} pull request and '
542 'cannot be deleted. \nYou can set the user to '
542 'cannot be deleted. \nYou can set the user to '
543 '"{}" instead of deleting it.').format(
543 '"{}" instead of deleting it.').format(
544 has_review, inactive_link))
544 has_review, inactive_link))
545 elif has_review:
545 elif has_review:
546 c.can_delete_user_message = h.literal(_(
546 c.can_delete_user_message = h.literal(_(
547 'The user participates as reviewer in {} pull requests and '
547 'The user participates as reviewer in {} pull requests and '
548 'cannot be deleted. \nYou can set the user to '
548 'cannot be deleted. \nYou can set the user to '
549 '"{}" instead of deleting it.').format(
549 '"{}" instead of deleting it.').format(
550 has_review, inactive_link))
550 has_review, inactive_link))
551
551
552 data = render(
552 data = render(
553 'rhodecode:templates/admin/users/user_edit.mako',
553 'rhodecode:templates/admin/users/user_edit.mako',
554 self._get_template_context(c), self.request)
554 self._get_template_context(c), self.request)
555 html = formencode.htmlfill.render(
555 html = formencode.htmlfill.render(
556 data,
556 data,
557 defaults=defaults,
557 defaults=defaults,
558 encoding="UTF-8",
558 encoding="UTF-8",
559 force_defaults=False
559 force_defaults=False
560 )
560 )
561 return Response(html)
561 return Response(html)
562
562
563 @LoginRequired()
563 @LoginRequired()
564 @HasPermissionAllDecorator('hg.admin')
564 @HasPermissionAllDecorator('hg.admin')
565 def user_edit_global_perms(self):
565 def user_edit_global_perms(self):
566 _ = self.request.translate
566 _ = self.request.translate
567 c = self.load_default_context()
567 c = self.load_default_context()
568 c.user = self.db_user
568 c.user = self.db_user
569
569
570 c.active = 'global_perms'
570 c.active = 'global_perms'
571
571
572 c.default_user = User.get_default_user()
572 c.default_user = User.get_default_user()
573 defaults = c.user.get_dict()
573 defaults = c.user.get_dict()
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
575 defaults.update(c.default_user.get_default_perms())
575 defaults.update(c.default_user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
577
577
578 data = render(
578 data = render(
579 'rhodecode:templates/admin/users/user_edit.mako',
579 'rhodecode:templates/admin/users/user_edit.mako',
580 self._get_template_context(c), self.request)
580 self._get_template_context(c), self.request)
581 html = formencode.htmlfill.render(
581 html = formencode.htmlfill.render(
582 data,
582 data,
583 defaults=defaults,
583 defaults=defaults,
584 encoding="UTF-8",
584 encoding="UTF-8",
585 force_defaults=False
585 force_defaults=False
586 )
586 )
587 return Response(html)
587 return Response(html)
588
588
589 @LoginRequired()
589 @LoginRequired()
590 @HasPermissionAllDecorator('hg.admin')
590 @HasPermissionAllDecorator('hg.admin')
591 @CSRFRequired()
591 @CSRFRequired()
592 def user_edit_global_perms_update(self):
592 def user_edit_global_perms_update(self):
593 _ = self.request.translate
593 _ = self.request.translate
594 c = self.load_default_context()
594 c = self.load_default_context()
595
595
596 user_id = self.db_user_id
596 user_id = self.db_user_id
597 c.user = self.db_user
597 c.user = self.db_user
598
598
599 c.active = 'global_perms'
599 c.active = 'global_perms'
600 try:
600 try:
601 # first stage that verifies the checkbox
601 # first stage that verifies the checkbox
602 _form = UserIndividualPermissionsForm(self.request.translate)
602 _form = UserIndividualPermissionsForm(self.request.translate)
603 form_result = _form.to_python(dict(self.request.POST))
603 form_result = _form.to_python(dict(self.request.POST))
604 inherit_perms = form_result['inherit_default_permissions']
604 inherit_perms = form_result['inherit_default_permissions']
605 c.user.inherit_default_permissions = inherit_perms
605 c.user.inherit_default_permissions = inherit_perms
606 Session().add(c.user)
606 Session().add(c.user)
607
607
608 if not inherit_perms:
608 if not inherit_perms:
609 # only update the individual ones if we un check the flag
609 # only update the individual ones if we un check the flag
610 _form = UserPermissionsForm(
610 _form = UserPermissionsForm(
611 self.request.translate,
611 self.request.translate,
612 [x[0] for x in c.repo_create_choices],
612 [x[0] for x in c.repo_create_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
614 [x[0] for x in c.repo_group_create_choices],
614 [x[0] for x in c.repo_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
616 [x[0] for x in c.fork_choices],
616 [x[0] for x in c.fork_choices],
617 [x[0] for x in c.inherit_default_permission_choices])()
617 [x[0] for x in c.inherit_default_permission_choices])()
618
618
619 form_result = _form.to_python(dict(self.request.POST))
619 form_result = _form.to_python(dict(self.request.POST))
620 form_result.update({'perm_user_id': c.user.user_id})
620 form_result.update({'perm_user_id': c.user.user_id})
621
621
622 PermissionModel().update_user_permissions(form_result)
622 PermissionModel().update_user_permissions(form_result)
623
623
624 # TODO(marcink): implement global permissions
624 # TODO(marcink): implement global permissions
625 # audit_log.store_web('user.edit.permissions')
625 # audit_log.store_web('user.edit.permissions')
626
626
627 Session().commit()
627 Session().commit()
628
628
629 h.flash(_('User global permissions updated successfully'),
629 h.flash(_('User global permissions updated successfully'),
630 category='success')
630 category='success')
631
631
632 except formencode.Invalid as errors:
632 except formencode.Invalid as errors:
633 data = render(
633 data = render(
634 'rhodecode:templates/admin/users/user_edit.mako',
634 'rhodecode:templates/admin/users/user_edit.mako',
635 self._get_template_context(c), self.request)
635 self._get_template_context(c), self.request)
636 html = formencode.htmlfill.render(
636 html = formencode.htmlfill.render(
637 data,
637 data,
638 defaults=errors.value,
638 defaults=errors.value,
639 errors=errors.error_dict or {},
639 errors=errors.error_dict or {},
640 prefix_error=False,
640 prefix_error=False,
641 encoding="UTF-8",
641 encoding="UTF-8",
642 force_defaults=False
642 force_defaults=False
643 )
643 )
644 return Response(html)
644 return Response(html)
645 except Exception:
645 except Exception:
646 log.exception("Exception during permissions saving")
646 log.exception("Exception during permissions saving")
647 h.flash(_('An error occurred during permissions saving'),
647 h.flash(_('An error occurred during permissions saving'),
648 category='error')
648 category='error')
649
649
650 affected_user_ids = [user_id]
650 affected_user_ids = [user_id]
651 PermissionModel().trigger_permission_flush(affected_user_ids)
651 PermissionModel().trigger_permission_flush(affected_user_ids)
652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
652 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
653
653
654 @LoginRequired()
654 @LoginRequired()
655 @HasPermissionAllDecorator('hg.admin')
655 @HasPermissionAllDecorator('hg.admin')
656 @CSRFRequired()
656 @CSRFRequired()
657 def user_enable_force_password_reset(self):
657 def user_enable_force_password_reset(self):
658 _ = self.request.translate
658 _ = self.request.translate
659 c = self.load_default_context()
659 c = self.load_default_context()
660
660
661 user_id = self.db_user_id
661 user_id = self.db_user_id
662 c.user = self.db_user
662 c.user = self.db_user
663
663
664 try:
664 try:
665 c.user.update_userdata(force_password_change=True)
665 c.user.update_userdata(force_password_change=True)
666
666
667 msg = _('Force password change enabled for user')
667 msg = _('Force password change enabled for user')
668 audit_logger.store_web('user.edit.password_reset.enabled',
668 audit_logger.store_web('user.edit.password_reset.enabled',
669 user=c.rhodecode_user)
669 user=c.rhodecode_user)
670
670
671 Session().commit()
671 Session().commit()
672 h.flash(msg, category='success')
672 h.flash(msg, category='success')
673 except Exception:
673 except Exception:
674 log.exception("Exception during password reset for user")
674 log.exception("Exception during password reset for user")
675 h.flash(_('An error occurred during password reset for user'),
675 h.flash(_('An error occurred during password reset for user'),
676 category='error')
676 category='error')
677
677
678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
678 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
679
679
680 @LoginRequired()
680 @LoginRequired()
681 @HasPermissionAllDecorator('hg.admin')
681 @HasPermissionAllDecorator('hg.admin')
682 @CSRFRequired()
682 @CSRFRequired()
683 def user_disable_force_password_reset(self):
683 def user_disable_force_password_reset(self):
684 _ = self.request.translate
684 _ = self.request.translate
685 c = self.load_default_context()
685 c = self.load_default_context()
686
686
687 user_id = self.db_user_id
687 user_id = self.db_user_id
688 c.user = self.db_user
688 c.user = self.db_user
689
689
690 try:
690 try:
691 c.user.update_userdata(force_password_change=False)
691 c.user.update_userdata(force_password_change=False)
692
692
693 msg = _('Force password change disabled for user')
693 msg = _('Force password change disabled for user')
694 audit_logger.store_web(
694 audit_logger.store_web(
695 'user.edit.password_reset.disabled',
695 'user.edit.password_reset.disabled',
696 user=c.rhodecode_user)
696 user=c.rhodecode_user)
697
697
698 Session().commit()
698 Session().commit()
699 h.flash(msg, category='success')
699 h.flash(msg, category='success')
700 except Exception:
700 except Exception:
701 log.exception("Exception during password reset for user")
701 log.exception("Exception during password reset for user")
702 h.flash(_('An error occurred during password reset for user'),
702 h.flash(_('An error occurred during password reset for user'),
703 category='error')
703 category='error')
704
704
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
705 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
706
706
707 @LoginRequired()
707 @LoginRequired()
708 @HasPermissionAllDecorator('hg.admin')
708 @HasPermissionAllDecorator('hg.admin')
709 @CSRFRequired()
709 @CSRFRequired()
710 def user_notice_dismiss(self):
710 def user_notice_dismiss(self):
711 _ = self.request.translate
711 _ = self.request.translate
712 c = self.load_default_context()
712 c = self.load_default_context()
713
713
714 user_id = self.db_user_id
714 user_id = self.db_user_id
715 c.user = self.db_user
715 c.user = self.db_user
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 notice = UserNotice().query()\
717 notice = UserNotice().query()\
718 .filter(UserNotice.user_id == user_id)\
718 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .scalar()
720 .scalar()
721 read = False
721 read = False
722 if notice:
722 if notice:
723 notice.notice_read = True
723 notice.notice_read = True
724 Session().add(notice)
724 Session().add(notice)
725 Session().commit()
725 Session().commit()
726 read = True
726 read = True
727
727
728 return {'notice': user_notice_id, 'read': read}
728 return {'notice': user_notice_id, 'read': read}
729
729
730 @LoginRequired()
730 @LoginRequired()
731 @HasPermissionAllDecorator('hg.admin')
731 @HasPermissionAllDecorator('hg.admin')
732 @CSRFRequired()
732 @CSRFRequired()
733 def user_create_personal_repo_group(self):
733 def user_create_personal_repo_group(self):
734 """
734 """
735 Create personal repository group for this user
735 Create personal repository group for this user
736 """
736 """
737 from rhodecode.model.repo_group import RepoGroupModel
737 from rhodecode.model.repo_group import RepoGroupModel
738
738
739 _ = self.request.translate
739 _ = self.request.translate
740 c = self.load_default_context()
740 c = self.load_default_context()
741
741
742 user_id = self.db_user_id
742 user_id = self.db_user_id
743 c.user = self.db_user
743 c.user = self.db_user
744
744
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
746 c.user.user_id)
746 c.user.user_id)
747 if personal_repo_group:
747 if personal_repo_group:
748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
748 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
749
749
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
751 named_personal_group = RepoGroup.get_by_group_name(
751 named_personal_group = RepoGroup.get_by_group_name(
752 personal_repo_group_name)
752 personal_repo_group_name)
753 try:
753 try:
754
754
755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
755 if named_personal_group and named_personal_group.user_id == c.user.user_id:
756 # migrate the same named group, and mark it as personal
756 # migrate the same named group, and mark it as personal
757 named_personal_group.personal = True
757 named_personal_group.personal = True
758 Session().add(named_personal_group)
758 Session().add(named_personal_group)
759 Session().commit()
759 Session().commit()
760 msg = _('Linked repository group `%s` as personal' % (
760 msg = _('Linked repository group `%s` as personal' % (
761 personal_repo_group_name,))
761 personal_repo_group_name,))
762 h.flash(msg, category='success')
762 h.flash(msg, category='success')
763 elif not named_personal_group:
763 elif not named_personal_group:
764 RepoGroupModel().create_personal_repo_group(c.user)
764 RepoGroupModel().create_personal_repo_group(c.user)
765
765
766 msg = _('Created repository group `%s`' % (
766 msg = _('Created repository group `%s`' % (
767 personal_repo_group_name,))
767 personal_repo_group_name,))
768 h.flash(msg, category='success')
768 h.flash(msg, category='success')
769 else:
769 else:
770 msg = _('Repository group `%s` is already taken' % (
770 msg = _('Repository group `%s` is already taken' % (
771 personal_repo_group_name,))
771 personal_repo_group_name,))
772 h.flash(msg, category='warning')
772 h.flash(msg, category='warning')
773 except Exception:
773 except Exception:
774 log.exception("Exception during repository group creation")
774 log.exception("Exception during repository group creation")
775 msg = _(
775 msg = _(
776 'An error occurred during repository group creation for user')
776 'An error occurred during repository group creation for user')
777 h.flash(msg, category='error')
777 h.flash(msg, category='error')
778 Session().rollback()
778 Session().rollback()
779
779
780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
780 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
781
781
782 @LoginRequired()
782 @LoginRequired()
783 @HasPermissionAllDecorator('hg.admin')
783 @HasPermissionAllDecorator('hg.admin')
784 def auth_tokens(self):
784 def auth_tokens(self):
785 _ = self.request.translate
785 _ = self.request.translate
786 c = self.load_default_context()
786 c = self.load_default_context()
787 c.user = self.db_user
787 c.user = self.db_user
788
788
789 c.active = 'auth_tokens'
789 c.active = 'auth_tokens'
790
790
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
792 c.role_values = [
792 c.role_values = [
793 (x, AuthTokenModel.cls._get_role_name(x))
793 (x, AuthTokenModel.cls._get_role_name(x))
794 for x in AuthTokenModel.cls.ROLES]
794 for x in AuthTokenModel.cls.ROLES]
795 c.role_options = [(c.role_values, _("Role"))]
795 c.role_options = [(c.role_values, _("Role"))]
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
797 c.user.user_id, show_expired=True)
797 c.user.user_id, show_expired=True)
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
799 return self._get_template_context(c)
799 return self._get_template_context(c)
800
800
801 @LoginRequired()
801 @LoginRequired()
802 @HasPermissionAllDecorator('hg.admin')
802 @HasPermissionAllDecorator('hg.admin')
803 def auth_tokens_view(self):
803 def auth_tokens_view(self):
804 _ = self.request.translate
804 _ = self.request.translate
805 c = self.load_default_context()
805 c = self.load_default_context()
806 c.user = self.db_user
806 c.user = self.db_user
807
807
808 auth_token_id = self.request.POST.get('auth_token_id')
808 auth_token_id = self.request.POST.get('auth_token_id')
809
809
810 if auth_token_id:
810 if auth_token_id:
811 token = UserApiKeys.get_or_404(auth_token_id)
811 token = UserApiKeys.get_or_404(auth_token_id)
812
812
813 return {
813 return {
814 'auth_token': token.api_key
814 'auth_token': token.api_key
815 }
815 }
816
816
817 def maybe_attach_token_scope(self, token):
817 def maybe_attach_token_scope(self, token):
818 # implemented in EE edition
818 # implemented in EE edition
819 pass
819 pass
820
820
821 @LoginRequired()
821 @LoginRequired()
822 @HasPermissionAllDecorator('hg.admin')
822 @HasPermissionAllDecorator('hg.admin')
823 @CSRFRequired()
823 @CSRFRequired()
824 def auth_tokens_add(self):
824 def auth_tokens_add(self):
825 _ = self.request.translate
825 _ = self.request.translate
826 c = self.load_default_context()
826 c = self.load_default_context()
827
827
828 user_id = self.db_user_id
828 user_id = self.db_user_id
829 c.user = self.db_user
829 c.user = self.db_user
830
830
831 user_data = c.user.get_api_data()
831 user_data = c.user.get_api_data()
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
833 description = self.request.POST.get('description')
833 description = self.request.POST.get('description')
834 role = self.request.POST.get('role')
834 role = self.request.POST.get('role')
835
835
836 token = UserModel().add_auth_token(
836 token = UserModel().add_auth_token(
837 user=c.user.user_id,
837 user=c.user.user_id,
838 lifetime_minutes=lifetime, role=role, description=description,
838 lifetime_minutes=lifetime, role=role, description=description,
839 scope_callback=self.maybe_attach_token_scope)
839 scope_callback=self.maybe_attach_token_scope)
840 token_data = token.get_api_data()
840 token_data = token.get_api_data()
841
841
842 audit_logger.store_web(
842 audit_logger.store_web(
843 'user.edit.token.add', action_data={
843 'user.edit.token.add', action_data={
844 'data': {'token': token_data, 'user': user_data}},
844 'data': {'token': token_data, 'user': user_data}},
845 user=self._rhodecode_user, )
845 user=self._rhodecode_user, )
846 Session().commit()
846 Session().commit()
847
847
848 h.flash(_("Auth token successfully created"), category='success')
848 h.flash(_("Auth token successfully created"), category='success')
849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
849 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
850
850
851 @LoginRequired()
851 @LoginRequired()
852 @HasPermissionAllDecorator('hg.admin')
852 @HasPermissionAllDecorator('hg.admin')
853 @CSRFRequired()
853 @CSRFRequired()
854 def auth_tokens_delete(self):
854 def auth_tokens_delete(self):
855 _ = self.request.translate
855 _ = self.request.translate
856 c = self.load_default_context()
856 c = self.load_default_context()
857
857
858 user_id = self.db_user_id
858 user_id = self.db_user_id
859 c.user = self.db_user
859 c.user = self.db_user
860
860
861 user_data = c.user.get_api_data()
861 user_data = c.user.get_api_data()
862
862
863 del_auth_token = self.request.POST.get('del_auth_token')
863 del_auth_token = self.request.POST.get('del_auth_token')
864
864
865 if del_auth_token:
865 if del_auth_token:
866 token = UserApiKeys.get_or_404(del_auth_token)
866 token = UserApiKeys.get_or_404(del_auth_token)
867 token_data = token.get_api_data()
867 token_data = token.get_api_data()
868
868
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
870 audit_logger.store_web(
870 audit_logger.store_web(
871 'user.edit.token.delete', action_data={
871 'user.edit.token.delete', action_data={
872 'data': {'token': token_data, 'user': user_data}},
872 'data': {'token': token_data, 'user': user_data}},
873 user=self._rhodecode_user,)
873 user=self._rhodecode_user,)
874 Session().commit()
874 Session().commit()
875 h.flash(_("Auth token successfully deleted"), category='success')
875 h.flash(_("Auth token successfully deleted"), category='success')
876
876
877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
877 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
878
878
879 @LoginRequired()
879 @LoginRequired()
880 @HasPermissionAllDecorator('hg.admin')
880 @HasPermissionAllDecorator('hg.admin')
881 def ssh_keys(self):
881 def ssh_keys(self):
882 _ = self.request.translate
882 _ = self.request.translate
883 c = self.load_default_context()
883 c = self.load_default_context()
884 c.user = self.db_user
884 c.user = self.db_user
885
885
886 c.active = 'ssh_keys'
886 c.active = 'ssh_keys'
887 c.default_key = self.request.GET.get('default_key')
887 c.default_key = self.request.GET.get('default_key')
888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
888 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
889 return self._get_template_context(c)
889 return self._get_template_context(c)
890
890
891 @LoginRequired()
891 @LoginRequired()
892 @HasPermissionAllDecorator('hg.admin')
892 @HasPermissionAllDecorator('hg.admin')
893 def ssh_keys_generate_keypair(self):
893 def ssh_keys_generate_keypair(self):
894 _ = self.request.translate
894 _ = self.request.translate
895 c = self.load_default_context()
895 c = self.load_default_context()
896
896
897 c.user = self.db_user
897 c.user = self.db_user
898
898
899 c.active = 'ssh_keys_generate'
899 c.active = 'ssh_keys_generate'
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
901 private_format = self.request.GET.get('private_format') \
901 private_format = self.request.GET.get('private_format') \
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
903 c.private, c.public = SshKeyModel().generate_keypair(
903 c.private, c.public = SshKeyModel().generate_keypair(
904 comment=comment, private_format=private_format)
904 comment=comment, private_format=private_format)
905
905
906 return self._get_template_context(c)
906 return self._get_template_context(c)
907
907
908 @LoginRequired()
908 @LoginRequired()
909 @HasPermissionAllDecorator('hg.admin')
909 @HasPermissionAllDecorator('hg.admin')
910 @CSRFRequired()
910 @CSRFRequired()
911 def ssh_keys_add(self):
911 def ssh_keys_add(self):
912 _ = self.request.translate
912 _ = self.request.translate
913 c = self.load_default_context()
913 c = self.load_default_context()
914
914
915 user_id = self.db_user_id
915 user_id = self.db_user_id
916 c.user = self.db_user
916 c.user = self.db_user
917
917
918 user_data = c.user.get_api_data()
918 user_data = c.user.get_api_data()
919 key_data = self.request.POST.get('key_data')
919 key_data = self.request.POST.get('key_data')
920 description = self.request.POST.get('description')
920 description = self.request.POST.get('description')
921
921
922 fingerprint = 'unknown'
922 fingerprint = 'unknown'
923 try:
923 try:
924 if not key_data:
924 if not key_data:
925 raise ValueError('Please add a valid public key')
925 raise ValueError('Please add a valid public key')
926
926
927 key = SshKeyModel().parse_key(key_data.strip())
927 key = SshKeyModel().parse_key(key_data.strip())
928 fingerprint = key.hash_md5()
928 fingerprint = key.hash_md5()
929
929
930 ssh_key = SshKeyModel().create(
930 ssh_key = SshKeyModel().create(
931 c.user.user_id, fingerprint, key.keydata, description)
931 c.user.user_id, fingerprint, key.keydata, description)
932 ssh_key_data = ssh_key.get_api_data()
932 ssh_key_data = ssh_key.get_api_data()
933
933
934 audit_logger.store_web(
934 audit_logger.store_web(
935 'user.edit.ssh_key.add', action_data={
935 'user.edit.ssh_key.add', action_data={
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
937 user=self._rhodecode_user, )
937 user=self._rhodecode_user, )
938 Session().commit()
938 Session().commit()
939
939
940 # Trigger an event on change of keys.
940 # Trigger an event on change of keys.
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
942
942
943 h.flash(_("Ssh Key successfully created"), category='success')
943 h.flash(_("Ssh Key successfully created"), category='success')
944
944
945 except IntegrityError:
945 except IntegrityError:
946 log.exception("Exception during ssh key saving")
946 log.exception("Exception during ssh key saving")
947 err = 'Such key with fingerprint `{}` already exists, ' \
947 err = 'Such key with fingerprint `{}` already exists, ' \
948 'please use a different one'.format(fingerprint)
948 'please use a different one'.format(fingerprint)
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
950 category='error')
950 category='error')
951 except Exception as e:
951 except Exception as e:
952 log.exception("Exception during ssh key saving")
952 log.exception("Exception during ssh key saving")
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
954 category='error')
954 category='error')
955
955
956 return HTTPFound(
956 return HTTPFound(
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
958
958
959 @LoginRequired()
959 @LoginRequired()
960 @HasPermissionAllDecorator('hg.admin')
960 @HasPermissionAllDecorator('hg.admin')
961 @CSRFRequired()
961 @CSRFRequired()
962 def ssh_keys_delete(self):
962 def ssh_keys_delete(self):
963 _ = self.request.translate
963 _ = self.request.translate
964 c = self.load_default_context()
964 c = self.load_default_context()
965
965
966 user_id = self.db_user_id
966 user_id = self.db_user_id
967 c.user = self.db_user
967 c.user = self.db_user
968
968
969 user_data = c.user.get_api_data()
969 user_data = c.user.get_api_data()
970
970
971 del_ssh_key = self.request.POST.get('del_ssh_key')
971 del_ssh_key = self.request.POST.get('del_ssh_key')
972
972
973 if del_ssh_key:
973 if del_ssh_key:
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
975 ssh_key_data = ssh_key.get_api_data()
975 ssh_key_data = ssh_key.get_api_data()
976
976
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
978 audit_logger.store_web(
978 audit_logger.store_web(
979 'user.edit.ssh_key.delete', action_data={
979 'user.edit.ssh_key.delete', action_data={
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
981 user=self._rhodecode_user,)
981 user=self._rhodecode_user,)
982 Session().commit()
982 Session().commit()
983 # Trigger an event on change of keys.
983 # Trigger an event on change of keys.
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
985 h.flash(_("Ssh key successfully deleted"), category='success')
985 h.flash(_("Ssh key successfully deleted"), category='success')
986
986
987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
987 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
988
988
989 @LoginRequired()
989 @LoginRequired()
990 @HasPermissionAllDecorator('hg.admin')
990 @HasPermissionAllDecorator('hg.admin')
991 def emails(self):
991 def emails(self):
992 _ = self.request.translate
992 _ = self.request.translate
993 c = self.load_default_context()
993 c = self.load_default_context()
994 c.user = self.db_user
994 c.user = self.db_user
995
995
996 c.active = 'emails'
996 c.active = 'emails'
997 c.user_email_map = UserEmailMap.query() \
997 c.user_email_map = UserEmailMap.query() \
998 .filter(UserEmailMap.user == c.user).all()
998 .filter(UserEmailMap.user == c.user).all()
999
999
1000 return self._get_template_context(c)
1000 return self._get_template_context(c)
1001
1001
1002 @LoginRequired()
1002 @LoginRequired()
1003 @HasPermissionAllDecorator('hg.admin')
1003 @HasPermissionAllDecorator('hg.admin')
1004 @CSRFRequired()
1004 @CSRFRequired()
1005 def emails_add(self):
1005 def emails_add(self):
1006 _ = self.request.translate
1006 _ = self.request.translate
1007 c = self.load_default_context()
1007 c = self.load_default_context()
1008
1008
1009 user_id = self.db_user_id
1009 user_id = self.db_user_id
1010 c.user = self.db_user
1010 c.user = self.db_user
1011
1011
1012 email = self.request.POST.get('new_email')
1012 email = self.request.POST.get('new_email')
1013 user_data = c.user.get_api_data()
1013 user_data = c.user.get_api_data()
1014 try:
1014 try:
1015
1015
1016 form = UserExtraEmailForm(self.request.translate)()
1016 form = UserExtraEmailForm(self.request.translate)()
1017 data = form.to_python({'email': email})
1017 data = form.to_python({'email': email})
1018 email = data['email']
1018 email = data['email']
1019
1019
1020 UserModel().add_extra_email(c.user.user_id, email)
1020 UserModel().add_extra_email(c.user.user_id, email)
1021 audit_logger.store_web(
1021 audit_logger.store_web(
1022 'user.edit.email.add',
1022 'user.edit.email.add',
1023 action_data={'email': email, 'user': user_data},
1023 action_data={'email': email, 'user': user_data},
1024 user=self._rhodecode_user)
1024 user=self._rhodecode_user)
1025 Session().commit()
1025 Session().commit()
1026 h.flash(_("Added new email address `%s` for user account") % email,
1026 h.flash(_("Added new email address `%s` for user account") % email,
1027 category='success')
1027 category='success')
1028 except formencode.Invalid as error:
1028 except formencode.Invalid as error:
1029 h.flash(h.escape(error.error_dict['email']), category='error')
1029 h.flash(h.escape(error.error_dict['email']), category='error')
1030 except IntegrityError:
1030 except IntegrityError:
1031 log.warning("Email %s already exists", email)
1031 log.warning("Email %s already exists", email)
1032 h.flash(_('Email `{}` is already registered for another user.').format(email),
1032 h.flash(_('Email `{}` is already registered for another user.').format(email),
1033 category='error')
1033 category='error')
1034 except Exception:
1034 except Exception:
1035 log.exception("Exception during email saving")
1035 log.exception("Exception during email saving")
1036 h.flash(_('An error occurred during email saving'),
1036 h.flash(_('An error occurred during email saving'),
1037 category='error')
1037 category='error')
1038 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1038 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1039
1039
1040 @LoginRequired()
1040 @LoginRequired()
1041 @HasPermissionAllDecorator('hg.admin')
1041 @HasPermissionAllDecorator('hg.admin')
1042 @CSRFRequired()
1042 @CSRFRequired()
1043 def emails_delete(self):
1043 def emails_delete(self):
1044 _ = self.request.translate
1044 _ = self.request.translate
1045 c = self.load_default_context()
1045 c = self.load_default_context()
1046
1046
1047 user_id = self.db_user_id
1047 user_id = self.db_user_id
1048 c.user = self.db_user
1048 c.user = self.db_user
1049
1049
1050 email_id = self.request.POST.get('del_email_id')
1050 email_id = self.request.POST.get('del_email_id')
1051 user_model = UserModel()
1051 user_model = UserModel()
1052
1052
1053 email = UserEmailMap.query().get(email_id).email
1053 email = UserEmailMap.query().get(email_id).email
1054 user_data = c.user.get_api_data()
1054 user_data = c.user.get_api_data()
1055 user_model.delete_extra_email(c.user.user_id, email_id)
1055 user_model.delete_extra_email(c.user.user_id, email_id)
1056 audit_logger.store_web(
1056 audit_logger.store_web(
1057 'user.edit.email.delete',
1057 'user.edit.email.delete',
1058 action_data={'email': email, 'user': user_data},
1058 action_data={'email': email, 'user': user_data},
1059 user=self._rhodecode_user)
1059 user=self._rhodecode_user)
1060 Session().commit()
1060 Session().commit()
1061 h.flash(_("Removed email address from user account"),
1061 h.flash(_("Removed email address from user account"),
1062 category='success')
1062 category='success')
1063 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1063 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1064
1064
1065 @LoginRequired()
1065 @LoginRequired()
1066 @HasPermissionAllDecorator('hg.admin')
1066 @HasPermissionAllDecorator('hg.admin')
1067 def ips(self):
1067 def ips(self):
1068 _ = self.request.translate
1068 _ = self.request.translate
1069 c = self.load_default_context()
1069 c = self.load_default_context()
1070 c.user = self.db_user
1070 c.user = self.db_user
1071
1071
1072 c.active = 'ips'
1072 c.active = 'ips'
1073 c.user_ip_map = UserIpMap.query() \
1073 c.user_ip_map = UserIpMap.query() \
1074 .filter(UserIpMap.user == c.user).all()
1074 .filter(UserIpMap.user == c.user).all()
1075
1075
1076 c.inherit_default_ips = c.user.inherit_default_permissions
1076 c.inherit_default_ips = c.user.inherit_default_permissions
1077 c.default_user_ip_map = UserIpMap.query() \
1077 c.default_user_ip_map = UserIpMap.query() \
1078 .filter(UserIpMap.user == User.get_default_user()).all()
1078 .filter(UserIpMap.user == User.get_default_user()).all()
1079
1079
1080 return self._get_template_context(c)
1080 return self._get_template_context(c)
1081
1081
1082 @LoginRequired()
1082 @LoginRequired()
1083 @HasPermissionAllDecorator('hg.admin')
1083 @HasPermissionAllDecorator('hg.admin')
1084 @CSRFRequired()
1084 @CSRFRequired()
1085 # NOTE(marcink): this view is allowed for default users, as we can
1085 # NOTE(marcink): this view is allowed for default users, as we can
1086 # edit their IP white list
1086 # edit their IP white list
1087 def ips_add(self):
1087 def ips_add(self):
1088 _ = self.request.translate
1088 _ = self.request.translate
1089 c = self.load_default_context()
1089 c = self.load_default_context()
1090
1090
1091 user_id = self.db_user_id
1091 user_id = self.db_user_id
1092 c.user = self.db_user
1092 c.user = self.db_user
1093
1093
1094 user_model = UserModel()
1094 user_model = UserModel()
1095 desc = self.request.POST.get('description')
1095 desc = self.request.POST.get('description')
1096 try:
1096 try:
1097 ip_list = user_model.parse_ip_range(
1097 ip_list = user_model.parse_ip_range(
1098 self.request.POST.get('new_ip'))
1098 self.request.POST.get('new_ip'))
1099 except Exception as e:
1099 except Exception as e:
1100 ip_list = []
1100 ip_list = []
1101 log.exception("Exception during ip saving")
1101 log.exception("Exception during ip saving")
1102 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1102 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1103 category='error')
1103 category='error')
1104 added = []
1104 added = []
1105 user_data = c.user.get_api_data()
1105 user_data = c.user.get_api_data()
1106 for ip in ip_list:
1106 for ip in ip_list:
1107 try:
1107 try:
1108 form = UserExtraIpForm(self.request.translate)()
1108 form = UserExtraIpForm(self.request.translate)()
1109 data = form.to_python({'ip': ip})
1109 data = form.to_python({'ip': ip})
1110 ip = data['ip']
1110 ip = data['ip']
1111
1111
1112 user_model.add_extra_ip(c.user.user_id, ip, desc)
1112 user_model.add_extra_ip(c.user.user_id, ip, desc)
1113 audit_logger.store_web(
1113 audit_logger.store_web(
1114 'user.edit.ip.add',
1114 'user.edit.ip.add',
1115 action_data={'ip': ip, 'user': user_data},
1115 action_data={'ip': ip, 'user': user_data},
1116 user=self._rhodecode_user)
1116 user=self._rhodecode_user)
1117 Session().commit()
1117 Session().commit()
1118 added.append(ip)
1118 added.append(ip)
1119 except formencode.Invalid as error:
1119 except formencode.Invalid as error:
1120 msg = error.error_dict['ip']
1120 msg = error.error_dict['ip']
1121 h.flash(msg, category='error')
1121 h.flash(msg, category='error')
1122 except Exception:
1122 except Exception:
1123 log.exception("Exception during ip saving")
1123 log.exception("Exception during ip saving")
1124 h.flash(_('An error occurred during ip saving'),
1124 h.flash(_('An error occurred during ip saving'),
1125 category='error')
1125 category='error')
1126 if added:
1126 if added:
1127 h.flash(
1127 h.flash(
1128 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1128 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1129 category='success')
1129 category='success')
1130 if 'default_user' in self.request.POST:
1130 if 'default_user' in self.request.POST:
1131 # case for editing global IP list we do it for 'DEFAULT' user
1131 # case for editing global IP list we do it for 'DEFAULT' user
1132 raise HTTPFound(h.route_path('admin_permissions_ips'))
1132 raise HTTPFound(h.route_path('admin_permissions_ips'))
1133 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1133 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1134
1134
1135 @LoginRequired()
1135 @LoginRequired()
1136 @HasPermissionAllDecorator('hg.admin')
1136 @HasPermissionAllDecorator('hg.admin')
1137 @CSRFRequired()
1137 @CSRFRequired()
1138 # NOTE(marcink): this view is allowed for default users, as we can
1138 # NOTE(marcink): this view is allowed for default users, as we can
1139 # edit their IP white list
1139 # edit their IP white list
1140 def ips_delete(self):
1140 def ips_delete(self):
1141 _ = self.request.translate
1141 _ = self.request.translate
1142 c = self.load_default_context()
1142 c = self.load_default_context()
1143
1143
1144 user_id = self.db_user_id
1144 user_id = self.db_user_id
1145 c.user = self.db_user
1145 c.user = self.db_user
1146
1146
1147 ip_id = self.request.POST.get('del_ip_id')
1147 ip_id = self.request.POST.get('del_ip_id')
1148 user_model = UserModel()
1148 user_model = UserModel()
1149 user_data = c.user.get_api_data()
1149 user_data = c.user.get_api_data()
1150 ip = UserIpMap.query().get(ip_id).ip_addr
1150 ip = UserIpMap.query().get(ip_id).ip_addr
1151 user_model.delete_extra_ip(c.user.user_id, ip_id)
1151 user_model.delete_extra_ip(c.user.user_id, ip_id)
1152 audit_logger.store_web(
1152 audit_logger.store_web(
1153 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1153 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1154 user=self._rhodecode_user)
1154 user=self._rhodecode_user)
1155 Session().commit()
1155 Session().commit()
1156 h.flash(_("Removed ip address from user whitelist"), category='success')
1156 h.flash(_("Removed ip address from user whitelist"), category='success')
1157
1157
1158 if 'default_user' in self.request.POST:
1158 if 'default_user' in self.request.POST:
1159 # case for editing global IP list we do it for 'DEFAULT' user
1159 # case for editing global IP list we do it for 'DEFAULT' user
1160 raise HTTPFound(h.route_path('admin_permissions_ips'))
1160 raise HTTPFound(h.route_path('admin_permissions_ips'))
1161 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1161 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1162
1162
1163 @LoginRequired()
1163 @LoginRequired()
1164 @HasPermissionAllDecorator('hg.admin')
1164 @HasPermissionAllDecorator('hg.admin')
1165 def groups_management(self):
1165 def groups_management(self):
1166 c = self.load_default_context()
1166 c = self.load_default_context()
1167 c.user = self.db_user
1167 c.user = self.db_user
1168 c.data = c.user.group_member
1168 c.data = c.user.group_member
1169
1169
1170 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1170 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1171 for group in c.user.group_member]
1171 for group in c.user.group_member]
1172 c.groups = json.dumps(groups)
1172 c.groups = ext_json.str_json(groups)
1173 c.active = 'groups'
1173 c.active = 'groups'
1174
1174
1175 return self._get_template_context(c)
1175 return self._get_template_context(c)
1176
1176
1177 @LoginRequired()
1177 @LoginRequired()
1178 @HasPermissionAllDecorator('hg.admin')
1178 @HasPermissionAllDecorator('hg.admin')
1179 @CSRFRequired()
1179 @CSRFRequired()
1180 def groups_management_updates(self):
1180 def groups_management_updates(self):
1181 _ = self.request.translate
1181 _ = self.request.translate
1182 c = self.load_default_context()
1182 c = self.load_default_context()
1183
1183
1184 user_id = self.db_user_id
1184 user_id = self.db_user_id
1185 c.user = self.db_user
1185 c.user = self.db_user
1186
1186
1187 user_groups = set(self.request.POST.getall('users_group_id'))
1187 user_groups = set(self.request.POST.getall('users_group_id'))
1188 user_groups_objects = []
1188 user_groups_objects = []
1189
1189
1190 for ugid in user_groups:
1190 for ugid in user_groups:
1191 user_groups_objects.append(
1191 user_groups_objects.append(
1192 UserGroupModel().get_group(safe_int(ugid)))
1192 UserGroupModel().get_group(safe_int(ugid)))
1193 user_group_model = UserGroupModel()
1193 user_group_model = UserGroupModel()
1194 added_to_groups, removed_from_groups = \
1194 added_to_groups, removed_from_groups = \
1195 user_group_model.change_groups(c.user, user_groups_objects)
1195 user_group_model.change_groups(c.user, user_groups_objects)
1196
1196
1197 user_data = c.user.get_api_data()
1197 user_data = c.user.get_api_data()
1198 for user_group_id in added_to_groups:
1198 for user_group_id in added_to_groups:
1199 user_group = UserGroup.get(user_group_id)
1199 user_group = UserGroup.get(user_group_id)
1200 old_values = user_group.get_api_data()
1200 old_values = user_group.get_api_data()
1201 audit_logger.store_web(
1201 audit_logger.store_web(
1202 'user_group.edit.member.add',
1202 'user_group.edit.member.add',
1203 action_data={'user': user_data, 'old_data': old_values},
1203 action_data={'user': user_data, 'old_data': old_values},
1204 user=self._rhodecode_user)
1204 user=self._rhodecode_user)
1205
1205
1206 for user_group_id in removed_from_groups:
1206 for user_group_id in removed_from_groups:
1207 user_group = UserGroup.get(user_group_id)
1207 user_group = UserGroup.get(user_group_id)
1208 old_values = user_group.get_api_data()
1208 old_values = user_group.get_api_data()
1209 audit_logger.store_web(
1209 audit_logger.store_web(
1210 'user_group.edit.member.delete',
1210 'user_group.edit.member.delete',
1211 action_data={'user': user_data, 'old_data': old_values},
1211 action_data={'user': user_data, 'old_data': old_values},
1212 user=self._rhodecode_user)
1212 user=self._rhodecode_user)
1213
1213
1214 Session().commit()
1214 Session().commit()
1215 c.active = 'user_groups_management'
1215 c.active = 'user_groups_management'
1216 h.flash(_("Groups successfully changed"), category='success')
1216 h.flash(_("Groups successfully changed"), category='success')
1217
1217
1218 return HTTPFound(h.route_path(
1218 return HTTPFound(h.route_path(
1219 'edit_user_groups_management', user_id=user_id))
1219 'edit_user_groups_management', user_id=user_id))
1220
1220
1221 @LoginRequired()
1221 @LoginRequired()
1222 @HasPermissionAllDecorator('hg.admin')
1222 @HasPermissionAllDecorator('hg.admin')
1223 def user_audit_logs(self):
1223 def user_audit_logs(self):
1224 _ = self.request.translate
1224 _ = self.request.translate
1225 c = self.load_default_context()
1225 c = self.load_default_context()
1226 c.user = self.db_user
1226 c.user = self.db_user
1227
1227
1228 c.active = 'audit'
1228 c.active = 'audit'
1229
1229
1230 p = safe_int(self.request.GET.get('page', 1), 1)
1230 p = safe_int(self.request.GET.get('page', 1), 1)
1231
1231
1232 filter_term = self.request.GET.get('filter')
1232 filter_term = self.request.GET.get('filter')
1233 user_log = UserModel().get_user_log(c.user, filter_term)
1233 user_log = UserModel().get_user_log(c.user, filter_term)
1234
1234
1235 def url_generator(page_num):
1235 def url_generator(page_num):
1236 query_params = {
1236 query_params = {
1237 'page': page_num
1237 'page': page_num
1238 }
1238 }
1239 if filter_term:
1239 if filter_term:
1240 query_params['filter'] = filter_term
1240 query_params['filter'] = filter_term
1241 return self.request.current_route_path(_query=query_params)
1241 return self.request.current_route_path(_query=query_params)
1242
1242
1243 c.audit_logs = SqlPage(
1243 c.audit_logs = SqlPage(
1244 user_log, page=p, items_per_page=10, url_maker=url_generator)
1244 user_log, page=p, items_per_page=10, url_maker=url_generator)
1245 c.filter_term = filter_term
1245 c.filter_term = filter_term
1246 return self._get_template_context(c)
1246 return self._get_template_context(c)
1247
1247
1248 @LoginRequired()
1248 @LoginRequired()
1249 @HasPermissionAllDecorator('hg.admin')
1249 @HasPermissionAllDecorator('hg.admin')
1250 def user_audit_logs_download(self):
1250 def user_audit_logs_download(self):
1251 _ = self.request.translate
1251 _ = self.request.translate
1252 c = self.load_default_context()
1252 c = self.load_default_context()
1253 c.user = self.db_user
1253 c.user = self.db_user
1254
1254
1255 user_log = UserModel().get_user_log(c.user, filter_term=None)
1255 user_log = UserModel().get_user_log(c.user, filter_term=None)
1256
1256
1257 audit_log_data = {}
1257 audit_log_data = {}
1258 for entry in user_log:
1258 for entry in user_log:
1259 audit_log_data[entry.user_log_id] = entry.get_dict()
1259 audit_log_data[entry.user_log_id] = entry.get_dict()
1260
1260
1261 response = Response(json.dumps(audit_log_data, indent=4))
1261 response = Response(ext_json.formatted_str_json(audit_log_data))
1262 response.content_disposition = str(
1262 response.content_disposition = f'attachment; filename=user_{c.user.user_id}_audit_logs.json'
1263 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1264 response.content_type = 'application/json'
1263 response.content_type = 'application/json'
1265
1264
1266 return response
1265 return response
1267
1266
1268 @LoginRequired()
1267 @LoginRequired()
1269 @HasPermissionAllDecorator('hg.admin')
1268 @HasPermissionAllDecorator('hg.admin')
1270 def user_perms_summary(self):
1269 def user_perms_summary(self):
1271 _ = self.request.translate
1270 _ = self.request.translate
1272 c = self.load_default_context()
1271 c = self.load_default_context()
1273 c.user = self.db_user
1272 c.user = self.db_user
1274
1273
1275 c.active = 'perms_summary'
1274 c.active = 'perms_summary'
1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1275 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1277
1276
1278 return self._get_template_context(c)
1277 return self._get_template_context(c)
1279
1278
1280 @LoginRequired()
1279 @LoginRequired()
1281 @HasPermissionAllDecorator('hg.admin')
1280 @HasPermissionAllDecorator('hg.admin')
1282 def user_perms_summary_json(self):
1281 def user_perms_summary_json(self):
1283 self.load_default_context()
1282 self.load_default_context()
1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1283 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1285
1284
1286 return perm_user.permissions
1285 return perm_user.permissions
1287
1286
1288 @LoginRequired()
1287 @LoginRequired()
1289 @HasPermissionAllDecorator('hg.admin')
1288 @HasPermissionAllDecorator('hg.admin')
1290 def user_caches(self):
1289 def user_caches(self):
1291 _ = self.request.translate
1290 _ = self.request.translate
1292 c = self.load_default_context()
1291 c = self.load_default_context()
1293 c.user = self.db_user
1292 c.user = self.db_user
1294
1293
1295 c.active = 'caches'
1294 c.active = 'caches'
1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1295 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1297
1296
1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1297 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1298 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1300 c.backend = c.region.backend
1299 c.backend = c.region.backend
1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1300 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1302
1301
1303 return self._get_template_context(c)
1302 return self._get_template_context(c)
1304
1303
1305 @LoginRequired()
1304 @LoginRequired()
1306 @HasPermissionAllDecorator('hg.admin')
1305 @HasPermissionAllDecorator('hg.admin')
1307 @CSRFRequired()
1306 @CSRFRequired()
1308 def user_caches_update(self):
1307 def user_caches_update(self):
1309 _ = self.request.translate
1308 _ = self.request.translate
1310 c = self.load_default_context()
1309 c = self.load_default_context()
1311 c.user = self.db_user
1310 c.user = self.db_user
1312
1311
1313 c.active = 'caches'
1312 c.active = 'caches'
1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1313 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1315
1314
1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1315 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1316 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1318
1317
1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1318 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1320
1319
1321 return HTTPFound(h.route_path(
1320 return HTTPFound(h.route_path(
1322 'edit_user_caches', user_id=c.user.user_id))
1321 'edit_user_caches', user_id=c.user.user_id))
@@ -1,106 +1,107 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import os
21 import os
22
22
23 from pyramid.events import ApplicationCreated
23 from pyramid.events import ApplicationCreated
24 from pyramid.settings import asbool
24 from pyramid.settings import asbool
25
25
26 from rhodecode.apps._base import ADMIN_PREFIX
26 from rhodecode.apps._base import ADMIN_PREFIX
27 from rhodecode.lib.ext_json import json
27 from rhodecode.lib.ext_json import json
28 from rhodecode.lib.str_utils import safe_str
28
29
29
30
30 def url_gen(request):
31 def url_gen(request):
31 registry = request.registry
32 registry = request.registry
32 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
33 longpoll_url = registry.settings.get('channelstream.longpoll_url', '')
33 ws_url = registry.settings.get('channelstream.ws_url', '')
34 ws_url = registry.settings.get('channelstream.ws_url', '')
34 proxy_url = request.route_url('channelstream_proxy')
35 proxy_url = request.route_url('channelstream_proxy')
35 urls = {
36 urls = {
36 'connect': request.route_path('channelstream_connect'),
37 'connect': request.route_path('channelstream_connect'),
37 'subscribe': request.route_path('channelstream_subscribe'),
38 'subscribe': request.route_path('channelstream_subscribe'),
38 'longpoll': longpoll_url or proxy_url,
39 'longpoll': longpoll_url or proxy_url,
39 'ws': ws_url or proxy_url.replace('http', 'ws')
40 'ws': ws_url or proxy_url.replace('http', 'ws')
40 }
41 }
41 return json.dumps(urls)
42 return safe_str(json.dumps(urls))
42
43
43
44
44 PLUGIN_DEFINITION = {
45 PLUGIN_DEFINITION = {
45 'name': 'channelstream',
46 'name': 'channelstream',
46 'config': {
47 'config': {
47 'javascript': [],
48 'javascript': [],
48 'css': [],
49 'css': [],
49 'template_hooks': {
50 'template_hooks': {
50 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
51 'plugin_init_template': 'rhodecode:templates/channelstream/plugin_init.mako'
51 },
52 },
52 'url_gen': url_gen,
53 'url_gen': url_gen,
53 'static': None,
54 'static': None,
54 'enabled': False,
55 'enabled': False,
55 'server': '',
56 'server': '',
56 'secret': ''
57 'secret': ''
57 }
58 }
58 }
59 }
59
60
60
61
61 def maybe_create_history_store(event):
62 def maybe_create_history_store(event):
62 # create plugin history location
63 # create plugin history location
63 settings = event.app.registry.settings
64 settings = event.app.registry.settings
64 history_dir = settings.get('channelstream.history.location', '')
65 history_dir = settings.get('channelstream.history.location', '')
65 if history_dir and not os.path.exists(history_dir):
66 if history_dir and not os.path.exists(history_dir):
66 os.makedirs(history_dir, 0o750)
67 os.makedirs(history_dir, 0o750)
67
68
68
69
69 def includeme(config):
70 def includeme(config):
70 from rhodecode.apps.channelstream.views import ChannelstreamView
71 from rhodecode.apps.channelstream.views import ChannelstreamView
71
72
72 settings = config.registry.settings
73 settings = config.registry.settings
73 PLUGIN_DEFINITION['config']['enabled'] = asbool(
74 PLUGIN_DEFINITION['config']['enabled'] = asbool(
74 settings.get('channelstream.enabled'))
75 settings.get('channelstream.enabled'))
75 PLUGIN_DEFINITION['config']['server'] = settings.get(
76 PLUGIN_DEFINITION['config']['server'] = settings.get(
76 'channelstream.server', '')
77 'channelstream.server', '')
77 PLUGIN_DEFINITION['config']['secret'] = settings.get(
78 PLUGIN_DEFINITION['config']['secret'] = settings.get(
78 'channelstream.secret', '')
79 'channelstream.secret', '')
79 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
80 PLUGIN_DEFINITION['config']['history.location'] = settings.get(
80 'channelstream.history.location', '')
81 'channelstream.history.location', '')
81 config.register_rhodecode_plugin(
82 config.register_rhodecode_plugin(
82 PLUGIN_DEFINITION['name'],
83 PLUGIN_DEFINITION['name'],
83 PLUGIN_DEFINITION['config']
84 PLUGIN_DEFINITION['config']
84 )
85 )
85 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
86 config.add_subscriber(maybe_create_history_store, ApplicationCreated)
86
87
87 config.add_route(
88 config.add_route(
88 name='channelstream_connect',
89 name='channelstream_connect',
89 pattern=ADMIN_PREFIX + '/channelstream/connect')
90 pattern=ADMIN_PREFIX + '/channelstream/connect')
90 config.add_view(
91 config.add_view(
91 ChannelstreamView,
92 ChannelstreamView,
92 attr='channelstream_connect',
93 attr='channelstream_connect',
93 route_name='channelstream_connect', renderer='json_ext')
94 route_name='channelstream_connect', renderer='json_ext')
94
95
95 config.add_route(
96 config.add_route(
96 name='channelstream_subscribe',
97 name='channelstream_subscribe',
97 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
98 pattern=ADMIN_PREFIX + '/channelstream/subscribe')
98 config.add_view(
99 config.add_view(
99 ChannelstreamView,
100 ChannelstreamView,
100 attr='channelstream_subscribe',
101 attr='channelstream_subscribe',
101 route_name='channelstream_subscribe', renderer='json_ext')
102 route_name='channelstream_subscribe', renderer='json_ext')
102
103
103 config.add_route(
104 config.add_route(
104 name='channelstream_proxy',
105 name='channelstream_proxy',
105 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
106 pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
106
107
@@ -1,386 +1,386 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2013-2020 RhodeCode GmbH
3 # Copyright (C) 2013-2020 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 import time
21 import time
22 import logging
22 import logging
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27
27
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound, HTTPBadRequest
29 from pyramid.renderers import render
29 from pyramid.renderers import render
30 from pyramid.response import Response
30 from pyramid.response import Response
31
31
32 from rhodecode.apps._base import BaseAppView
32 from rhodecode.apps._base import BaseAppView
33 from rhodecode.lib import helpers as h
33 from rhodecode.lib import helpers as h, ext_json
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
34 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
35 from rhodecode.lib.utils2 import time_to_datetime
35 from rhodecode.lib.utils2 import time_to_datetime
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
37 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
38 from rhodecode.model.gist import GistModel
38 from rhodecode.model.gist import GistModel
39 from rhodecode.model.meta import Session
39 from rhodecode.model.meta import Session
40 from rhodecode.model.db import Gist, User, or_
40 from rhodecode.model.db import Gist, User, or_
41 from rhodecode.model import validation_schema
41 from rhodecode.model import validation_schema
42 from rhodecode.model.validation_schema.schemas import gist_schema
42 from rhodecode.model.validation_schema.schemas import gist_schema
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class GistView(BaseAppView):
48 class GistView(BaseAppView):
49
49
50 def load_default_context(self):
50 def load_default_context(self):
51 _ = self.request.translate
51 _ = self.request.translate
52 c = self._get_local_tmpl_context()
52 c = self._get_local_tmpl_context()
53 c.user = c.auth_user.get_instance()
53 c.user = c.auth_user.get_instance()
54
54
55 c.lifetime_values = [
55 c.lifetime_values = [
56 (-1, _('forever')),
56 (-1, _('forever')),
57 (5, _('5 minutes')),
57 (5, _('5 minutes')),
58 (60, _('1 hour')),
58 (60, _('1 hour')),
59 (60 * 24, _('1 day')),
59 (60 * 24, _('1 day')),
60 (60 * 24 * 30, _('1 month')),
60 (60 * 24 * 30, _('1 month')),
61 ]
61 ]
62
62
63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
63 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
64 c.acl_options = [
64 c.acl_options = [
65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
65 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
66 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
67 ]
67 ]
68
68
69 return c
69 return c
70
70
71 @LoginRequired()
71 @LoginRequired()
72 def gist_show_all(self):
72 def gist_show_all(self):
73 c = self.load_default_context()
73 c = self.load_default_context()
74
74
75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
75 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
76 c.show_private = self.request.GET.get('private') and not_default_user
76 c.show_private = self.request.GET.get('private') and not_default_user
77 c.show_public = self.request.GET.get('public') and not_default_user
77 c.show_public = self.request.GET.get('public') and not_default_user
78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
78 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
79
79
80 gists = _gists = Gist().query()\
80 gists = _gists = Gist().query()\
81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
81 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
82 .order_by(Gist.created_on.desc())
82 .order_by(Gist.created_on.desc())
83
83
84 c.active = 'public'
84 c.active = 'public'
85 # MY private
85 # MY private
86 if c.show_private and not c.show_public:
86 if c.show_private and not c.show_public:
87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
87 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
88 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
89 c.active = 'my_private'
89 c.active = 'my_private'
90 # MY public
90 # MY public
91 elif c.show_public and not c.show_private:
91 elif c.show_public and not c.show_private:
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 c.active = 'my_public'
94 c.active = 'my_public'
95 # MY public+private
95 # MY public+private
96 elif c.show_private and c.show_public:
96 elif c.show_private and c.show_public:
97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
97 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
98 Gist.gist_type == Gist.GIST_PRIVATE))\
98 Gist.gist_type == Gist.GIST_PRIVATE))\
99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
100 c.active = 'my_all'
100 c.active = 'my_all'
101 # Show all by super-admin
101 # Show all by super-admin
102 elif c.show_all:
102 elif c.show_all:
103 c.active = 'all'
103 c.active = 'all'
104 gists = _gists
104 gists = _gists
105
105
106 # default show ALL public gists
106 # default show ALL public gists
107 if not c.show_public and not c.show_private and not c.show_all:
107 if not c.show_public and not c.show_private and not c.show_all:
108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
108 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
109 c.active = 'public'
109 c.active = 'public'
110
110
111 _render = self.request.get_partial_renderer(
111 _render = self.request.get_partial_renderer(
112 'rhodecode:templates/data_table/_dt_elements.mako')
112 'rhodecode:templates/data_table/_dt_elements.mako')
113
113
114 data = []
114 data = []
115
115
116 for gist in gists:
116 for gist in gists:
117 data.append({
117 data.append({
118 'created_on': _render('gist_created', gist.created_on),
118 'created_on': _render('gist_created', gist.created_on),
119 'created_on_raw': gist.created_on,
119 'created_on_raw': gist.created_on,
120 'type': _render('gist_type', gist.gist_type),
120 'type': _render('gist_type', gist.gist_type),
121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
121 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
122 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
123 'author_raw': h.escape(gist.owner.full_contact),
123 'author_raw': h.escape(gist.owner.full_contact),
124 'expires': _render('gist_expires', gist.gist_expires),
124 'expires': _render('gist_expires', gist.gist_expires),
125 'description': _render('gist_description', gist.gist_description)
125 'description': _render('gist_description', gist.gist_description)
126 })
126 })
127 c.data = json.dumps(data)
127 c.data = ext_json.str_json(data)
128
128
129 return self._get_template_context(c)
129 return self._get_template_context(c)
130
130
131 @LoginRequired()
131 @LoginRequired()
132 @NotAnonymous()
132 @NotAnonymous()
133 def gist_new(self):
133 def gist_new(self):
134 c = self.load_default_context()
134 c = self.load_default_context()
135 return self._get_template_context(c)
135 return self._get_template_context(c)
136
136
137 @LoginRequired()
137 @LoginRequired()
138 @NotAnonymous()
138 @NotAnonymous()
139 @CSRFRequired()
139 @CSRFRequired()
140 def gist_create(self):
140 def gist_create(self):
141 _ = self.request.translate
141 _ = self.request.translate
142 c = self.load_default_context()
142 c = self.load_default_context()
143
143
144 data = dict(self.request.POST)
144 data = dict(self.request.POST)
145 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
145 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
146
146
147 data['nodes'] = [{
147 data['nodes'] = [{
148 'filename': data['filename'],
148 'filename': data['filename'],
149 'content': data.get('content'),
149 'content': data.get('content'),
150 'mimetype': data.get('mimetype') # None is autodetect
150 'mimetype': data.get('mimetype') # None is autodetect
151 }]
151 }]
152 gist_type = {
152 gist_type = {
153 'public': Gist.GIST_PUBLIC,
153 'public': Gist.GIST_PUBLIC,
154 'private': Gist.GIST_PRIVATE
154 'private': Gist.GIST_PRIVATE
155 }.get(data.get('gist_type')) or Gist.GIST_PRIVATE
155 }.get(data.get('gist_type')) or Gist.GIST_PRIVATE
156
156
157 data['gist_type'] = gist_type
157 data['gist_type'] = gist_type
158
158
159 data['gist_acl_level'] = (
159 data['gist_acl_level'] = (
160 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
160 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
161
161
162 schema = gist_schema.GistSchema().bind(
162 schema = gist_schema.GistSchema().bind(
163 lifetime_options=[x[0] for x in c.lifetime_values])
163 lifetime_options=[x[0] for x in c.lifetime_values])
164
164
165 try:
165 try:
166
166
167 schema_data = schema.deserialize(data)
167 schema_data = schema.deserialize(data)
168 # convert to safer format with just KEYs so we sure no duplicates
168 # convert to safer format with just KEYs so we sure no duplicates
169 schema_data['nodes'] = gist_schema.sequence_to_nodes(
169 schema_data['nodes'] = gist_schema.sequence_to_nodes(
170 schema_data['nodes'])
170 schema_data['nodes'])
171
171
172 gist = GistModel().create(
172 gist = GistModel().create(
173 gist_id=schema_data['gistid'], # custom access id not real ID
173 gist_id=schema_data['gistid'], # custom access id not real ID
174 description=schema_data['description'],
174 description=schema_data['description'],
175 owner=self._rhodecode_user.user_id,
175 owner=self._rhodecode_user.user_id,
176 gist_mapping=schema_data['nodes'],
176 gist_mapping=schema_data['nodes'],
177 gist_type=schema_data['gist_type'],
177 gist_type=schema_data['gist_type'],
178 lifetime=schema_data['lifetime'],
178 lifetime=schema_data['lifetime'],
179 gist_acl_level=schema_data['gist_acl_level']
179 gist_acl_level=schema_data['gist_acl_level']
180 )
180 )
181 Session().commit()
181 Session().commit()
182 new_gist_id = gist.gist_access_id
182 new_gist_id = gist.gist_access_id
183 except validation_schema.Invalid as errors:
183 except validation_schema.Invalid as errors:
184 defaults = data
184 defaults = data
185 errors = errors.asdict()
185 errors = errors.asdict()
186
186
187 if 'nodes.0.content' in errors:
187 if 'nodes.0.content' in errors:
188 errors['content'] = errors['nodes.0.content']
188 errors['content'] = errors['nodes.0.content']
189 del errors['nodes.0.content']
189 del errors['nodes.0.content']
190 if 'nodes.0.filename' in errors:
190 if 'nodes.0.filename' in errors:
191 errors['filename'] = errors['nodes.0.filename']
191 errors['filename'] = errors['nodes.0.filename']
192 del errors['nodes.0.filename']
192 del errors['nodes.0.filename']
193
193
194 data = render('rhodecode:templates/admin/gists/gist_new.mako',
194 data = render('rhodecode:templates/admin/gists/gist_new.mako',
195 self._get_template_context(c), self.request)
195 self._get_template_context(c), self.request)
196 html = formencode.htmlfill.render(
196 html = formencode.htmlfill.render(
197 data,
197 data,
198 defaults=defaults,
198 defaults=defaults,
199 errors=errors,
199 errors=errors,
200 prefix_error=False,
200 prefix_error=False,
201 encoding="UTF-8",
201 encoding="UTF-8",
202 force_defaults=False
202 force_defaults=False
203 )
203 )
204 return Response(html)
204 return Response(html)
205
205
206 except Exception:
206 except Exception:
207 log.exception("Exception while trying to create a gist")
207 log.exception("Exception while trying to create a gist")
208 h.flash(_('Error occurred during gist creation'), category='error')
208 h.flash(_('Error occurred during gist creation'), category='error')
209 raise HTTPFound(h.route_url('gists_new'))
209 raise HTTPFound(h.route_url('gists_new'))
210 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
210 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
211
211
212 @LoginRequired()
212 @LoginRequired()
213 @NotAnonymous()
213 @NotAnonymous()
214 @CSRFRequired()
214 @CSRFRequired()
215 def gist_delete(self):
215 def gist_delete(self):
216 _ = self.request.translate
216 _ = self.request.translate
217 gist_id = self.request.matchdict['gist_id']
217 gist_id = self.request.matchdict['gist_id']
218
218
219 c = self.load_default_context()
219 c = self.load_default_context()
220 c.gist = Gist.get_or_404(gist_id)
220 c.gist = Gist.get_or_404(gist_id)
221
221
222 owner = c.gist.gist_owner == self._rhodecode_user.user_id
222 owner = c.gist.gist_owner == self._rhodecode_user.user_id
223 if not (h.HasPermissionAny('hg.admin')() or owner):
223 if not (h.HasPermissionAny('hg.admin')() or owner):
224 log.warning('Deletion of Gist was forbidden '
224 log.warning('Deletion of Gist was forbidden '
225 'by unauthorized user: `%s`', self._rhodecode_user)
225 'by unauthorized user: `%s`', self._rhodecode_user)
226 raise HTTPNotFound()
226 raise HTTPNotFound()
227
227
228 GistModel().delete(c.gist)
228 GistModel().delete(c.gist)
229 Session().commit()
229 Session().commit()
230 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
230 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
231
231
232 raise HTTPFound(h.route_url('gists_show'))
232 raise HTTPFound(h.route_url('gists_show'))
233
233
234 def _get_gist(self, gist_id):
234 def _get_gist(self, gist_id):
235
235
236 gist = Gist.get_or_404(gist_id)
236 gist = Gist.get_or_404(gist_id)
237
237
238 # Check if this gist is expired
238 # Check if this gist is expired
239 if gist.gist_expires != -1:
239 if gist.gist_expires != -1:
240 if time.time() > gist.gist_expires:
240 if time.time() > gist.gist_expires:
241 log.error(
241 log.error(
242 'Gist expired at %s', time_to_datetime(gist.gist_expires))
242 'Gist expired at %s', time_to_datetime(gist.gist_expires))
243 raise HTTPNotFound()
243 raise HTTPNotFound()
244
244
245 # check if this gist requires a login
245 # check if this gist requires a login
246 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
246 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
247 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
247 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
248 log.error("Anonymous user %s tried to access protected gist `%s`",
248 log.error("Anonymous user %s tried to access protected gist `%s`",
249 self._rhodecode_user, gist_id)
249 self._rhodecode_user, gist_id)
250 raise HTTPNotFound()
250 raise HTTPNotFound()
251 return gist
251 return gist
252
252
253 @LoginRequired()
253 @LoginRequired()
254 def gist_show(self):
254 def gist_show(self):
255 gist_id = self.request.matchdict['gist_id']
255 gist_id = self.request.matchdict['gist_id']
256
256
257 # TODO(marcink): expose those via matching dict
257 # TODO(marcink): expose those via matching dict
258 revision = self.request.matchdict.get('revision', 'tip')
258 revision = self.request.matchdict.get('revision', 'tip')
259 f_path = self.request.matchdict.get('f_path', None)
259 f_path = self.request.matchdict.get('f_path', None)
260 return_format = self.request.matchdict.get('format')
260 return_format = self.request.matchdict.get('format')
261
261
262 c = self.load_default_context()
262 c = self.load_default_context()
263 c.gist = self._get_gist(gist_id)
263 c.gist = self._get_gist(gist_id)
264 c.render = not self.request.GET.get('no-render', False)
264 c.render = not self.request.GET.get('no-render', False)
265
265
266 try:
266 try:
267 c.file_last_commit, c.files = GistModel().get_gist_files(
267 c.file_last_commit, c.files = GistModel().get_gist_files(
268 gist_id, revision=revision)
268 gist_id, revision=revision)
269 except VCSError:
269 except VCSError:
270 log.exception("Exception in gist show")
270 log.exception("Exception in gist show")
271 raise HTTPNotFound()
271 raise HTTPNotFound()
272
272
273 if return_format == 'raw':
273 if return_format == 'raw':
274 content = '\n\n'.join([f.content for f in c.files
274 content = '\n\n'.join([f.content for f in c.files
275 if (f_path is None or f.path == f_path)])
275 if (f_path is None or f.path == f_path)])
276 response = Response(content)
276 response = Response(content)
277 response.content_type = 'text/plain'
277 response.content_type = 'text/plain'
278 return response
278 return response
279 elif return_format:
279 elif return_format:
280 raise HTTPBadRequest()
280 raise HTTPBadRequest()
281
281
282 return self._get_template_context(c)
282 return self._get_template_context(c)
283
283
284 @LoginRequired()
284 @LoginRequired()
285 @NotAnonymous()
285 @NotAnonymous()
286 def gist_edit(self):
286 def gist_edit(self):
287 _ = self.request.translate
287 _ = self.request.translate
288 gist_id = self.request.matchdict['gist_id']
288 gist_id = self.request.matchdict['gist_id']
289 c = self.load_default_context()
289 c = self.load_default_context()
290 c.gist = self._get_gist(gist_id)
290 c.gist = self._get_gist(gist_id)
291
291
292 owner = c.gist.gist_owner == self._rhodecode_user.user_id
292 owner = c.gist.gist_owner == self._rhodecode_user.user_id
293 if not (h.HasPermissionAny('hg.admin')() or owner):
293 if not (h.HasPermissionAny('hg.admin')() or owner):
294 raise HTTPNotFound()
294 raise HTTPNotFound()
295
295
296 try:
296 try:
297 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
297 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
298 except VCSError:
298 except VCSError:
299 log.exception("Exception in gist edit")
299 log.exception("Exception in gist edit")
300 raise HTTPNotFound()
300 raise HTTPNotFound()
301
301
302 if c.gist.gist_expires == -1:
302 if c.gist.gist_expires == -1:
303 expiry = _('never')
303 expiry = _('never')
304 else:
304 else:
305 # this cannot use timeago, since it's used in select2 as a value
305 # this cannot use timeago, since it's used in select2 as a value
306 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
306 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
307
307
308 c.lifetime_values.append(
308 c.lifetime_values.append(
309 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
309 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
310 )
310 )
311
311
312 return self._get_template_context(c)
312 return self._get_template_context(c)
313
313
314 @LoginRequired()
314 @LoginRequired()
315 @NotAnonymous()
315 @NotAnonymous()
316 @CSRFRequired()
316 @CSRFRequired()
317 def gist_update(self):
317 def gist_update(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 gist_id = self.request.matchdict['gist_id']
319 gist_id = self.request.matchdict['gist_id']
320 c = self.load_default_context()
320 c = self.load_default_context()
321 c.gist = self._get_gist(gist_id)
321 c.gist = self._get_gist(gist_id)
322
322
323 owner = c.gist.gist_owner == self._rhodecode_user.user_id
323 owner = c.gist.gist_owner == self._rhodecode_user.user_id
324 if not (h.HasPermissionAny('hg.admin')() or owner):
324 if not (h.HasPermissionAny('hg.admin')() or owner):
325 raise HTTPNotFound()
325 raise HTTPNotFound()
326
326
327 data = peppercorn.parse(self.request.POST.items())
327 data = peppercorn.parse(self.request.POST.items())
328
328
329 schema = gist_schema.GistSchema()
329 schema = gist_schema.GistSchema()
330 schema = schema.bind(
330 schema = schema.bind(
331 # '0' is special value to leave lifetime untouched
331 # '0' is special value to leave lifetime untouched
332 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
332 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
333 )
333 )
334
334
335 try:
335 try:
336 schema_data = schema.deserialize(data)
336 schema_data = schema.deserialize(data)
337 # convert to safer format with just KEYs so we sure no duplicates
337 # convert to safer format with just KEYs so we sure no duplicates
338 schema_data['nodes'] = gist_schema.sequence_to_nodes(
338 schema_data['nodes'] = gist_schema.sequence_to_nodes(
339 schema_data['nodes'])
339 schema_data['nodes'])
340
340
341 GistModel().update(
341 GistModel().update(
342 gist=c.gist,
342 gist=c.gist,
343 description=schema_data['description'],
343 description=schema_data['description'],
344 owner=c.gist.owner,
344 owner=c.gist.owner,
345 gist_mapping=schema_data['nodes'],
345 gist_mapping=schema_data['nodes'],
346 lifetime=schema_data['lifetime'],
346 lifetime=schema_data['lifetime'],
347 gist_acl_level=schema_data['gist_acl_level']
347 gist_acl_level=schema_data['gist_acl_level']
348 )
348 )
349
349
350 Session().commit()
350 Session().commit()
351 h.flash(_('Successfully updated gist content'), category='success')
351 h.flash(_('Successfully updated gist content'), category='success')
352 except NodeNotChangedError:
352 except NodeNotChangedError:
353 # raised if nothing was changed in repo itself. We anyway then
353 # raised if nothing was changed in repo itself. We anyway then
354 # store only DB stuff for gist
354 # store only DB stuff for gist
355 Session().commit()
355 Session().commit()
356 h.flash(_('Successfully updated gist data'), category='success')
356 h.flash(_('Successfully updated gist data'), category='success')
357 except validation_schema.Invalid as errors:
357 except validation_schema.Invalid as errors:
358 errors = h.escape(errors.asdict())
358 errors = h.escape(errors.asdict())
359 h.flash(_('Error occurred during update of gist {}: {}').format(
359 h.flash(_('Error occurred during update of gist {}: {}').format(
360 gist_id, errors), category='error')
360 gist_id, errors), category='error')
361 except Exception:
361 except Exception:
362 log.exception("Exception in gist edit")
362 log.exception("Exception in gist edit")
363 h.flash(_('Error occurred during update of gist %s') % gist_id,
363 h.flash(_('Error occurred during update of gist %s') % gist_id,
364 category='error')
364 category='error')
365
365
366 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
366 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
367
367
368 @LoginRequired()
368 @LoginRequired()
369 @NotAnonymous()
369 @NotAnonymous()
370 def gist_edit_check_revision(self):
370 def gist_edit_check_revision(self):
371 _ = self.request.translate
371 _ = self.request.translate
372 gist_id = self.request.matchdict['gist_id']
372 gist_id = self.request.matchdict['gist_id']
373 c = self.load_default_context()
373 c = self.load_default_context()
374 c.gist = self._get_gist(gist_id)
374 c.gist = self._get_gist(gist_id)
375
375
376 last_rev = c.gist.scm_instance().get_commit()
376 last_rev = c.gist.scm_instance().get_commit()
377 success = True
377 success = True
378 revision = self.request.GET.get('revision')
378 revision = self.request.GET.get('revision')
379
379
380 if revision != last_rev.raw_id:
380 if revision != last_rev.raw_id:
381 log.error('Last revision %s is different then submitted %s',
381 log.error('Last revision %s is different then submitted %s',
382 revision, last_rev)
382 revision, last_rev)
383 # our gist has newer version than we
383 # our gist has newer version than we
384 success = False
384 success = False
385
385
386 return {'success': success}
386 return {'success': success}
@@ -1,783 +1,783 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 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 import logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib import ext_json
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, NotAnonymous, CSRFRequired,
36 LoginRequired, NotAnonymous, CSRFRequired,
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 from rhodecode.lib.channelstream import (
38 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator,
44 IntegrityError, or_, in_filter_generator,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.user_group import UserGroupModel
50 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 return c
67 return c
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @NotAnonymous()
70 @NotAnonymous()
71 def my_account_profile(self):
71 def my_account_profile(self):
72 c = self.load_default_context()
72 c = self.load_default_context()
73 c.active = 'profile'
73 c.active = 'profile'
74 c.extern_type = c.user.extern_type
74 c.extern_type = c.user.extern_type
75 return self._get_template_context(c)
75 return self._get_template_context(c)
76
76
77 @LoginRequired()
77 @LoginRequired()
78 @NotAnonymous()
78 @NotAnonymous()
79 def my_account_edit(self):
79 def my_account_edit(self):
80 c = self.load_default_context()
80 c = self.load_default_context()
81 c.active = 'profile_edit'
81 c.active = 'profile_edit'
82 c.extern_type = c.user.extern_type
82 c.extern_type = c.user.extern_type
83 c.extern_name = c.user.extern_name
83 c.extern_name = c.user.extern_name
84
84
85 schema = user_schema.UserProfileSchema().bind(
85 schema = user_schema.UserProfileSchema().bind(
86 username=c.user.username, user_emails=c.user.emails)
86 username=c.user.username, user_emails=c.user.emails)
87 appstruct = {
87 appstruct = {
88 'username': c.user.username,
88 'username': c.user.username,
89 'email': c.user.email,
89 'email': c.user.email,
90 'firstname': c.user.firstname,
90 'firstname': c.user.firstname,
91 'lastname': c.user.lastname,
91 'lastname': c.user.lastname,
92 'description': c.user.description,
92 'description': c.user.description,
93 }
93 }
94 c.form = forms.RcForm(
94 c.form = forms.RcForm(
95 schema, appstruct=appstruct,
95 schema, appstruct=appstruct,
96 action=h.route_path('my_account_update'),
96 action=h.route_path('my_account_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
97 buttons=(forms.buttons.save, forms.buttons.reset))
98
98
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 def my_account_update(self):
104 def my_account_update(self):
105 _ = self.request.translate
105 _ = self.request.translate
106 c = self.load_default_context()
106 c = self.load_default_context()
107 c.active = 'profile_edit'
107 c.active = 'profile_edit'
108 c.perm_user = c.auth_user
108 c.perm_user = c.auth_user
109 c.extern_type = c.user.extern_type
109 c.extern_type = c.user.extern_type
110 c.extern_name = c.user.extern_name
110 c.extern_name = c.user.extern_name
111
111
112 schema = user_schema.UserProfileSchema().bind(
112 schema = user_schema.UserProfileSchema().bind(
113 username=c.user.username, user_emails=c.user.emails)
113 username=c.user.username, user_emails=c.user.emails)
114 form = forms.RcForm(
114 form = forms.RcForm(
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116
116
117 controls = self.request.POST.items()
117 controls = self.request.POST.items()
118 try:
118 try:
119 valid_data = form.validate(controls)
119 valid_data = form.validate(controls)
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 'new_password', 'password_confirmation']
121 'new_password', 'password_confirmation']
122 if c.extern_type != "rhodecode":
122 if c.extern_type != "rhodecode":
123 # forbid updating username for external accounts
123 # forbid updating username for external accounts
124 skip_attrs.append('username')
124 skip_attrs.append('username')
125 old_email = c.user.email
125 old_email = c.user.email
126 UserModel().update_user(
126 UserModel().update_user(
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 **valid_data)
128 **valid_data)
129 if old_email != valid_data['email']:
129 if old_email != valid_data['email']:
130 old = UserEmailMap.query() \
130 old = UserEmailMap.query() \
131 .filter(UserEmailMap.user == c.user)\
131 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.email == valid_data['email'])\
132 .filter(UserEmailMap.email == valid_data['email'])\
133 .first()
133 .first()
134 old.email = old_email
134 old.email = old_email
135 h.flash(_('Your account was updated successfully'), category='success')
135 h.flash(_('Your account was updated successfully'), category='success')
136 Session().commit()
136 Session().commit()
137 except forms.ValidationFailure as e:
137 except forms.ValidationFailure as e:
138 c.form = e
138 c.form = e
139 return self._get_template_context(c)
139 return self._get_template_context(c)
140 except Exception:
140 except Exception:
141 log.exception("Exception updating user")
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
142 h.flash(_('Error occurred during update of user'),
143 category='error')
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
144 raise HTTPFound(h.route_path('my_account_profile'))
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @NotAnonymous()
147 @NotAnonymous()
148 def my_account_password(self):
148 def my_account_password(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150 c.active = 'password'
150 c.active = 'password'
151 c.extern_type = c.user.extern_type
151 c.extern_type = c.user.extern_type
152
152
153 schema = user_schema.ChangePasswordSchema().bind(
153 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
154 username=c.user.username)
155
155
156 form = forms.Form(
156 form = forms.Form(
157 schema,
157 schema,
158 action=h.route_path('my_account_password_update'),
158 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
159 buttons=(forms.buttons.save, forms.buttons.reset))
160
160
161 c.form = form
161 c.form = form
162 return self._get_template_context(c)
162 return self._get_template_context(c)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @NotAnonymous()
165 @NotAnonymous()
166 @CSRFRequired()
166 @CSRFRequired()
167 def my_account_password_update(self):
167 def my_account_password_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'password'
170 c.active = 'password'
171 c.extern_type = c.user.extern_type
171 c.extern_type = c.user.extern_type
172
172
173 schema = user_schema.ChangePasswordSchema().bind(
173 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
174 username=c.user.username)
175
175
176 form = forms.Form(
176 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
178
179 if c.extern_type != 'rhodecode':
179 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
180 raise HTTPFound(self.request.route_path('my_account_password'))
181
181
182 controls = self.request.POST.items()
182 controls = self.request.POST.items()
183 try:
183 try:
184 valid_data = form.validate(controls)
184 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
186 c.user.update_userdata(force_password_change=False)
187 Session().commit()
187 Session().commit()
188 except forms.ValidationFailure as e:
188 except forms.ValidationFailure as e:
189 c.form = e
189 c.form = e
190 return self._get_template_context(c)
190 return self._get_template_context(c)
191
191
192 except Exception:
192 except Exception:
193 log.exception("Exception updating password")
193 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
194 h.flash(_('Error occurred during update of user password'),
195 category='error')
195 category='error')
196 else:
196 else:
197 instance = c.auth_user.get_instance()
197 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
198 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5(instance.password)})
199 {'password': md5(instance.password)})
200 self.session.save()
200 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
201 h.flash(_("Successfully updated password"), category='success')
202
202
203 raise HTTPFound(self.request.route_path('my_account_password'))
203 raise HTTPFound(self.request.route_path('my_account_password'))
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 def my_account_auth_tokens(self):
207 def my_account_auth_tokens(self):
208 _ = self.request.translate
208 _ = self.request.translate
209
209
210 c = self.load_default_context()
210 c = self.load_default_context()
211 c.active = 'auth_tokens'
211 c.active = 'auth_tokens'
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.role_values = [
213 c.role_values = [
214 (x, AuthTokenModel.cls._get_role_name(x))
214 (x, AuthTokenModel.cls._get_role_name(x))
215 for x in AuthTokenModel.cls.ROLES]
215 for x in AuthTokenModel.cls.ROLES]
216 c.role_options = [(c.role_values, _("Role"))]
216 c.role_options = [(c.role_values, _("Role"))]
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user.user_id, show_expired=True)
218 c.user.user_id, show_expired=True)
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 return self._get_template_context(c)
220 return self._get_template_context(c)
221
221
222 @LoginRequired()
222 @LoginRequired()
223 @NotAnonymous()
223 @NotAnonymous()
224 @CSRFRequired()
224 @CSRFRequired()
225 def my_account_auth_tokens_view(self):
225 def my_account_auth_tokens_view(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 auth_token_id = self.request.POST.get('auth_token_id')
229 auth_token_id = self.request.POST.get('auth_token_id')
230
230
231 if auth_token_id:
231 if auth_token_id:
232 token = UserApiKeys.get_or_404(auth_token_id)
232 token = UserApiKeys.get_or_404(auth_token_id)
233 if token.user.user_id != c.user.user_id:
233 if token.user.user_id != c.user.user_id:
234 raise HTTPNotFound()
234 raise HTTPNotFound()
235
235
236 return {
236 return {
237 'auth_token': token.api_key
237 'auth_token': token.api_key
238 }
238 }
239
239
240 def maybe_attach_token_scope(self, token):
240 def maybe_attach_token_scope(self, token):
241 # implemented in EE edition
241 # implemented in EE edition
242 pass
242 pass
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @NotAnonymous()
245 @NotAnonymous()
246 @CSRFRequired()
246 @CSRFRequired()
247 def my_account_auth_tokens_add(self):
247 def my_account_auth_tokens_add(self):
248 _ = self.request.translate
248 _ = self.request.translate
249 c = self.load_default_context()
249 c = self.load_default_context()
250
250
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 description = self.request.POST.get('description')
252 description = self.request.POST.get('description')
253 role = self.request.POST.get('role')
253 role = self.request.POST.get('role')
254
254
255 token = UserModel().add_auth_token(
255 token = UserModel().add_auth_token(
256 user=c.user.user_id,
256 user=c.user.user_id,
257 lifetime_minutes=lifetime, role=role, description=description,
257 lifetime_minutes=lifetime, role=role, description=description,
258 scope_callback=self.maybe_attach_token_scope)
258 scope_callback=self.maybe_attach_token_scope)
259 token_data = token.get_api_data()
259 token_data = token.get_api_data()
260
260
261 audit_logger.store_web(
261 audit_logger.store_web(
262 'user.edit.token.add', action_data={
262 'user.edit.token.add', action_data={
263 'data': {'token': token_data, 'user': 'self'}},
263 'data': {'token': token_data, 'user': 'self'}},
264 user=self._rhodecode_user, )
264 user=self._rhodecode_user, )
265 Session().commit()
265 Session().commit()
266
266
267 h.flash(_("Auth token successfully created"), category='success')
267 h.flash(_("Auth token successfully created"), category='success')
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269
269
270 @LoginRequired()
270 @LoginRequired()
271 @NotAnonymous()
271 @NotAnonymous()
272 @CSRFRequired()
272 @CSRFRequired()
273 def my_account_auth_tokens_delete(self):
273 def my_account_auth_tokens_delete(self):
274 _ = self.request.translate
274 _ = self.request.translate
275 c = self.load_default_context()
275 c = self.load_default_context()
276
276
277 del_auth_token = self.request.POST.get('del_auth_token')
277 del_auth_token = self.request.POST.get('del_auth_token')
278
278
279 if del_auth_token:
279 if del_auth_token:
280 token = UserApiKeys.get_or_404(del_auth_token)
280 token = UserApiKeys.get_or_404(del_auth_token)
281 token_data = token.get_api_data()
281 token_data = token.get_api_data()
282
282
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 audit_logger.store_web(
284 audit_logger.store_web(
285 'user.edit.token.delete', action_data={
285 'user.edit.token.delete', action_data={
286 'data': {'token': token_data, 'user': 'self'}},
286 'data': {'token': token_data, 'user': 'self'}},
287 user=self._rhodecode_user,)
287 user=self._rhodecode_user,)
288 Session().commit()
288 Session().commit()
289 h.flash(_("Auth token successfully deleted"), category='success')
289 h.flash(_("Auth token successfully deleted"), category='success')
290
290
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @NotAnonymous()
294 @NotAnonymous()
295 def my_account_emails(self):
295 def my_account_emails(self):
296 _ = self.request.translate
296 _ = self.request.translate
297
297
298 c = self.load_default_context()
298 c = self.load_default_context()
299 c.active = 'emails'
299 c.active = 'emails'
300
300
301 c.user_email_map = UserEmailMap.query()\
301 c.user_email_map = UserEmailMap.query()\
302 .filter(UserEmailMap.user == c.user).all()
302 .filter(UserEmailMap.user == c.user).all()
303
303
304 schema = user_schema.AddEmailSchema().bind(
304 schema = user_schema.AddEmailSchema().bind(
305 username=c.user.username, user_emails=c.user.emails)
305 username=c.user.username, user_emails=c.user.emails)
306
306
307 form = forms.RcForm(schema,
307 form = forms.RcForm(schema,
308 action=h.route_path('my_account_emails_add'),
308 action=h.route_path('my_account_emails_add'),
309 buttons=(forms.buttons.save, forms.buttons.reset))
309 buttons=(forms.buttons.save, forms.buttons.reset))
310
310
311 c.form = form
311 c.form = form
312 return self._get_template_context(c)
312 return self._get_template_context(c)
313
313
314 @LoginRequired()
314 @LoginRequired()
315 @NotAnonymous()
315 @NotAnonymous()
316 @CSRFRequired()
316 @CSRFRequired()
317 def my_account_emails_add(self):
317 def my_account_emails_add(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320 c.active = 'emails'
320 c.active = 'emails'
321
321
322 schema = user_schema.AddEmailSchema().bind(
322 schema = user_schema.AddEmailSchema().bind(
323 username=c.user.username, user_emails=c.user.emails)
323 username=c.user.username, user_emails=c.user.emails)
324
324
325 form = forms.RcForm(
325 form = forms.RcForm(
326 schema, action=h.route_path('my_account_emails_add'),
326 schema, action=h.route_path('my_account_emails_add'),
327 buttons=(forms.buttons.save, forms.buttons.reset))
327 buttons=(forms.buttons.save, forms.buttons.reset))
328
328
329 controls = self.request.POST.items()
329 controls = self.request.POST.items()
330 try:
330 try:
331 valid_data = form.validate(controls)
331 valid_data = form.validate(controls)
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 audit_logger.store_web(
333 audit_logger.store_web(
334 'user.edit.email.add', action_data={
334 'user.edit.email.add', action_data={
335 'data': {'email': valid_data['email'], 'user': 'self'}},
335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 user=self._rhodecode_user,)
336 user=self._rhodecode_user,)
337 Session().commit()
337 Session().commit()
338 except formencode.Invalid as error:
338 except formencode.Invalid as error:
339 h.flash(h.escape(error.error_dict['email']), category='error')
339 h.flash(h.escape(error.error_dict['email']), category='error')
340 except forms.ValidationFailure as e:
340 except forms.ValidationFailure as e:
341 c.user_email_map = UserEmailMap.query() \
341 c.user_email_map = UserEmailMap.query() \
342 .filter(UserEmailMap.user == c.user).all()
342 .filter(UserEmailMap.user == c.user).all()
343 c.form = e
343 c.form = e
344 return self._get_template_context(c)
344 return self._get_template_context(c)
345 except Exception:
345 except Exception:
346 log.exception("Exception adding email")
346 log.exception("Exception adding email")
347 h.flash(_('Error occurred during adding email'),
347 h.flash(_('Error occurred during adding email'),
348 category='error')
348 category='error')
349 else:
349 else:
350 h.flash(_("Successfully added email"), category='success')
350 h.flash(_("Successfully added email"), category='success')
351
351
352 raise HTTPFound(self.request.route_path('my_account_emails'))
352 raise HTTPFound(self.request.route_path('my_account_emails'))
353
353
354 @LoginRequired()
354 @LoginRequired()
355 @NotAnonymous()
355 @NotAnonymous()
356 @CSRFRequired()
356 @CSRFRequired()
357 def my_account_emails_delete(self):
357 def my_account_emails_delete(self):
358 _ = self.request.translate
358 _ = self.request.translate
359 c = self.load_default_context()
359 c = self.load_default_context()
360
360
361 del_email_id = self.request.POST.get('del_email_id')
361 del_email_id = self.request.POST.get('del_email_id')
362 if del_email_id:
362 if del_email_id:
363 email = UserEmailMap.get_or_404(del_email_id).email
363 email = UserEmailMap.get_or_404(del_email_id).email
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 audit_logger.store_web(
365 audit_logger.store_web(
366 'user.edit.email.delete', action_data={
366 'user.edit.email.delete', action_data={
367 'data': {'email': email, 'user': 'self'}},
367 'data': {'email': email, 'user': 'self'}},
368 user=self._rhodecode_user,)
368 user=self._rhodecode_user,)
369 Session().commit()
369 Session().commit()
370 h.flash(_("Email successfully deleted"),
370 h.flash(_("Email successfully deleted"),
371 category='success')
371 category='success')
372 return HTTPFound(h.route_path('my_account_emails'))
372 return HTTPFound(h.route_path('my_account_emails'))
373
373
374 @LoginRequired()
374 @LoginRequired()
375 @NotAnonymous()
375 @NotAnonymous()
376 @CSRFRequired()
376 @CSRFRequired()
377 def my_account_notifications_test_channelstream(self):
377 def my_account_notifications_test_channelstream(self):
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 self._rhodecode_user.username, datetime.datetime.now())
379 self._rhodecode_user.username, datetime.datetime.now())
380 payload = {
380 payload = {
381 # 'channel': 'broadcast',
381 # 'channel': 'broadcast',
382 'type': 'message',
382 'type': 'message',
383 'timestamp': datetime.datetime.utcnow(),
383 'timestamp': datetime.datetime.utcnow(),
384 'user': 'system',
384 'user': 'system',
385 'pm_users': [self._rhodecode_user.username],
385 'pm_users': [self._rhodecode_user.username],
386 'message': {
386 'message': {
387 'message': message,
387 'message': message,
388 'level': 'info',
388 'level': 'info',
389 'topic': '/notifications'
389 'topic': '/notifications'
390 }
390 }
391 }
391 }
392
392
393 registry = self.request.registry
393 registry = self.request.registry
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396
396
397 try:
397 try:
398 channelstream_request(channelstream_config, [payload], '/message')
398 channelstream_request(channelstream_config, [payload], '/message')
399 except ChannelstreamException as e:
399 except ChannelstreamException as e:
400 log.exception('Failed to send channelstream data')
400 log.exception('Failed to send channelstream data')
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 return {"response": 'Channelstream data sent. '
402 return {"response": 'Channelstream data sent. '
403 'You should see a new live message now.'}
403 'You should see a new live message now.'}
404
404
405 def _load_my_repos_data(self, watched=False):
405 def _load_my_repos_data(self, watched=False):
406
406
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408
408
409 if watched:
409 if watched:
410 # repos user watch
410 # repos user watch
411 repo_list = Session().query(
411 repo_list = Session().query(
412 Repository
412 Repository
413 ) \
413 ) \
414 .join(
414 .join(
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 ) \
416 ) \
417 .filter(
417 .filter(
418 UserFollowing.user_id == self._rhodecode_user.user_id
418 UserFollowing.user_id == self._rhodecode_user.user_id
419 ) \
419 ) \
420 .filter(or_(
420 .filter(or_(
421 # generate multiple IN to fix limitation problems
421 # generate multiple IN to fix limitation problems
422 *in_filter_generator(Repository.repo_id, allowed_ids))
422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 ) \
423 ) \
424 .order_by(Repository.repo_name) \
424 .order_by(Repository.repo_name) \
425 .all()
425 .all()
426
426
427 else:
427 else:
428 # repos user is owner of
428 # repos user is owner of
429 repo_list = Session().query(
429 repo_list = Session().query(
430 Repository
430 Repository
431 ) \
431 ) \
432 .filter(
432 .filter(
433 Repository.user_id == self._rhodecode_user.user_id
433 Repository.user_id == self._rhodecode_user.user_id
434 ) \
434 ) \
435 .filter(or_(
435 .filter(or_(
436 # generate multiple IN to fix limitation problems
436 # generate multiple IN to fix limitation problems
437 *in_filter_generator(Repository.repo_id, allowed_ids))
437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 ) \
438 ) \
439 .order_by(Repository.repo_name) \
439 .order_by(Repository.repo_name) \
440 .all()
440 .all()
441
441
442 _render = self.request.get_partial_renderer(
442 _render = self.request.get_partial_renderer(
443 'rhodecode:templates/data_table/_dt_elements.mako')
443 'rhodecode:templates/data_table/_dt_elements.mako')
444
444
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 short_name=False, admin=False)
447 short_name=False, admin=False)
448
448
449 repos_data = []
449 repos_data = []
450 for repo in repo_list:
450 for repo in repo_list:
451 row = {
451 row = {
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 repo.private, repo.archived, repo.fork),
453 repo.private, repo.archived, repo.fork),
454 "name_raw": repo.repo_name.lower(),
454 "name_raw": repo.repo_name.lower(),
455 }
455 }
456
456
457 repos_data.append(row)
457 repos_data.append(row)
458
458
459 # json used to render the grid
459 # json used to render the grid
460 return json.dumps(repos_data)
460 return ext_json.str_json(repos_data)
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @NotAnonymous()
463 @NotAnonymous()
464 def my_account_repos(self):
464 def my_account_repos(self):
465 c = self.load_default_context()
465 c = self.load_default_context()
466 c.active = 'repos'
466 c.active = 'repos'
467
467
468 # json used to render the grid
468 # json used to render the grid
469 c.data = self._load_my_repos_data()
469 c.data = self._load_my_repos_data()
470 return self._get_template_context(c)
470 return self._get_template_context(c)
471
471
472 @LoginRequired()
472 @LoginRequired()
473 @NotAnonymous()
473 @NotAnonymous()
474 def my_account_watched(self):
474 def my_account_watched(self):
475 c = self.load_default_context()
475 c = self.load_default_context()
476 c.active = 'watched'
476 c.active = 'watched'
477
477
478 # json used to render the grid
478 # json used to render the grid
479 c.data = self._load_my_repos_data(watched=True)
479 c.data = self._load_my_repos_data(watched=True)
480 return self._get_template_context(c)
480 return self._get_template_context(c)
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 def my_account_bookmarks(self):
484 def my_account_bookmarks(self):
485 c = self.load_default_context()
485 c = self.load_default_context()
486 c.active = 'bookmarks'
486 c.active = 'bookmarks'
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 self._rhodecode_db_user.user_id, cache=False)
488 self._rhodecode_db_user.user_id, cache=False)
489 return self._get_template_context(c)
489 return self._get_template_context(c)
490
490
491 def _process_bookmark_entry(self, entry, user_id):
491 def _process_bookmark_entry(self, entry, user_id):
492 position = safe_int(entry.get('position'))
492 position = safe_int(entry.get('position'))
493 cur_position = safe_int(entry.get('cur_position'))
493 cur_position = safe_int(entry.get('cur_position'))
494 if position is None:
494 if position is None:
495 return
495 return
496
496
497 # check if this is an existing entry
497 # check if this is an existing entry
498 is_new = False
498 is_new = False
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500
500
501 if db_entry and str2bool(entry.get('remove')):
501 if db_entry and str2bool(entry.get('remove')):
502 log.debug('Marked bookmark %s for deletion', db_entry)
502 log.debug('Marked bookmark %s for deletion', db_entry)
503 Session().delete(db_entry)
503 Session().delete(db_entry)
504 return
504 return
505
505
506 if not db_entry:
506 if not db_entry:
507 # new
507 # new
508 db_entry = UserBookmark()
508 db_entry = UserBookmark()
509 is_new = True
509 is_new = True
510
510
511 should_save = False
511 should_save = False
512 default_redirect_url = ''
512 default_redirect_url = ''
513
513
514 # save repo
514 # save repo
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 repo = Repository.get(entry['bookmark_repo'])
516 repo = Repository.get(entry['bookmark_repo'])
517 perm_check = HasRepoPermissionAny(
517 perm_check = HasRepoPermissionAny(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 if repo and perm_check(repo_name=repo.repo_name):
519 if repo and perm_check(repo_name=repo.repo_name):
520 db_entry.repository = repo
520 db_entry.repository = repo
521 should_save = True
521 should_save = True
522 default_redirect_url = '${repo_url}'
522 default_redirect_url = '${repo_url}'
523 # save repo group
523 # save repo group
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 perm_check = HasRepoGroupPermissionAny(
526 perm_check = HasRepoGroupPermissionAny(
527 'group.read', 'group.write', 'group.admin')
527 'group.read', 'group.write', 'group.admin')
528
528
529 if repo_group and perm_check(group_name=repo_group.group_name):
529 if repo_group and perm_check(group_name=repo_group.group_name):
530 db_entry.repository_group = repo_group
530 db_entry.repository_group = repo_group
531 should_save = True
531 should_save = True
532 default_redirect_url = '${repo_group_url}'
532 default_redirect_url = '${repo_group_url}'
533 # save generic info
533 # save generic info
534 elif entry.get('title') and entry.get('redirect_url'):
534 elif entry.get('title') and entry.get('redirect_url'):
535 should_save = True
535 should_save = True
536
536
537 if should_save:
537 if should_save:
538 # mark user and position
538 # mark user and position
539 db_entry.user_id = user_id
539 db_entry.user_id = user_id
540 db_entry.position = position
540 db_entry.position = position
541 db_entry.title = entry.get('title')
541 db_entry.title = entry.get('title')
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544
544
545 Session().add(db_entry)
545 Session().add(db_entry)
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @NotAnonymous()
548 @NotAnonymous()
549 @CSRFRequired()
549 @CSRFRequired()
550 def my_account_bookmarks_update(self):
550 def my_account_bookmarks_update(self):
551 _ = self.request.translate
551 _ = self.request.translate
552 c = self.load_default_context()
552 c = self.load_default_context()
553 c.active = 'bookmarks'
553 c.active = 'bookmarks'
554
554
555 controls = peppercorn.parse(self.request.POST.items())
555 controls = peppercorn.parse(self.request.POST.items())
556 user_id = c.user.user_id
556 user_id = c.user.user_id
557
557
558 # validate positions
558 # validate positions
559 positions = {}
559 positions = {}
560 for entry in controls.get('bookmarks', []):
560 for entry in controls.get('bookmarks', []):
561 position = safe_int(entry['position'])
561 position = safe_int(entry['position'])
562 if position is None:
562 if position is None:
563 continue
563 continue
564
564
565 if position in positions:
565 if position in positions:
566 h.flash(_("Position {} is defined twice. "
566 h.flash(_("Position {} is defined twice. "
567 "Please correct this error.").format(position), category='error')
567 "Please correct this error.").format(position), category='error')
568 return HTTPFound(h.route_path('my_account_bookmarks'))
568 return HTTPFound(h.route_path('my_account_bookmarks'))
569
569
570 entry['position'] = position
570 entry['position'] = position
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 positions[position] = entry
572 positions[position] = entry
573
573
574 try:
574 try:
575 for entry in positions.values():
575 for entry in positions.values():
576 self._process_bookmark_entry(entry, user_id)
576 self._process_bookmark_entry(entry, user_id)
577
577
578 Session().commit()
578 Session().commit()
579 h.flash(_("Update Bookmarks"), category='success')
579 h.flash(_("Update Bookmarks"), category='success')
580 except IntegrityError:
580 except IntegrityError:
581 h.flash(_("Failed to update bookmarks. "
581 h.flash(_("Failed to update bookmarks. "
582 "Make sure an unique position is used."), category='error')
582 "Make sure an unique position is used."), category='error')
583
583
584 return HTTPFound(h.route_path('my_account_bookmarks'))
584 return HTTPFound(h.route_path('my_account_bookmarks'))
585
585
586 @LoginRequired()
586 @LoginRequired()
587 @NotAnonymous()
587 @NotAnonymous()
588 def my_account_goto_bookmark(self):
588 def my_account_goto_bookmark(self):
589
589
590 bookmark_id = self.request.matchdict['bookmark_id']
590 bookmark_id = self.request.matchdict['bookmark_id']
591 user_bookmark = UserBookmark().query()\
591 user_bookmark = UserBookmark().query()\
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.position == bookmark_id).scalar()
593 .filter(UserBookmark.position == bookmark_id).scalar()
594
594
595 redirect_url = h.route_path('my_account_bookmarks')
595 redirect_url = h.route_path('my_account_bookmarks')
596 if not user_bookmark:
596 if not user_bookmark:
597 raise HTTPFound(redirect_url)
597 raise HTTPFound(redirect_url)
598
598
599 # repository set
599 # repository set
600 if user_bookmark.repository:
600 if user_bookmark.repository:
601 repo_name = user_bookmark.repository.repo_name
601 repo_name = user_bookmark.repository.repo_name
602 base_redirect_url = h.route_path(
602 base_redirect_url = h.route_path(
603 'repo_summary', repo_name=repo_name)
603 'repo_summary', repo_name=repo_name)
604 if user_bookmark.redirect_url and \
604 if user_bookmark.redirect_url and \
605 '${repo_url}' in user_bookmark.redirect_url:
605 '${repo_url}' in user_bookmark.redirect_url:
606 redirect_url = string.Template(user_bookmark.redirect_url)\
606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 .safe_substitute({'repo_url': base_redirect_url})
607 .safe_substitute({'repo_url': base_redirect_url})
608 else:
608 else:
609 redirect_url = base_redirect_url
609 redirect_url = base_redirect_url
610 # repository group set
610 # repository group set
611 elif user_bookmark.repository_group:
611 elif user_bookmark.repository_group:
612 repo_group_name = user_bookmark.repository_group.group_name
612 repo_group_name = user_bookmark.repository_group.group_name
613 base_redirect_url = h.route_path(
613 base_redirect_url = h.route_path(
614 'repo_group_home', repo_group_name=repo_group_name)
614 'repo_group_home', repo_group_name=repo_group_name)
615 if user_bookmark.redirect_url and \
615 if user_bookmark.redirect_url and \
616 '${repo_group_url}' in user_bookmark.redirect_url:
616 '${repo_group_url}' in user_bookmark.redirect_url:
617 redirect_url = string.Template(user_bookmark.redirect_url)\
617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 .safe_substitute({'repo_group_url': base_redirect_url})
618 .safe_substitute({'repo_group_url': base_redirect_url})
619 else:
619 else:
620 redirect_url = base_redirect_url
620 redirect_url = base_redirect_url
621 # custom URL set
621 # custom URL set
622 elif user_bookmark.redirect_url:
622 elif user_bookmark.redirect_url:
623 server_url = h.route_url('home').rstrip('/')
623 server_url = h.route_url('home').rstrip('/')
624 redirect_url = string.Template(user_bookmark.redirect_url) \
624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 .safe_substitute({'server_url': server_url})
625 .safe_substitute({'server_url': server_url})
626
626
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 raise HTTPFound(redirect_url)
628 raise HTTPFound(redirect_url)
629
629
630 @LoginRequired()
630 @LoginRequired()
631 @NotAnonymous()
631 @NotAnonymous()
632 def my_account_perms(self):
632 def my_account_perms(self):
633 c = self.load_default_context()
633 c = self.load_default_context()
634 c.active = 'perms'
634 c.active = 'perms'
635
635
636 c.perm_user = c.auth_user
636 c.perm_user = c.auth_user
637 return self._get_template_context(c)
637 return self._get_template_context(c)
638
638
639 @LoginRequired()
639 @LoginRequired()
640 @NotAnonymous()
640 @NotAnonymous()
641 def my_notifications(self):
641 def my_notifications(self):
642 c = self.load_default_context()
642 c = self.load_default_context()
643 c.active = 'notifications'
643 c.active = 'notifications'
644
644
645 return self._get_template_context(c)
645 return self._get_template_context(c)
646
646
647 @LoginRequired()
647 @LoginRequired()
648 @NotAnonymous()
648 @NotAnonymous()
649 @CSRFRequired()
649 @CSRFRequired()
650 def my_notifications_toggle_visibility(self):
650 def my_notifications_toggle_visibility(self):
651 user = self._rhodecode_db_user
651 user = self._rhodecode_db_user
652 new_status = not user.user_data.get('notification_status', True)
652 new_status = not user.user_data.get('notification_status', True)
653 user.update_userdata(notification_status=new_status)
653 user.update_userdata(notification_status=new_status)
654 Session().commit()
654 Session().commit()
655 return user.user_data['notification_status']
655 return user.user_data['notification_status']
656
656
657 def _get_pull_requests_list(self, statuses, filter_type=None):
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
658 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
660
661 _render = self.request.get_partial_renderer(
661 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
662 'rhodecode:templates/data_table/_dt_elements.mako')
663
663
664 if filter_type == 'awaiting_my_review':
664 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
666 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
667 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
668 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
669 order_dir=order_dir)
670
670
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
673 else:
674 pull_requests = PullRequestModel().get_im_participating_in(
674 pull_requests = PullRequestModel().get_im_participating_in(
675 user_id=self._rhodecode_user.user_id,
675 user_id=self._rhodecode_user.user_id,
676 statuses=statuses, query=search_q,
676 statuses=statuses, query=search_q,
677 offset=start, length=limit, order_by=order_by,
677 offset=start, length=limit, order_by=order_by,
678 order_dir=order_dir)
678 order_dir=order_dir)
679
679
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
682
682
683 data = []
683 data = []
684 comments_model = CommentsModel()
684 comments_model = CommentsModel()
685 for pr in pull_requests:
685 for pr in pull_requests:
686 repo_id = pr.target_repo_id
686 repo_id = pr.target_repo_id
687 comments_count = comments_model.get_all_comments(
687 comments_count = comments_model.get_all_comments(
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
689 owned = pr.user_id == self._rhodecode_user.user_id
689 owned = pr.user_id == self._rhodecode_user.user_id
690
690
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
693 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
695 my_review_status = statuses[0][1].status
696
696
697 data.append({
697 data.append({
698 'target_repo': _render('pullrequest_target_repo',
698 'target_repo': _render('pullrequest_target_repo',
699 pr.target_repo.repo_name),
699 pr.target_repo.repo_name),
700 'name': _render('pullrequest_name',
700 'name': _render('pullrequest_name',
701 pr.pull_request_id, pr.pull_request_state,
701 pr.pull_request_id, pr.pull_request_state,
702 pr.work_in_progress, pr.target_repo.repo_name,
702 pr.work_in_progress, pr.target_repo.repo_name,
703 short=True),
703 short=True),
704 'name_raw': pr.pull_request_id,
704 'name_raw': pr.pull_request_id,
705 'status': _render('pullrequest_status',
705 'status': _render('pullrequest_status',
706 pr.calculated_review_status()),
706 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
707 'my_status': _render('pullrequest_status',
708 my_review_status),
708 my_review_status),
709 'title': _render('pullrequest_title', pr.title, pr.description),
709 'title': _render('pullrequest_title', pr.title, pr.description),
710 'description': h.escape(pr.description),
710 'description': h.escape(pr.description),
711 'updated_on': _render('pullrequest_updated_on',
711 'updated_on': _render('pullrequest_updated_on',
712 h.datetime_to_time(pr.updated_on),
712 h.datetime_to_time(pr.updated_on),
713 pr.versions_count),
713 pr.versions_count),
714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
715 'created_on': _render('pullrequest_updated_on',
715 'created_on': _render('pullrequest_updated_on',
716 h.datetime_to_time(pr.created_on)),
716 h.datetime_to_time(pr.created_on)),
717 'created_on_raw': h.datetime_to_time(pr.created_on),
717 'created_on_raw': h.datetime_to_time(pr.created_on),
718 'state': pr.pull_request_state,
718 'state': pr.pull_request_state,
719 'author': _render('pullrequest_author',
719 'author': _render('pullrequest_author',
720 pr.author.full_contact, ),
720 pr.author.full_contact, ),
721 'author_raw': pr.author.full_name,
721 'author_raw': pr.author.full_name,
722 'comments': _render('pullrequest_comments', comments_count),
722 'comments': _render('pullrequest_comments', comments_count),
723 'comments_raw': comments_count,
723 'comments_raw': comments_count,
724 'closed': pr.is_closed(),
724 'closed': pr.is_closed(),
725 'owned': owned
725 'owned': owned
726 })
726 })
727
727
728 # json used to render the grid
728 # json used to render the grid
729 data = ({
729 data = ({
730 'draw': draw,
730 'draw': draw,
731 'data': data,
731 'data': data,
732 'recordsTotal': pull_requests_total_count,
732 'recordsTotal': pull_requests_total_count,
733 'recordsFiltered': pull_requests_total_count,
733 'recordsFiltered': pull_requests_total_count,
734 })
734 })
735 return data
735 return data
736
736
737 @LoginRequired()
737 @LoginRequired()
738 @NotAnonymous()
738 @NotAnonymous()
739 def my_account_pullrequests(self):
739 def my_account_pullrequests(self):
740 c = self.load_default_context()
740 c = self.load_default_context()
741 c.active = 'pullrequests'
741 c.active = 'pullrequests'
742 req_get = self.request.GET
742 req_get = self.request.GET
743
743
744 c.closed = str2bool(req_get.get('closed'))
744 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
746
747 c.selected_filter = 'all'
747 c.selected_filter = 'all'
748 if c.closed:
748 if c.closed:
749 c.selected_filter = 'all_closed'
749 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
750 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
751 c.selected_filter = 'awaiting_my_review'
752
752
753 return self._get_template_context(c)
753 return self._get_template_context(c)
754
754
755 @LoginRequired()
755 @LoginRequired()
756 @NotAnonymous()
756 @NotAnonymous()
757 def my_account_pullrequests_data(self):
757 def my_account_pullrequests_data(self):
758 self.load_default_context()
758 self.load_default_context()
759 req_get = self.request.GET
759 req_get = self.request.GET
760
760
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
762 closed = str2bool(req_get.get('closed'))
762 closed = str2bool(req_get.get('closed'))
763
763
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
765 if closed:
765 if closed:
766 statuses += [PullRequest.STATUS_CLOSED]
766 statuses += [PullRequest.STATUS_CLOSED]
767
767
768 filter_type = \
768 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
769 'awaiting_my_review' if awaiting_my_review \
770 else None
770 else None
771
771
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
773 return data
773 return data
774
774
775 @LoginRequired()
775 @LoginRequired()
776 @NotAnonymous()
776 @NotAnonymous()
777 def my_account_user_group_membership(self):
777 def my_account_user_group_membership(self):
778 c = self.load_default_context()
778 c = self.load_default_context()
779 c.active = 'user_group_membership'
779 c.active = 'user_group_membership'
780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
781 for group in self._rhodecode_db_user.group_member]
781 for group in self._rhodecode_db_user.group_member]
782 c.user_groups = json.dumps(groups)
782 c.user_groups = ext_json.str_json(groups)
783 return self._get_template_context(c)
783 return self._get_template_context(c)
@@ -1,51 +1,50 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import logging
20 import logging
21
21
22 from pyramid.httpexceptions import HTTPNotFound
22 from pyramid.httpexceptions import HTTPNotFound
23
23
24
25 from rhodecode.apps._base import BaseReferencesView
24 from rhodecode.apps._base import BaseReferencesView
26 from rhodecode.lib.ext_json import json
25 from rhodecode.lib import ext_json
27 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
29
28
30
29
31 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
32
31
33
32
34 class RepoBookmarksView(BaseReferencesView):
33 class RepoBookmarksView(BaseReferencesView):
35
34
36 @LoginRequired()
35 @LoginRequired()
37 @HasRepoPermissionAnyDecorator(
36 @HasRepoPermissionAnyDecorator(
38 'repository.read', 'repository.write', 'repository.admin')
37 'repository.read', 'repository.write', 'repository.admin')
39 def bookmarks(self):
38 def bookmarks(self):
40 c = self.load_default_context()
39 c = self.load_default_context()
41
40
42 if not h.is_hg(self.db_repo):
41 if not h.is_hg(self.db_repo):
43 raise HTTPNotFound()
42 raise HTTPNotFound()
44
43
45 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
44 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
46 data = self.load_refs_context(
45 data = self.load_refs_context(
47 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
46 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
48
47
49 c.has_references = bool(data)
48 c.has_references = bool(data)
50 c.data = json.dumps(data)
49 c.data = ext_json.str_json(data)
51 return self._get_template_context(c)
50 return self._get_template_context(c)
@@ -1,46 +1,46 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import logging
21 import logging
22
22
23
23
24 from rhodecode.apps._base import BaseReferencesView
24 from rhodecode.apps._base import BaseReferencesView
25 from rhodecode.lib.ext_json import json
25 from rhodecode.lib import ext_json
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27
27
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class RepoBranchesView(BaseReferencesView):
32 class RepoBranchesView(BaseReferencesView):
33
33
34 @LoginRequired()
34 @LoginRequired()
35 @HasRepoPermissionAnyDecorator(
35 @HasRepoPermissionAnyDecorator(
36 'repository.read', 'repository.write', 'repository.admin')
36 'repository.read', 'repository.write', 'repository.admin')
37 def branches(self):
37 def branches(self):
38 c = self.load_default_context()
38 c = self.load_default_context()
39
39
40 ref_items = self.rhodecode_vcs_repo.branches_all.items()
40 ref_items = self.rhodecode_vcs_repo.branches_all.items()
41 data = self.load_refs_context(
41 data = self.load_refs_context(
42 ref_items=ref_items, partials_template='branches/branches_data.mako')
42 ref_items=ref_items, partials_template='branches/branches_data.mako')
43
43
44 c.has_references = bool(data)
44 c.has_references = bool(data)
45 c.data = json.dumps(data)
45 c.data = ext_json.str_json(data)
46 return self._get_template_context(c)
46 return self._get_template_context(c)
@@ -1,355 +1,356 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
24 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
25
25
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 import rhodecode.lib.helpers as h
30 import rhodecode.lib.helpers as h
31 from rhodecode.lib import ext_json
31 from rhodecode.lib.auth import (
32 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator)
33 LoginRequired, HasRepoPermissionAnyDecorator)
33
34
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.graphmod import _colored, _dagwalker
36 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.helpers import RepoPage
37 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
38 from rhodecode.lib.utils2 import safe_int, safe_str, str2bool, safe_unicode
38 from rhodecode.lib.vcs.exceptions import (
39 from rhodecode.lib.vcs.exceptions import (
39 RepositoryError, CommitDoesNotExistError,
40 RepositoryError, CommitDoesNotExistError,
40 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41 CommitError, NodeDoesNotExistError, EmptyRepositoryError)
41
42
42 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
43
44
44 DEFAULT_CHANGELOG_SIZE = 20
45 DEFAULT_CHANGELOG_SIZE = 20
45
46
46
47
47 class RepoChangelogView(RepoAppView):
48 class RepoChangelogView(RepoAppView):
48
49
49 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 def _get_commit_or_redirect(self, commit_id, redirect_after=True):
50 """
51 """
51 This is a safe way to get commit. If an error occurs it redirects to
52 This is a safe way to get commit. If an error occurs it redirects to
52 tip with proper message
53 tip with proper message
53
54
54 :param commit_id: id of commit to fetch
55 :param commit_id: id of commit to fetch
55 :param redirect_after: toggle redirection
56 :param redirect_after: toggle redirection
56 """
57 """
57 _ = self.request.translate
58 _ = self.request.translate
58
59
59 try:
60 try:
60 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 return self.rhodecode_vcs_repo.get_commit(commit_id)
61 except EmptyRepositoryError:
62 except EmptyRepositoryError:
62 if not redirect_after:
63 if not redirect_after:
63 return None
64 return None
64
65
65 h.flash(h.literal(
66 h.flash(h.literal(
66 _('There are no commits yet')), category='warning')
67 _('There are no commits yet')), category='warning')
67 raise HTTPFound(
68 raise HTTPFound(
68 h.route_path('repo_summary', repo_name=self.db_repo_name))
69 h.route_path('repo_summary', repo_name=self.db_repo_name))
69
70
70 except (CommitDoesNotExistError, LookupError):
71 except (CommitDoesNotExistError, LookupError):
71 msg = _('No such commit exists for this repository')
72 msg = _('No such commit exists for this repository')
72 h.flash(msg, category='error')
73 h.flash(msg, category='error')
73 raise HTTPNotFound()
74 raise HTTPNotFound()
74 except RepositoryError as e:
75 except RepositoryError as e:
75 h.flash(h.escape(safe_str(e)), category='error')
76 h.flash(h.escape(safe_str(e)), category='error')
76 raise HTTPNotFound()
77 raise HTTPNotFound()
77
78
78 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 def _graph(self, repo, commits, prev_data=None, next_data=None):
79 """
80 """
80 Generates a DAG graph for repo
81 Generates a DAG graph for repo
81
82
82 :param repo: repo instance
83 :param repo: repo instance
83 :param commits: list of commits
84 :param commits: list of commits
84 """
85 """
85 if not commits:
86 if not commits:
86 return json.dumps([]), json.dumps([])
87 return json.dumps([]), json.dumps([])
87
88
88 def serialize(commit, parents=True):
89 def serialize(commit, parents=True):
89 data = dict(
90 data = dict(
90 raw_id=commit.raw_id,
91 raw_id=commit.raw_id,
91 idx=commit.idx,
92 idx=commit.idx,
92 branch=None,
93 branch=None,
93 )
94 )
94 if parents:
95 if parents:
95 data['parents'] = [
96 data['parents'] = [
96 serialize(x, parents=False) for x in commit.parents]
97 serialize(x, parents=False) for x in commit.parents]
97 return data
98 return data
98
99
99 prev_data = prev_data or []
100 prev_data = prev_data or []
100 next_data = next_data or []
101 next_data = next_data or []
101
102
102 current = [serialize(x) for x in commits]
103 current = [serialize(x) for x in commits]
103 commits = prev_data + current + next_data
104 commits = prev_data + current + next_data
104
105
105 dag = _dagwalker(repo, commits)
106 dag = _dagwalker(repo, commits)
106
107
107 data = [[commit_id, vtx, edges, branch]
108 data = [[commit_id, vtx, edges, branch]
108 for commit_id, vtx, edges, branch in _colored(dag)]
109 for commit_id, vtx, edges, branch in _colored(dag)]
109 return json.dumps(data), json.dumps(current)
110 return ext_json.str_json(data), ext_json.str_json(current)
110
111
111 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 if branch_name not in self.rhodecode_vcs_repo.branches_all:
113 h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
114 h.flash(u'Branch {} is not found.'.format(h.escape(safe_unicode(branch_name))),
114 category='warning')
115 category='warning')
115 redirect_url = h.route_path(
116 redirect_url = h.route_path(
116 'repo_commits_file', repo_name=repo_name,
117 'repo_commits_file', repo_name=repo_name,
117 commit_id=branch_name, f_path=f_path or '')
118 commit_id=branch_name, f_path=f_path or '')
118 raise HTTPFound(redirect_url)
119 raise HTTPFound(redirect_url)
119
120
120 def _load_changelog_data(
121 def _load_changelog_data(
121 self, c, collection, page, chunk_size, branch_name=None,
122 self, c, collection, page, chunk_size, branch_name=None,
122 dynamic=False, f_path=None, commit_id=None):
123 dynamic=False, f_path=None, commit_id=None):
123
124
124 def url_generator(page_num):
125 def url_generator(page_num):
125 query_params = {
126 query_params = {
126 'page': page_num
127 'page': page_num
127 }
128 }
128
129
129 if branch_name:
130 if branch_name:
130 query_params.update({
131 query_params.update({
131 'branch': branch_name
132 'branch': branch_name
132 })
133 })
133
134
134 if f_path:
135 if f_path:
135 # changelog for file
136 # changelog for file
136 return h.route_path(
137 return h.route_path(
137 'repo_commits_file',
138 'repo_commits_file',
138 repo_name=c.rhodecode_db_repo.repo_name,
139 repo_name=c.rhodecode_db_repo.repo_name,
139 commit_id=commit_id, f_path=f_path,
140 commit_id=commit_id, f_path=f_path,
140 _query=query_params)
141 _query=query_params)
141 else:
142 else:
142 return h.route_path(
143 return h.route_path(
143 'repo_commits',
144 'repo_commits',
144 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
145
146
146 c.total_cs = len(collection)
147 c.total_cs = len(collection)
147 c.showing_commits = min(chunk_size, c.total_cs)
148 c.showing_commits = min(chunk_size, c.total_cs)
148 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 c.pagination = RepoPage(collection, page=page, item_count=c.total_cs,
149 items_per_page=chunk_size, url_maker=url_generator)
150 items_per_page=chunk_size, url_maker=url_generator)
150
151
151 c.next_page = c.pagination.next_page
152 c.next_page = c.pagination.next_page
152 c.prev_page = c.pagination.previous_page
153 c.prev_page = c.pagination.previous_page
153
154
154 if dynamic:
155 if dynamic:
155 if self.request.GET.get('chunk') != 'next':
156 if self.request.GET.get('chunk') != 'next':
156 c.next_page = None
157 c.next_page = None
157 if self.request.GET.get('chunk') != 'prev':
158 if self.request.GET.get('chunk') != 'prev':
158 c.prev_page = None
159 c.prev_page = None
159
160
160 page_commit_ids = [x.raw_id for x in c.pagination]
161 page_commit_ids = [x.raw_id for x in c.pagination]
161 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 c.comments = c.rhodecode_db_repo.get_comments(page_commit_ids)
162 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163 c.statuses = c.rhodecode_db_repo.statuses(page_commit_ids)
163
164
164 def load_default_context(self):
165 def load_default_context(self):
165 c = self._get_local_tmpl_context(include_app_defaults=True)
166 c = self._get_local_tmpl_context(include_app_defaults=True)
166
167
167 c.rhodecode_repo = self.rhodecode_vcs_repo
168 c.rhodecode_repo = self.rhodecode_vcs_repo
168
169
169 return c
170 return c
170
171
171 @LoginRequired()
172 @LoginRequired()
172 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
173 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
174 def repo_changelog(self):
175 def repo_changelog(self):
175 c = self.load_default_context()
176 c = self.load_default_context()
176
177
177 commit_id = self.request.matchdict.get('commit_id')
178 commit_id = self.request.matchdict.get('commit_id')
178 f_path = self._get_f_path(self.request.matchdict)
179 f_path = self._get_f_path(self.request.matchdict)
179 show_hidden = str2bool(self.request.GET.get('evolve'))
180 show_hidden = str2bool(self.request.GET.get('evolve'))
180
181
181 chunk_size = 20
182 chunk_size = 20
182
183
183 c.branch_name = branch_name = self.request.GET.get('branch') or ''
184 c.branch_name = branch_name = self.request.GET.get('branch') or ''
184 c.book_name = book_name = self.request.GET.get('bookmark') or ''
185 c.book_name = book_name = self.request.GET.get('bookmark') or ''
185 c.f_path = f_path
186 c.f_path = f_path
186 c.commit_id = commit_id
187 c.commit_id = commit_id
187 c.show_hidden = show_hidden
188 c.show_hidden = show_hidden
188
189
189 hist_limit = safe_int(self.request.GET.get('limit')) or None
190 hist_limit = safe_int(self.request.GET.get('limit')) or None
190
191
191 p = safe_int(self.request.GET.get('page', 1), 1)
192 p = safe_int(self.request.GET.get('page', 1), 1)
192
193
193 c.selected_name = branch_name or book_name
194 c.selected_name = branch_name or book_name
194 if not commit_id and branch_name:
195 if not commit_id and branch_name:
195 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
196 self._check_if_valid_branch(branch_name, self.db_repo_name, f_path)
196
197
197 c.changelog_for_path = f_path
198 c.changelog_for_path = f_path
198 pre_load = self.get_commit_preload_attrs()
199 pre_load = self.get_commit_preload_attrs()
199
200
200 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
201 partial_xhr = self.request.environ.get('HTTP_X_PARTIAL_XHR')
201
202
202 try:
203 try:
203 if f_path:
204 if f_path:
204 log.debug('generating changelog for path %s', f_path)
205 log.debug('generating changelog for path %s', f_path)
205 # get the history for the file !
206 # get the history for the file !
206 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
207
208
208 try:
209 try:
209 collection = base_commit.get_path_history(
210 collection = base_commit.get_path_history(
210 f_path, limit=hist_limit, pre_load=pre_load)
211 f_path, limit=hist_limit, pre_load=pre_load)
211 if collection and partial_xhr:
212 if collection and partial_xhr:
212 # for ajax call we remove first one since we're looking
213 # for ajax call we remove first one since we're looking
213 # at it right now in the context of a file commit
214 # at it right now in the context of a file commit
214 collection.pop(0)
215 collection.pop(0)
215 except (NodeDoesNotExistError, CommitError):
216 except (NodeDoesNotExistError, CommitError):
216 # this node is not present at tip!
217 # this node is not present at tip!
217 try:
218 try:
218 commit = self._get_commit_or_redirect(commit_id)
219 commit = self._get_commit_or_redirect(commit_id)
219 collection = commit.get_path_history(f_path)
220 collection = commit.get_path_history(f_path)
220 except RepositoryError as e:
221 except RepositoryError as e:
221 h.flash(safe_str(e), category='warning')
222 h.flash(safe_str(e), category='warning')
222 redirect_url = h.route_path(
223 redirect_url = h.route_path(
223 'repo_commits', repo_name=self.db_repo_name)
224 'repo_commits', repo_name=self.db_repo_name)
224 raise HTTPFound(redirect_url)
225 raise HTTPFound(redirect_url)
225 collection = list(reversed(collection))
226 collection = list(reversed(collection))
226 else:
227 else:
227 collection = self.rhodecode_vcs_repo.get_commits(
228 collection = self.rhodecode_vcs_repo.get_commits(
228 branch_name=branch_name, show_hidden=show_hidden,
229 branch_name=branch_name, show_hidden=show_hidden,
229 pre_load=pre_load, translate_tags=False)
230 pre_load=pre_load, translate_tags=False)
230
231
231 self._load_changelog_data(
232 self._load_changelog_data(
232 c, collection, p, chunk_size, c.branch_name,
233 c, collection, p, chunk_size, c.branch_name,
233 f_path=f_path, commit_id=commit_id)
234 f_path=f_path, commit_id=commit_id)
234
235
235 except EmptyRepositoryError as e:
236 except EmptyRepositoryError as e:
236 h.flash(h.escape(safe_str(e)), category='warning')
237 h.flash(h.escape(safe_str(e)), category='warning')
237 raise HTTPFound(
238 raise HTTPFound(
238 h.route_path('repo_summary', repo_name=self.db_repo_name))
239 h.route_path('repo_summary', repo_name=self.db_repo_name))
239 except HTTPFound:
240 except HTTPFound:
240 raise
241 raise
241 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
242 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
242 log.exception(safe_str(e))
243 log.exception(safe_str(e))
243 h.flash(h.escape(safe_str(e)), category='error')
244 h.flash(h.escape(safe_str(e)), category='error')
244
245
245 if commit_id:
246 if commit_id:
246 # from single commit page, we redirect to main commits
247 # from single commit page, we redirect to main commits
247 raise HTTPFound(
248 raise HTTPFound(
248 h.route_path('repo_commits', repo_name=self.db_repo_name))
249 h.route_path('repo_commits', repo_name=self.db_repo_name))
249 else:
250 else:
250 # otherwise we redirect to summary
251 # otherwise we redirect to summary
251 raise HTTPFound(
252 raise HTTPFound(
252 h.route_path('repo_summary', repo_name=self.db_repo_name))
253 h.route_path('repo_summary', repo_name=self.db_repo_name))
253
254
254
255
255
256
256 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
257 if partial_xhr or self.request.environ.get('HTTP_X_PJAX'):
257 # case when loading dynamic file history in file view
258 # case when loading dynamic file history in file view
258 # loading from ajax, we don't want the first result, it's popped
259 # loading from ajax, we don't want the first result, it's popped
259 # in the code above
260 # in the code above
260 html = render(
261 html = render(
261 'rhodecode:templates/commits/changelog_file_history.mako',
262 'rhodecode:templates/commits/changelog_file_history.mako',
262 self._get_template_context(c), self.request)
263 self._get_template_context(c), self.request)
263 return Response(html)
264 return Response(html)
264
265
265 commit_ids = []
266 commit_ids = []
266 if not f_path:
267 if not f_path:
267 # only load graph data when not in file history mode
268 # only load graph data when not in file history mode
268 commit_ids = c.pagination
269 commit_ids = c.pagination
269
270
270 c.graph_data, c.graph_commits = self._graph(
271 c.graph_data, c.graph_commits = self._graph(
271 self.rhodecode_vcs_repo, commit_ids)
272 self.rhodecode_vcs_repo, commit_ids)
272
273
273 return self._get_template_context(c)
274 return self._get_template_context(c)
274
275
275 @LoginRequired()
276 @LoginRequired()
276 @HasRepoPermissionAnyDecorator(
277 @HasRepoPermissionAnyDecorator(
277 'repository.read', 'repository.write', 'repository.admin')
278 'repository.read', 'repository.write', 'repository.admin')
278 def repo_commits_elements(self):
279 def repo_commits_elements(self):
279 c = self.load_default_context()
280 c = self.load_default_context()
280 commit_id = self.request.matchdict.get('commit_id')
281 commit_id = self.request.matchdict.get('commit_id')
281 f_path = self._get_f_path(self.request.matchdict)
282 f_path = self._get_f_path(self.request.matchdict)
282 show_hidden = str2bool(self.request.GET.get('evolve'))
283 show_hidden = str2bool(self.request.GET.get('evolve'))
283
284
284 chunk_size = 20
285 chunk_size = 20
285 hist_limit = safe_int(self.request.GET.get('limit')) or None
286 hist_limit = safe_int(self.request.GET.get('limit')) or None
286
287
287 def wrap_for_error(err):
288 def wrap_for_error(err):
288 html = '<tr>' \
289 html = '<tr>' \
289 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
290 '<td colspan="9" class="alert alert-error">ERROR: {}</td>' \
290 '</tr>'.format(err)
291 '</tr>'.format(err)
291 return Response(html)
292 return Response(html)
292
293
293 c.branch_name = branch_name = self.request.GET.get('branch') or ''
294 c.branch_name = branch_name = self.request.GET.get('branch') or ''
294 c.book_name = book_name = self.request.GET.get('bookmark') or ''
295 c.book_name = book_name = self.request.GET.get('bookmark') or ''
295 c.f_path = f_path
296 c.f_path = f_path
296 c.commit_id = commit_id
297 c.commit_id = commit_id
297 c.show_hidden = show_hidden
298 c.show_hidden = show_hidden
298
299
299 c.selected_name = branch_name or book_name
300 c.selected_name = branch_name or book_name
300 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
301 if branch_name and branch_name not in self.rhodecode_vcs_repo.branches_all:
301 return wrap_for_error(
302 return wrap_for_error(
302 safe_str('Branch: {} is not valid'.format(branch_name)))
303 safe_str('Branch: {} is not valid'.format(branch_name)))
303
304
304 pre_load = self.get_commit_preload_attrs()
305 pre_load = self.get_commit_preload_attrs()
305
306
306 if f_path:
307 if f_path:
307 try:
308 try:
308 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
309 base_commit = self.rhodecode_vcs_repo.get_commit(commit_id)
309 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
310 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
310 log.exception(safe_str(e))
311 log.exception(safe_str(e))
311 raise HTTPFound(
312 raise HTTPFound(
312 h.route_path('repo_commits', repo_name=self.db_repo_name))
313 h.route_path('repo_commits', repo_name=self.db_repo_name))
313
314
314 collection = base_commit.get_path_history(
315 collection = base_commit.get_path_history(
315 f_path, limit=hist_limit, pre_load=pre_load)
316 f_path, limit=hist_limit, pre_load=pre_load)
316 collection = list(reversed(collection))
317 collection = list(reversed(collection))
317 else:
318 else:
318 collection = self.rhodecode_vcs_repo.get_commits(
319 collection = self.rhodecode_vcs_repo.get_commits(
319 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
320 branch_name=branch_name, show_hidden=show_hidden, pre_load=pre_load,
320 translate_tags=False)
321 translate_tags=False)
321
322
322 p = safe_int(self.request.GET.get('page', 1), 1)
323 p = safe_int(self.request.GET.get('page', 1), 1)
323 try:
324 try:
324 self._load_changelog_data(
325 self._load_changelog_data(
325 c, collection, p, chunk_size, dynamic=True,
326 c, collection, p, chunk_size, dynamic=True,
326 f_path=f_path, commit_id=commit_id)
327 f_path=f_path, commit_id=commit_id)
327 except EmptyRepositoryError as e:
328 except EmptyRepositoryError as e:
328 return wrap_for_error(safe_str(e))
329 return wrap_for_error(safe_str(e))
329 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
330 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
330 log.exception('Failed to fetch commits')
331 log.exception('Failed to fetch commits')
331 return wrap_for_error(safe_str(e))
332 return wrap_for_error(safe_str(e))
332
333
333 prev_data = None
334 prev_data = None
334 next_data = None
335 next_data = None
335
336
336 try:
337 try:
337 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
338 prev_graph = json.loads(self.request.POST.get('graph') or '{}')
338 except json.JSONDecodeError:
339 except json.JSONDecodeError:
339 prev_graph = {}
340 prev_graph = {}
340
341
341 if self.request.GET.get('chunk') == 'prev':
342 if self.request.GET.get('chunk') == 'prev':
342 next_data = prev_graph
343 next_data = prev_graph
343 elif self.request.GET.get('chunk') == 'next':
344 elif self.request.GET.get('chunk') == 'next':
344 prev_data = prev_graph
345 prev_data = prev_graph
345
346
346 commit_ids = []
347 commit_ids = []
347 if not f_path:
348 if not f_path:
348 # only load graph data when not in file history mode
349 # only load graph data when not in file history mode
349 commit_ids = c.pagination
350 commit_ids = c.pagination
350
351
351 c.graph_data, c.graph_commits = self._graph(
352 c.graph_data, c.graph_commits = self._graph(
352 self.rhodecode_vcs_repo, commit_ids,
353 self.rhodecode_vcs_repo, commit_ids,
353 prev_data=prev_data, next_data=next_data)
354 prev_data=prev_data, next_data=next_data)
354
355
355 return self._get_template_context(c)
356 return self._get_template_context(c)
@@ -1,819 +1,819 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import logging
21 import logging
22 import collections
22 import collections
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.renderers import render
26 from pyramid.renderers import render
27 from pyramid.response import Response
27 from pyramid.response import Response
28
28
29 from rhodecode.apps._base import RepoAppView
29 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps.file_store import utils as store_utils
30 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32
32
33 from rhodecode.lib import diffs, codeblocks, channelstream
33 from rhodecode.lib import diffs, codeblocks, channelstream
34 from rhodecode.lib.auth import (
34 from rhodecode.lib.auth import (
35 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 from rhodecode.lib.ext_json import json
36 from rhodecode.lib import ext_json
37 from collections import OrderedDict
37 from collections import OrderedDict
38 from rhodecode.lib.diffs import (
38 from rhodecode.lib.diffs import (
39 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 get_diff_whitespace_flag)
40 get_diff_whitespace_flag)
41 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
41 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 import rhodecode.lib.helpers as h
42 import rhodecode.lib.helpers as h
43 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
43 from rhodecode.lib.utils2 import safe_unicode, str2bool, StrictAttributeDict, safe_str
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 RepositoryError, CommitDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError)
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
47 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 ChangesetCommentHistory
48 ChangesetCommentHistory
49 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.settings import VcsSettingsModel
52 from rhodecode.model.settings import VcsSettingsModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def _update_with_GET(params, request):
57 def _update_with_GET(params, request):
58 for k in ['diff1', 'diff2', 'diff']:
58 for k in ['diff1', 'diff2', 'diff']:
59 params[k] += request.GET.getall(k)
59 params[k] += request.GET.getall(k)
60
60
61
61
62 class RepoCommitsView(RepoAppView):
62 class RepoCommitsView(RepoAppView):
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c.rhodecode_repo = self.rhodecode_vcs_repo
65 c.rhodecode_repo = self.rhodecode_vcs_repo
66
66
67 return c
67 return c
68
68
69 def _is_diff_cache_enabled(self, target_repo):
69 def _is_diff_cache_enabled(self, target_repo):
70 caching_enabled = self._get_general_setting(
70 caching_enabled = self._get_general_setting(
71 target_repo, 'rhodecode_diff_cache')
71 target_repo, 'rhodecode_diff_cache')
72 log.debug('Diff caching enabled: %s', caching_enabled)
72 log.debug('Diff caching enabled: %s', caching_enabled)
73 return caching_enabled
73 return caching_enabled
74
74
75 def _commit(self, commit_id_range, method):
75 def _commit(self, commit_id_range, method):
76 _ = self.request.translate
76 _ = self.request.translate
77 c = self.load_default_context()
77 c = self.load_default_context()
78 c.fulldiff = self.request.GET.get('fulldiff')
78 c.fulldiff = self.request.GET.get('fulldiff')
79 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
79 redirect_to_combined = str2bool(self.request.GET.get('redirect_combined'))
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90 # get ranges of commit ids if preset
90 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
91 commit_range = commit_id_range.split('...')[:2]
92
92
93 try:
93 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
95 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
97 pre_load += ['hidden', 'obsolete', 'phase']
98
98
99 if len(commit_range) == 2:
99 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
100 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
101 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
102 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
103 commits = list(commits)
104 else:
104 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
106 commit_id=commit_id_range, pre_load=pre_load)]
107
107
108 c.commit_ranges = commits
108 c.commit_ranges = commits
109 if not c.commit_ranges:
109 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
110 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
111 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
112 msg = _('No such commit exists. Org exception: `{}`').format(safe_str(e))
113 h.flash(msg, category='error')
113 h.flash(msg, category='error')
114 raise HTTPNotFound()
114 raise HTTPNotFound()
115 except Exception:
115 except Exception:
116 log.exception("General failure")
116 log.exception("General failure")
117 raise HTTPNotFound()
117 raise HTTPNotFound()
118 single_commit = len(c.commit_ranges) == 1
118 single_commit = len(c.commit_ranges) == 1
119
119
120 if redirect_to_combined and not single_commit:
120 if redirect_to_combined and not single_commit:
121 source_ref = getattr(c.commit_ranges[0].parents[0]
121 source_ref = getattr(c.commit_ranges[0].parents[0]
122 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
122 if c.commit_ranges[0].parents else h.EmptyCommit(), 'raw_id')
123 target_ref = c.commit_ranges[-1].raw_id
123 target_ref = c.commit_ranges[-1].raw_id
124 next_url = h.route_path(
124 next_url = h.route_path(
125 'repo_compare',
125 'repo_compare',
126 repo_name=c.repo_name,
126 repo_name=c.repo_name,
127 source_ref_type='rev',
127 source_ref_type='rev',
128 source_ref=source_ref,
128 source_ref=source_ref,
129 target_ref_type='rev',
129 target_ref_type='rev',
130 target_ref=target_ref)
130 target_ref=target_ref)
131 raise HTTPFound(next_url)
131 raise HTTPFound(next_url)
132
132
133 c.changes = OrderedDict()
133 c.changes = OrderedDict()
134 c.lines_added = 0
134 c.lines_added = 0
135 c.lines_deleted = 0
135 c.lines_deleted = 0
136
136
137 # auto collapse if we have more than limit
137 # auto collapse if we have more than limit
138 collapse_limit = diffs.DiffProcessor._collapse_commits_over
138 collapse_limit = diffs.DiffProcessor._collapse_commits_over
139 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
139 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
140
140
141 c.commit_statuses = ChangesetStatus.STATUSES
141 c.commit_statuses = ChangesetStatus.STATUSES
142 c.inline_comments = []
142 c.inline_comments = []
143 c.files = []
143 c.files = []
144
144
145 c.comments = []
145 c.comments = []
146 c.unresolved_comments = []
146 c.unresolved_comments = []
147 c.resolved_comments = []
147 c.resolved_comments = []
148
148
149 # Single commit
149 # Single commit
150 if single_commit:
150 if single_commit:
151 commit = c.commit_ranges[0]
151 commit = c.commit_ranges[0]
152 c.comments = CommentsModel().get_comments(
152 c.comments = CommentsModel().get_comments(
153 self.db_repo.repo_id,
153 self.db_repo.repo_id,
154 revision=commit.raw_id)
154 revision=commit.raw_id)
155
155
156 # comments from PR
156 # comments from PR
157 statuses = ChangesetStatusModel().get_statuses(
157 statuses = ChangesetStatusModel().get_statuses(
158 self.db_repo.repo_id, commit.raw_id,
158 self.db_repo.repo_id, commit.raw_id,
159 with_revisions=True)
159 with_revisions=True)
160
160
161 prs = set()
161 prs = set()
162 reviewers = list()
162 reviewers = list()
163 reviewers_duplicates = set() # to not have duplicates from multiple votes
163 reviewers_duplicates = set() # to not have duplicates from multiple votes
164 for c_status in statuses:
164 for c_status in statuses:
165
165
166 # extract associated pull-requests from votes
166 # extract associated pull-requests from votes
167 if c_status.pull_request:
167 if c_status.pull_request:
168 prs.add(c_status.pull_request)
168 prs.add(c_status.pull_request)
169
169
170 # extract reviewers
170 # extract reviewers
171 _user_id = c_status.author.user_id
171 _user_id = c_status.author.user_id
172 if _user_id not in reviewers_duplicates:
172 if _user_id not in reviewers_duplicates:
173 reviewers.append(
173 reviewers.append(
174 StrictAttributeDict({
174 StrictAttributeDict({
175 'user': c_status.author,
175 'user': c_status.author,
176
176
177 # fake attributed for commit, page that we don't have
177 # fake attributed for commit, page that we don't have
178 # but we share the display with PR page
178 # but we share the display with PR page
179 'mandatory': False,
179 'mandatory': False,
180 'reasons': [],
180 'reasons': [],
181 'rule_user_group_data': lambda: None
181 'rule_user_group_data': lambda: None
182 })
182 })
183 )
183 )
184 reviewers_duplicates.add(_user_id)
184 reviewers_duplicates.add(_user_id)
185
185
186 c.reviewers_count = len(reviewers)
186 c.reviewers_count = len(reviewers)
187 c.observers_count = 0
187 c.observers_count = 0
188
188
189 # from associated statuses, check the pull requests, and
189 # from associated statuses, check the pull requests, and
190 # show comments from them
190 # show comments from them
191 for pr in prs:
191 for pr in prs:
192 c.comments.extend(pr.comments)
192 c.comments.extend(pr.comments)
193
193
194 c.unresolved_comments = CommentsModel()\
194 c.unresolved_comments = CommentsModel()\
195 .get_commit_unresolved_todos(commit.raw_id)
195 .get_commit_unresolved_todos(commit.raw_id)
196 c.resolved_comments = CommentsModel()\
196 c.resolved_comments = CommentsModel()\
197 .get_commit_resolved_todos(commit.raw_id)
197 .get_commit_resolved_todos(commit.raw_id)
198
198
199 c.inline_comments_flat = CommentsModel()\
199 c.inline_comments_flat = CommentsModel()\
200 .get_commit_inline_comments(commit.raw_id)
200 .get_commit_inline_comments(commit.raw_id)
201
201
202 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
202 review_statuses = ChangesetStatusModel().aggregate_votes_by_user(
203 statuses, reviewers)
203 statuses, reviewers)
204
204
205 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
205 c.commit_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
206
206
207 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
207 c.commit_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
208
208
209 for review_obj, member, reasons, mandatory, status in review_statuses:
209 for review_obj, member, reasons, mandatory, status in review_statuses:
210 member_reviewer = h.reviewer_as_json(
210 member_reviewer = h.reviewer_as_json(
211 member, reasons=reasons, mandatory=mandatory, role=None,
211 member, reasons=reasons, mandatory=mandatory, role=None,
212 user_group=None
212 user_group=None
213 )
213 )
214
214
215 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
215 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
216 member_reviewer['review_status'] = current_review_status
216 member_reviewer['review_status'] = current_review_status
217 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
217 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
218 member_reviewer['allowed_to_update'] = False
218 member_reviewer['allowed_to_update'] = False
219 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
219 c.commit_set_reviewers_data_json['reviewers'].append(member_reviewer)
220
220
221 c.commit_set_reviewers_data_json = json.dumps(c.commit_set_reviewers_data_json)
221 c.commit_set_reviewers_data_json = ext_json.str_json(c.commit_set_reviewers_data_json)
222
222
223 # NOTE(marcink): this uses the same voting logic as in pull-requests
223 # NOTE(marcink): this uses the same voting logic as in pull-requests
224 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
224 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
225 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
225 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
226
226
227 diff = None
227 diff = None
228 # Iterate over ranges (default commit view is always one commit)
228 # Iterate over ranges (default commit view is always one commit)
229 for commit in c.commit_ranges:
229 for commit in c.commit_ranges:
230 c.changes[commit.raw_id] = []
230 c.changes[commit.raw_id] = []
231
231
232 commit2 = commit
232 commit2 = commit
233 commit1 = commit.first_parent
233 commit1 = commit.first_parent
234
234
235 if method == 'show':
235 if method == 'show':
236 inline_comments = CommentsModel().get_inline_comments(
236 inline_comments = CommentsModel().get_inline_comments(
237 self.db_repo.repo_id, revision=commit.raw_id)
237 self.db_repo.repo_id, revision=commit.raw_id)
238 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
238 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
239 inline_comments))
239 inline_comments))
240 c.inline_comments = inline_comments
240 c.inline_comments = inline_comments
241
241
242 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
242 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
243 self.db_repo)
243 self.db_repo)
244 cache_file_path = diff_cache_exist(
244 cache_file_path = diff_cache_exist(
245 cache_path, 'diff', commit.raw_id,
245 cache_path, 'diff', commit.raw_id,
246 hide_whitespace_changes, diff_context, c.fulldiff)
246 hide_whitespace_changes, diff_context, c.fulldiff)
247
247
248 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
248 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
249 force_recache = str2bool(self.request.GET.get('force_recache'))
249 force_recache = str2bool(self.request.GET.get('force_recache'))
250
250
251 cached_diff = None
251 cached_diff = None
252 if caching_enabled:
252 if caching_enabled:
253 cached_diff = load_cached_diff(cache_file_path)
253 cached_diff = load_cached_diff(cache_file_path)
254
254
255 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
255 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
256 if not force_recache and has_proper_diff_cache:
256 if not force_recache and has_proper_diff_cache:
257 diffset = cached_diff['diff']
257 diffset = cached_diff['diff']
258 else:
258 else:
259 vcs_diff = self.rhodecode_vcs_repo.get_diff(
259 vcs_diff = self.rhodecode_vcs_repo.get_diff(
260 commit1, commit2,
260 commit1, commit2,
261 ignore_whitespace=hide_whitespace_changes,
261 ignore_whitespace=hide_whitespace_changes,
262 context=diff_context)
262 context=diff_context)
263
263
264 diff_processor = diffs.DiffProcessor(
264 diff_processor = diffs.DiffProcessor(
265 vcs_diff, format='newdiff', diff_limit=diff_limit,
265 vcs_diff, format='newdiff', diff_limit=diff_limit,
266 file_limit=file_limit, show_full_diff=c.fulldiff)
266 file_limit=file_limit, show_full_diff=c.fulldiff)
267
267
268 _parsed = diff_processor.prepare()
268 _parsed = diff_processor.prepare()
269
269
270 diffset = codeblocks.DiffSet(
270 diffset = codeblocks.DiffSet(
271 repo_name=self.db_repo_name,
271 repo_name=self.db_repo_name,
272 source_node_getter=codeblocks.diffset_node_getter(commit1),
272 source_node_getter=codeblocks.diffset_node_getter(commit1),
273 target_node_getter=codeblocks.diffset_node_getter(commit2))
273 target_node_getter=codeblocks.diffset_node_getter(commit2))
274
274
275 diffset = self.path_filter.render_patchset_filtered(
275 diffset = self.path_filter.render_patchset_filtered(
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
276 diffset, _parsed, commit1.raw_id, commit2.raw_id)
277
277
278 # save cached diff
278 # save cached diff
279 if caching_enabled:
279 if caching_enabled:
280 cache_diff(cache_file_path, diffset, None)
280 cache_diff(cache_file_path, diffset, None)
281
281
282 c.limited_diff = diffset.limited_diff
282 c.limited_diff = diffset.limited_diff
283 c.changes[commit.raw_id] = diffset
283 c.changes[commit.raw_id] = diffset
284 else:
284 else:
285 # TODO(marcink): no cache usage here...
285 # TODO(marcink): no cache usage here...
286 _diff = self.rhodecode_vcs_repo.get_diff(
286 _diff = self.rhodecode_vcs_repo.get_diff(
287 commit1, commit2,
287 commit1, commit2,
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
288 ignore_whitespace=hide_whitespace_changes, context=diff_context)
289 diff_processor = diffs.DiffProcessor(
289 diff_processor = diffs.DiffProcessor(
290 _diff, format='newdiff', diff_limit=diff_limit,
290 _diff, format='newdiff', diff_limit=diff_limit,
291 file_limit=file_limit, show_full_diff=c.fulldiff)
291 file_limit=file_limit, show_full_diff=c.fulldiff)
292 # downloads/raw we only need RAW diff nothing else
292 # downloads/raw we only need RAW diff nothing else
293 diff = self.path_filter.get_raw_patch(diff_processor)
293 diff = self.path_filter.get_raw_patch(diff_processor)
294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
294 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
295
295
296 # sort comments by how they were generated
296 # sort comments by how they were generated
297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
297 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
298 c.at_version_num = None
298 c.at_version_num = None
299
299
300 if len(c.commit_ranges) == 1:
300 if len(c.commit_ranges) == 1:
301 c.commit = c.commit_ranges[0]
301 c.commit = c.commit_ranges[0]
302 c.parent_tmpl = ''.join(
302 c.parent_tmpl = ''.join(
303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
303 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
304
304
305 if method == 'download':
305 if method == 'download':
306 response = Response(diff)
306 response = Response(diff)
307 response.content_type = 'text/plain'
307 response.content_type = 'text/plain'
308 response.content_disposition = (
308 response.content_disposition = (
309 'attachment; filename=%s.diff' % commit_id_range[:12])
309 'attachment; filename=%s.diff' % commit_id_range[:12])
310 return response
310 return response
311 elif method == 'patch':
311 elif method == 'patch':
312 c.diff = safe_unicode(diff)
312 c.diff = safe_unicode(diff)
313 patch = render(
313 patch = render(
314 'rhodecode:templates/changeset/patch_changeset.mako',
314 'rhodecode:templates/changeset/patch_changeset.mako',
315 self._get_template_context(c), self.request)
315 self._get_template_context(c), self.request)
316 response = Response(patch)
316 response = Response(patch)
317 response.content_type = 'text/plain'
317 response.content_type = 'text/plain'
318 return response
318 return response
319 elif method == 'raw':
319 elif method == 'raw':
320 response = Response(diff)
320 response = Response(diff)
321 response.content_type = 'text/plain'
321 response.content_type = 'text/plain'
322 return response
322 return response
323 elif method == 'show':
323 elif method == 'show':
324 if len(c.commit_ranges) == 1:
324 if len(c.commit_ranges) == 1:
325 html = render(
325 html = render(
326 'rhodecode:templates/changeset/changeset.mako',
326 'rhodecode:templates/changeset/changeset.mako',
327 self._get_template_context(c), self.request)
327 self._get_template_context(c), self.request)
328 return Response(html)
328 return Response(html)
329 else:
329 else:
330 c.ancestor = None
330 c.ancestor = None
331 c.target_repo = self.db_repo
331 c.target_repo = self.db_repo
332 html = render(
332 html = render(
333 'rhodecode:templates/changeset/changeset_range.mako',
333 'rhodecode:templates/changeset/changeset_range.mako',
334 self._get_template_context(c), self.request)
334 self._get_template_context(c), self.request)
335 return Response(html)
335 return Response(html)
336
336
337 raise HTTPBadRequest()
337 raise HTTPBadRequest()
338
338
339 @LoginRequired()
339 @LoginRequired()
340 @HasRepoPermissionAnyDecorator(
340 @HasRepoPermissionAnyDecorator(
341 'repository.read', 'repository.write', 'repository.admin')
341 'repository.read', 'repository.write', 'repository.admin')
342 def repo_commit_show(self):
342 def repo_commit_show(self):
343 commit_id = self.request.matchdict['commit_id']
343 commit_id = self.request.matchdict['commit_id']
344 return self._commit(commit_id, method='show')
344 return self._commit(commit_id, method='show')
345
345
346 @LoginRequired()
346 @LoginRequired()
347 @HasRepoPermissionAnyDecorator(
347 @HasRepoPermissionAnyDecorator(
348 'repository.read', 'repository.write', 'repository.admin')
348 'repository.read', 'repository.write', 'repository.admin')
349 def repo_commit_raw(self):
349 def repo_commit_raw(self):
350 commit_id = self.request.matchdict['commit_id']
350 commit_id = self.request.matchdict['commit_id']
351 return self._commit(commit_id, method='raw')
351 return self._commit(commit_id, method='raw')
352
352
353 @LoginRequired()
353 @LoginRequired()
354 @HasRepoPermissionAnyDecorator(
354 @HasRepoPermissionAnyDecorator(
355 'repository.read', 'repository.write', 'repository.admin')
355 'repository.read', 'repository.write', 'repository.admin')
356 def repo_commit_patch(self):
356 def repo_commit_patch(self):
357 commit_id = self.request.matchdict['commit_id']
357 commit_id = self.request.matchdict['commit_id']
358 return self._commit(commit_id, method='patch')
358 return self._commit(commit_id, method='patch')
359
359
360 @LoginRequired()
360 @LoginRequired()
361 @HasRepoPermissionAnyDecorator(
361 @HasRepoPermissionAnyDecorator(
362 'repository.read', 'repository.write', 'repository.admin')
362 'repository.read', 'repository.write', 'repository.admin')
363 def repo_commit_download(self):
363 def repo_commit_download(self):
364 commit_id = self.request.matchdict['commit_id']
364 commit_id = self.request.matchdict['commit_id']
365 return self._commit(commit_id, method='download')
365 return self._commit(commit_id, method='download')
366
366
367 def _commit_comments_create(self, commit_id, comments):
367 def _commit_comments_create(self, commit_id, comments):
368 _ = self.request.translate
368 _ = self.request.translate
369 data = {}
369 data = {}
370 if not comments:
370 if not comments:
371 return
371 return
372
372
373 commit = self.db_repo.get_commit(commit_id)
373 commit = self.db_repo.get_commit(commit_id)
374
374
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
375 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
376 for entry in comments:
376 for entry in comments:
377 c = self.load_default_context()
377 c = self.load_default_context()
378 comment_type = entry['comment_type']
378 comment_type = entry['comment_type']
379 text = entry['text']
379 text = entry['text']
380 status = entry['status']
380 status = entry['status']
381 is_draft = str2bool(entry['is_draft'])
381 is_draft = str2bool(entry['is_draft'])
382 resolves_comment_id = entry['resolves_comment_id']
382 resolves_comment_id = entry['resolves_comment_id']
383 f_path = entry['f_path']
383 f_path = entry['f_path']
384 line_no = entry['line']
384 line_no = entry['line']
385 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
385 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
386
386
387 if status:
387 if status:
388 text = text or (_('Status change %(transition_icon)s %(status)s')
388 text = text or (_('Status change %(transition_icon)s %(status)s')
389 % {'transition_icon': '>',
389 % {'transition_icon': '>',
390 'status': ChangesetStatus.get_status_lbl(status)})
390 'status': ChangesetStatus.get_status_lbl(status)})
391
391
392 comment = CommentsModel().create(
392 comment = CommentsModel().create(
393 text=text,
393 text=text,
394 repo=self.db_repo.repo_id,
394 repo=self.db_repo.repo_id,
395 user=self._rhodecode_db_user.user_id,
395 user=self._rhodecode_db_user.user_id,
396 commit_id=commit_id,
396 commit_id=commit_id,
397 f_path=f_path,
397 f_path=f_path,
398 line_no=line_no,
398 line_no=line_no,
399 status_change=(ChangesetStatus.get_status_lbl(status)
399 status_change=(ChangesetStatus.get_status_lbl(status)
400 if status else None),
400 if status else None),
401 status_change_type=status,
401 status_change_type=status,
402 comment_type=comment_type,
402 comment_type=comment_type,
403 is_draft=is_draft,
403 is_draft=is_draft,
404 resolves_comment_id=resolves_comment_id,
404 resolves_comment_id=resolves_comment_id,
405 auth_user=self._rhodecode_user,
405 auth_user=self._rhodecode_user,
406 send_email=not is_draft, # skip notification for draft comments
406 send_email=not is_draft, # skip notification for draft comments
407 )
407 )
408 is_inline = comment.is_inline
408 is_inline = comment.is_inline
409
409
410 # get status if set !
410 # get status if set !
411 if status:
411 if status:
412 # `dont_allow_on_closed_pull_request = True` means
412 # `dont_allow_on_closed_pull_request = True` means
413 # if latest status was from pull request and it's closed
413 # if latest status was from pull request and it's closed
414 # disallow changing status !
414 # disallow changing status !
415
415
416 try:
416 try:
417 ChangesetStatusModel().set_status(
417 ChangesetStatusModel().set_status(
418 self.db_repo.repo_id,
418 self.db_repo.repo_id,
419 status,
419 status,
420 self._rhodecode_db_user.user_id,
420 self._rhodecode_db_user.user_id,
421 comment,
421 comment,
422 revision=commit_id,
422 revision=commit_id,
423 dont_allow_on_closed_pull_request=True
423 dont_allow_on_closed_pull_request=True
424 )
424 )
425 except StatusChangeOnClosedPullRequestError:
425 except StatusChangeOnClosedPullRequestError:
426 msg = _('Changing the status of a commit associated with '
426 msg = _('Changing the status of a commit associated with '
427 'a closed pull request is not allowed')
427 'a closed pull request is not allowed')
428 log.exception(msg)
428 log.exception(msg)
429 h.flash(msg, category='warning')
429 h.flash(msg, category='warning')
430 raise HTTPFound(h.route_path(
430 raise HTTPFound(h.route_path(
431 'repo_commit', repo_name=self.db_repo_name,
431 'repo_commit', repo_name=self.db_repo_name,
432 commit_id=commit_id))
432 commit_id=commit_id))
433
433
434 Session().flush()
434 Session().flush()
435 # this is somehow required to get access to some relationship
435 # this is somehow required to get access to some relationship
436 # loaded on comment
436 # loaded on comment
437 Session().refresh(comment)
437 Session().refresh(comment)
438
438
439 # skip notifications for drafts
439 # skip notifications for drafts
440 if not is_draft:
440 if not is_draft:
441 CommentsModel().trigger_commit_comment_hook(
441 CommentsModel().trigger_commit_comment_hook(
442 self.db_repo, self._rhodecode_user, 'create',
442 self.db_repo, self._rhodecode_user, 'create',
443 data={'comment': comment, 'commit': commit})
443 data={'comment': comment, 'commit': commit})
444
444
445 comment_id = comment.comment_id
445 comment_id = comment.comment_id
446 data[comment_id] = {
446 data[comment_id] = {
447 'target_id': target_elem_id
447 'target_id': target_elem_id
448 }
448 }
449 Session().flush()
449 Session().flush()
450
450
451 c.co = comment
451 c.co = comment
452 c.at_version_num = 0
452 c.at_version_num = 0
453 c.is_new = True
453 c.is_new = True
454 rendered_comment = render(
454 rendered_comment = render(
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
455 'rhodecode:templates/changeset/changeset_comment_block.mako',
456 self._get_template_context(c), self.request)
456 self._get_template_context(c), self.request)
457
457
458 data[comment_id].update(comment.get_dict())
458 data[comment_id].update(comment.get_dict())
459 data[comment_id].update({'rendered_text': rendered_comment})
459 data[comment_id].update({'rendered_text': rendered_comment})
460
460
461 # finalize, commit and redirect
461 # finalize, commit and redirect
462 Session().commit()
462 Session().commit()
463
463
464 # skip channelstream for draft comments
464 # skip channelstream for draft comments
465 if not all_drafts:
465 if not all_drafts:
466 comment_broadcast_channel = channelstream.comment_channel(
466 comment_broadcast_channel = channelstream.comment_channel(
467 self.db_repo_name, commit_obj=commit)
467 self.db_repo_name, commit_obj=commit)
468
468
469 comment_data = data
469 comment_data = data
470 posted_comment_type = 'inline' if is_inline else 'general'
470 posted_comment_type = 'inline' if is_inline else 'general'
471 if len(data) == 1:
471 if len(data) == 1:
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
472 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
473 else:
473 else:
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
474 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
475
475
476 channelstream.comment_channelstream_push(
476 channelstream.comment_channelstream_push(
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
477 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
478 comment_data=comment_data)
478 comment_data=comment_data)
479
479
480 return data
480 return data
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 @HasRepoPermissionAnyDecorator(
484 @HasRepoPermissionAnyDecorator(
485 'repository.read', 'repository.write', 'repository.admin')
485 'repository.read', 'repository.write', 'repository.admin')
486 @CSRFRequired()
486 @CSRFRequired()
487 def repo_commit_comment_create(self):
487 def repo_commit_comment_create(self):
488 _ = self.request.translate
488 _ = self.request.translate
489 commit_id = self.request.matchdict['commit_id']
489 commit_id = self.request.matchdict['commit_id']
490
490
491 multi_commit_ids = []
491 multi_commit_ids = []
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
492 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
493 if _commit_id not in ['', None, EmptyCommit.raw_id]:
494 if _commit_id not in multi_commit_ids:
494 if _commit_id not in multi_commit_ids:
495 multi_commit_ids.append(_commit_id)
495 multi_commit_ids.append(_commit_id)
496
496
497 commit_ids = multi_commit_ids or [commit_id]
497 commit_ids = multi_commit_ids or [commit_id]
498
498
499 data = []
499 data = []
500 # Multiple comments for each passed commit id
500 # Multiple comments for each passed commit id
501 for current_id in filter(None, commit_ids):
501 for current_id in filter(None, commit_ids):
502 comment_data = {
502 comment_data = {
503 'comment_type': self.request.POST.get('comment_type'),
503 'comment_type': self.request.POST.get('comment_type'),
504 'text': self.request.POST.get('text'),
504 'text': self.request.POST.get('text'),
505 'status': self.request.POST.get('changeset_status', None),
505 'status': self.request.POST.get('changeset_status', None),
506 'is_draft': self.request.POST.get('draft'),
506 'is_draft': self.request.POST.get('draft'),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
507 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
508 'close_pull_request': self.request.POST.get('close_pull_request'),
509 'f_path': self.request.POST.get('f_path'),
509 'f_path': self.request.POST.get('f_path'),
510 'line': self.request.POST.get('line'),
510 'line': self.request.POST.get('line'),
511 }
511 }
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
512 comment = self._commit_comments_create(commit_id=current_id, comments=[comment_data])
513 data.append(comment)
513 data.append(comment)
514
514
515 return data if len(data) > 1 else data[0]
515 return data if len(data) > 1 else data[0]
516
516
517 @LoginRequired()
517 @LoginRequired()
518 @NotAnonymous()
518 @NotAnonymous()
519 @HasRepoPermissionAnyDecorator(
519 @HasRepoPermissionAnyDecorator(
520 'repository.read', 'repository.write', 'repository.admin')
520 'repository.read', 'repository.write', 'repository.admin')
521 @CSRFRequired()
521 @CSRFRequired()
522 def repo_commit_comment_preview(self):
522 def repo_commit_comment_preview(self):
523 # Technically a CSRF token is not needed as no state changes with this
523 # Technically a CSRF token is not needed as no state changes with this
524 # call. However, as this is a POST is better to have it, so automated
524 # call. However, as this is a POST is better to have it, so automated
525 # tools don't flag it as potential CSRF.
525 # tools don't flag it as potential CSRF.
526 # Post is required because the payload could be bigger than the maximum
526 # Post is required because the payload could be bigger than the maximum
527 # allowed by GET.
527 # allowed by GET.
528
528
529 text = self.request.POST.get('text')
529 text = self.request.POST.get('text')
530 renderer = self.request.POST.get('renderer') or 'rst'
530 renderer = self.request.POST.get('renderer') or 'rst'
531 if text:
531 if text:
532 return h.render(text, renderer=renderer, mentions=True,
532 return h.render(text, renderer=renderer, mentions=True,
533 repo_name=self.db_repo_name)
533 repo_name=self.db_repo_name)
534 return ''
534 return ''
535
535
536 @LoginRequired()
536 @LoginRequired()
537 @HasRepoPermissionAnyDecorator(
537 @HasRepoPermissionAnyDecorator(
538 'repository.read', 'repository.write', 'repository.admin')
538 'repository.read', 'repository.write', 'repository.admin')
539 @CSRFRequired()
539 @CSRFRequired()
540 def repo_commit_comment_history_view(self):
540 def repo_commit_comment_history_view(self):
541 c = self.load_default_context()
541 c = self.load_default_context()
542 comment_id = self.request.matchdict['comment_id']
542 comment_id = self.request.matchdict['comment_id']
543 comment_history_id = self.request.matchdict['comment_history_id']
543 comment_history_id = self.request.matchdict['comment_history_id']
544
544
545 comment = ChangesetComment.get_or_404(comment_id)
545 comment = ChangesetComment.get_or_404(comment_id)
546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
546 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
547 if comment.draft and not comment_owner:
547 if comment.draft and not comment_owner:
548 # if we see draft comments history, we only allow this for owner
548 # if we see draft comments history, we only allow this for owner
549 raise HTTPNotFound()
549 raise HTTPNotFound()
550
550
551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
551 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
552 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
553
553
554 if is_repo_comment:
554 if is_repo_comment:
555 c.comment_history = comment_history
555 c.comment_history = comment_history
556
556
557 rendered_comment = render(
557 rendered_comment = render(
558 'rhodecode:templates/changeset/comment_history.mako',
558 'rhodecode:templates/changeset/comment_history.mako',
559 self._get_template_context(c), self.request)
559 self._get_template_context(c), self.request)
560 return rendered_comment
560 return rendered_comment
561 else:
561 else:
562 log.warning('No permissions for user %s to show comment_history_id: %s',
562 log.warning('No permissions for user %s to show comment_history_id: %s',
563 self._rhodecode_db_user, comment_history_id)
563 self._rhodecode_db_user, comment_history_id)
564 raise HTTPNotFound()
564 raise HTTPNotFound()
565
565
566 @LoginRequired()
566 @LoginRequired()
567 @NotAnonymous()
567 @NotAnonymous()
568 @HasRepoPermissionAnyDecorator(
568 @HasRepoPermissionAnyDecorator(
569 'repository.read', 'repository.write', 'repository.admin')
569 'repository.read', 'repository.write', 'repository.admin')
570 @CSRFRequired()
570 @CSRFRequired()
571 def repo_commit_comment_attachment_upload(self):
571 def repo_commit_comment_attachment_upload(self):
572 c = self.load_default_context()
572 c = self.load_default_context()
573 upload_key = 'attachment'
573 upload_key = 'attachment'
574
574
575 file_obj = self.request.POST.get(upload_key)
575 file_obj = self.request.POST.get(upload_key)
576
576
577 if file_obj is None:
577 if file_obj is None:
578 self.request.response.status = 400
578 self.request.response.status = 400
579 return {'store_fid': None,
579 return {'store_fid': None,
580 'access_path': None,
580 'access_path': None,
581 'error': '{} data field is missing'.format(upload_key)}
581 'error': '{} data field is missing'.format(upload_key)}
582
582
583 if not hasattr(file_obj, 'filename'):
583 if not hasattr(file_obj, 'filename'):
584 self.request.response.status = 400
584 self.request.response.status = 400
585 return {'store_fid': None,
585 return {'store_fid': None,
586 'access_path': None,
586 'access_path': None,
587 'error': 'filename cannot be read from the data field'}
587 'error': 'filename cannot be read from the data field'}
588
588
589 filename = file_obj.filename
589 filename = file_obj.filename
590 file_display_name = filename
590 file_display_name = filename
591
591
592 metadata = {
592 metadata = {
593 'user_uploaded': {'username': self._rhodecode_user.username,
593 'user_uploaded': {'username': self._rhodecode_user.username,
594 'user_id': self._rhodecode_user.user_id,
594 'user_id': self._rhodecode_user.user_id,
595 'ip': self._rhodecode_user.ip_addr}}
595 'ip': self._rhodecode_user.ip_addr}}
596
596
597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
597 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
598 allowed_extensions = [
598 allowed_extensions = [
599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
599 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
600 '.pptx', '.txt', '.xlsx', '.zip']
600 '.pptx', '.txt', '.xlsx', '.zip']
601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
601 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
602
602
603 try:
603 try:
604 storage = store_utils.get_file_storage(self.request.registry.settings)
604 storage = store_utils.get_file_storage(self.request.registry.settings)
605 store_uid, metadata = storage.save_file(
605 store_uid, metadata = storage.save_file(
606 file_obj.file, filename, extra_metadata=metadata,
606 file_obj.file, filename, extra_metadata=metadata,
607 extensions=allowed_extensions, max_filesize=max_file_size)
607 extensions=allowed_extensions, max_filesize=max_file_size)
608 except FileNotAllowedException:
608 except FileNotAllowedException:
609 self.request.response.status = 400
609 self.request.response.status = 400
610 permitted_extensions = ', '.join(allowed_extensions)
610 permitted_extensions = ', '.join(allowed_extensions)
611 error_msg = 'File `{}` is not allowed. ' \
611 error_msg = 'File `{}` is not allowed. ' \
612 'Only following extensions are permitted: {}'.format(
612 'Only following extensions are permitted: {}'.format(
613 filename, permitted_extensions)
613 filename, permitted_extensions)
614 return {'store_fid': None,
614 return {'store_fid': None,
615 'access_path': None,
615 'access_path': None,
616 'error': error_msg}
616 'error': error_msg}
617 except FileOverSizeException:
617 except FileOverSizeException:
618 self.request.response.status = 400
618 self.request.response.status = 400
619 limit_mb = h.format_byte_size_binary(max_file_size)
619 limit_mb = h.format_byte_size_binary(max_file_size)
620 return {'store_fid': None,
620 return {'store_fid': None,
621 'access_path': None,
621 'access_path': None,
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
622 'error': 'File {} is exceeding allowed limit of {}.'.format(
623 filename, limit_mb)}
623 filename, limit_mb)}
624
624
625 try:
625 try:
626 entry = FileStore.create(
626 entry = FileStore.create(
627 file_uid=store_uid, filename=metadata["filename"],
627 file_uid=store_uid, filename=metadata["filename"],
628 file_hash=metadata["sha256"], file_size=metadata["size"],
628 file_hash=metadata["sha256"], file_size=metadata["size"],
629 file_display_name=file_display_name,
629 file_display_name=file_display_name,
630 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
630 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
631 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
632 scope_repo_id=self.db_repo.repo_id
632 scope_repo_id=self.db_repo.repo_id
633 )
633 )
634 Session().add(entry)
634 Session().add(entry)
635 Session().commit()
635 Session().commit()
636 log.debug('Stored upload in DB as %s', entry)
636 log.debug('Stored upload in DB as %s', entry)
637 except Exception:
637 except Exception:
638 log.exception('Failed to store file %s', filename)
638 log.exception('Failed to store file %s', filename)
639 self.request.response.status = 400
639 self.request.response.status = 400
640 return {'store_fid': None,
640 return {'store_fid': None,
641 'access_path': None,
641 'access_path': None,
642 'error': 'File {} failed to store in DB.'.format(filename)}
642 'error': 'File {} failed to store in DB.'.format(filename)}
643
643
644 Session().commit()
644 Session().commit()
645
645
646 return {
646 return {
647 'store_fid': store_uid,
647 'store_fid': store_uid,
648 'access_path': h.route_path(
648 'access_path': h.route_path(
649 'download_file', fid=store_uid),
649 'download_file', fid=store_uid),
650 'fqn_access_path': h.route_url(
650 'fqn_access_path': h.route_url(
651 'download_file', fid=store_uid),
651 'download_file', fid=store_uid),
652 'repo_access_path': h.route_path(
652 'repo_access_path': h.route_path(
653 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
653 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
654 'repo_fqn_access_path': h.route_url(
654 'repo_fqn_access_path': h.route_url(
655 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
655 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
656 }
656 }
657
657
658 @LoginRequired()
658 @LoginRequired()
659 @NotAnonymous()
659 @NotAnonymous()
660 @HasRepoPermissionAnyDecorator(
660 @HasRepoPermissionAnyDecorator(
661 'repository.read', 'repository.write', 'repository.admin')
661 'repository.read', 'repository.write', 'repository.admin')
662 @CSRFRequired()
662 @CSRFRequired()
663 def repo_commit_comment_delete(self):
663 def repo_commit_comment_delete(self):
664 commit_id = self.request.matchdict['commit_id']
664 commit_id = self.request.matchdict['commit_id']
665 comment_id = self.request.matchdict['comment_id']
665 comment_id = self.request.matchdict['comment_id']
666
666
667 comment = ChangesetComment.get_or_404(comment_id)
667 comment = ChangesetComment.get_or_404(comment_id)
668 if not comment:
668 if not comment:
669 log.debug('Comment with id:%s not found, skipping', comment_id)
669 log.debug('Comment with id:%s not found, skipping', comment_id)
670 # comment already deleted in another call probably
670 # comment already deleted in another call probably
671 return True
671 return True
672
672
673 if comment.immutable:
673 if comment.immutable:
674 # don't allow deleting comments that are immutable
674 # don't allow deleting comments that are immutable
675 raise HTTPForbidden()
675 raise HTTPForbidden()
676
676
677 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
677 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
678 super_admin = h.HasPermissionAny('hg.admin')()
678 super_admin = h.HasPermissionAny('hg.admin')()
679 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
679 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
680 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
680 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
681 comment_repo_admin = is_repo_admin and is_repo_comment
681 comment_repo_admin = is_repo_admin and is_repo_comment
682
682
683 if comment.draft and not comment_owner:
683 if comment.draft and not comment_owner:
684 # We never allow to delete draft comments for other than owners
684 # We never allow to delete draft comments for other than owners
685 raise HTTPNotFound()
685 raise HTTPNotFound()
686
686
687 if super_admin or comment_owner or comment_repo_admin:
687 if super_admin or comment_owner or comment_repo_admin:
688 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
688 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
689 Session().commit()
689 Session().commit()
690 return True
690 return True
691 else:
691 else:
692 log.warning('No permissions for user %s to delete comment_id: %s',
692 log.warning('No permissions for user %s to delete comment_id: %s',
693 self._rhodecode_db_user, comment_id)
693 self._rhodecode_db_user, comment_id)
694 raise HTTPNotFound()
694 raise HTTPNotFound()
695
695
696 @LoginRequired()
696 @LoginRequired()
697 @NotAnonymous()
697 @NotAnonymous()
698 @HasRepoPermissionAnyDecorator(
698 @HasRepoPermissionAnyDecorator(
699 'repository.read', 'repository.write', 'repository.admin')
699 'repository.read', 'repository.write', 'repository.admin')
700 @CSRFRequired()
700 @CSRFRequired()
701 def repo_commit_comment_edit(self):
701 def repo_commit_comment_edit(self):
702 self.load_default_context()
702 self.load_default_context()
703
703
704 commit_id = self.request.matchdict['commit_id']
704 commit_id = self.request.matchdict['commit_id']
705 comment_id = self.request.matchdict['comment_id']
705 comment_id = self.request.matchdict['comment_id']
706 comment = ChangesetComment.get_or_404(comment_id)
706 comment = ChangesetComment.get_or_404(comment_id)
707
707
708 if comment.immutable:
708 if comment.immutable:
709 # don't allow deleting comments that are immutable
709 # don't allow deleting comments that are immutable
710 raise HTTPForbidden()
710 raise HTTPForbidden()
711
711
712 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
712 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
713 super_admin = h.HasPermissionAny('hg.admin')()
713 super_admin = h.HasPermissionAny('hg.admin')()
714 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
714 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
715 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
715 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
716 comment_repo_admin = is_repo_admin and is_repo_comment
716 comment_repo_admin = is_repo_admin and is_repo_comment
717
717
718 if super_admin or comment_owner or comment_repo_admin:
718 if super_admin or comment_owner or comment_repo_admin:
719 text = self.request.POST.get('text')
719 text = self.request.POST.get('text')
720 version = self.request.POST.get('version')
720 version = self.request.POST.get('version')
721 if text == comment.text:
721 if text == comment.text:
722 log.warning(
722 log.warning(
723 'Comment(repo): '
723 'Comment(repo): '
724 'Trying to create new version '
724 'Trying to create new version '
725 'with the same comment body {}'.format(
725 'with the same comment body {}'.format(
726 comment_id,
726 comment_id,
727 )
727 )
728 )
728 )
729 raise HTTPNotFound()
729 raise HTTPNotFound()
730
730
731 if version.isdigit():
731 if version.isdigit():
732 version = int(version)
732 version = int(version)
733 else:
733 else:
734 log.warning(
734 log.warning(
735 'Comment(repo): Wrong version type {} {} '
735 'Comment(repo): Wrong version type {} {} '
736 'for comment {}'.format(
736 'for comment {}'.format(
737 version,
737 version,
738 type(version),
738 type(version),
739 comment_id,
739 comment_id,
740 )
740 )
741 )
741 )
742 raise HTTPNotFound()
742 raise HTTPNotFound()
743
743
744 try:
744 try:
745 comment_history = CommentsModel().edit(
745 comment_history = CommentsModel().edit(
746 comment_id=comment_id,
746 comment_id=comment_id,
747 text=text,
747 text=text,
748 auth_user=self._rhodecode_user,
748 auth_user=self._rhodecode_user,
749 version=version,
749 version=version,
750 )
750 )
751 except CommentVersionMismatch:
751 except CommentVersionMismatch:
752 raise HTTPConflict()
752 raise HTTPConflict()
753
753
754 if not comment_history:
754 if not comment_history:
755 raise HTTPNotFound()
755 raise HTTPNotFound()
756
756
757 if not comment.draft:
757 if not comment.draft:
758 commit = self.db_repo.get_commit(commit_id)
758 commit = self.db_repo.get_commit(commit_id)
759 CommentsModel().trigger_commit_comment_hook(
759 CommentsModel().trigger_commit_comment_hook(
760 self.db_repo, self._rhodecode_user, 'edit',
760 self.db_repo, self._rhodecode_user, 'edit',
761 data={'comment': comment, 'commit': commit})
761 data={'comment': comment, 'commit': commit})
762
762
763 Session().commit()
763 Session().commit()
764 return {
764 return {
765 'comment_history_id': comment_history.comment_history_id,
765 'comment_history_id': comment_history.comment_history_id,
766 'comment_id': comment.comment_id,
766 'comment_id': comment.comment_id,
767 'comment_version': comment_history.version,
767 'comment_version': comment_history.version,
768 'comment_author_username': comment_history.author.username,
768 'comment_author_username': comment_history.author.username,
769 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
769 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
770 'comment_created_on': h.age_component(comment_history.created_on,
770 'comment_created_on': h.age_component(comment_history.created_on,
771 time_is_local=True),
771 time_is_local=True),
772 }
772 }
773 else:
773 else:
774 log.warning('No permissions for user %s to edit comment_id: %s',
774 log.warning('No permissions for user %s to edit comment_id: %s',
775 self._rhodecode_db_user, comment_id)
775 self._rhodecode_db_user, comment_id)
776 raise HTTPNotFound()
776 raise HTTPNotFound()
777
777
778 @LoginRequired()
778 @LoginRequired()
779 @HasRepoPermissionAnyDecorator(
779 @HasRepoPermissionAnyDecorator(
780 'repository.read', 'repository.write', 'repository.admin')
780 'repository.read', 'repository.write', 'repository.admin')
781 def repo_commit_data(self):
781 def repo_commit_data(self):
782 commit_id = self.request.matchdict['commit_id']
782 commit_id = self.request.matchdict['commit_id']
783 self.load_default_context()
783 self.load_default_context()
784
784
785 try:
785 try:
786 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
786 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
787 except CommitDoesNotExistError as e:
787 except CommitDoesNotExistError as e:
788 return EmptyCommit(message=str(e))
788 return EmptyCommit(message=str(e))
789
789
790 @LoginRequired()
790 @LoginRequired()
791 @HasRepoPermissionAnyDecorator(
791 @HasRepoPermissionAnyDecorator(
792 'repository.read', 'repository.write', 'repository.admin')
792 'repository.read', 'repository.write', 'repository.admin')
793 def repo_commit_children(self):
793 def repo_commit_children(self):
794 commit_id = self.request.matchdict['commit_id']
794 commit_id = self.request.matchdict['commit_id']
795 self.load_default_context()
795 self.load_default_context()
796
796
797 try:
797 try:
798 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
798 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
799 children = commit.children
799 children = commit.children
800 except CommitDoesNotExistError:
800 except CommitDoesNotExistError:
801 children = []
801 children = []
802
802
803 result = {"results": children}
803 result = {"results": children}
804 return result
804 return result
805
805
806 @LoginRequired()
806 @LoginRequired()
807 @HasRepoPermissionAnyDecorator(
807 @HasRepoPermissionAnyDecorator(
808 'repository.read', 'repository.write', 'repository.admin')
808 'repository.read', 'repository.write', 'repository.admin')
809 def repo_commit_parents(self):
809 def repo_commit_parents(self):
810 commit_id = self.request.matchdict['commit_id']
810 commit_id = self.request.matchdict['commit_id']
811 self.load_default_context()
811 self.load_default_context()
812
812
813 try:
813 try:
814 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
814 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
815 parents = commit.parents
815 parents = commit.parents
816 except CommitDoesNotExistError:
816 except CommitDoesNotExistError:
817 parents = []
817 parents = []
818 result = {"results": parents}
818 result = {"results": parents}
819 return result
819 return result
@@ -1,1877 +1,1877 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib import ext_json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
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, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name,
82 repo_name,
83 search_q=search_q, statuses=statuses,
83 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name,
86 repo_name,
87 search_q=search_q, statuses=statuses)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, self._rhodecode_user.user_id,
90 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
95 search_q=search_q, statuses=statuses)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
110 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
111
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
116 my_review_status = statuses[0][1].status
117
117
118 data.append({
118 data.append({
119 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
120 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
121 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
122 short=True),
122 short=True),
123 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
124 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
125 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
126 'my_status': _render('pullrequest_status',
127 my_review_status),
127 my_review_status),
128 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
129 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
130 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
131 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
132 pr.versions_count),
132 pr.versions_count),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
135 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
138 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
139 pr.author.full_contact, ),
139 pr.author.full_contact, ),
140 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
141 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
142 'comments_raw': comments_count,
142 'comments_raw': comments_count,
143 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
144 })
144 })
145
145
146 data = ({
146 data = ({
147 'draw': draw,
147 'draw': draw,
148 'data': data,
148 'data': data,
149 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
151 })
151 })
152 return data
152 return data
153
153
154 @LoginRequired()
154 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
157 def pull_request_list(self):
157 def pull_request_list(self):
158 c = self.load_default_context()
158 c = self.load_default_context()
159
159
160 req_get = self.request.GET
160 req_get = self.request.GET
161 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
162 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
163 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166
166
167 c.active = 'open'
167 c.active = 'open'
168 if c.my:
168 if c.my:
169 c.active = 'my'
169 c.active = 'my'
170 if c.closed:
170 if c.closed:
171 c.active = 'closed'
171 c.active = 'closed'
172 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
173 c.active = 'awaiting'
173 c.active = 'awaiting'
174 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
175 c.active = 'source'
175 c.active = 'source'
176 if c.awaiting_my_review:
176 if c.awaiting_my_review:
177 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
178
178
179 return self._get_template_context(c)
179 return self._get_template_context(c)
180
180
181 @LoginRequired()
181 @LoginRequired()
182 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
184 def pull_request_list_data(self):
184 def pull_request_list_data(self):
185 self.load_default_context()
185 self.load_default_context()
186
186
187 # additional filters
187 # additional filters
188 req_get = self.request.GET
188 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
194
195 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
197 else None
197 else None
198
198
199 opened_by = None
199 opened_by = None
200 if my:
200 if my:
201 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
202
202
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
204 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
210
211 return data
211 return data
212
212
213 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
217 return caching_enabled
218
218
219 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
220 ancestor_commit,
220 ancestor_commit,
221 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
222 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224
224
225 target_commit_final = target_commit
225 target_commit_final = target_commit
226 source_commit_final = source_commit
226 source_commit_final = source_commit
227
227
228 if use_ancestor:
228 if use_ancestor:
229 # we might want to not use it for versions
229 # we might want to not use it for versions
230 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
231 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
232
232
233 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
234 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
235 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
236
236
237 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
240
240
241 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
242
242
243 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
244 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
245 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 )
248 )
249 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
250 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
251
251
252 return diffset
252 return diffset
253
253
254 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
255 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
256 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
257 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
258 commit1, commit2,
258 commit1, commit2,
259 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
260 context=diff_context)
260 context=diff_context)
261
261
262 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
265
265
266 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
267
267
268 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
269 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272
272
273 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275
275
276 return diffset
276 return diffset
277
277
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 comments_model = CommentsModel()
279 comments_model = CommentsModel()
280
280
281 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 if not include_drafts:
284 if not include_drafts:
285 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
286 general_comments = q
286 general_comments = q
287
287
288 # pick comments we want to render at current version
288 # pick comments we want to render at current version
289 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
290 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
291
291
292 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
295 if not include_drafts:
295 if not include_drafts:
296 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
297 inline_comments = q
297 inline_comments = q
298
298
299 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
300 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
301
301
302 # Comments inline+general
302 # Comments inline+general
303 if c.at_version:
303 if c.at_version:
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
306 else:
306 else:
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
309
309
310 return general_comments, inline_comments
310 return general_comments, inline_comments
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
314 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
315 def pull_request_show(self):
315 def pull_request_show(self):
316 _ = self.request.translate
316 _ = self.request.translate
317 c = self.load_default_context()
317 c = self.load_default_context()
318
318
319 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
320 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
321 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
322
322
323 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325
325
326 _new_state = {
326 _new_state = {
327 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
328 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
329 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
330
330
331 if can_force_state and _new_state:
331 if can_force_state and _new_state:
332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
333 h.flash(
333 h.flash(
334 _('Pull Request state was force changed to `{}`').format(_new_state),
334 _('Pull Request state was force changed to `{}`').format(_new_state),
335 category='success')
335 category='success')
336 Session().commit()
336 Session().commit()
337
337
338 raise HTTPFound(h.route_path(
338 raise HTTPFound(h.route_path(
339 'pullrequest_show', repo_name=self.db_repo_name,
339 'pullrequest_show', repo_name=self.db_repo_name,
340 pull_request_id=pull_request_id))
340 pull_request_id=pull_request_id))
341
341
342 version = self.request.GET.get('version')
342 version = self.request.GET.get('version')
343 from_version = self.request.GET.get('from_version') or version
343 from_version = self.request.GET.get('from_version') or version
344 merge_checks = self.request.GET.get('merge_checks')
344 merge_checks = self.request.GET.get('merge_checks')
345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 force_refresh = str2bool(self.request.GET.get('force_refresh'))
347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347 c.range_diff_on = self.request.GET.get('range-diff') == "1"
348
348
349 # fetch global flags of ignore ws or context lines
349 # fetch global flags of ignore ws or context lines
350 diff_context = diffs.get_diff_context(self.request)
350 diff_context = diffs.get_diff_context(self.request)
351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
352
352
353 (pull_request_latest,
353 (pull_request_latest,
354 pull_request_at_ver,
354 pull_request_at_ver,
355 pull_request_display_obj,
355 pull_request_display_obj,
356 at_version) = PullRequestModel().get_pr_version(
356 at_version) = PullRequestModel().get_pr_version(
357 pull_request_id, version=version)
357 pull_request_id, version=version)
358
358
359 pr_closed = pull_request_latest.is_closed()
359 pr_closed = pull_request_latest.is_closed()
360
360
361 if pr_closed and (version or from_version):
361 if pr_closed and (version or from_version):
362 # not allow to browse versions for closed PR
362 # not allow to browse versions for closed PR
363 raise HTTPFound(h.route_path(
363 raise HTTPFound(h.route_path(
364 'pullrequest_show', repo_name=self.db_repo_name,
364 'pullrequest_show', repo_name=self.db_repo_name,
365 pull_request_id=pull_request_id))
365 pull_request_id=pull_request_id))
366
366
367 versions = pull_request_display_obj.versions()
367 versions = pull_request_display_obj.versions()
368
368
369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
370
370
371 # used to store per-commit range diffs
371 # used to store per-commit range diffs
372 c.changes = collections.OrderedDict()
372 c.changes = collections.OrderedDict()
373
373
374 c.at_version = at_version
374 c.at_version = at_version
375 c.at_version_num = (at_version
375 c.at_version_num = (at_version
376 if at_version and at_version != PullRequest.LATEST_VER
376 if at_version and at_version != PullRequest.LATEST_VER
377 else None)
377 else None)
378
378
379 c.at_version_index = ChangesetComment.get_index_from_version(
379 c.at_version_index = ChangesetComment.get_index_from_version(
380 c.at_version_num, versions)
380 c.at_version_num, versions)
381
381
382 (prev_pull_request_latest,
382 (prev_pull_request_latest,
383 prev_pull_request_at_ver,
383 prev_pull_request_at_ver,
384 prev_pull_request_display_obj,
384 prev_pull_request_display_obj,
385 prev_at_version) = PullRequestModel().get_pr_version(
385 prev_at_version) = PullRequestModel().get_pr_version(
386 pull_request_id, version=from_version)
386 pull_request_id, version=from_version)
387
387
388 c.from_version = prev_at_version
388 c.from_version = prev_at_version
389 c.from_version_num = (prev_at_version
389 c.from_version_num = (prev_at_version
390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
391 else None)
391 else None)
392 c.from_version_index = ChangesetComment.get_index_from_version(
392 c.from_version_index = ChangesetComment.get_index_from_version(
393 c.from_version_num, versions)
393 c.from_version_num, versions)
394
394
395 # define if we're in COMPARE mode or VIEW at version mode
395 # define if we're in COMPARE mode or VIEW at version mode
396 compare = at_version != prev_at_version
396 compare = at_version != prev_at_version
397
397
398 # pull_requests repo_name we opened it against
398 # pull_requests repo_name we opened it against
399 # ie. target_repo must match
399 # ie. target_repo must match
400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
401 log.warning('Mismatch between the current repo: %s, and target %s',
401 log.warning('Mismatch between the current repo: %s, and target %s',
402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
403 raise HTTPNotFound()
403 raise HTTPNotFound()
404
404
405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
406
406
407 c.pull_request = pull_request_display_obj
407 c.pull_request = pull_request_display_obj
408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 c.renderer = pull_request_at_ver.description_renderer or c.renderer
409 c.pull_request_latest = pull_request_latest
409 c.pull_request_latest = pull_request_latest
410
410
411 # inject latest version
411 # inject latest version
412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
413 c.versions = versions + [latest_ver]
413 c.versions = versions + [latest_ver]
414
414
415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
416 c.allowed_to_change_status = False
416 c.allowed_to_change_status = False
417 c.allowed_to_update = False
417 c.allowed_to_update = False
418 c.allowed_to_merge = False
418 c.allowed_to_merge = False
419 c.allowed_to_delete = False
419 c.allowed_to_delete = False
420 c.allowed_to_comment = False
420 c.allowed_to_comment = False
421 c.allowed_to_close = False
421 c.allowed_to_close = False
422 else:
422 else:
423 can_change_status = PullRequestModel().check_user_change_status(
423 can_change_status = PullRequestModel().check_user_change_status(
424 pull_request_at_ver, self._rhodecode_user)
424 pull_request_at_ver, self._rhodecode_user)
425 c.allowed_to_change_status = can_change_status and not pr_closed
425 c.allowed_to_change_status = can_change_status and not pr_closed
426
426
427 c.allowed_to_update = PullRequestModel().check_user_update(
427 c.allowed_to_update = PullRequestModel().check_user_update(
428 pull_request_latest, self._rhodecode_user) and not pr_closed
428 pull_request_latest, self._rhodecode_user) and not pr_closed
429 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 c.allowed_to_merge = PullRequestModel().check_user_merge(
430 pull_request_latest, self._rhodecode_user) and not pr_closed
430 pull_request_latest, self._rhodecode_user) and not pr_closed
431 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 c.allowed_to_delete = PullRequestModel().check_user_delete(
432 pull_request_latest, self._rhodecode_user) and not pr_closed
432 pull_request_latest, self._rhodecode_user) and not pr_closed
433 c.allowed_to_comment = not pr_closed
433 c.allowed_to_comment = not pr_closed
434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434 c.allowed_to_close = c.allowed_to_merge and not pr_closed
435
435
436 c.forbid_adding_reviewers = False
436 c.forbid_adding_reviewers = False
437
437
438 if pull_request_latest.reviewer_data and \
438 if pull_request_latest.reviewer_data and \
439 'rules' in pull_request_latest.reviewer_data:
439 'rules' in pull_request_latest.reviewer_data:
440 rules = pull_request_latest.reviewer_data['rules'] or {}
440 rules = pull_request_latest.reviewer_data['rules'] or {}
441 try:
441 try:
442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
443 except Exception:
443 except Exception:
444 pass
444 pass
445
445
446 # check merge capabilities
446 # check merge capabilities
447 _merge_check = MergeCheck.validate(
447 _merge_check = MergeCheck.validate(
448 pull_request_latest, auth_user=self._rhodecode_user,
448 pull_request_latest, auth_user=self._rhodecode_user,
449 translator=self.request.translate,
449 translator=self.request.translate,
450 force_shadow_repo_refresh=force_refresh)
450 force_shadow_repo_refresh=force_refresh)
451
451
452 c.pr_merge_errors = _merge_check.error_details
452 c.pr_merge_errors = _merge_check.error_details
453 c.pr_merge_possible = not _merge_check.failed
453 c.pr_merge_possible = not _merge_check.failed
454 c.pr_merge_message = _merge_check.merge_msg
454 c.pr_merge_message = _merge_check.merge_msg
455 c.pr_merge_source_commit = _merge_check.source_commit
455 c.pr_merge_source_commit = _merge_check.source_commit
456 c.pr_merge_target_commit = _merge_check.target_commit
456 c.pr_merge_target_commit = _merge_check.target_commit
457
457
458 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 c.pr_merge_info = MergeCheck.get_merge_conditions(
459 pull_request_latest, translator=self.request.translate)
459 pull_request_latest, translator=self.request.translate)
460
460
461 c.pull_request_review_status = _merge_check.review_status
461 c.pull_request_review_status = _merge_check.review_status
462 if merge_checks:
462 if merge_checks:
463 self.request.override_renderer = \
463 self.request.override_renderer = \
464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
465 return self._get_template_context(c)
465 return self._get_template_context(c)
466
466
467 c.reviewers_count = pull_request.reviewers_count
467 c.reviewers_count = pull_request.reviewers_count
468 c.observers_count = pull_request.observers_count
468 c.observers_count = pull_request.observers_count
469
469
470 # reviewers and statuses
470 # reviewers and statuses
471 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
471 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
474
474
475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
476 member_reviewer = h.reviewer_as_json(
476 member_reviewer = h.reviewer_as_json(
477 member, reasons=reasons, mandatory=mandatory,
477 member, reasons=reasons, mandatory=mandatory,
478 role=review_obj.role,
478 role=review_obj.role,
479 user_group=review_obj.rule_user_group_data()
479 user_group=review_obj.rule_user_group_data()
480 )
480 )
481
481
482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
483 member_reviewer['review_status'] = current_review_status
483 member_reviewer['review_status'] = current_review_status
484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
485 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 member_reviewer['allowed_to_update'] = c.allowed_to_update
486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
487
487
488 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
488 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
489
489
490 for observer_obj, member in pull_request_at_ver.observers():
490 for observer_obj, member in pull_request_at_ver.observers():
491 member_observer = h.reviewer_as_json(
491 member_observer = h.reviewer_as_json(
492 member, reasons=[], mandatory=False,
492 member, reasons=[], mandatory=False,
493 role=observer_obj.role,
493 role=observer_obj.role,
494 user_group=observer_obj.rule_user_group_data()
494 user_group=observer_obj.rule_user_group_data()
495 )
495 )
496 member_observer['allowed_to_update'] = c.allowed_to_update
496 member_observer['allowed_to_update'] = c.allowed_to_update
497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497 c.pull_request_set_observers_data_json['observers'].append(member_observer)
498
498
499 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
499 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
500
500
501 general_comments, inline_comments = \
501 general_comments, inline_comments = \
502 self.register_comments_vars(c, pull_request_latest, versions)
502 self.register_comments_vars(c, pull_request_latest, versions)
503
503
504 # TODOs
504 # TODOs
505 c.unresolved_comments = CommentsModel() \
505 c.unresolved_comments = CommentsModel() \
506 .get_pull_request_unresolved_todos(pull_request_latest)
506 .get_pull_request_unresolved_todos(pull_request_latest)
507 c.resolved_comments = CommentsModel() \
507 c.resolved_comments = CommentsModel() \
508 .get_pull_request_resolved_todos(pull_request_latest)
508 .get_pull_request_resolved_todos(pull_request_latest)
509
509
510 # Drafts
510 # Drafts
511 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 c.draft_comments = CommentsModel().get_pull_request_drafts(
512 self._rhodecode_db_user.user_id,
512 self._rhodecode_db_user.user_id,
513 pull_request_latest)
513 pull_request_latest)
514
514
515 # if we use version, then do not show later comments
515 # if we use version, then do not show later comments
516 # than current version
516 # than current version
517 display_inline_comments = collections.defaultdict(
517 display_inline_comments = collections.defaultdict(
518 lambda: collections.defaultdict(list))
518 lambda: collections.defaultdict(list))
519 for co in inline_comments:
519 for co in inline_comments:
520 if c.at_version_num:
520 if c.at_version_num:
521 # pick comments that are at least UPTO given version, so we
521 # pick comments that are at least UPTO given version, so we
522 # don't render comments for higher version
522 # don't render comments for higher version
523 should_render = co.pull_request_version_id and \
523 should_render = co.pull_request_version_id and \
524 co.pull_request_version_id <= c.at_version_num
524 co.pull_request_version_id <= c.at_version_num
525 else:
525 else:
526 # showing all, for 'latest'
526 # showing all, for 'latest'
527 should_render = True
527 should_render = True
528
528
529 if should_render:
529 if should_render:
530 display_inline_comments[co.f_path][co.line_no].append(co)
530 display_inline_comments[co.f_path][co.line_no].append(co)
531
531
532 # load diff data into template context, if we use compare mode then
532 # load diff data into template context, if we use compare mode then
533 # diff is calculated based on changes between versions of PR
533 # diff is calculated based on changes between versions of PR
534
534
535 source_repo = pull_request_at_ver.source_repo
535 source_repo = pull_request_at_ver.source_repo
536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
537
537
538 target_repo = pull_request_at_ver.target_repo
538 target_repo = pull_request_at_ver.target_repo
539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
540
540
541 if compare:
541 if compare:
542 # in compare switch the diff base to latest commit from prev version
542 # in compare switch the diff base to latest commit from prev version
543 target_ref_id = prev_pull_request_display_obj.revisions[0]
543 target_ref_id = prev_pull_request_display_obj.revisions[0]
544
544
545 # despite opening commits for bookmarks/branches/tags, we always
545 # despite opening commits for bookmarks/branches/tags, we always
546 # convert this to rev to prevent changes after bookmark or branch change
546 # convert this to rev to prevent changes after bookmark or branch change
547 c.source_ref_type = 'rev'
547 c.source_ref_type = 'rev'
548 c.source_ref = source_ref_id
548 c.source_ref = source_ref_id
549
549
550 c.target_ref_type = 'rev'
550 c.target_ref_type = 'rev'
551 c.target_ref = target_ref_id
551 c.target_ref = target_ref_id
552
552
553 c.source_repo = source_repo
553 c.source_repo = source_repo
554 c.target_repo = target_repo
554 c.target_repo = target_repo
555
555
556 c.commit_ranges = []
556 c.commit_ranges = []
557 source_commit = EmptyCommit()
557 source_commit = EmptyCommit()
558 target_commit = EmptyCommit()
558 target_commit = EmptyCommit()
559 c.missing_requirements = False
559 c.missing_requirements = False
560
560
561 source_scm = source_repo.scm_instance()
561 source_scm = source_repo.scm_instance()
562 target_scm = target_repo.scm_instance()
562 target_scm = target_repo.scm_instance()
563
563
564 shadow_scm = None
564 shadow_scm = None
565 try:
565 try:
566 shadow_scm = pull_request_latest.get_shadow_repo()
566 shadow_scm = pull_request_latest.get_shadow_repo()
567 except Exception:
567 except Exception:
568 log.debug('Failed to get shadow repo', exc_info=True)
568 log.debug('Failed to get shadow repo', exc_info=True)
569 # try first the existing source_repo, and then shadow
569 # try first the existing source_repo, and then shadow
570 # repo if we can obtain one
570 # repo if we can obtain one
571 commits_source_repo = source_scm
571 commits_source_repo = source_scm
572 if shadow_scm:
572 if shadow_scm:
573 commits_source_repo = shadow_scm
573 commits_source_repo = shadow_scm
574
574
575 c.commits_source_repo = commits_source_repo
575 c.commits_source_repo = commits_source_repo
576 c.ancestor = None # set it to None, to hide it from PR view
576 c.ancestor = None # set it to None, to hide it from PR view
577
577
578 # empty version means latest, so we keep this to prevent
578 # empty version means latest, so we keep this to prevent
579 # double caching
579 # double caching
580 version_normalized = version or PullRequest.LATEST_VER
580 version_normalized = version or PullRequest.LATEST_VER
581 from_version_normalized = from_version or PullRequest.LATEST_VER
581 from_version_normalized = from_version or PullRequest.LATEST_VER
582
582
583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
584 cache_file_path = diff_cache_exist(
584 cache_file_path = diff_cache_exist(
585 cache_path, 'pull_request', pull_request_id, version_normalized,
585 cache_path, 'pull_request', pull_request_id, version_normalized,
586 from_version_normalized, source_ref_id, target_ref_id,
586 from_version_normalized, source_ref_id, target_ref_id,
587 hide_whitespace_changes, diff_context, c.fulldiff)
587 hide_whitespace_changes, diff_context, c.fulldiff)
588
588
589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
590 force_recache = self.get_recache_flag()
590 force_recache = self.get_recache_flag()
591
591
592 cached_diff = None
592 cached_diff = None
593 if caching_enabled:
593 if caching_enabled:
594 cached_diff = load_cached_diff(cache_file_path)
594 cached_diff = load_cached_diff(cache_file_path)
595
595
596 has_proper_commit_cache = (
596 has_proper_commit_cache = (
597 cached_diff and cached_diff.get('commits')
597 cached_diff and cached_diff.get('commits')
598 and len(cached_diff.get('commits', [])) == 5
598 and len(cached_diff.get('commits', [])) == 5
599 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[0]
600 and cached_diff.get('commits')[3])
600 and cached_diff.get('commits')[3])
601
601
602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
603 diff_commit_cache = \
603 diff_commit_cache = \
604 (ancestor_commit, commit_cache, missing_requirements,
604 (ancestor_commit, commit_cache, missing_requirements,
605 source_commit, target_commit) = cached_diff['commits']
605 source_commit, target_commit) = cached_diff['commits']
606 else:
606 else:
607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # NOTE(marcink): we reach potentially unreachable errors when a PR has
608 # merge errors resulting in potentially hidden commits in the shadow repo.
608 # merge errors resulting in potentially hidden commits in the shadow repo.
609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
610 and _merge_check.merge_response
610 and _merge_check.merge_response
611 maybe_unreachable = maybe_unreachable \
611 maybe_unreachable = maybe_unreachable \
612 and _merge_check.merge_response.metadata.get('unresolved_files')
612 and _merge_check.merge_response.metadata.get('unresolved_files')
613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
614 diff_commit_cache = \
614 diff_commit_cache = \
615 (ancestor_commit, commit_cache, missing_requirements,
615 (ancestor_commit, commit_cache, missing_requirements,
616 source_commit, target_commit) = self.get_commits(
616 source_commit, target_commit) = self.get_commits(
617 commits_source_repo,
617 commits_source_repo,
618 pull_request_at_ver,
618 pull_request_at_ver,
619 source_commit,
619 source_commit,
620 source_ref_id,
620 source_ref_id,
621 source_scm,
621 source_scm,
622 target_commit,
622 target_commit,
623 target_ref_id,
623 target_ref_id,
624 target_scm,
624 target_scm,
625 maybe_unreachable=maybe_unreachable)
625 maybe_unreachable=maybe_unreachable)
626
626
627 # register our commit range
627 # register our commit range
628 for comm in commit_cache.values():
628 for comm in commit_cache.values():
629 c.commit_ranges.append(comm)
629 c.commit_ranges.append(comm)
630
630
631 c.missing_requirements = missing_requirements
631 c.missing_requirements = missing_requirements
632 c.ancestor_commit = ancestor_commit
632 c.ancestor_commit = ancestor_commit
633 c.statuses = source_repo.statuses(
633 c.statuses = source_repo.statuses(
634 [x.raw_id for x in c.commit_ranges])
634 [x.raw_id for x in c.commit_ranges])
635
635
636 # auto collapse if we have more than limit
636 # auto collapse if we have more than limit
637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 collapse_limit = diffs.DiffProcessor._collapse_commits_over
638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
639 c.compare_mode = compare
639 c.compare_mode = compare
640
640
641 # diff_limit is the old behavior, will cut off the whole diff
641 # diff_limit is the old behavior, will cut off the whole diff
642 # if the limit is applied otherwise will just hide the
642 # if the limit is applied otherwise will just hide the
643 # big files from the front-end
643 # big files from the front-end
644 diff_limit = c.visual.cut_off_limit_diff
644 diff_limit = c.visual.cut_off_limit_diff
645 file_limit = c.visual.cut_off_limit_file
645 file_limit = c.visual.cut_off_limit_file
646
646
647 c.missing_commits = False
647 c.missing_commits = False
648 if (c.missing_requirements
648 if (c.missing_requirements
649 or isinstance(source_commit, EmptyCommit)
649 or isinstance(source_commit, EmptyCommit)
650 or source_commit == target_commit):
650 or source_commit == target_commit):
651
651
652 c.missing_commits = True
652 c.missing_commits = True
653 else:
653 else:
654 c.inline_comments = display_inline_comments
654 c.inline_comments = display_inline_comments
655
655
656 use_ancestor = True
656 use_ancestor = True
657 if from_version_normalized != version_normalized:
657 if from_version_normalized != version_normalized:
658 use_ancestor = False
658 use_ancestor = False
659
659
660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
661 if not force_recache and has_proper_diff_cache:
661 if not force_recache and has_proper_diff_cache:
662 c.diffset = cached_diff['diff']
662 c.diffset = cached_diff['diff']
663 else:
663 else:
664 try:
664 try:
665 c.diffset = self._get_diffset(
665 c.diffset = self._get_diffset(
666 c.source_repo.repo_name, commits_source_repo,
666 c.source_repo.repo_name, commits_source_repo,
667 c.ancestor_commit,
667 c.ancestor_commit,
668 source_ref_id, target_ref_id,
668 source_ref_id, target_ref_id,
669 target_commit, source_commit,
669 target_commit, source_commit,
670 diff_limit, file_limit, c.fulldiff,
670 diff_limit, file_limit, c.fulldiff,
671 hide_whitespace_changes, diff_context,
671 hide_whitespace_changes, diff_context,
672 use_ancestor=use_ancestor
672 use_ancestor=use_ancestor
673 )
673 )
674
674
675 # save cached diff
675 # save cached diff
676 if caching_enabled:
676 if caching_enabled:
677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
678 except CommitDoesNotExistError:
678 except CommitDoesNotExistError:
679 log.exception('Failed to generate diffset')
679 log.exception('Failed to generate diffset')
680 c.missing_commits = True
680 c.missing_commits = True
681
681
682 if not c.missing_commits:
682 if not c.missing_commits:
683
683
684 c.limited_diff = c.diffset.limited_diff
684 c.limited_diff = c.diffset.limited_diff
685
685
686 # calculate removed files that are bound to comments
686 # calculate removed files that are bound to comments
687 comment_deleted_files = [
687 comment_deleted_files = [
688 fname for fname in display_inline_comments
688 fname for fname in display_inline_comments
689 if fname not in c.diffset.file_stats]
689 if fname not in c.diffset.file_stats]
690
690
691 c.deleted_files_comments = collections.defaultdict(dict)
691 c.deleted_files_comments = collections.defaultdict(dict)
692 for fname, per_line_comments in display_inline_comments.items():
692 for fname, per_line_comments in display_inline_comments.items():
693 if fname in comment_deleted_files:
693 if fname in comment_deleted_files:
694 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['stats'] = 0
695 c.deleted_files_comments[fname]['comments'] = list()
695 c.deleted_files_comments[fname]['comments'] = list()
696 for lno, comments in per_line_comments.items():
696 for lno, comments in per_line_comments.items():
697 c.deleted_files_comments[fname]['comments'].extend(comments)
697 c.deleted_files_comments[fname]['comments'].extend(comments)
698
698
699 # maybe calculate the range diff
699 # maybe calculate the range diff
700 if c.range_diff_on:
700 if c.range_diff_on:
701 # TODO(marcink): set whitespace/context
701 # TODO(marcink): set whitespace/context
702 context_lcl = 3
702 context_lcl = 3
703 ign_whitespace_lcl = False
703 ign_whitespace_lcl = False
704
704
705 for commit in c.commit_ranges:
705 for commit in c.commit_ranges:
706 commit2 = commit
706 commit2 = commit
707 commit1 = commit.first_parent
707 commit1 = commit.first_parent
708
708
709 range_diff_cache_file_path = diff_cache_exist(
709 range_diff_cache_file_path = diff_cache_exist(
710 cache_path, 'diff', commit.raw_id,
710 cache_path, 'diff', commit.raw_id,
711 ign_whitespace_lcl, context_lcl, c.fulldiff)
711 ign_whitespace_lcl, context_lcl, c.fulldiff)
712
712
713 cached_diff = None
713 cached_diff = None
714 if caching_enabled:
714 if caching_enabled:
715 cached_diff = load_cached_diff(range_diff_cache_file_path)
715 cached_diff = load_cached_diff(range_diff_cache_file_path)
716
716
717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
718 if not force_recache and has_proper_diff_cache:
718 if not force_recache and has_proper_diff_cache:
719 diffset = cached_diff['diff']
719 diffset = cached_diff['diff']
720 else:
720 else:
721 diffset = self._get_range_diffset(
721 diffset = self._get_range_diffset(
722 commits_source_repo, source_repo,
722 commits_source_repo, source_repo,
723 commit1, commit2, diff_limit, file_limit,
723 commit1, commit2, diff_limit, file_limit,
724 c.fulldiff, ign_whitespace_lcl, context_lcl
724 c.fulldiff, ign_whitespace_lcl, context_lcl
725 )
725 )
726
726
727 # save cached diff
727 # save cached diff
728 if caching_enabled:
728 if caching_enabled:
729 cache_diff(range_diff_cache_file_path, diffset, None)
729 cache_diff(range_diff_cache_file_path, diffset, None)
730
730
731 c.changes[commit.raw_id] = diffset
731 c.changes[commit.raw_id] = diffset
732
732
733 # this is a hack to properly display links, when creating PR, the
733 # this is a hack to properly display links, when creating PR, the
734 # compare view and others uses different notation, and
734 # compare view and others uses different notation, and
735 # compare_commits.mako renders links based on the target_repo.
735 # compare_commits.mako renders links based on the target_repo.
736 # We need to swap that here to generate it properly on the html side
736 # We need to swap that here to generate it properly on the html side
737 c.target_repo = c.source_repo
737 c.target_repo = c.source_repo
738
738
739 c.commit_statuses = ChangesetStatus.STATUSES
739 c.commit_statuses = ChangesetStatus.STATUSES
740
740
741 c.show_version_changes = not pr_closed
741 c.show_version_changes = not pr_closed
742 if c.show_version_changes:
742 if c.show_version_changes:
743 cur_obj = pull_request_at_ver
743 cur_obj = pull_request_at_ver
744 prev_obj = prev_pull_request_at_ver
744 prev_obj = prev_pull_request_at_ver
745
745
746 old_commit_ids = prev_obj.revisions
746 old_commit_ids = prev_obj.revisions
747 new_commit_ids = cur_obj.revisions
747 new_commit_ids = cur_obj.revisions
748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 commit_changes = PullRequestModel()._calculate_commit_id_changes(
749 old_commit_ids, new_commit_ids)
749 old_commit_ids, new_commit_ids)
750 c.commit_changes_summary = commit_changes
750 c.commit_changes_summary = commit_changes
751
751
752 # calculate the diff for commits between versions
752 # calculate the diff for commits between versions
753 c.commit_changes = []
753 c.commit_changes = []
754
754
755 def mark(cs, fw):
755 def mark(cs, fw):
756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
756 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
757
757
758 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 for c_type, raw_id in mark(commit_changes.added, 'a') \
759 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.removed, 'r') \
760 + mark(commit_changes.common, 'c'):
760 + mark(commit_changes.common, 'c'):
761
761
762 if raw_id in commit_cache:
762 if raw_id in commit_cache:
763 commit = commit_cache[raw_id]
763 commit = commit_cache[raw_id]
764 else:
764 else:
765 try:
765 try:
766 commit = commits_source_repo.get_commit(raw_id)
766 commit = commits_source_repo.get_commit(raw_id)
767 except CommitDoesNotExistError:
767 except CommitDoesNotExistError:
768 # in case we fail extracting still use "dummy" commit
768 # in case we fail extracting still use "dummy" commit
769 # for display in commit diff
769 # for display in commit diff
770 commit = h.AttributeDict(
770 commit = h.AttributeDict(
771 {'raw_id': raw_id,
771 {'raw_id': raw_id,
772 'message': 'EMPTY or MISSING COMMIT'})
772 'message': 'EMPTY or MISSING COMMIT'})
773 c.commit_changes.append([c_type, commit])
773 c.commit_changes.append([c_type, commit])
774
774
775 # current user review statuses for each version
775 # current user review statuses for each version
776 c.review_versions = {}
776 c.review_versions = {}
777 is_reviewer = PullRequestModel().is_user_reviewer(
777 is_reviewer = PullRequestModel().is_user_reviewer(
778 pull_request, self._rhodecode_user)
778 pull_request, self._rhodecode_user)
779 if is_reviewer:
779 if is_reviewer:
780 for co in general_comments:
780 for co in general_comments:
781 if co.author.user_id == self._rhodecode_user.user_id:
781 if co.author.user_id == self._rhodecode_user.user_id:
782 status = co.status_change
782 status = co.status_change
783 if status:
783 if status:
784 _ver_pr = status[0].comment.pull_request_version_id
784 _ver_pr = status[0].comment.pull_request_version_id
785 c.review_versions[_ver_pr] = status[0]
785 c.review_versions[_ver_pr] = status[0]
786
786
787 return self._get_template_context(c)
787 return self._get_template_context(c)
788
788
789 def get_commits(
789 def get_commits(
790 self, commits_source_repo, pull_request_at_ver, source_commit,
790 self, commits_source_repo, pull_request_at_ver, source_commit,
791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
792 maybe_unreachable=False):
792 maybe_unreachable=False):
793
793
794 commit_cache = collections.OrderedDict()
794 commit_cache = collections.OrderedDict()
795 missing_requirements = False
795 missing_requirements = False
796
796
797 try:
797 try:
798 pre_load = ["author", "date", "message", "branch", "parents"]
798 pre_load = ["author", "date", "message", "branch", "parents"]
799
799
800 pull_request_commits = pull_request_at_ver.revisions
800 pull_request_commits = pull_request_at_ver.revisions
801 log.debug('Loading %s commits from %s',
801 log.debug('Loading %s commits from %s',
802 len(pull_request_commits), commits_source_repo)
802 len(pull_request_commits), commits_source_repo)
803
803
804 for rev in pull_request_commits:
804 for rev in pull_request_commits:
805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
806 maybe_unreachable=maybe_unreachable)
806 maybe_unreachable=maybe_unreachable)
807 commit_cache[comm.raw_id] = comm
807 commit_cache[comm.raw_id] = comm
808
808
809 # Order here matters, we first need to get target, and then
809 # Order here matters, we first need to get target, and then
810 # the source
810 # the source
811 target_commit = commits_source_repo.get_commit(
811 target_commit = commits_source_repo.get_commit(
812 commit_id=safe_str(target_ref_id))
812 commit_id=safe_str(target_ref_id))
813
813
814 source_commit = commits_source_repo.get_commit(
814 source_commit = commits_source_repo.get_commit(
815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
816 except CommitDoesNotExistError:
816 except CommitDoesNotExistError:
817 log.warning('Failed to get commit from `{}` repo'.format(
817 log.warning('Failed to get commit from `{}` repo'.format(
818 commits_source_repo), exc_info=True)
818 commits_source_repo), exc_info=True)
819 except RepositoryRequirementError:
819 except RepositoryRequirementError:
820 log.warning('Failed to get all required data from repo', exc_info=True)
820 log.warning('Failed to get all required data from repo', exc_info=True)
821 missing_requirements = True
821 missing_requirements = True
822
822
823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
824
824
825 try:
825 try:
826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
827 except Exception:
827 except Exception:
828 ancestor_commit = None
828 ancestor_commit = None
829
829
830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
831
831
832 def assure_not_empty_repo(self):
832 def assure_not_empty_repo(self):
833 _ = self.request.translate
833 _ = self.request.translate
834
834
835 try:
835 try:
836 self.db_repo.scm_instance().get_commit()
836 self.db_repo.scm_instance().get_commit()
837 except EmptyRepositoryError:
837 except EmptyRepositoryError:
838 h.flash(h.literal(_('There are no commits yet')),
838 h.flash(h.literal(_('There are no commits yet')),
839 category='warning')
839 category='warning')
840 raise HTTPFound(
840 raise HTTPFound(
841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
842
842
843 @LoginRequired()
843 @LoginRequired()
844 @NotAnonymous()
844 @NotAnonymous()
845 @HasRepoPermissionAnyDecorator(
845 @HasRepoPermissionAnyDecorator(
846 'repository.read', 'repository.write', 'repository.admin')
846 'repository.read', 'repository.write', 'repository.admin')
847 def pull_request_new(self):
847 def pull_request_new(self):
848 _ = self.request.translate
848 _ = self.request.translate
849 c = self.load_default_context()
849 c = self.load_default_context()
850
850
851 self.assure_not_empty_repo()
851 self.assure_not_empty_repo()
852 source_repo = self.db_repo
852 source_repo = self.db_repo
853
853
854 commit_id = self.request.GET.get('commit')
854 commit_id = self.request.GET.get('commit')
855 branch_ref = self.request.GET.get('branch')
855 branch_ref = self.request.GET.get('branch')
856 bookmark_ref = self.request.GET.get('bookmark')
856 bookmark_ref = self.request.GET.get('bookmark')
857
857
858 try:
858 try:
859 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo_data = PullRequestModel().generate_repo_data(
860 source_repo, commit_id=commit_id,
860 source_repo, commit_id=commit_id,
861 branch=branch_ref, bookmark=bookmark_ref,
861 branch=branch_ref, bookmark=bookmark_ref,
862 translator=self.request.translate)
862 translator=self.request.translate)
863 except CommitDoesNotExistError as e:
863 except CommitDoesNotExistError as e:
864 log.exception(e)
864 log.exception(e)
865 h.flash(_('Commit does not exist'), 'error')
865 h.flash(_('Commit does not exist'), 'error')
866 raise HTTPFound(
866 raise HTTPFound(
867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
868
868
869 default_target_repo = source_repo
869 default_target_repo = source_repo
870
870
871 if source_repo.parent and c.has_origin_repo_read_perm:
871 if source_repo.parent and c.has_origin_repo_read_perm:
872 parent_vcs_obj = source_repo.parent.scm_instance()
872 parent_vcs_obj = source_repo.parent.scm_instance()
873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 if parent_vcs_obj and not parent_vcs_obj.is_empty():
874 # change default if we have a parent repo
874 # change default if we have a parent repo
875 default_target_repo = source_repo.parent
875 default_target_repo = source_repo.parent
876
876
877 target_repo_data = PullRequestModel().generate_repo_data(
877 target_repo_data = PullRequestModel().generate_repo_data(
878 default_target_repo, translator=self.request.translate)
878 default_target_repo, translator=self.request.translate)
879
879
880 selected_source_ref = source_repo_data['refs']['selected_ref']
880 selected_source_ref = source_repo_data['refs']['selected_ref']
881 title_source_ref = ''
881 title_source_ref = ''
882 if selected_source_ref:
882 if selected_source_ref:
883 title_source_ref = selected_source_ref.split(':', 2)[1]
883 title_source_ref = selected_source_ref.split(':', 2)[1]
884 c.default_title = PullRequestModel().generate_pullrequest_title(
884 c.default_title = PullRequestModel().generate_pullrequest_title(
885 source=source_repo.repo_name,
885 source=source_repo.repo_name,
886 source_ref=title_source_ref,
886 source_ref=title_source_ref,
887 target=default_target_repo.repo_name
887 target=default_target_repo.repo_name
888 )
888 )
889
889
890 c.default_repo_data = {
890 c.default_repo_data = {
891 'source_repo_name': source_repo.repo_name,
891 'source_repo_name': source_repo.repo_name,
892 'source_refs_json': json.dumps(source_repo_data),
892 'source_refs_json': ext_json.str_json(source_repo_data),
893 'target_repo_name': default_target_repo.repo_name,
893 'target_repo_name': default_target_repo.repo_name,
894 'target_refs_json': json.dumps(target_repo_data),
894 'target_refs_json': ext_json.str_json(target_repo_data),
895 }
895 }
896 c.default_source_ref = selected_source_ref
896 c.default_source_ref = selected_source_ref
897
897
898 return self._get_template_context(c)
898 return self._get_template_context(c)
899
899
900 @LoginRequired()
900 @LoginRequired()
901 @NotAnonymous()
901 @NotAnonymous()
902 @HasRepoPermissionAnyDecorator(
902 @HasRepoPermissionAnyDecorator(
903 'repository.read', 'repository.write', 'repository.admin')
903 'repository.read', 'repository.write', 'repository.admin')
904 def pull_request_repo_refs(self):
904 def pull_request_repo_refs(self):
905 self.load_default_context()
905 self.load_default_context()
906 target_repo_name = self.request.matchdict['target_repo_name']
906 target_repo_name = self.request.matchdict['target_repo_name']
907 repo = Repository.get_by_repo_name(target_repo_name)
907 repo = Repository.get_by_repo_name(target_repo_name)
908 if not repo:
908 if not repo:
909 raise HTTPNotFound()
909 raise HTTPNotFound()
910
910
911 target_perm = HasRepoPermissionAny(
911 target_perm = HasRepoPermissionAny(
912 'repository.read', 'repository.write', 'repository.admin')(
912 'repository.read', 'repository.write', 'repository.admin')(
913 target_repo_name)
913 target_repo_name)
914 if not target_perm:
914 if not target_perm:
915 raise HTTPNotFound()
915 raise HTTPNotFound()
916
916
917 return PullRequestModel().generate_repo_data(
917 return PullRequestModel().generate_repo_data(
918 repo, translator=self.request.translate)
918 repo, translator=self.request.translate)
919
919
920 @LoginRequired()
920 @LoginRequired()
921 @NotAnonymous()
921 @NotAnonymous()
922 @HasRepoPermissionAnyDecorator(
922 @HasRepoPermissionAnyDecorator(
923 'repository.read', 'repository.write', 'repository.admin')
923 'repository.read', 'repository.write', 'repository.admin')
924 def pullrequest_repo_targets(self):
924 def pullrequest_repo_targets(self):
925 _ = self.request.translate
925 _ = self.request.translate
926 filter_query = self.request.GET.get('query')
926 filter_query = self.request.GET.get('query')
927
927
928 # get the parents
928 # get the parents
929 parent_target_repos = []
929 parent_target_repos = []
930 if self.db_repo.parent:
930 if self.db_repo.parent:
931 parents_query = Repository.query() \
931 parents_query = Repository.query() \
932 .order_by(func.length(Repository.repo_name)) \
932 .order_by(func.length(Repository.repo_name)) \
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
934
934
935 if filter_query:
935 if filter_query:
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 parents_query = parents_query.filter(
937 parents_query = parents_query.filter(
938 Repository.repo_name.ilike(ilike_expression))
938 Repository.repo_name.ilike(ilike_expression))
939 parents = parents_query.limit(20).all()
939 parents = parents_query.limit(20).all()
940
940
941 for parent in parents:
941 for parent in parents:
942 parent_vcs_obj = parent.scm_instance()
942 parent_vcs_obj = parent.scm_instance()
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 if parent_vcs_obj and not parent_vcs_obj.is_empty():
944 parent_target_repos.append(parent)
944 parent_target_repos.append(parent)
945
945
946 # get other forks, and repo itself
946 # get other forks, and repo itself
947 query = Repository.query() \
947 query = Repository.query() \
948 .order_by(func.length(Repository.repo_name)) \
948 .order_by(func.length(Repository.repo_name)) \
949 .filter(
949 .filter(
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
952 ) \
952 ) \
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
954
954
955 if filter_query:
955 if filter_query:
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957 query = query.filter(Repository.repo_name.ilike(ilike_expression))
958
958
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 limit = max(20 - len(parent_target_repos), 5) # not less then 5
960 target_repos = query.limit(limit).all()
960 target_repos = query.limit(limit).all()
961
961
962 all_target_repos = target_repos + parent_target_repos
962 all_target_repos = target_repos + parent_target_repos
963
963
964 repos = []
964 repos = []
965 # This checks permissions to the repositories
965 # This checks permissions to the repositories
966 for obj in ScmModel().get_repos(all_target_repos):
966 for obj in ScmModel().get_repos(all_target_repos):
967 repos.append({
967 repos.append({
968 'id': obj['name'],
968 'id': obj['name'],
969 'text': obj['name'],
969 'text': obj['name'],
970 'type': 'repo',
970 'type': 'repo',
971 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_id': obj['dbrepo']['repo_id'],
972 'repo_type': obj['dbrepo']['repo_type'],
972 'repo_type': obj['dbrepo']['repo_type'],
973 'private': obj['dbrepo']['private'],
973 'private': obj['dbrepo']['private'],
974
974
975 })
975 })
976
976
977 data = {
977 data = {
978 'more': False,
978 'more': False,
979 'results': [{
979 'results': [{
980 'text': _('Repositories'),
980 'text': _('Repositories'),
981 'children': repos
981 'children': repos
982 }] if repos else []
982 }] if repos else []
983 }
983 }
984 return data
984 return data
985
985
986 @classmethod
986 @classmethod
987 def get_comment_ids(cls, post_data):
987 def get_comment_ids(cls, post_data):
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
989
989
990 @LoginRequired()
990 @LoginRequired()
991 @NotAnonymous()
991 @NotAnonymous()
992 @HasRepoPermissionAnyDecorator(
992 @HasRepoPermissionAnyDecorator(
993 'repository.read', 'repository.write', 'repository.admin')
993 'repository.read', 'repository.write', 'repository.admin')
994 def pullrequest_comments(self):
994 def pullrequest_comments(self):
995 self.load_default_context()
995 self.load_default_context()
996
996
997 pull_request = PullRequest.get_or_404(
997 pull_request = PullRequest.get_or_404(
998 self.request.matchdict['pull_request_id'])
998 self.request.matchdict['pull_request_id'])
999 pull_request_id = pull_request.pull_request_id
999 pull_request_id = pull_request.pull_request_id
1000 version = self.request.GET.get('version')
1000 version = self.request.GET.get('version')
1001
1001
1002 _render = self.request.get_partial_renderer(
1002 _render = self.request.get_partial_renderer(
1003 'rhodecode:templates/base/sidebar.mako')
1003 'rhodecode:templates/base/sidebar.mako')
1004 c = _render.get_call_context()
1004 c = _render.get_call_context()
1005
1005
1006 (pull_request_latest,
1006 (pull_request_latest,
1007 pull_request_at_ver,
1007 pull_request_at_ver,
1008 pull_request_display_obj,
1008 pull_request_display_obj,
1009 at_version) = PullRequestModel().get_pr_version(
1009 at_version) = PullRequestModel().get_pr_version(
1010 pull_request_id, version=version)
1010 pull_request_id, version=version)
1011 versions = pull_request_display_obj.versions()
1011 versions = pull_request_display_obj.versions()
1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1013 c.versions = versions + [latest_ver]
1013 c.versions = versions + [latest_ver]
1014
1014
1015 c.at_version = at_version
1015 c.at_version = at_version
1016 c.at_version_num = (at_version
1016 c.at_version_num = (at_version
1017 if at_version and at_version != PullRequest.LATEST_VER
1017 if at_version and at_version != PullRequest.LATEST_VER
1018 else None)
1018 else None)
1019
1019
1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1021 all_comments = c.inline_comments_flat + c.comments
1021 all_comments = c.inline_comments_flat + c.comments
1022
1022
1023 existing_ids = self.get_comment_ids(self.request.POST)
1023 existing_ids = self.get_comment_ids(self.request.POST)
1024 return _render('comments_table', all_comments, len(all_comments),
1024 return _render('comments_table', all_comments, len(all_comments),
1025 existing_ids=existing_ids)
1025 existing_ids=existing_ids)
1026
1026
1027 @LoginRequired()
1027 @LoginRequired()
1028 @NotAnonymous()
1028 @NotAnonymous()
1029 @HasRepoPermissionAnyDecorator(
1029 @HasRepoPermissionAnyDecorator(
1030 'repository.read', 'repository.write', 'repository.admin')
1030 'repository.read', 'repository.write', 'repository.admin')
1031 def pullrequest_todos(self):
1031 def pullrequest_todos(self):
1032 self.load_default_context()
1032 self.load_default_context()
1033
1033
1034 pull_request = PullRequest.get_or_404(
1034 pull_request = PullRequest.get_or_404(
1035 self.request.matchdict['pull_request_id'])
1035 self.request.matchdict['pull_request_id'])
1036 pull_request_id = pull_request.pull_request_id
1036 pull_request_id = pull_request.pull_request_id
1037 version = self.request.GET.get('version')
1037 version = self.request.GET.get('version')
1038
1038
1039 _render = self.request.get_partial_renderer(
1039 _render = self.request.get_partial_renderer(
1040 'rhodecode:templates/base/sidebar.mako')
1040 'rhodecode:templates/base/sidebar.mako')
1041 c = _render.get_call_context()
1041 c = _render.get_call_context()
1042 (pull_request_latest,
1042 (pull_request_latest,
1043 pull_request_at_ver,
1043 pull_request_at_ver,
1044 pull_request_display_obj,
1044 pull_request_display_obj,
1045 at_version) = PullRequestModel().get_pr_version(
1045 at_version) = PullRequestModel().get_pr_version(
1046 pull_request_id, version=version)
1046 pull_request_id, version=version)
1047 versions = pull_request_display_obj.versions()
1047 versions = pull_request_display_obj.versions()
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1049 c.versions = versions + [latest_ver]
1049 c.versions = versions + [latest_ver]
1050
1050
1051 c.at_version = at_version
1051 c.at_version = at_version
1052 c.at_version_num = (at_version
1052 c.at_version_num = (at_version
1053 if at_version and at_version != PullRequest.LATEST_VER
1053 if at_version and at_version != PullRequest.LATEST_VER
1054 else None)
1054 else None)
1055
1055
1056 c.unresolved_comments = CommentsModel() \
1056 c.unresolved_comments = CommentsModel() \
1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1058 c.resolved_comments = CommentsModel() \
1058 c.resolved_comments = CommentsModel() \
1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1060
1060
1061 all_comments = c.unresolved_comments + c.resolved_comments
1061 all_comments = c.unresolved_comments + c.resolved_comments
1062 existing_ids = self.get_comment_ids(self.request.POST)
1062 existing_ids = self.get_comment_ids(self.request.POST)
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 return _render('comments_table', all_comments, len(c.unresolved_comments),
1064 todo_comments=True, existing_ids=existing_ids)
1064 todo_comments=True, existing_ids=existing_ids)
1065
1065
1066 @LoginRequired()
1066 @LoginRequired()
1067 @NotAnonymous()
1067 @NotAnonymous()
1068 @HasRepoPermissionAnyDecorator(
1068 @HasRepoPermissionAnyDecorator(
1069 'repository.read', 'repository.write', 'repository.admin')
1069 'repository.read', 'repository.write', 'repository.admin')
1070 def pullrequest_drafts(self):
1070 def pullrequest_drafts(self):
1071 self.load_default_context()
1071 self.load_default_context()
1072
1072
1073 pull_request = PullRequest.get_or_404(
1073 pull_request = PullRequest.get_or_404(
1074 self.request.matchdict['pull_request_id'])
1074 self.request.matchdict['pull_request_id'])
1075 pull_request_id = pull_request.pull_request_id
1075 pull_request_id = pull_request.pull_request_id
1076 version = self.request.GET.get('version')
1076 version = self.request.GET.get('version')
1077
1077
1078 _render = self.request.get_partial_renderer(
1078 _render = self.request.get_partial_renderer(
1079 'rhodecode:templates/base/sidebar.mako')
1079 'rhodecode:templates/base/sidebar.mako')
1080 c = _render.get_call_context()
1080 c = _render.get_call_context()
1081
1081
1082 (pull_request_latest,
1082 (pull_request_latest,
1083 pull_request_at_ver,
1083 pull_request_at_ver,
1084 pull_request_display_obj,
1084 pull_request_display_obj,
1085 at_version) = PullRequestModel().get_pr_version(
1085 at_version) = PullRequestModel().get_pr_version(
1086 pull_request_id, version=version)
1086 pull_request_id, version=version)
1087 versions = pull_request_display_obj.versions()
1087 versions = pull_request_display_obj.versions()
1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1089 c.versions = versions + [latest_ver]
1089 c.versions = versions + [latest_ver]
1090
1090
1091 c.at_version = at_version
1091 c.at_version = at_version
1092 c.at_version_num = (at_version
1092 c.at_version_num = (at_version
1093 if at_version and at_version != PullRequest.LATEST_VER
1093 if at_version and at_version != PullRequest.LATEST_VER
1094 else None)
1094 else None)
1095
1095
1096 c.draft_comments = CommentsModel() \
1096 c.draft_comments = CommentsModel() \
1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1098
1098
1099 all_comments = c.draft_comments
1099 all_comments = c.draft_comments
1100
1100
1101 existing_ids = self.get_comment_ids(self.request.POST)
1101 existing_ids = self.get_comment_ids(self.request.POST)
1102 return _render('comments_table', all_comments, len(all_comments),
1102 return _render('comments_table', all_comments, len(all_comments),
1103 existing_ids=existing_ids, draft_comments=True)
1103 existing_ids=existing_ids, draft_comments=True)
1104
1104
1105 @LoginRequired()
1105 @LoginRequired()
1106 @NotAnonymous()
1106 @NotAnonymous()
1107 @HasRepoPermissionAnyDecorator(
1107 @HasRepoPermissionAnyDecorator(
1108 'repository.read', 'repository.write', 'repository.admin')
1108 'repository.read', 'repository.write', 'repository.admin')
1109 @CSRFRequired()
1109 @CSRFRequired()
1110 def pull_request_create(self):
1110 def pull_request_create(self):
1111 _ = self.request.translate
1111 _ = self.request.translate
1112 self.assure_not_empty_repo()
1112 self.assure_not_empty_repo()
1113 self.load_default_context()
1113 self.load_default_context()
1114
1114
1115 controls = peppercorn.parse(self.request.POST.items())
1115 controls = peppercorn.parse(self.request.POST.items())
1116
1116
1117 try:
1117 try:
1118 form = PullRequestForm(
1118 form = PullRequestForm(
1119 self.request.translate, self.db_repo.repo_id)()
1119 self.request.translate, self.db_repo.repo_id)()
1120 _form = form.to_python(controls)
1120 _form = form.to_python(controls)
1121 except formencode.Invalid as errors:
1121 except formencode.Invalid as errors:
1122 if errors.error_dict.get('revisions'):
1122 if errors.error_dict.get('revisions'):
1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 msg = 'Revisions: %s' % errors.error_dict['revisions']
1124 elif errors.error_dict.get('pullrequest_title'):
1124 elif errors.error_dict.get('pullrequest_title'):
1125 msg = errors.error_dict.get('pullrequest_title')
1125 msg = errors.error_dict.get('pullrequest_title')
1126 else:
1126 else:
1127 msg = _('Error creating pull request: {}').format(errors)
1127 msg = _('Error creating pull request: {}').format(errors)
1128 log.exception(msg)
1128 log.exception(msg)
1129 h.flash(msg, 'error')
1129 h.flash(msg, 'error')
1130
1130
1131 # would rather just go back to form ...
1131 # would rather just go back to form ...
1132 raise HTTPFound(
1132 raise HTTPFound(
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1134
1134
1135 source_repo = _form['source_repo']
1135 source_repo = _form['source_repo']
1136 source_ref = _form['source_ref']
1136 source_ref = _form['source_ref']
1137 target_repo = _form['target_repo']
1137 target_repo = _form['target_repo']
1138 target_ref = _form['target_ref']
1138 target_ref = _form['target_ref']
1139 commit_ids = _form['revisions'][::-1]
1139 commit_ids = _form['revisions'][::-1]
1140 common_ancestor_id = _form['common_ancestor']
1140 common_ancestor_id = _form['common_ancestor']
1141
1141
1142 # find the ancestor for this pr
1142 # find the ancestor for this pr
1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1145
1145
1146 if not (source_db_repo or target_db_repo):
1146 if not (source_db_repo or target_db_repo):
1147 h.flash(_('source_repo or target repo not found'), category='error')
1147 h.flash(_('source_repo or target repo not found'), category='error')
1148 raise HTTPFound(
1148 raise HTTPFound(
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1150
1150
1151 # re-check permissions again here
1151 # re-check permissions again here
1152 # source_repo we must have read permissions
1152 # source_repo we must have read permissions
1153
1153
1154 source_perm = HasRepoPermissionAny(
1154 source_perm = HasRepoPermissionAny(
1155 'repository.read', 'repository.write', 'repository.admin')(
1155 'repository.read', 'repository.write', 'repository.admin')(
1156 source_db_repo.repo_name)
1156 source_db_repo.repo_name)
1157 if not source_perm:
1157 if not source_perm:
1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 msg = _('Not Enough permissions to source repo `{}`.'.format(
1159 source_db_repo.repo_name))
1159 source_db_repo.repo_name))
1160 h.flash(msg, category='error')
1160 h.flash(msg, category='error')
1161 # copy the args back to redirect
1161 # copy the args back to redirect
1162 org_query = self.request.GET.mixed()
1162 org_query = self.request.GET.mixed()
1163 raise HTTPFound(
1163 raise HTTPFound(
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1165 _query=org_query))
1165 _query=org_query))
1166
1166
1167 # target repo we must have read permissions, and also later on
1167 # target repo we must have read permissions, and also later on
1168 # we want to check branch permissions here
1168 # we want to check branch permissions here
1169 target_perm = HasRepoPermissionAny(
1169 target_perm = HasRepoPermissionAny(
1170 'repository.read', 'repository.write', 'repository.admin')(
1170 'repository.read', 'repository.write', 'repository.admin')(
1171 target_db_repo.repo_name)
1171 target_db_repo.repo_name)
1172 if not target_perm:
1172 if not target_perm:
1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 msg = _('Not Enough permissions to target repo `{}`.'.format(
1174 target_db_repo.repo_name))
1174 target_db_repo.repo_name))
1175 h.flash(msg, category='error')
1175 h.flash(msg, category='error')
1176 # copy the args back to redirect
1176 # copy the args back to redirect
1177 org_query = self.request.GET.mixed()
1177 org_query = self.request.GET.mixed()
1178 raise HTTPFound(
1178 raise HTTPFound(
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1180 _query=org_query))
1180 _query=org_query))
1181
1181
1182 source_scm = source_db_repo.scm_instance()
1182 source_scm = source_db_repo.scm_instance()
1183 target_scm = target_db_repo.scm_instance()
1183 target_scm = target_db_repo.scm_instance()
1184
1184
1185 source_ref_obj = unicode_to_reference(source_ref)
1185 source_ref_obj = unicode_to_reference(source_ref)
1186 target_ref_obj = unicode_to_reference(target_ref)
1186 target_ref_obj = unicode_to_reference(target_ref)
1187
1187
1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1190
1190
1191 ancestor = source_scm.get_common_ancestor(
1191 ancestor = source_scm.get_common_ancestor(
1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1192 source_commit.raw_id, target_commit.raw_id, target_scm)
1193
1193
1194 # recalculate target ref based on ancestor
1194 # recalculate target ref based on ancestor
1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1196
1196
1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1198 PullRequestModel().get_reviewer_functions()
1198 PullRequestModel().get_reviewer_functions()
1199
1199
1200 # recalculate reviewers logic, to make sure we can validate this
1200 # recalculate reviewers logic, to make sure we can validate this
1201 reviewer_rules = get_default_reviewers_data(
1201 reviewer_rules = get_default_reviewers_data(
1202 self._rhodecode_db_user,
1202 self._rhodecode_db_user,
1203 source_db_repo,
1203 source_db_repo,
1204 source_ref_obj,
1204 source_ref_obj,
1205 target_db_repo,
1205 target_db_repo,
1206 target_ref_obj,
1206 target_ref_obj,
1207 include_diff_info=False)
1207 include_diff_info=False)
1208
1208
1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210 observers = validate_observers(_form['observer_members'], reviewer_rules)
1211
1211
1212 pullrequest_title = _form['pullrequest_title']
1212 pullrequest_title = _form['pullrequest_title']
1213 title_source_ref = source_ref_obj.name
1213 title_source_ref = source_ref_obj.name
1214 if not pullrequest_title:
1214 if not pullrequest_title:
1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1216 source=source_repo,
1216 source=source_repo,
1217 source_ref=title_source_ref,
1217 source_ref=title_source_ref,
1218 target=target_repo
1218 target=target_repo
1219 )
1219 )
1220
1220
1221 description = _form['pullrequest_desc']
1221 description = _form['pullrequest_desc']
1222 description_renderer = _form['description_renderer']
1222 description_renderer = _form['description_renderer']
1223
1223
1224 try:
1224 try:
1225 pull_request = PullRequestModel().create(
1225 pull_request = PullRequestModel().create(
1226 created_by=self._rhodecode_user.user_id,
1226 created_by=self._rhodecode_user.user_id,
1227 source_repo=source_repo,
1227 source_repo=source_repo,
1228 source_ref=source_ref,
1228 source_ref=source_ref,
1229 target_repo=target_repo,
1229 target_repo=target_repo,
1230 target_ref=target_ref,
1230 target_ref=target_ref,
1231 revisions=commit_ids,
1231 revisions=commit_ids,
1232 common_ancestor_id=common_ancestor_id,
1232 common_ancestor_id=common_ancestor_id,
1233 reviewers=reviewers,
1233 reviewers=reviewers,
1234 observers=observers,
1234 observers=observers,
1235 title=pullrequest_title,
1235 title=pullrequest_title,
1236 description=description,
1236 description=description,
1237 description_renderer=description_renderer,
1237 description_renderer=description_renderer,
1238 reviewer_data=reviewer_rules,
1238 reviewer_data=reviewer_rules,
1239 auth_user=self._rhodecode_user
1239 auth_user=self._rhodecode_user
1240 )
1240 )
1241 Session().commit()
1241 Session().commit()
1242
1242
1243 h.flash(_('Successfully opened new pull request'),
1243 h.flash(_('Successfully opened new pull request'),
1244 category='success')
1244 category='success')
1245 except Exception:
1245 except Exception:
1246 msg = _('Error occurred during creation of this pull request.')
1246 msg = _('Error occurred during creation of this pull request.')
1247 log.exception(msg)
1247 log.exception(msg)
1248 h.flash(msg, category='error')
1248 h.flash(msg, category='error')
1249
1249
1250 # copy the args back to redirect
1250 # copy the args back to redirect
1251 org_query = self.request.GET.mixed()
1251 org_query = self.request.GET.mixed()
1252 raise HTTPFound(
1252 raise HTTPFound(
1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1254 _query=org_query))
1254 _query=org_query))
1255
1255
1256 raise HTTPFound(
1256 raise HTTPFound(
1257 h.route_path('pullrequest_show', repo_name=target_repo,
1257 h.route_path('pullrequest_show', repo_name=target_repo,
1258 pull_request_id=pull_request.pull_request_id))
1258 pull_request_id=pull_request.pull_request_id))
1259
1259
1260 @LoginRequired()
1260 @LoginRequired()
1261 @NotAnonymous()
1261 @NotAnonymous()
1262 @HasRepoPermissionAnyDecorator(
1262 @HasRepoPermissionAnyDecorator(
1263 'repository.read', 'repository.write', 'repository.admin')
1263 'repository.read', 'repository.write', 'repository.admin')
1264 @CSRFRequired()
1264 @CSRFRequired()
1265 def pull_request_update(self):
1265 def pull_request_update(self):
1266 pull_request = PullRequest.get_or_404(
1266 pull_request = PullRequest.get_or_404(
1267 self.request.matchdict['pull_request_id'])
1267 self.request.matchdict['pull_request_id'])
1268 _ = self.request.translate
1268 _ = self.request.translate
1269
1269
1270 c = self.load_default_context()
1270 c = self.load_default_context()
1271 redirect_url = None
1271 redirect_url = None
1272 # we do this check as first, because we want to know ASAP in the flow that
1272 # we do this check as first, because we want to know ASAP in the flow that
1273 # pr is updating currently
1273 # pr is updating currently
1274 is_state_changing = pull_request.is_state_changing()
1274 is_state_changing = pull_request.is_state_changing()
1275
1275
1276 if pull_request.is_closed():
1276 if pull_request.is_closed():
1277 log.debug('update: forbidden because pull request is closed')
1277 log.debug('update: forbidden because pull request is closed')
1278 msg = _(u'Cannot update closed pull requests.')
1278 msg = _(u'Cannot update closed pull requests.')
1279 h.flash(msg, category='error')
1279 h.flash(msg, category='error')
1280 return {'response': True,
1280 return {'response': True,
1281 'redirect_url': redirect_url}
1281 'redirect_url': redirect_url}
1282
1282
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1283 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1284
1284
1285 # only owner or admin can update it
1285 # only owner or admin can update it
1286 allowed_to_update = PullRequestModel().check_user_update(
1286 allowed_to_update = PullRequestModel().check_user_update(
1287 pull_request, self._rhodecode_user)
1287 pull_request, self._rhodecode_user)
1288
1288
1289 if allowed_to_update:
1289 if allowed_to_update:
1290 controls = peppercorn.parse(self.request.POST.items())
1290 controls = peppercorn.parse(self.request.POST.items())
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1291 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1292 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1293
1293
1294 if 'review_members' in controls:
1294 if 'review_members' in controls:
1295 self._update_reviewers(
1295 self._update_reviewers(
1296 c,
1296 c,
1297 pull_request, controls['review_members'],
1297 pull_request, controls['review_members'],
1298 pull_request.reviewer_data,
1298 pull_request.reviewer_data,
1299 PullRequestReviewers.ROLE_REVIEWER)
1299 PullRequestReviewers.ROLE_REVIEWER)
1300 elif 'observer_members' in controls:
1300 elif 'observer_members' in controls:
1301 self._update_reviewers(
1301 self._update_reviewers(
1302 c,
1302 c,
1303 pull_request, controls['observer_members'],
1303 pull_request, controls['observer_members'],
1304 pull_request.reviewer_data,
1304 pull_request.reviewer_data,
1305 PullRequestReviewers.ROLE_OBSERVER)
1305 PullRequestReviewers.ROLE_OBSERVER)
1306 elif do_update_commits:
1306 elif do_update_commits:
1307 if is_state_changing:
1307 if is_state_changing:
1308 log.debug('commits update: forbidden because pull request is in state %s',
1308 log.debug('commits update: forbidden because pull request is in state %s',
1309 pull_request.pull_request_state)
1309 pull_request.pull_request_state)
1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1310 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1311 u'Current state is: `{}`').format(
1311 u'Current state is: `{}`').format(
1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1312 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1313 h.flash(msg, category='error')
1313 h.flash(msg, category='error')
1314 return {'response': True,
1314 return {'response': True,
1315 'redirect_url': redirect_url}
1315 'redirect_url': redirect_url}
1316
1316
1317 self._update_commits(c, pull_request)
1317 self._update_commits(c, pull_request)
1318 if force_refresh:
1318 if force_refresh:
1319 redirect_url = h.route_path(
1319 redirect_url = h.route_path(
1320 'pullrequest_show', repo_name=self.db_repo_name,
1320 'pullrequest_show', repo_name=self.db_repo_name,
1321 pull_request_id=pull_request.pull_request_id,
1321 pull_request_id=pull_request.pull_request_id,
1322 _query={"force_refresh": 1})
1322 _query={"force_refresh": 1})
1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1323 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1324 self._edit_pull_request(pull_request)
1324 self._edit_pull_request(pull_request)
1325 else:
1325 else:
1326 log.error('Unhandled update data.')
1326 log.error('Unhandled update data.')
1327 raise HTTPBadRequest()
1327 raise HTTPBadRequest()
1328
1328
1329 return {'response': True,
1329 return {'response': True,
1330 'redirect_url': redirect_url}
1330 'redirect_url': redirect_url}
1331 raise HTTPForbidden()
1331 raise HTTPForbidden()
1332
1332
1333 def _edit_pull_request(self, pull_request):
1333 def _edit_pull_request(self, pull_request):
1334 """
1334 """
1335 Edit title and description
1335 Edit title and description
1336 """
1336 """
1337 _ = self.request.translate
1337 _ = self.request.translate
1338
1338
1339 try:
1339 try:
1340 PullRequestModel().edit(
1340 PullRequestModel().edit(
1341 pull_request,
1341 pull_request,
1342 self.request.POST.get('title'),
1342 self.request.POST.get('title'),
1343 self.request.POST.get('description'),
1343 self.request.POST.get('description'),
1344 self.request.POST.get('description_renderer'),
1344 self.request.POST.get('description_renderer'),
1345 self._rhodecode_user)
1345 self._rhodecode_user)
1346 except ValueError:
1346 except ValueError:
1347 msg = _(u'Cannot update closed pull requests.')
1347 msg = _(u'Cannot update closed pull requests.')
1348 h.flash(msg, category='error')
1348 h.flash(msg, category='error')
1349 return
1349 return
1350 else:
1350 else:
1351 Session().commit()
1351 Session().commit()
1352
1352
1353 msg = _(u'Pull request title & description updated.')
1353 msg = _(u'Pull request title & description updated.')
1354 h.flash(msg, category='success')
1354 h.flash(msg, category='success')
1355 return
1355 return
1356
1356
1357 def _update_commits(self, c, pull_request):
1357 def _update_commits(self, c, pull_request):
1358 _ = self.request.translate
1358 _ = self.request.translate
1359 log.debug('pull-request: running update commits actions')
1359 log.debug('pull-request: running update commits actions')
1360
1360
1361 @retry(exception=Exception, n_tries=3, delay=2)
1361 @retry(exception=Exception, n_tries=3, delay=2)
1362 def commits_update():
1362 def commits_update():
1363 return PullRequestModel().update_commits(
1363 return PullRequestModel().update_commits(
1364 pull_request, self._rhodecode_db_user)
1364 pull_request, self._rhodecode_db_user)
1365
1365
1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1366 with pull_request.set_state(PullRequest.STATE_UPDATING):
1367 resp = commits_update() # retry x3
1367 resp = commits_update() # retry x3
1368
1368
1369 if resp.executed:
1369 if resp.executed:
1370
1370
1371 if resp.target_changed and resp.source_changed:
1371 if resp.target_changed and resp.source_changed:
1372 changed = 'target and source repositories'
1372 changed = 'target and source repositories'
1373 elif resp.target_changed and not resp.source_changed:
1373 elif resp.target_changed and not resp.source_changed:
1374 changed = 'target repository'
1374 changed = 'target repository'
1375 elif not resp.target_changed and resp.source_changed:
1375 elif not resp.target_changed and resp.source_changed:
1376 changed = 'source repository'
1376 changed = 'source repository'
1377 else:
1377 else:
1378 changed = 'nothing'
1378 changed = 'nothing'
1379
1379
1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1380 msg = _(u'Pull request updated to "{source_commit_id}" with '
1381 u'{count_added} added, {count_removed} removed commits. '
1381 u'{count_added} added, {count_removed} removed commits. '
1382 u'Source of changes: {change_source}.')
1382 u'Source of changes: {change_source}.')
1383 msg = msg.format(
1383 msg = msg.format(
1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1384 source_commit_id=pull_request.source_ref_parts.commit_id,
1385 count_added=len(resp.changes.added),
1385 count_added=len(resp.changes.added),
1386 count_removed=len(resp.changes.removed),
1386 count_removed=len(resp.changes.removed),
1387 change_source=changed)
1387 change_source=changed)
1388 h.flash(msg, category='success')
1388 h.flash(msg, category='success')
1389 channelstream.pr_update_channelstream_push(
1389 channelstream.pr_update_channelstream_push(
1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1390 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1391 else:
1391 else:
1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1392 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1393 warning_reasons = [
1393 warning_reasons = [
1394 UpdateFailureReason.NO_CHANGE,
1394 UpdateFailureReason.NO_CHANGE,
1395 UpdateFailureReason.WRONG_REF_TYPE,
1395 UpdateFailureReason.WRONG_REF_TYPE,
1396 ]
1396 ]
1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1397 category = 'warning' if resp.reason in warning_reasons else 'error'
1398 h.flash(msg, category=category)
1398 h.flash(msg, category=category)
1399
1399
1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1400 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1401 _ = self.request.translate
1401 _ = self.request.translate
1402
1402
1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1403 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1404 PullRequestModel().get_reviewer_functions()
1404 PullRequestModel().get_reviewer_functions()
1405
1405
1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1406 if role == PullRequestReviewers.ROLE_REVIEWER:
1407 try:
1407 try:
1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1408 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1409 except ValueError as e:
1409 except ValueError as e:
1410 log.error('Reviewers Validation: {}'.format(e))
1410 log.error('Reviewers Validation: {}'.format(e))
1411 h.flash(e, category='error')
1411 h.flash(e, category='error')
1412 return
1412 return
1413
1413
1414 old_calculated_status = pull_request.calculated_review_status()
1414 old_calculated_status = pull_request.calculated_review_status()
1415 PullRequestModel().update_reviewers(
1415 PullRequestModel().update_reviewers(
1416 pull_request, reviewers, self._rhodecode_db_user)
1416 pull_request, reviewers, self._rhodecode_db_user)
1417
1417
1418 Session().commit()
1418 Session().commit()
1419
1419
1420 msg = _('Pull request reviewers updated.')
1420 msg = _('Pull request reviewers updated.')
1421 h.flash(msg, category='success')
1421 h.flash(msg, category='success')
1422 channelstream.pr_update_channelstream_push(
1422 channelstream.pr_update_channelstream_push(
1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1423 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1424
1424
1425 # trigger status changed if change in reviewers changes the status
1425 # trigger status changed if change in reviewers changes the status
1426 calculated_status = pull_request.calculated_review_status()
1426 calculated_status = pull_request.calculated_review_status()
1427 if old_calculated_status != calculated_status:
1427 if old_calculated_status != calculated_status:
1428 PullRequestModel().trigger_pull_request_hook(
1428 PullRequestModel().trigger_pull_request_hook(
1429 pull_request, self._rhodecode_user, 'review_status_change',
1429 pull_request, self._rhodecode_user, 'review_status_change',
1430 data={'status': calculated_status})
1430 data={'status': calculated_status})
1431
1431
1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1432 elif role == PullRequestReviewers.ROLE_OBSERVER:
1433 try:
1433 try:
1434 observers = validate_observers(review_members, reviewer_rules)
1434 observers = validate_observers(review_members, reviewer_rules)
1435 except ValueError as e:
1435 except ValueError as e:
1436 log.error('Observers Validation: {}'.format(e))
1436 log.error('Observers Validation: {}'.format(e))
1437 h.flash(e, category='error')
1437 h.flash(e, category='error')
1438 return
1438 return
1439
1439
1440 PullRequestModel().update_observers(
1440 PullRequestModel().update_observers(
1441 pull_request, observers, self._rhodecode_db_user)
1441 pull_request, observers, self._rhodecode_db_user)
1442
1442
1443 Session().commit()
1443 Session().commit()
1444 msg = _('Pull request observers updated.')
1444 msg = _('Pull request observers updated.')
1445 h.flash(msg, category='success')
1445 h.flash(msg, category='success')
1446 channelstream.pr_update_channelstream_push(
1446 channelstream.pr_update_channelstream_push(
1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1447 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1448
1448
1449 @LoginRequired()
1449 @LoginRequired()
1450 @NotAnonymous()
1450 @NotAnonymous()
1451 @HasRepoPermissionAnyDecorator(
1451 @HasRepoPermissionAnyDecorator(
1452 'repository.read', 'repository.write', 'repository.admin')
1452 'repository.read', 'repository.write', 'repository.admin')
1453 @CSRFRequired()
1453 @CSRFRequired()
1454 def pull_request_merge(self):
1454 def pull_request_merge(self):
1455 """
1455 """
1456 Merge will perform a server-side merge of the specified
1456 Merge will perform a server-side merge of the specified
1457 pull request, if the pull request is approved and mergeable.
1457 pull request, if the pull request is approved and mergeable.
1458 After successful merging, the pull request is automatically
1458 After successful merging, the pull request is automatically
1459 closed, with a relevant comment.
1459 closed, with a relevant comment.
1460 """
1460 """
1461 pull_request = PullRequest.get_or_404(
1461 pull_request = PullRequest.get_or_404(
1462 self.request.matchdict['pull_request_id'])
1462 self.request.matchdict['pull_request_id'])
1463 _ = self.request.translate
1463 _ = self.request.translate
1464
1464
1465 if pull_request.is_state_changing():
1465 if pull_request.is_state_changing():
1466 log.debug('show: forbidden because pull request is in state %s',
1466 log.debug('show: forbidden because pull request is in state %s',
1467 pull_request.pull_request_state)
1467 pull_request.pull_request_state)
1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1468 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1469 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1470 pull_request.pull_request_state)
1470 pull_request.pull_request_state)
1471 h.flash(msg, category='error')
1471 h.flash(msg, category='error')
1472 raise HTTPFound(
1472 raise HTTPFound(
1473 h.route_path('pullrequest_show',
1473 h.route_path('pullrequest_show',
1474 repo_name=pull_request.target_repo.repo_name,
1474 repo_name=pull_request.target_repo.repo_name,
1475 pull_request_id=pull_request.pull_request_id))
1475 pull_request_id=pull_request.pull_request_id))
1476
1476
1477 self.load_default_context()
1477 self.load_default_context()
1478
1478
1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 with pull_request.set_state(PullRequest.STATE_UPDATING):
1480 check = MergeCheck.validate(
1480 check = MergeCheck.validate(
1481 pull_request, auth_user=self._rhodecode_user,
1481 pull_request, auth_user=self._rhodecode_user,
1482 translator=self.request.translate)
1482 translator=self.request.translate)
1483 merge_possible = not check.failed
1483 merge_possible = not check.failed
1484
1484
1485 for err_type, error_msg in check.errors:
1485 for err_type, error_msg in check.errors:
1486 h.flash(error_msg, category=err_type)
1486 h.flash(error_msg, category=err_type)
1487
1487
1488 if merge_possible:
1488 if merge_possible:
1489 log.debug("Pre-conditions checked, trying to merge.")
1489 log.debug("Pre-conditions checked, trying to merge.")
1490 extras = vcs_operation_context(
1490 extras = vcs_operation_context(
1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1491 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1492 username=self._rhodecode_db_user.username, action='push',
1492 username=self._rhodecode_db_user.username, action='push',
1493 scm=pull_request.target_repo.repo_type)
1493 scm=pull_request.target_repo.repo_type)
1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1494 with pull_request.set_state(PullRequest.STATE_UPDATING):
1495 self._merge_pull_request(
1495 self._merge_pull_request(
1496 pull_request, self._rhodecode_db_user, extras)
1496 pull_request, self._rhodecode_db_user, extras)
1497 else:
1497 else:
1498 log.debug("Pre-conditions failed, NOT merging.")
1498 log.debug("Pre-conditions failed, NOT merging.")
1499
1499
1500 raise HTTPFound(
1500 raise HTTPFound(
1501 h.route_path('pullrequest_show',
1501 h.route_path('pullrequest_show',
1502 repo_name=pull_request.target_repo.repo_name,
1502 repo_name=pull_request.target_repo.repo_name,
1503 pull_request_id=pull_request.pull_request_id))
1503 pull_request_id=pull_request.pull_request_id))
1504
1504
1505 def _merge_pull_request(self, pull_request, user, extras):
1505 def _merge_pull_request(self, pull_request, user, extras):
1506 _ = self.request.translate
1506 _ = self.request.translate
1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1507 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1508
1508
1509 if merge_resp.executed:
1509 if merge_resp.executed:
1510 log.debug("The merge was successful, closing the pull request.")
1510 log.debug("The merge was successful, closing the pull request.")
1511 PullRequestModel().close_pull_request(
1511 PullRequestModel().close_pull_request(
1512 pull_request.pull_request_id, user)
1512 pull_request.pull_request_id, user)
1513 Session().commit()
1513 Session().commit()
1514 msg = _('Pull request was successfully merged and closed.')
1514 msg = _('Pull request was successfully merged and closed.')
1515 h.flash(msg, category='success')
1515 h.flash(msg, category='success')
1516 else:
1516 else:
1517 log.debug(
1517 log.debug(
1518 "The merge was not successful. Merge response: %s", merge_resp)
1518 "The merge was not successful. Merge response: %s", merge_resp)
1519 msg = merge_resp.merge_status_message
1519 msg = merge_resp.merge_status_message
1520 h.flash(msg, category='error')
1520 h.flash(msg, category='error')
1521
1521
1522 @LoginRequired()
1522 @LoginRequired()
1523 @NotAnonymous()
1523 @NotAnonymous()
1524 @HasRepoPermissionAnyDecorator(
1524 @HasRepoPermissionAnyDecorator(
1525 'repository.read', 'repository.write', 'repository.admin')
1525 'repository.read', 'repository.write', 'repository.admin')
1526 @CSRFRequired()
1526 @CSRFRequired()
1527 def pull_request_delete(self):
1527 def pull_request_delete(self):
1528 _ = self.request.translate
1528 _ = self.request.translate
1529
1529
1530 pull_request = PullRequest.get_or_404(
1530 pull_request = PullRequest.get_or_404(
1531 self.request.matchdict['pull_request_id'])
1531 self.request.matchdict['pull_request_id'])
1532 self.load_default_context()
1532 self.load_default_context()
1533
1533
1534 pr_closed = pull_request.is_closed()
1534 pr_closed = pull_request.is_closed()
1535 allowed_to_delete = PullRequestModel().check_user_delete(
1535 allowed_to_delete = PullRequestModel().check_user_delete(
1536 pull_request, self._rhodecode_user) and not pr_closed
1536 pull_request, self._rhodecode_user) and not pr_closed
1537
1537
1538 # only owner can delete it !
1538 # only owner can delete it !
1539 if allowed_to_delete:
1539 if allowed_to_delete:
1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1540 PullRequestModel().delete(pull_request, self._rhodecode_user)
1541 Session().commit()
1541 Session().commit()
1542 h.flash(_('Successfully deleted pull request'),
1542 h.flash(_('Successfully deleted pull request'),
1543 category='success')
1543 category='success')
1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1544 raise HTTPFound(h.route_path('pullrequest_show_all',
1545 repo_name=self.db_repo_name))
1545 repo_name=self.db_repo_name))
1546
1546
1547 log.warning('user %s tried to delete pull request without access',
1547 log.warning('user %s tried to delete pull request without access',
1548 self._rhodecode_user)
1548 self._rhodecode_user)
1549 raise HTTPNotFound()
1549 raise HTTPNotFound()
1550
1550
1551 def _pull_request_comments_create(self, pull_request, comments):
1551 def _pull_request_comments_create(self, pull_request, comments):
1552 _ = self.request.translate
1552 _ = self.request.translate
1553 data = {}
1553 data = {}
1554 if not comments:
1554 if not comments:
1555 return
1555 return
1556 pull_request_id = pull_request.pull_request_id
1556 pull_request_id = pull_request.pull_request_id
1557
1557
1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1558 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1559
1559
1560 for entry in comments:
1560 for entry in comments:
1561 c = self.load_default_context()
1561 c = self.load_default_context()
1562 comment_type = entry['comment_type']
1562 comment_type = entry['comment_type']
1563 text = entry['text']
1563 text = entry['text']
1564 status = entry['status']
1564 status = entry['status']
1565 is_draft = str2bool(entry['is_draft'])
1565 is_draft = str2bool(entry['is_draft'])
1566 resolves_comment_id = entry['resolves_comment_id']
1566 resolves_comment_id = entry['resolves_comment_id']
1567 close_pull_request = entry['close_pull_request']
1567 close_pull_request = entry['close_pull_request']
1568 f_path = entry['f_path']
1568 f_path = entry['f_path']
1569 line_no = entry['line']
1569 line_no = entry['line']
1570 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1570 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1571
1571
1572 # the logic here should work like following, if we submit close
1572 # the logic here should work like following, if we submit close
1573 # pr comment, use `close_pull_request_with_comment` function
1573 # pr comment, use `close_pull_request_with_comment` function
1574 # else handle regular comment logic
1574 # else handle regular comment logic
1575
1575
1576 if close_pull_request:
1576 if close_pull_request:
1577 # only owner or admin or person with write permissions
1577 # only owner or admin or person with write permissions
1578 allowed_to_close = PullRequestModel().check_user_update(
1578 allowed_to_close = PullRequestModel().check_user_update(
1579 pull_request, self._rhodecode_user)
1579 pull_request, self._rhodecode_user)
1580 if not allowed_to_close:
1580 if not allowed_to_close:
1581 log.debug('comment: forbidden because not allowed to close '
1581 log.debug('comment: forbidden because not allowed to close '
1582 'pull request %s', pull_request_id)
1582 'pull request %s', pull_request_id)
1583 raise HTTPForbidden()
1583 raise HTTPForbidden()
1584
1584
1585 # This also triggers `review_status_change`
1585 # This also triggers `review_status_change`
1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1586 comment, status = PullRequestModel().close_pull_request_with_comment(
1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1587 pull_request, self._rhodecode_user, self.db_repo, message=text,
1588 auth_user=self._rhodecode_user)
1588 auth_user=self._rhodecode_user)
1589 Session().flush()
1589 Session().flush()
1590 is_inline = comment.is_inline
1590 is_inline = comment.is_inline
1591
1591
1592 PullRequestModel().trigger_pull_request_hook(
1592 PullRequestModel().trigger_pull_request_hook(
1593 pull_request, self._rhodecode_user, 'comment',
1593 pull_request, self._rhodecode_user, 'comment',
1594 data={'comment': comment})
1594 data={'comment': comment})
1595
1595
1596 else:
1596 else:
1597 # regular comment case, could be inline, or one with status.
1597 # regular comment case, could be inline, or one with status.
1598 # for that one we check also permissions
1598 # for that one we check also permissions
1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1599 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1600 allowed_to_change_status = PullRequestModel().check_user_change_status(
1601 pull_request, self._rhodecode_user) and not is_draft
1601 pull_request, self._rhodecode_user) and not is_draft
1602
1602
1603 if status and allowed_to_change_status:
1603 if status and allowed_to_change_status:
1604 message = (_('Status change %(transition_icon)s %(status)s')
1604 message = (_('Status change %(transition_icon)s %(status)s')
1605 % {'transition_icon': '>',
1605 % {'transition_icon': '>',
1606 'status': ChangesetStatus.get_status_lbl(status)})
1606 'status': ChangesetStatus.get_status_lbl(status)})
1607 text = text or message
1607 text = text or message
1608
1608
1609 comment = CommentsModel().create(
1609 comment = CommentsModel().create(
1610 text=text,
1610 text=text,
1611 repo=self.db_repo.repo_id,
1611 repo=self.db_repo.repo_id,
1612 user=self._rhodecode_user.user_id,
1612 user=self._rhodecode_user.user_id,
1613 pull_request=pull_request,
1613 pull_request=pull_request,
1614 f_path=f_path,
1614 f_path=f_path,
1615 line_no=line_no,
1615 line_no=line_no,
1616 status_change=(ChangesetStatus.get_status_lbl(status)
1616 status_change=(ChangesetStatus.get_status_lbl(status)
1617 if status and allowed_to_change_status else None),
1617 if status and allowed_to_change_status else None),
1618 status_change_type=(status
1618 status_change_type=(status
1619 if status and allowed_to_change_status else None),
1619 if status and allowed_to_change_status else None),
1620 comment_type=comment_type,
1620 comment_type=comment_type,
1621 is_draft=is_draft,
1621 is_draft=is_draft,
1622 resolves_comment_id=resolves_comment_id,
1622 resolves_comment_id=resolves_comment_id,
1623 auth_user=self._rhodecode_user,
1623 auth_user=self._rhodecode_user,
1624 send_email=not is_draft, # skip notification for draft comments
1624 send_email=not is_draft, # skip notification for draft comments
1625 )
1625 )
1626 is_inline = comment.is_inline
1626 is_inline = comment.is_inline
1627
1627
1628 if allowed_to_change_status:
1628 if allowed_to_change_status:
1629 # calculate old status before we change it
1629 # calculate old status before we change it
1630 old_calculated_status = pull_request.calculated_review_status()
1630 old_calculated_status = pull_request.calculated_review_status()
1631
1631
1632 # get status if set !
1632 # get status if set !
1633 if status:
1633 if status:
1634 ChangesetStatusModel().set_status(
1634 ChangesetStatusModel().set_status(
1635 self.db_repo.repo_id,
1635 self.db_repo.repo_id,
1636 status,
1636 status,
1637 self._rhodecode_user.user_id,
1637 self._rhodecode_user.user_id,
1638 comment,
1638 comment,
1639 pull_request=pull_request
1639 pull_request=pull_request
1640 )
1640 )
1641
1641
1642 Session().flush()
1642 Session().flush()
1643 # this is somehow required to get access to some relationship
1643 # this is somehow required to get access to some relationship
1644 # loaded on comment
1644 # loaded on comment
1645 Session().refresh(comment)
1645 Session().refresh(comment)
1646
1646
1647 # skip notifications for drafts
1647 # skip notifications for drafts
1648 if not is_draft:
1648 if not is_draft:
1649 PullRequestModel().trigger_pull_request_hook(
1649 PullRequestModel().trigger_pull_request_hook(
1650 pull_request, self._rhodecode_user, 'comment',
1650 pull_request, self._rhodecode_user, 'comment',
1651 data={'comment': comment})
1651 data={'comment': comment})
1652
1652
1653 # we now calculate the status of pull request, and based on that
1653 # we now calculate the status of pull request, and based on that
1654 # calculation we set the commits status
1654 # calculation we set the commits status
1655 calculated_status = pull_request.calculated_review_status()
1655 calculated_status = pull_request.calculated_review_status()
1656 if old_calculated_status != calculated_status:
1656 if old_calculated_status != calculated_status:
1657 PullRequestModel().trigger_pull_request_hook(
1657 PullRequestModel().trigger_pull_request_hook(
1658 pull_request, self._rhodecode_user, 'review_status_change',
1658 pull_request, self._rhodecode_user, 'review_status_change',
1659 data={'status': calculated_status})
1659 data={'status': calculated_status})
1660
1660
1661 comment_id = comment.comment_id
1661 comment_id = comment.comment_id
1662 data[comment_id] = {
1662 data[comment_id] = {
1663 'target_id': target_elem_id
1663 'target_id': target_elem_id
1664 }
1664 }
1665 Session().flush()
1665 Session().flush()
1666
1666
1667 c.co = comment
1667 c.co = comment
1668 c.at_version_num = None
1668 c.at_version_num = None
1669 c.is_new = True
1669 c.is_new = True
1670 rendered_comment = render(
1670 rendered_comment = render(
1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1671 'rhodecode:templates/changeset/changeset_comment_block.mako',
1672 self._get_template_context(c), self.request)
1672 self._get_template_context(c), self.request)
1673
1673
1674 data[comment_id].update(comment.get_dict())
1674 data[comment_id].update(comment.get_dict())
1675 data[comment_id].update({'rendered_text': rendered_comment})
1675 data[comment_id].update({'rendered_text': rendered_comment})
1676
1676
1677 Session().commit()
1677 Session().commit()
1678
1678
1679 # skip channelstream for draft comments
1679 # skip channelstream for draft comments
1680 if not all_drafts:
1680 if not all_drafts:
1681 comment_broadcast_channel = channelstream.comment_channel(
1681 comment_broadcast_channel = channelstream.comment_channel(
1682 self.db_repo_name, pull_request_obj=pull_request)
1682 self.db_repo_name, pull_request_obj=pull_request)
1683
1683
1684 comment_data = data
1684 comment_data = data
1685 posted_comment_type = 'inline' if is_inline else 'general'
1685 posted_comment_type = 'inline' if is_inline else 'general'
1686 if len(data) == 1:
1686 if len(data) == 1:
1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1687 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1688 else:
1688 else:
1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1689 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1690
1690
1691 channelstream.comment_channelstream_push(
1691 channelstream.comment_channelstream_push(
1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1692 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1693 comment_data=comment_data)
1693 comment_data=comment_data)
1694
1694
1695 return data
1695 return data
1696
1696
1697 @LoginRequired()
1697 @LoginRequired()
1698 @NotAnonymous()
1698 @NotAnonymous()
1699 @HasRepoPermissionAnyDecorator(
1699 @HasRepoPermissionAnyDecorator(
1700 'repository.read', 'repository.write', 'repository.admin')
1700 'repository.read', 'repository.write', 'repository.admin')
1701 @CSRFRequired()
1701 @CSRFRequired()
1702 def pull_request_comment_create(self):
1702 def pull_request_comment_create(self):
1703 _ = self.request.translate
1703 _ = self.request.translate
1704
1704
1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1705 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1706
1706
1707 if pull_request.is_closed():
1707 if pull_request.is_closed():
1708 log.debug('comment: forbidden because pull request is closed')
1708 log.debug('comment: forbidden because pull request is closed')
1709 raise HTTPForbidden()
1709 raise HTTPForbidden()
1710
1710
1711 allowed_to_comment = PullRequestModel().check_user_comment(
1711 allowed_to_comment = PullRequestModel().check_user_comment(
1712 pull_request, self._rhodecode_user)
1712 pull_request, self._rhodecode_user)
1713 if not allowed_to_comment:
1713 if not allowed_to_comment:
1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1714 log.debug('comment: forbidden because pull request is from forbidden repo')
1715 raise HTTPForbidden()
1715 raise HTTPForbidden()
1716
1716
1717 comment_data = {
1717 comment_data = {
1718 'comment_type': self.request.POST.get('comment_type'),
1718 'comment_type': self.request.POST.get('comment_type'),
1719 'text': self.request.POST.get('text'),
1719 'text': self.request.POST.get('text'),
1720 'status': self.request.POST.get('changeset_status', None),
1720 'status': self.request.POST.get('changeset_status', None),
1721 'is_draft': self.request.POST.get('draft'),
1721 'is_draft': self.request.POST.get('draft'),
1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1722 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1723 'close_pull_request': self.request.POST.get('close_pull_request'),
1724 'f_path': self.request.POST.get('f_path'),
1724 'f_path': self.request.POST.get('f_path'),
1725 'line': self.request.POST.get('line'),
1725 'line': self.request.POST.get('line'),
1726 }
1726 }
1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1727 data = self._pull_request_comments_create(pull_request, [comment_data])
1728
1728
1729 return data
1729 return data
1730
1730
1731 @LoginRequired()
1731 @LoginRequired()
1732 @NotAnonymous()
1732 @NotAnonymous()
1733 @HasRepoPermissionAnyDecorator(
1733 @HasRepoPermissionAnyDecorator(
1734 'repository.read', 'repository.write', 'repository.admin')
1734 'repository.read', 'repository.write', 'repository.admin')
1735 @CSRFRequired()
1735 @CSRFRequired()
1736 def pull_request_comment_delete(self):
1736 def pull_request_comment_delete(self):
1737 pull_request = PullRequest.get_or_404(
1737 pull_request = PullRequest.get_or_404(
1738 self.request.matchdict['pull_request_id'])
1738 self.request.matchdict['pull_request_id'])
1739
1739
1740 comment = ChangesetComment.get_or_404(
1740 comment = ChangesetComment.get_or_404(
1741 self.request.matchdict['comment_id'])
1741 self.request.matchdict['comment_id'])
1742 comment_id = comment.comment_id
1742 comment_id = comment.comment_id
1743
1743
1744 if comment.immutable:
1744 if comment.immutable:
1745 # don't allow deleting comments that are immutable
1745 # don't allow deleting comments that are immutable
1746 raise HTTPForbidden()
1746 raise HTTPForbidden()
1747
1747
1748 if pull_request.is_closed():
1748 if pull_request.is_closed():
1749 log.debug('comment: forbidden because pull request is closed')
1749 log.debug('comment: forbidden because pull request is closed')
1750 raise HTTPForbidden()
1750 raise HTTPForbidden()
1751
1751
1752 if not comment:
1752 if not comment:
1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1753 log.debug('Comment with id:%s not found, skipping', comment_id)
1754 # comment already deleted in another call probably
1754 # comment already deleted in another call probably
1755 return True
1755 return True
1756
1756
1757 if comment.pull_request.is_closed():
1757 if comment.pull_request.is_closed():
1758 # don't allow deleting comments on closed pull request
1758 # don't allow deleting comments on closed pull request
1759 raise HTTPForbidden()
1759 raise HTTPForbidden()
1760
1760
1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1761 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1762 super_admin = h.HasPermissionAny('hg.admin')()
1762 super_admin = h.HasPermissionAny('hg.admin')()
1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1763 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1764 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1765 comment_repo_admin = is_repo_admin and is_repo_comment
1765 comment_repo_admin = is_repo_admin and is_repo_comment
1766
1766
1767 if comment.draft and not comment_owner:
1767 if comment.draft and not comment_owner:
1768 # We never allow to delete draft comments for other than owners
1768 # We never allow to delete draft comments for other than owners
1769 raise HTTPNotFound()
1769 raise HTTPNotFound()
1770
1770
1771 if super_admin or comment_owner or comment_repo_admin:
1771 if super_admin or comment_owner or comment_repo_admin:
1772 old_calculated_status = comment.pull_request.calculated_review_status()
1772 old_calculated_status = comment.pull_request.calculated_review_status()
1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1773 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1774 Session().commit()
1774 Session().commit()
1775 calculated_status = comment.pull_request.calculated_review_status()
1775 calculated_status = comment.pull_request.calculated_review_status()
1776 if old_calculated_status != calculated_status:
1776 if old_calculated_status != calculated_status:
1777 PullRequestModel().trigger_pull_request_hook(
1777 PullRequestModel().trigger_pull_request_hook(
1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1778 comment.pull_request, self._rhodecode_user, 'review_status_change',
1779 data={'status': calculated_status})
1779 data={'status': calculated_status})
1780 return True
1780 return True
1781 else:
1781 else:
1782 log.warning('No permissions for user %s to delete comment_id: %s',
1782 log.warning('No permissions for user %s to delete comment_id: %s',
1783 self._rhodecode_db_user, comment_id)
1783 self._rhodecode_db_user, comment_id)
1784 raise HTTPNotFound()
1784 raise HTTPNotFound()
1785
1785
1786 @LoginRequired()
1786 @LoginRequired()
1787 @NotAnonymous()
1787 @NotAnonymous()
1788 @HasRepoPermissionAnyDecorator(
1788 @HasRepoPermissionAnyDecorator(
1789 'repository.read', 'repository.write', 'repository.admin')
1789 'repository.read', 'repository.write', 'repository.admin')
1790 @CSRFRequired()
1790 @CSRFRequired()
1791 def pull_request_comment_edit(self):
1791 def pull_request_comment_edit(self):
1792 self.load_default_context()
1792 self.load_default_context()
1793
1793
1794 pull_request = PullRequest.get_or_404(
1794 pull_request = PullRequest.get_or_404(
1795 self.request.matchdict['pull_request_id']
1795 self.request.matchdict['pull_request_id']
1796 )
1796 )
1797 comment = ChangesetComment.get_or_404(
1797 comment = ChangesetComment.get_or_404(
1798 self.request.matchdict['comment_id']
1798 self.request.matchdict['comment_id']
1799 )
1799 )
1800 comment_id = comment.comment_id
1800 comment_id = comment.comment_id
1801
1801
1802 if comment.immutable:
1802 if comment.immutable:
1803 # don't allow deleting comments that are immutable
1803 # don't allow deleting comments that are immutable
1804 raise HTTPForbidden()
1804 raise HTTPForbidden()
1805
1805
1806 if pull_request.is_closed():
1806 if pull_request.is_closed():
1807 log.debug('comment: forbidden because pull request is closed')
1807 log.debug('comment: forbidden because pull request is closed')
1808 raise HTTPForbidden()
1808 raise HTTPForbidden()
1809
1809
1810 if comment.pull_request.is_closed():
1810 if comment.pull_request.is_closed():
1811 # don't allow deleting comments on closed pull request
1811 # don't allow deleting comments on closed pull request
1812 raise HTTPForbidden()
1812 raise HTTPForbidden()
1813
1813
1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1814 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1815 super_admin = h.HasPermissionAny('hg.admin')()
1815 super_admin = h.HasPermissionAny('hg.admin')()
1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1816 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1817 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1818 comment_repo_admin = is_repo_admin and is_repo_comment
1818 comment_repo_admin = is_repo_admin and is_repo_comment
1819
1819
1820 if super_admin or comment_owner or comment_repo_admin:
1820 if super_admin or comment_owner or comment_repo_admin:
1821 text = self.request.POST.get('text')
1821 text = self.request.POST.get('text')
1822 version = self.request.POST.get('version')
1822 version = self.request.POST.get('version')
1823 if text == comment.text:
1823 if text == comment.text:
1824 log.warning(
1824 log.warning(
1825 'Comment(PR): '
1825 'Comment(PR): '
1826 'Trying to create new version '
1826 'Trying to create new version '
1827 'with the same comment body {}'.format(
1827 'with the same comment body {}'.format(
1828 comment_id,
1828 comment_id,
1829 )
1829 )
1830 )
1830 )
1831 raise HTTPNotFound()
1831 raise HTTPNotFound()
1832
1832
1833 if version.isdigit():
1833 if version.isdigit():
1834 version = int(version)
1834 version = int(version)
1835 else:
1835 else:
1836 log.warning(
1836 log.warning(
1837 'Comment(PR): Wrong version type {} {} '
1837 'Comment(PR): Wrong version type {} {} '
1838 'for comment {}'.format(
1838 'for comment {}'.format(
1839 version,
1839 version,
1840 type(version),
1840 type(version),
1841 comment_id,
1841 comment_id,
1842 )
1842 )
1843 )
1843 )
1844 raise HTTPNotFound()
1844 raise HTTPNotFound()
1845
1845
1846 try:
1846 try:
1847 comment_history = CommentsModel().edit(
1847 comment_history = CommentsModel().edit(
1848 comment_id=comment_id,
1848 comment_id=comment_id,
1849 text=text,
1849 text=text,
1850 auth_user=self._rhodecode_user,
1850 auth_user=self._rhodecode_user,
1851 version=version,
1851 version=version,
1852 )
1852 )
1853 except CommentVersionMismatch:
1853 except CommentVersionMismatch:
1854 raise HTTPConflict()
1854 raise HTTPConflict()
1855
1855
1856 if not comment_history:
1856 if not comment_history:
1857 raise HTTPNotFound()
1857 raise HTTPNotFound()
1858
1858
1859 Session().commit()
1859 Session().commit()
1860 if not comment.draft:
1860 if not comment.draft:
1861 PullRequestModel().trigger_pull_request_hook(
1861 PullRequestModel().trigger_pull_request_hook(
1862 pull_request, self._rhodecode_user, 'comment_edit',
1862 pull_request, self._rhodecode_user, 'comment_edit',
1863 data={'comment': comment})
1863 data={'comment': comment})
1864
1864
1865 return {
1865 return {
1866 'comment_history_id': comment_history.comment_history_id,
1866 'comment_history_id': comment_history.comment_history_id,
1867 'comment_id': comment.comment_id,
1867 'comment_id': comment.comment_id,
1868 'comment_version': comment_history.version,
1868 'comment_version': comment_history.version,
1869 'comment_author_username': comment_history.author.username,
1869 'comment_author_username': comment_history.author.username,
1870 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1870 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1871 'comment_created_on': h.age_component(comment_history.created_on,
1871 'comment_created_on': h.age_component(comment_history.created_on,
1872 time_is_local=True),
1872 time_is_local=True),
1873 }
1873 }
1874 else:
1874 else:
1875 log.warning('No permissions for user %s to edit comment_id: %s',
1875 log.warning('No permissions for user %s to edit comment_id: %s',
1876 self._rhodecode_db_user, comment_id)
1876 self._rhodecode_db_user, comment_id)
1877 raise HTTPNotFound()
1877 raise HTTPNotFound()
@@ -1,44 +1,44 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 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 import logging
21 import logging
22
22
23 from rhodecode.apps._base import BaseReferencesView
23 from rhodecode.apps._base import BaseReferencesView
24 from rhodecode.lib.ext_json import json
24 from rhodecode.lib import ext_json
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26
26
27 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
28
28
29
29
30 class RepoTagsView(BaseReferencesView):
30 class RepoTagsView(BaseReferencesView):
31
31
32 @LoginRequired()
32 @LoginRequired()
33 @HasRepoPermissionAnyDecorator(
33 @HasRepoPermissionAnyDecorator(
34 'repository.read', 'repository.write', 'repository.admin')
34 'repository.read', 'repository.write', 'repository.admin')
35 def tags(self):
35 def tags(self):
36 c = self.load_default_context()
36 c = self.load_default_context()
37
37
38 ref_items = self.rhodecode_vcs_repo.tags.items()
38 ref_items = self.rhodecode_vcs_repo.tags.items()
39 data = self.load_refs_context(
39 data = self.load_refs_context(
40 ref_items=ref_items, partials_template='tags/tags_data.mako')
40 ref_items=ref_items, partials_template='tags/tags_data.mako')
41
41
42 c.has_references = bool(data)
42 c.has_references = bool(data)
43 c.data = json.dumps(data)
43 c.data = ext_json.str_json(data)
44 return self._get_template_context(c)
44 return self._get_template_context(c)
@@ -1,40 +1,41 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 from rhodecode.lib.ext_json import json
21 from rhodecode.lib import ext_json
22
22
23
23
24 def pyramid_ext_json(info):
24 def pyramid_ext_json(info):
25 """
25 """
26 Custom json renderer for pyramid to use our ext_json lib
26 Custom json renderer for pyramid to use our ext_json lib
27 """
27 """
28 def _render(value, system):
28 def _render(value, system):
29 request = system.get('request')
29 request = system.get('request')
30 indent = None
30 indent = None
31 if request is not None:
31 if request is not None:
32 response = request.response
32 response = request.response
33 ct = response.content_type
33 ct = response.content_type
34 if ct == response.default_content_type:
34 if ct == response.default_content_type:
35 response.content_type = 'application/json'
35 response.content_type = 'application/json'
36 indent = getattr(request, 'ext_json_indent', None)
36 indent = getattr(request, 'ext_json_indent', None)
37
37 if indent:
38 return json.dumps(value, indent=indent)
38 return ext_json.formatted_json(value)
39 return ext_json.json.dumps(value)
39
40
40 return _render
41 return _render
@@ -1,2155 +1,2165 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import base64
27 import base64
28 import collections
28 import collections
29
29
30 import os
30 import os
31 import random
31 import random
32 import hashlib
32 import hashlib
33 from io import StringIO
33 from io import StringIO
34 import textwrap
34 import textwrap
35 import urllib.request, urllib.parse, urllib.error
35 import urllib.request, urllib.parse, urllib.error
36 import math
36 import math
37 import logging
37 import logging
38 import re
38 import re
39 import time
39 import time
40 import string
40 import string
41 import hashlib
41 import hashlib
42 import regex
42 import regex
43 from collections import OrderedDict
43 from collections import OrderedDict
44
44
45 import pygments
45 import pygments
46 import itertools
46 import itertools
47 import fnmatch
47 import fnmatch
48 import bleach
48 import bleach
49
49
50 from datetime import datetime
50 from datetime import datetime
51 from functools import partial
51 from functools import partial
52 from pygments.formatters.html import HtmlFormatter
52 from pygments.formatters.html import HtmlFormatter
53 from pygments.lexers import (
53 from pygments.lexers import (
54 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
55
55
56 from pyramid.threadlocal import get_current_request
56 from pyramid.threadlocal import get_current_request
57 from tempita import looper
57 from tempita import looper
58 from webhelpers2.html import literal, HTML, escape
58 from webhelpers2.html import literal, HTML, escape
59 from webhelpers2.html._autolink import _auto_link_urls
59 from webhelpers2.html._autolink import _auto_link_urls
60 from webhelpers2.html.tools import (
60 from webhelpers2.html.tools import (
61 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61 button_to, highlight, js_obfuscate, strip_links, strip_tags)
62
62
63 from webhelpers2.text import (
63 from webhelpers2.text import (
64 chop_at, collapse, convert_accented_entities,
64 chop_at, collapse, convert_accented_entities,
65 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 convert_misc_entities, lchop, plural, rchop, remove_formatting,
66 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 replace_whitespace, urlify, truncate, wrap_paragraphs)
67 from webhelpers2.date import time_ago_in_words
67 from webhelpers2.date import time_ago_in_words
68
68
69 from webhelpers2.html.tags import (
69 from webhelpers2.html.tags import (
70 _input, NotGiven, _make_safe_id_component as safeid,
70 _input, NotGiven, _make_safe_id_component as safeid,
71 form as insecure_form,
71 form as insecure_form,
72 auto_discovery_link, checkbox, end_form, file,
72 auto_discovery_link, checkbox, end_form, file,
73 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
74 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 select as raw_select, stylesheet_link, submit, text, password, textarea,
75 ul, radio, Options)
75 ul, radio, Options)
76
76
77 from webhelpers2.number import format_byte_size
77 from webhelpers2.number import format_byte_size
78
78
79 from rhodecode.lib.action_parser import action_parser
79 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
81 from rhodecode.lib import ext_json
81 from rhodecode.lib.ext_json import json
82 from rhodecode.lib.ext_json import json
83 from rhodecode.lib.str_utils import safe_bytes
82 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
84 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
83 from rhodecode.lib.utils2 import (
85 from rhodecode.lib.utils2 import (
84 str2bool, safe_unicode, safe_str,
86 str2bool, safe_unicode, safe_str,
85 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
87 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
86 AttributeDict, safe_int, md5, md5_safe, get_host_info)
88 AttributeDict, safe_int, md5, md5_safe, get_host_info)
87 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
89 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
88 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
90 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
89 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
91 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
90 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
92 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
91 from rhodecode.lib.index.search_utils import get_matching_line_offsets
93 from rhodecode.lib.index.search_utils import get_matching_line_offsets
92 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
94 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
93 from rhodecode.model.changeset_status import ChangesetStatusModel
95 from rhodecode.model.changeset_status import ChangesetStatusModel
94 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
96 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
95 from rhodecode.model.repo_group import RepoGroupModel
97 from rhodecode.model.repo_group import RepoGroupModel
96 from rhodecode.model.settings import IssueTrackerSettingsModel
98 from rhodecode.model.settings import IssueTrackerSettingsModel
97
99
98
100
99 log = logging.getLogger(__name__)
101 log = logging.getLogger(__name__)
100
102
101
103
102 DEFAULT_USER = User.DEFAULT_USER
104 DEFAULT_USER = User.DEFAULT_USER
103 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
105 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
104
106
105
107
106 def asset(path, ver=None, **kwargs):
108 def asset(path, ver=None, **kwargs):
107 """
109 """
108 Helper to generate a static asset file path for rhodecode assets
110 Helper to generate a static asset file path for rhodecode assets
109
111
110 eg. h.asset('images/image.png', ver='3923')
112 eg. h.asset('images/image.png', ver='3923')
111
113
112 :param path: path of asset
114 :param path: path of asset
113 :param ver: optional version query param to append as ?ver=
115 :param ver: optional version query param to append as ?ver=
114 """
116 """
115 request = get_current_request()
117 request = get_current_request()
116 query = {}
118 query = {}
117 query.update(kwargs)
119 query.update(kwargs)
118 if ver:
120 if ver:
119 query = {'ver': ver}
121 query = {'ver': ver}
120 return request.static_path(
122 return request.static_path(
121 'rhodecode:public/{}'.format(path), _query=query)
123 'rhodecode:public/{}'.format(path), _query=query)
122
124
123
125
124 default_html_escape_table = {
126 default_html_escape_table = {
125 ord('&'): u'&amp;',
127 ord('&'): u'&amp;',
126 ord('<'): u'&lt;',
128 ord('<'): u'&lt;',
127 ord('>'): u'&gt;',
129 ord('>'): u'&gt;',
128 ord('"'): u'&quot;',
130 ord('"'): u'&quot;',
129 ord("'"): u'&#39;',
131 ord("'"): u'&#39;',
130 }
132 }
131
133
132
134
133 def html_escape(text, html_escape_table=default_html_escape_table):
135 def html_escape(text, html_escape_table=default_html_escape_table):
134 """Produce entities within text."""
136 """Produce entities within text."""
135 return text.translate(html_escape_table)
137 return text.translate(html_escape_table)
136
138
137
139
140 def str_json(*args, **kwargs):
141 return ext_json.str_json(*args, **kwargs)
142
143
144 def formatted_str_json(*args, **kwargs):
145 return ext_json.formatted_str_json(*args, **kwargs)
146
147
138 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
148 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
139 """
149 """
140 Truncate string ``s`` at the first occurrence of ``sub``.
150 Truncate string ``s`` at the first occurrence of ``sub``.
141
151
142 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
152 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
143 """
153 """
144 suffix_if_chopped = suffix_if_chopped or ''
154 suffix_if_chopped = suffix_if_chopped or ''
145 pos = s.find(sub)
155 pos = s.find(sub)
146 if pos == -1:
156 if pos == -1:
147 return s
157 return s
148
158
149 if inclusive:
159 if inclusive:
150 pos += len(sub)
160 pos += len(sub)
151
161
152 chopped = s[:pos]
162 chopped = s[:pos]
153 left = s[pos:].strip()
163 left = s[pos:].strip()
154
164
155 if left and suffix_if_chopped:
165 if left and suffix_if_chopped:
156 chopped += suffix_if_chopped
166 chopped += suffix_if_chopped
157
167
158 return chopped
168 return chopped
159
169
160
170
161 def shorter(text, size=20, prefix=False):
171 def shorter(text, size=20, prefix=False):
162 postfix = '...'
172 postfix = '...'
163 if len(text) > size:
173 if len(text) > size:
164 if prefix:
174 if prefix:
165 # shorten in front
175 # shorten in front
166 return postfix + text[-(size - len(postfix)):]
176 return postfix + text[-(size - len(postfix)):]
167 else:
177 else:
168 return text[:size - len(postfix)] + postfix
178 return text[:size - len(postfix)] + postfix
169 return text
179 return text
170
180
171
181
172 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
182 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
173 """
183 """
174 Reset button
184 Reset button
175 """
185 """
176 return _input(type, name, value, id, attrs)
186 return _input(type, name, value, id, attrs)
177
187
178
188
179 def select(name, selected_values, options, id=NotGiven, **attrs):
189 def select(name, selected_values, options, id=NotGiven, **attrs):
180
190
181 if isinstance(options, (list, tuple)):
191 if isinstance(options, (list, tuple)):
182 options_iter = options
192 options_iter = options
183 # Handle old value,label lists ... where value also can be value,label lists
193 # Handle old value,label lists ... where value also can be value,label lists
184 options = Options()
194 options = Options()
185 for opt in options_iter:
195 for opt in options_iter:
186 if isinstance(opt, tuple) and len(opt) == 2:
196 if isinstance(opt, tuple) and len(opt) == 2:
187 value, label = opt
197 value, label = opt
188 elif isinstance(opt, str):
198 elif isinstance(opt, str):
189 value = label = opt
199 value = label = opt
190 else:
200 else:
191 raise ValueError('invalid select option type %r' % type(opt))
201 raise ValueError('invalid select option type %r' % type(opt))
192
202
193 if isinstance(value, (list, tuple)):
203 if isinstance(value, (list, tuple)):
194 option_group = options.add_optgroup(label)
204 option_group = options.add_optgroup(label)
195 for opt2 in value:
205 for opt2 in value:
196 if isinstance(opt2, tuple) and len(opt2) == 2:
206 if isinstance(opt2, tuple) and len(opt2) == 2:
197 group_value, group_label = opt2
207 group_value, group_label = opt2
198 elif isinstance(opt2, str):
208 elif isinstance(opt2, str):
199 group_value = group_label = opt2
209 group_value = group_label = opt2
200 else:
210 else:
201 raise ValueError('invalid select option type %r' % type(opt2))
211 raise ValueError('invalid select option type %r' % type(opt2))
202
212
203 option_group.add_option(group_label, group_value)
213 option_group.add_option(group_label, group_value)
204 else:
214 else:
205 options.add_option(label, value)
215 options.add_option(label, value)
206
216
207 return raw_select(name, selected_values, options, id=id, **attrs)
217 return raw_select(name, selected_values, options, id=id, **attrs)
208
218
209
219
210 def branding(name, length=40):
220 def branding(name, length=40):
211 return truncate(name, length, indicator="")
221 return truncate(name, length, indicator="")
212
222
213
223
214 def FID(raw_id, path):
224 def FID(raw_id, path):
215 """
225 """
216 Creates a unique ID for filenode based on it's hash of path and commit
226 Creates a unique ID for filenode based on it's hash of path and commit
217 it's safe to use in urls
227 it's safe to use in urls
218
228
219 :param raw_id:
229 :param raw_id:
220 :param path:
230 :param path:
221 """
231 """
222
232
223 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
233 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
224
234
225
235
226 class _GetError(object):
236 class _GetError(object):
227 """Get error from form_errors, and represent it as span wrapped error
237 """Get error from form_errors, and represent it as span wrapped error
228 message
238 message
229
239
230 :param field_name: field to fetch errors for
240 :param field_name: field to fetch errors for
231 :param form_errors: form errors dict
241 :param form_errors: form errors dict
232 """
242 """
233
243
234 def __call__(self, field_name, form_errors):
244 def __call__(self, field_name, form_errors):
235 tmpl = """<span class="error_msg">%s</span>"""
245 tmpl = """<span class="error_msg">%s</span>"""
236 if form_errors and field_name in form_errors:
246 if form_errors and field_name in form_errors:
237 return literal(tmpl % form_errors.get(field_name))
247 return literal(tmpl % form_errors.get(field_name))
238
248
239
249
240 get_error = _GetError()
250 get_error = _GetError()
241
251
242
252
243 class _ToolTip(object):
253 class _ToolTip(object):
244
254
245 def __call__(self, tooltip_title, trim_at=50):
255 def __call__(self, tooltip_title, trim_at=50):
246 """
256 """
247 Special function just to wrap our text into nice formatted
257 Special function just to wrap our text into nice formatted
248 autowrapped text
258 autowrapped text
249
259
250 :param tooltip_title:
260 :param tooltip_title:
251 """
261 """
252 tooltip_title = escape(tooltip_title)
262 tooltip_title = escape(tooltip_title)
253 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
263 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
254 return tooltip_title
264 return tooltip_title
255
265
256
266
257 tooltip = _ToolTip()
267 tooltip = _ToolTip()
258
268
259 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
269 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
260
270
261
271
262 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
272 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
263 limit_items=False, linkify_last_item=False, hide_last_item=False,
273 limit_items=False, linkify_last_item=False, hide_last_item=False,
264 copy_path_icon=True):
274 copy_path_icon=True):
265 if isinstance(file_path, str):
275 if isinstance(file_path, str):
266 file_path = safe_unicode(file_path)
276 file_path = safe_unicode(file_path)
267
277
268 if at_ref:
278 if at_ref:
269 route_qry = {'at': at_ref}
279 route_qry = {'at': at_ref}
270 default_landing_ref = at_ref or landing_ref_name or commit_id
280 default_landing_ref = at_ref or landing_ref_name or commit_id
271 else:
281 else:
272 route_qry = None
282 route_qry = None
273 default_landing_ref = commit_id
283 default_landing_ref = commit_id
274
284
275 # first segment is a `HOME` link to repo files root location
285 # first segment is a `HOME` link to repo files root location
276 root_name = literal(u'<i class="icon-home"></i>')
286 root_name = literal(u'<i class="icon-home"></i>')
277
287
278 url_segments = [
288 url_segments = [
279 link_to(
289 link_to(
280 root_name,
290 root_name,
281 repo_files_by_ref_url(
291 repo_files_by_ref_url(
282 repo_name,
292 repo_name,
283 repo_type,
293 repo_type,
284 f_path=None, # None here is a special case for SVN repos,
294 f_path=None, # None here is a special case for SVN repos,
285 # that won't prefix with a ref
295 # that won't prefix with a ref
286 ref_name=default_landing_ref,
296 ref_name=default_landing_ref,
287 commit_id=commit_id,
297 commit_id=commit_id,
288 query=route_qry
298 query=route_qry
289 )
299 )
290 )]
300 )]
291
301
292 path_segments = file_path.split('/')
302 path_segments = file_path.split('/')
293 last_cnt = len(path_segments) - 1
303 last_cnt = len(path_segments) - 1
294 for cnt, segment in enumerate(path_segments):
304 for cnt, segment in enumerate(path_segments):
295 if not segment:
305 if not segment:
296 continue
306 continue
297 segment_html = escape(segment)
307 segment_html = escape(segment)
298
308
299 last_item = cnt == last_cnt
309 last_item = cnt == last_cnt
300
310
301 if last_item and hide_last_item:
311 if last_item and hide_last_item:
302 # iterate over and hide last element
312 # iterate over and hide last element
303 continue
313 continue
304
314
305 if last_item and linkify_last_item is False:
315 if last_item and linkify_last_item is False:
306 # plain version
316 # plain version
307 url_segments.append(segment_html)
317 url_segments.append(segment_html)
308 else:
318 else:
309 url_segments.append(
319 url_segments.append(
310 link_to(
320 link_to(
311 segment_html,
321 segment_html,
312 repo_files_by_ref_url(
322 repo_files_by_ref_url(
313 repo_name,
323 repo_name,
314 repo_type,
324 repo_type,
315 f_path='/'.join(path_segments[:cnt + 1]),
325 f_path='/'.join(path_segments[:cnt + 1]),
316 ref_name=default_landing_ref,
326 ref_name=default_landing_ref,
317 commit_id=commit_id,
327 commit_id=commit_id,
318 query=route_qry
328 query=route_qry
319 ),
329 ),
320 ))
330 ))
321
331
322 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
332 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
323 if limit_items and len(limited_url_segments) < len(url_segments):
333 if limit_items and len(limited_url_segments) < len(url_segments):
324 url_segments = limited_url_segments
334 url_segments = limited_url_segments
325
335
326 full_path = file_path
336 full_path = file_path
327 if copy_path_icon:
337 if copy_path_icon:
328 icon = files_icon.format(escape(full_path))
338 icon = files_icon.format(escape(full_path))
329 else:
339 else:
330 icon = ''
340 icon = ''
331
341
332 if file_path == '':
342 if file_path == '':
333 return root_name
343 return root_name
334 else:
344 else:
335 return literal(' / '.join(url_segments) + icon)
345 return literal(' / '.join(url_segments) + icon)
336
346
337
347
338 def files_url_data(request):
348 def files_url_data(request):
339 import urllib.request, urllib.parse, urllib.error
349 import urllib.request, urllib.parse, urllib.error
340 matchdict = request.matchdict
350 matchdict = request.matchdict
341
351
342 if 'f_path' not in matchdict:
352 if 'f_path' not in matchdict:
343 matchdict['f_path'] = ''
353 matchdict['f_path'] = ''
344 else:
354 else:
345 matchdict['f_path'] = urllib.parse.quote(safe_str(matchdict['f_path']))
355 matchdict['f_path'] = urllib.parse.quote(safe_str(matchdict['f_path']))
346 if 'commit_id' not in matchdict:
356 if 'commit_id' not in matchdict:
347 matchdict['commit_id'] = 'tip'
357 matchdict['commit_id'] = 'tip'
348
358
349 return json.dumps(matchdict)
359 return ext_json.str_json(matchdict)
350
360
351
361
352 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
362 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
353 _is_svn = is_svn(db_repo_type)
363 _is_svn = is_svn(db_repo_type)
354 final_f_path = f_path
364 final_f_path = f_path
355
365
356 if _is_svn:
366 if _is_svn:
357 """
367 """
358 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
368 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
359 actually commit_id followed by the ref_name. This should be done only in case
369 actually commit_id followed by the ref_name. This should be done only in case
360 This is a initial landing url, without additional paths.
370 This is a initial landing url, without additional paths.
361
371
362 like: /1000/tags/1.0.0/?at=tags/1.0.0
372 like: /1000/tags/1.0.0/?at=tags/1.0.0
363 """
373 """
364
374
365 if ref_name and ref_name != 'tip':
375 if ref_name and ref_name != 'tip':
366 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
376 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
367 # for SVN we only do this magic prefix if it's root, .eg landing revision
377 # for SVN we only do this magic prefix if it's root, .eg landing revision
368 # of files link. If we are in the tree we don't need this since we traverse the url
378 # of files link. If we are in the tree we don't need this since we traverse the url
369 # that has everything stored
379 # that has everything stored
370 if f_path in ['', '/']:
380 if f_path in ['', '/']:
371 final_f_path = '/'.join([ref_name, f_path])
381 final_f_path = '/'.join([ref_name, f_path])
372
382
373 # SVN always needs a commit_id explicitly, without a named REF
383 # SVN always needs a commit_id explicitly, without a named REF
374 default_commit_id = commit_id
384 default_commit_id = commit_id
375 else:
385 else:
376 """
386 """
377 For git and mercurial we construct a new URL using the names instead of commit_id
387 For git and mercurial we construct a new URL using the names instead of commit_id
378 like: /master/some_path?at=master
388 like: /master/some_path?at=master
379 """
389 """
380 # We currently do not support branches with slashes
390 # We currently do not support branches with slashes
381 if '/' in ref_name:
391 if '/' in ref_name:
382 default_commit_id = commit_id
392 default_commit_id = commit_id
383 else:
393 else:
384 default_commit_id = ref_name
394 default_commit_id = ref_name
385
395
386 # sometimes we pass f_path as None, to indicate explicit no prefix,
396 # sometimes we pass f_path as None, to indicate explicit no prefix,
387 # we translate it to string to not have None
397 # we translate it to string to not have None
388 final_f_path = final_f_path or ''
398 final_f_path = final_f_path or ''
389
399
390 files_url = route_path(
400 files_url = route_path(
391 'repo_files',
401 'repo_files',
392 repo_name=db_repo_name,
402 repo_name=db_repo_name,
393 commit_id=default_commit_id,
403 commit_id=default_commit_id,
394 f_path=final_f_path,
404 f_path=final_f_path,
395 _query=query
405 _query=query
396 )
406 )
397 return files_url
407 return files_url
398
408
399
409
400 def code_highlight(code, lexer, formatter, use_hl_filter=False):
410 def code_highlight(code, lexer, formatter, use_hl_filter=False):
401 """
411 """
402 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
412 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
403
413
404 If ``outfile`` is given and a valid file object (an object
414 If ``outfile`` is given and a valid file object (an object
405 with a ``write`` method), the result will be written to it, otherwise
415 with a ``write`` method), the result will be written to it, otherwise
406 it is returned as a string.
416 it is returned as a string.
407 """
417 """
408 if use_hl_filter:
418 if use_hl_filter:
409 # add HL filter
419 # add HL filter
410 from rhodecode.lib.index import search_utils
420 from rhodecode.lib.index import search_utils
411 lexer.add_filter(search_utils.ElasticSearchHLFilter())
421 lexer.add_filter(search_utils.ElasticSearchHLFilter())
412 return pygments.format(pygments.lex(code, lexer), formatter)
422 return pygments.format(pygments.lex(code, lexer), formatter)
413
423
414
424
415 class CodeHtmlFormatter(HtmlFormatter):
425 class CodeHtmlFormatter(HtmlFormatter):
416 """
426 """
417 My code Html Formatter for source codes
427 My code Html Formatter for source codes
418 """
428 """
419
429
420 def wrap(self, source, outfile):
430 def wrap(self, source, outfile):
421 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
431 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
422
432
423 def _wrap_code(self, source):
433 def _wrap_code(self, source):
424 for cnt, it in enumerate(source):
434 for cnt, it in enumerate(source):
425 i, t = it
435 i, t = it
426 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
436 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
427 yield i, t
437 yield i, t
428
438
429 def _wrap_tablelinenos(self, inner):
439 def _wrap_tablelinenos(self, inner):
430 dummyoutfile = StringIO.StringIO()
440 dummyoutfile = StringIO.StringIO()
431 lncount = 0
441 lncount = 0
432 for t, line in inner:
442 for t, line in inner:
433 if t:
443 if t:
434 lncount += 1
444 lncount += 1
435 dummyoutfile.write(line)
445 dummyoutfile.write(line)
436
446
437 fl = self.linenostart
447 fl = self.linenostart
438 mw = len(str(lncount + fl - 1))
448 mw = len(str(lncount + fl - 1))
439 sp = self.linenospecial
449 sp = self.linenospecial
440 st = self.linenostep
450 st = self.linenostep
441 la = self.lineanchors
451 la = self.lineanchors
442 aln = self.anchorlinenos
452 aln = self.anchorlinenos
443 nocls = self.noclasses
453 nocls = self.noclasses
444 if sp:
454 if sp:
445 lines = []
455 lines = []
446
456
447 for i in range(fl, fl + lncount):
457 for i in range(fl, fl + lncount):
448 if i % st == 0:
458 if i % st == 0:
449 if i % sp == 0:
459 if i % sp == 0:
450 if aln:
460 if aln:
451 lines.append('<a href="#%s%d" class="special">%*d</a>' %
461 lines.append('<a href="#%s%d" class="special">%*d</a>' %
452 (la, i, mw, i))
462 (la, i, mw, i))
453 else:
463 else:
454 lines.append('<span class="special">%*d</span>' % (mw, i))
464 lines.append('<span class="special">%*d</span>' % (mw, i))
455 else:
465 else:
456 if aln:
466 if aln:
457 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
467 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
458 else:
468 else:
459 lines.append('%*d' % (mw, i))
469 lines.append('%*d' % (mw, i))
460 else:
470 else:
461 lines.append('')
471 lines.append('')
462 ls = '\n'.join(lines)
472 ls = '\n'.join(lines)
463 else:
473 else:
464 lines = []
474 lines = []
465 for i in range(fl, fl + lncount):
475 for i in range(fl, fl + lncount):
466 if i % st == 0:
476 if i % st == 0:
467 if aln:
477 if aln:
468 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
478 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
469 else:
479 else:
470 lines.append('%*d' % (mw, i))
480 lines.append('%*d' % (mw, i))
471 else:
481 else:
472 lines.append('')
482 lines.append('')
473 ls = '\n'.join(lines)
483 ls = '\n'.join(lines)
474
484
475 # in case you wonder about the seemingly redundant <div> here: since the
485 # in case you wonder about the seemingly redundant <div> here: since the
476 # content in the other cell also is wrapped in a div, some browsers in
486 # content in the other cell also is wrapped in a div, some browsers in
477 # some configurations seem to mess up the formatting...
487 # some configurations seem to mess up the formatting...
478 if nocls:
488 if nocls:
479 yield 0, ('<table class="%stable">' % self.cssclass +
489 yield 0, ('<table class="%stable">' % self.cssclass +
480 '<tr><td><div class="linenodiv" '
490 '<tr><td><div class="linenodiv" '
481 'style="background-color: #f0f0f0; padding-right: 10px">'
491 'style="background-color: #f0f0f0; padding-right: 10px">'
482 '<pre style="line-height: 125%">' +
492 '<pre style="line-height: 125%">' +
483 ls + '</pre></div></td><td id="hlcode" class="code">')
493 ls + '</pre></div></td><td id="hlcode" class="code">')
484 else:
494 else:
485 yield 0, ('<table class="%stable">' % self.cssclass +
495 yield 0, ('<table class="%stable">' % self.cssclass +
486 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
496 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
487 ls + '</pre></div></td><td id="hlcode" class="code">')
497 ls + '</pre></div></td><td id="hlcode" class="code">')
488 yield 0, dummyoutfile.getvalue()
498 yield 0, dummyoutfile.getvalue()
489 yield 0, '</td></tr></table>'
499 yield 0, '</td></tr></table>'
490
500
491
501
492 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
502 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
493 def __init__(self, **kw):
503 def __init__(self, **kw):
494 # only show these line numbers if set
504 # only show these line numbers if set
495 self.only_lines = kw.pop('only_line_numbers', [])
505 self.only_lines = kw.pop('only_line_numbers', [])
496 self.query_terms = kw.pop('query_terms', [])
506 self.query_terms = kw.pop('query_terms', [])
497 self.max_lines = kw.pop('max_lines', 5)
507 self.max_lines = kw.pop('max_lines', 5)
498 self.line_context = kw.pop('line_context', 3)
508 self.line_context = kw.pop('line_context', 3)
499 self.url = kw.pop('url', None)
509 self.url = kw.pop('url', None)
500
510
501 super(CodeHtmlFormatter, self).__init__(**kw)
511 super(CodeHtmlFormatter, self).__init__(**kw)
502
512
503 def _wrap_code(self, source):
513 def _wrap_code(self, source):
504 for cnt, it in enumerate(source):
514 for cnt, it in enumerate(source):
505 i, t = it
515 i, t = it
506 t = '<pre>%s</pre>' % t
516 t = '<pre>%s</pre>' % t
507 yield i, t
517 yield i, t
508
518
509 def _wrap_tablelinenos(self, inner):
519 def _wrap_tablelinenos(self, inner):
510 yield 0, '<table class="code-highlight %stable">' % self.cssclass
520 yield 0, '<table class="code-highlight %stable">' % self.cssclass
511
521
512 last_shown_line_number = 0
522 last_shown_line_number = 0
513 current_line_number = 1
523 current_line_number = 1
514
524
515 for t, line in inner:
525 for t, line in inner:
516 if not t:
526 if not t:
517 yield t, line
527 yield t, line
518 continue
528 continue
519
529
520 if current_line_number in self.only_lines:
530 if current_line_number in self.only_lines:
521 if last_shown_line_number + 1 != current_line_number:
531 if last_shown_line_number + 1 != current_line_number:
522 yield 0, '<tr>'
532 yield 0, '<tr>'
523 yield 0, '<td class="line">...</td>'
533 yield 0, '<td class="line">...</td>'
524 yield 0, '<td id="hlcode" class="code"></td>'
534 yield 0, '<td id="hlcode" class="code"></td>'
525 yield 0, '</tr>'
535 yield 0, '</tr>'
526
536
527 yield 0, '<tr>'
537 yield 0, '<tr>'
528 if self.url:
538 if self.url:
529 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
539 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
530 self.url, current_line_number, current_line_number)
540 self.url, current_line_number, current_line_number)
531 else:
541 else:
532 yield 0, '<td class="line"><a href="">%i</a></td>' % (
542 yield 0, '<td class="line"><a href="">%i</a></td>' % (
533 current_line_number)
543 current_line_number)
534 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
544 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
535 yield 0, '</tr>'
545 yield 0, '</tr>'
536
546
537 last_shown_line_number = current_line_number
547 last_shown_line_number = current_line_number
538
548
539 current_line_number += 1
549 current_line_number += 1
540
550
541 yield 0, '</table>'
551 yield 0, '</table>'
542
552
543
553
544 def hsv_to_rgb(h, s, v):
554 def hsv_to_rgb(h, s, v):
545 """ Convert hsv color values to rgb """
555 """ Convert hsv color values to rgb """
546
556
547 if s == 0.0:
557 if s == 0.0:
548 return v, v, v
558 return v, v, v
549 i = int(h * 6.0) # XXX assume int() truncates!
559 i = int(h * 6.0) # XXX assume int() truncates!
550 f = (h * 6.0) - i
560 f = (h * 6.0) - i
551 p = v * (1.0 - s)
561 p = v * (1.0 - s)
552 q = v * (1.0 - s * f)
562 q = v * (1.0 - s * f)
553 t = v * (1.0 - s * (1.0 - f))
563 t = v * (1.0 - s * (1.0 - f))
554 i = i % 6
564 i = i % 6
555 if i == 0:
565 if i == 0:
556 return v, t, p
566 return v, t, p
557 if i == 1:
567 if i == 1:
558 return q, v, p
568 return q, v, p
559 if i == 2:
569 if i == 2:
560 return p, v, t
570 return p, v, t
561 if i == 3:
571 if i == 3:
562 return p, q, v
572 return p, q, v
563 if i == 4:
573 if i == 4:
564 return t, p, v
574 return t, p, v
565 if i == 5:
575 if i == 5:
566 return v, p, q
576 return v, p, q
567
577
568
578
569 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
579 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
570 """
580 """
571 Generator for getting n of evenly distributed colors using
581 Generator for getting n of evenly distributed colors using
572 hsv color and golden ratio. It always return same order of colors
582 hsv color and golden ratio. It always return same order of colors
573
583
574 :param n: number of colors to generate
584 :param n: number of colors to generate
575 :param saturation: saturation of returned colors
585 :param saturation: saturation of returned colors
576 :param lightness: lightness of returned colors
586 :param lightness: lightness of returned colors
577 :returns: RGB tuple
587 :returns: RGB tuple
578 """
588 """
579
589
580 golden_ratio = 0.618033988749895
590 golden_ratio = 0.618033988749895
581 h = 0.22717784590367374
591 h = 0.22717784590367374
582
592
583 for _ in range(n):
593 for _ in range(n):
584 h += golden_ratio
594 h += golden_ratio
585 h %= 1
595 h %= 1
586 HSV_tuple = [h, saturation, lightness]
596 HSV_tuple = [h, saturation, lightness]
587 RGB_tuple = hsv_to_rgb(*HSV_tuple)
597 RGB_tuple = hsv_to_rgb(*HSV_tuple)
588 yield map(lambda x: str(int(x * 256)), RGB_tuple)
598 yield map(lambda x: str(int(x * 256)), RGB_tuple)
589
599
590
600
591 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
601 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
592 """
602 """
593 Returns a function which when called with an argument returns a unique
603 Returns a function which when called with an argument returns a unique
594 color for that argument, eg.
604 color for that argument, eg.
595
605
596 :param n: number of colors to generate
606 :param n: number of colors to generate
597 :param saturation: saturation of returned colors
607 :param saturation: saturation of returned colors
598 :param lightness: lightness of returned colors
608 :param lightness: lightness of returned colors
599 :returns: css RGB string
609 :returns: css RGB string
600
610
601 >>> color_hash = color_hasher()
611 >>> color_hash = color_hasher()
602 >>> color_hash('hello')
612 >>> color_hash('hello')
603 'rgb(34, 12, 59)'
613 'rgb(34, 12, 59)'
604 >>> color_hash('hello')
614 >>> color_hash('hello')
605 'rgb(34, 12, 59)'
615 'rgb(34, 12, 59)'
606 >>> color_hash('other')
616 >>> color_hash('other')
607 'rgb(90, 224, 159)'
617 'rgb(90, 224, 159)'
608 """
618 """
609
619
610 color_dict = {}
620 color_dict = {}
611 cgenerator = unique_color_generator(
621 cgenerator = unique_color_generator(
612 saturation=saturation, lightness=lightness)
622 saturation=saturation, lightness=lightness)
613
623
614 def get_color_string(thing):
624 def get_color_string(thing):
615 if thing in color_dict:
625 if thing in color_dict:
616 col = color_dict[thing]
626 col = color_dict[thing]
617 else:
627 else:
618 col = color_dict[thing] = next(cgenerator)
628 col = color_dict[thing] = next(cgenerator)
619 return "rgb(%s)" % (', '.join(col))
629 return "rgb(%s)" % (', '.join(col))
620
630
621 return get_color_string
631 return get_color_string
622
632
623
633
624 def get_lexer_safe(mimetype=None, filepath=None):
634 def get_lexer_safe(mimetype=None, filepath=None):
625 """
635 """
626 Tries to return a relevant pygments lexer using mimetype/filepath name,
636 Tries to return a relevant pygments lexer using mimetype/filepath name,
627 defaulting to plain text if none could be found
637 defaulting to plain text if none could be found
628 """
638 """
629 lexer = None
639 lexer = None
630 try:
640 try:
631 if mimetype:
641 if mimetype:
632 lexer = get_lexer_for_mimetype(mimetype)
642 lexer = get_lexer_for_mimetype(mimetype)
633 if not lexer:
643 if not lexer:
634 lexer = get_lexer_for_filename(filepath)
644 lexer = get_lexer_for_filename(filepath)
635 except pygments.util.ClassNotFound:
645 except pygments.util.ClassNotFound:
636 pass
646 pass
637
647
638 if not lexer:
648 if not lexer:
639 lexer = get_lexer_by_name('text')
649 lexer = get_lexer_by_name('text')
640
650
641 return lexer
651 return lexer
642
652
643
653
644 def get_lexer_for_filenode(filenode):
654 def get_lexer_for_filenode(filenode):
645 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
655 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
646 return lexer
656 return lexer
647
657
648
658
649 def pygmentize(filenode, **kwargs):
659 def pygmentize(filenode, **kwargs):
650 """
660 """
651 pygmentize function using pygments
661 pygmentize function using pygments
652
662
653 :param filenode:
663 :param filenode:
654 """
664 """
655 lexer = get_lexer_for_filenode(filenode)
665 lexer = get_lexer_for_filenode(filenode)
656 return literal(code_highlight(filenode.content, lexer,
666 return literal(code_highlight(filenode.content, lexer,
657 CodeHtmlFormatter(**kwargs)))
667 CodeHtmlFormatter(**kwargs)))
658
668
659
669
660 def is_following_repo(repo_name, user_id):
670 def is_following_repo(repo_name, user_id):
661 from rhodecode.model.scm import ScmModel
671 from rhodecode.model.scm import ScmModel
662 return ScmModel().is_following_repo(repo_name, user_id)
672 return ScmModel().is_following_repo(repo_name, user_id)
663
673
664
674
665 class _Message(object):
675 class _Message(object):
666 """A message returned by ``Flash.pop_messages()``.
676 """A message returned by ``Flash.pop_messages()``.
667
677
668 Converting the message to a string returns the message text. Instances
678 Converting the message to a string returns the message text. Instances
669 also have the following attributes:
679 also have the following attributes:
670
680
671 * ``message``: the message text.
681 * ``message``: the message text.
672 * ``category``: the category specified when the message was created.
682 * ``category``: the category specified when the message was created.
673 """
683 """
674
684
675 def __init__(self, category, message, sub_data=None):
685 def __init__(self, category, message, sub_data=None):
676 self.category = category
686 self.category = category
677 self.message = message
687 self.message = message
678 self.sub_data = sub_data or {}
688 self.sub_data = sub_data or {}
679
689
680 def __str__(self):
690 def __str__(self):
681 return self.message
691 return self.message
682
692
683 __unicode__ = __str__
693 __unicode__ = __str__
684
694
685 def __html__(self):
695 def __html__(self):
686 return escape(safe_unicode(self.message))
696 return escape(safe_unicode(self.message))
687
697
688
698
689 class Flash(object):
699 class Flash(object):
690 # List of allowed categories. If None, allow any category.
700 # List of allowed categories. If None, allow any category.
691 categories = ["warning", "notice", "error", "success"]
701 categories = ["warning", "notice", "error", "success"]
692
702
693 # Default category if none is specified.
703 # Default category if none is specified.
694 default_category = "notice"
704 default_category = "notice"
695
705
696 def __init__(self, session_key="flash", categories=None,
706 def __init__(self, session_key="flash", categories=None,
697 default_category=None):
707 default_category=None):
698 """
708 """
699 Instantiate a ``Flash`` object.
709 Instantiate a ``Flash`` object.
700
710
701 ``session_key`` is the key to save the messages under in the user's
711 ``session_key`` is the key to save the messages under in the user's
702 session.
712 session.
703
713
704 ``categories`` is an optional list which overrides the default list
714 ``categories`` is an optional list which overrides the default list
705 of categories.
715 of categories.
706
716
707 ``default_category`` overrides the default category used for messages
717 ``default_category`` overrides the default category used for messages
708 when none is specified.
718 when none is specified.
709 """
719 """
710 self.session_key = session_key
720 self.session_key = session_key
711 if categories is not None:
721 if categories is not None:
712 self.categories = categories
722 self.categories = categories
713 if default_category is not None:
723 if default_category is not None:
714 self.default_category = default_category
724 self.default_category = default_category
715 if self.categories and self.default_category not in self.categories:
725 if self.categories and self.default_category not in self.categories:
716 raise ValueError(
726 raise ValueError(
717 "unrecognized default category %r" % (self.default_category,))
727 "unrecognized default category %r" % (self.default_category,))
718
728
719 def pop_messages(self, session=None, request=None):
729 def pop_messages(self, session=None, request=None):
720 """
730 """
721 Return all accumulated messages and delete them from the session.
731 Return all accumulated messages and delete them from the session.
722
732
723 The return value is a list of ``Message`` objects.
733 The return value is a list of ``Message`` objects.
724 """
734 """
725 messages = []
735 messages = []
726
736
727 if not session:
737 if not session:
728 if not request:
738 if not request:
729 request = get_current_request()
739 request = get_current_request()
730 session = request.session
740 session = request.session
731
741
732 # Pop the 'old' pylons flash messages. They are tuples of the form
742 # Pop the 'old' pylons flash messages. They are tuples of the form
733 # (category, message)
743 # (category, message)
734 for cat, msg in session.pop(self.session_key, []):
744 for cat, msg in session.pop(self.session_key, []):
735 messages.append(_Message(cat, msg))
745 messages.append(_Message(cat, msg))
736
746
737 # Pop the 'new' pyramid flash messages for each category as list
747 # Pop the 'new' pyramid flash messages for each category as list
738 # of strings.
748 # of strings.
739 for cat in self.categories:
749 for cat in self.categories:
740 for msg in session.pop_flash(queue=cat):
750 for msg in session.pop_flash(queue=cat):
741 sub_data = {}
751 sub_data = {}
742 if hasattr(msg, 'rsplit'):
752 if hasattr(msg, 'rsplit'):
743 flash_data = msg.rsplit('|DELIM|', 1)
753 flash_data = msg.rsplit('|DELIM|', 1)
744 org_message = flash_data[0]
754 org_message = flash_data[0]
745 if len(flash_data) > 1:
755 if len(flash_data) > 1:
746 sub_data = json.loads(flash_data[1])
756 sub_data = json.loads(flash_data[1])
747 else:
757 else:
748 org_message = msg
758 org_message = msg
749
759
750 messages.append(_Message(cat, org_message, sub_data=sub_data))
760 messages.append(_Message(cat, org_message, sub_data=sub_data))
751
761
752 # Map messages from the default queue to the 'notice' category.
762 # Map messages from the default queue to the 'notice' category.
753 for msg in session.pop_flash():
763 for msg in session.pop_flash():
754 messages.append(_Message('notice', msg))
764 messages.append(_Message('notice', msg))
755
765
756 session.save()
766 session.save()
757 return messages
767 return messages
758
768
759 def json_alerts(self, session=None, request=None):
769 def json_alerts(self, session=None, request=None):
760 payloads = []
770 payloads = []
761 messages = flash.pop_messages(session=session, request=request) or []
771 messages = flash.pop_messages(session=session, request=request) or []
762 for message in messages:
772 for message in messages:
763 payloads.append({
773 payloads.append({
764 'message': {
774 'message': {
765 'message': u'{}'.format(message.message),
775 'message': u'{}'.format(message.message),
766 'level': message.category,
776 'level': message.category,
767 'force': True,
777 'force': True,
768 'subdata': message.sub_data
778 'subdata': message.sub_data
769 }
779 }
770 })
780 })
771 return json.dumps(payloads)
781 return safe_str(json.dumps(payloads))
772
782
773 def __call__(self, message, category=None, ignore_duplicate=True,
783 def __call__(self, message, category=None, ignore_duplicate=True,
774 session=None, request=None):
784 session=None, request=None):
775
785
776 if not session:
786 if not session:
777 if not request:
787 if not request:
778 request = get_current_request()
788 request = get_current_request()
779 session = request.session
789 session = request.session
780
790
781 session.flash(
791 session.flash(
782 message, queue=category, allow_duplicate=not ignore_duplicate)
792 message, queue=category, allow_duplicate=not ignore_duplicate)
783
793
784
794
785 flash = Flash()
795 flash = Flash()
786
796
787 #==============================================================================
797 #==============================================================================
788 # SCM FILTERS available via h.
798 # SCM FILTERS available via h.
789 #==============================================================================
799 #==============================================================================
790 from rhodecode.lib.vcs.utils import author_name, author_email
800 from rhodecode.lib.vcs.utils import author_name, author_email
791 from rhodecode.lib.utils2 import age, age_from_seconds
801 from rhodecode.lib.utils2 import age, age_from_seconds
792 from rhodecode.model.db import User, ChangesetStatus
802 from rhodecode.model.db import User, ChangesetStatus
793
803
794
804
795 email = author_email
805 email = author_email
796
806
797
807
798 def capitalize(raw_text):
808 def capitalize(raw_text):
799 return raw_text.capitalize()
809 return raw_text.capitalize()
800
810
801
811
802 def short_id(long_id):
812 def short_id(long_id):
803 return long_id[:12]
813 return long_id[:12]
804
814
805
815
806 def hide_credentials(url):
816 def hide_credentials(url):
807 from rhodecode.lib.utils2 import credentials_filter
817 from rhodecode.lib.utils2 import credentials_filter
808 return credentials_filter(url)
818 return credentials_filter(url)
809
819
810
820
811 import pytz
821 import pytz
812 import tzlocal
822 import tzlocal
813 local_timezone = tzlocal.get_localzone()
823 local_timezone = tzlocal.get_localzone()
814
824
815
825
816 def get_timezone(datetime_iso, time_is_local=False):
826 def get_timezone(datetime_iso, time_is_local=False):
817 tzinfo = '+00:00'
827 tzinfo = '+00:00'
818
828
819 # detect if we have a timezone info, otherwise, add it
829 # detect if we have a timezone info, otherwise, add it
820 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
830 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
821 force_timezone = os.environ.get('RC_TIMEZONE', '')
831 force_timezone = os.environ.get('RC_TIMEZONE', '')
822 if force_timezone:
832 if force_timezone:
823 force_timezone = pytz.timezone(force_timezone)
833 force_timezone = pytz.timezone(force_timezone)
824 timezone = force_timezone or local_timezone
834 timezone = force_timezone or local_timezone
825 offset = timezone.localize(datetime_iso).strftime('%z')
835 offset = timezone.localize(datetime_iso).strftime('%z')
826 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
836 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
827 return tzinfo
837 return tzinfo
828
838
829
839
830 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
840 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
831 title = value or format_date(datetime_iso)
841 title = value or format_date(datetime_iso)
832 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
842 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
833
843
834 return literal(
844 return literal(
835 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
845 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
836 cls='tooltip' if tooltip else '',
846 cls='tooltip' if tooltip else '',
837 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
847 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
838 title=title, dt=datetime_iso, tzinfo=tzinfo
848 title=title, dt=datetime_iso, tzinfo=tzinfo
839 ))
849 ))
840
850
841
851
842 def _shorten_commit_id(commit_id, commit_len=None):
852 def _shorten_commit_id(commit_id, commit_len=None):
843 if commit_len is None:
853 if commit_len is None:
844 request = get_current_request()
854 request = get_current_request()
845 commit_len = request.call_context.visual.show_sha_length
855 commit_len = request.call_context.visual.show_sha_length
846 return commit_id[:commit_len]
856 return commit_id[:commit_len]
847
857
848
858
849 def show_id(commit, show_idx=None, commit_len=None):
859 def show_id(commit, show_idx=None, commit_len=None):
850 """
860 """
851 Configurable function that shows ID
861 Configurable function that shows ID
852 by default it's r123:fffeeefffeee
862 by default it's r123:fffeeefffeee
853
863
854 :param commit: commit instance
864 :param commit: commit instance
855 """
865 """
856 if show_idx is None:
866 if show_idx is None:
857 request = get_current_request()
867 request = get_current_request()
858 show_idx = request.call_context.visual.show_revision_number
868 show_idx = request.call_context.visual.show_revision_number
859
869
860 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
870 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
861 if show_idx:
871 if show_idx:
862 return 'r%s:%s' % (commit.idx, raw_id)
872 return 'r%s:%s' % (commit.idx, raw_id)
863 else:
873 else:
864 return '%s' % (raw_id, )
874 return '%s' % (raw_id, )
865
875
866
876
867 def format_date(date):
877 def format_date(date):
868 """
878 """
869 use a standardized formatting for dates used in RhodeCode
879 use a standardized formatting for dates used in RhodeCode
870
880
871 :param date: date/datetime object
881 :param date: date/datetime object
872 :return: formatted date
882 :return: formatted date
873 """
883 """
874
884
875 if date:
885 if date:
876 _fmt = "%a, %d %b %Y %H:%M:%S"
886 _fmt = "%a, %d %b %Y %H:%M:%S"
877 return safe_unicode(date.strftime(_fmt))
887 return safe_unicode(date.strftime(_fmt))
878
888
879 return u""
889 return u""
880
890
881
891
882 class _RepoChecker(object):
892 class _RepoChecker(object):
883
893
884 def __init__(self, backend_alias):
894 def __init__(self, backend_alias):
885 self._backend_alias = backend_alias
895 self._backend_alias = backend_alias
886
896
887 def __call__(self, repository):
897 def __call__(self, repository):
888 if hasattr(repository, 'alias'):
898 if hasattr(repository, 'alias'):
889 _type = repository.alias
899 _type = repository.alias
890 elif hasattr(repository, 'repo_type'):
900 elif hasattr(repository, 'repo_type'):
891 _type = repository.repo_type
901 _type = repository.repo_type
892 else:
902 else:
893 _type = repository
903 _type = repository
894 return _type == self._backend_alias
904 return _type == self._backend_alias
895
905
896
906
897 is_git = _RepoChecker('git')
907 is_git = _RepoChecker('git')
898 is_hg = _RepoChecker('hg')
908 is_hg = _RepoChecker('hg')
899 is_svn = _RepoChecker('svn')
909 is_svn = _RepoChecker('svn')
900
910
901
911
902 def get_repo_type_by_name(repo_name):
912 def get_repo_type_by_name(repo_name):
903 repo = Repository.get_by_repo_name(repo_name)
913 repo = Repository.get_by_repo_name(repo_name)
904 if repo:
914 if repo:
905 return repo.repo_type
915 return repo.repo_type
906
916
907
917
908 def is_svn_without_proxy(repository):
918 def is_svn_without_proxy(repository):
909 if is_svn(repository):
919 if is_svn(repository):
910 from rhodecode.model.settings import VcsSettingsModel
920 from rhodecode.model.settings import VcsSettingsModel
911 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
921 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
912 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
922 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
913 return False
923 return False
914
924
915
925
916 def discover_user(author):
926 def discover_user(author):
917 """
927 """
918 Tries to discover RhodeCode User based on the author string. Author string
928 Tries to discover RhodeCode User based on the author string. Author string
919 is typically `FirstName LastName <email@address.com>`
929 is typically `FirstName LastName <email@address.com>`
920 """
930 """
921
931
922 # if author is already an instance use it for extraction
932 # if author is already an instance use it for extraction
923 if isinstance(author, User):
933 if isinstance(author, User):
924 return author
934 return author
925
935
926 # Valid email in the attribute passed, see if they're in the system
936 # Valid email in the attribute passed, see if they're in the system
927 _email = author_email(author)
937 _email = author_email(author)
928 if _email != '':
938 if _email != '':
929 user = User.get_by_email(_email, case_insensitive=True, cache=True)
939 user = User.get_by_email(_email, case_insensitive=True, cache=True)
930 if user is not None:
940 if user is not None:
931 return user
941 return user
932
942
933 # Maybe it's a username, we try to extract it and fetch by username ?
943 # Maybe it's a username, we try to extract it and fetch by username ?
934 _author = author_name(author)
944 _author = author_name(author)
935 user = User.get_by_username(_author, case_insensitive=True, cache=True)
945 user = User.get_by_username(_author, case_insensitive=True, cache=True)
936 if user is not None:
946 if user is not None:
937 return user
947 return user
938
948
939 return None
949 return None
940
950
941
951
942 def email_or_none(author):
952 def email_or_none(author):
943 # extract email from the commit string
953 # extract email from the commit string
944 _email = author_email(author)
954 _email = author_email(author)
945
955
946 # If we have an email, use it, otherwise
956 # If we have an email, use it, otherwise
947 # see if it contains a username we can get an email from
957 # see if it contains a username we can get an email from
948 if _email != '':
958 if _email != '':
949 return _email
959 return _email
950 else:
960 else:
951 user = User.get_by_username(
961 user = User.get_by_username(
952 author_name(author), case_insensitive=True, cache=True)
962 author_name(author), case_insensitive=True, cache=True)
953
963
954 if user is not None:
964 if user is not None:
955 return user.email
965 return user.email
956
966
957 # No valid email, not a valid user in the system, none!
967 # No valid email, not a valid user in the system, none!
958 return None
968 return None
959
969
960
970
961 def link_to_user(author, length=0, **kwargs):
971 def link_to_user(author, length=0, **kwargs):
962 user = discover_user(author)
972 user = discover_user(author)
963 # user can be None, but if we have it already it means we can re-use it
973 # user can be None, but if we have it already it means we can re-use it
964 # in the person() function, so we save 1 intensive-query
974 # in the person() function, so we save 1 intensive-query
965 if user:
975 if user:
966 author = user
976 author = user
967
977
968 display_person = person(author, 'username_or_name_or_email')
978 display_person = person(author, 'username_or_name_or_email')
969 if length:
979 if length:
970 display_person = shorter(display_person, length)
980 display_person = shorter(display_person, length)
971
981
972 if user and user.username != user.DEFAULT_USER:
982 if user and user.username != user.DEFAULT_USER:
973 return link_to(
983 return link_to(
974 escape(display_person),
984 escape(display_person),
975 route_path('user_profile', username=user.username),
985 route_path('user_profile', username=user.username),
976 **kwargs)
986 **kwargs)
977 else:
987 else:
978 return escape(display_person)
988 return escape(display_person)
979
989
980
990
981 def link_to_group(users_group_name, **kwargs):
991 def link_to_group(users_group_name, **kwargs):
982 return link_to(
992 return link_to(
983 escape(users_group_name),
993 escape(users_group_name),
984 route_path('user_group_profile', user_group_name=users_group_name),
994 route_path('user_group_profile', user_group_name=users_group_name),
985 **kwargs)
995 **kwargs)
986
996
987
997
988 def person(author, show_attr="username_and_name"):
998 def person(author, show_attr="username_and_name"):
989 user = discover_user(author)
999 user = discover_user(author)
990 if user:
1000 if user:
991 return getattr(user, show_attr)
1001 return getattr(user, show_attr)
992 else:
1002 else:
993 _author = author_name(author)
1003 _author = author_name(author)
994 _email = email(author)
1004 _email = email(author)
995 return _author or _email
1005 return _author or _email
996
1006
997
1007
998 def author_string(email):
1008 def author_string(email):
999 if email:
1009 if email:
1000 user = User.get_by_email(email, case_insensitive=True, cache=True)
1010 user = User.get_by_email(email, case_insensitive=True, cache=True)
1001 if user:
1011 if user:
1002 if user.first_name or user.last_name:
1012 if user.first_name or user.last_name:
1003 return '%s %s &lt;%s&gt;' % (
1013 return '%s %s &lt;%s&gt;' % (
1004 user.first_name, user.last_name, email)
1014 user.first_name, user.last_name, email)
1005 else:
1015 else:
1006 return email
1016 return email
1007 else:
1017 else:
1008 return email
1018 return email
1009 else:
1019 else:
1010 return None
1020 return None
1011
1021
1012
1022
1013 def person_by_id(id_, show_attr="username_and_name"):
1023 def person_by_id(id_, show_attr="username_and_name"):
1014 # attr to return from fetched user
1024 # attr to return from fetched user
1015 person_getter = lambda usr: getattr(usr, show_attr)
1025 person_getter = lambda usr: getattr(usr, show_attr)
1016
1026
1017 #maybe it's an ID ?
1027 #maybe it's an ID ?
1018 if str(id_).isdigit() or isinstance(id_, int):
1028 if str(id_).isdigit() or isinstance(id_, int):
1019 id_ = int(id_)
1029 id_ = int(id_)
1020 user = User.get(id_)
1030 user = User.get(id_)
1021 if user is not None:
1031 if user is not None:
1022 return person_getter(user)
1032 return person_getter(user)
1023 return id_
1033 return id_
1024
1034
1025
1035
1026 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1036 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1027 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1037 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1028 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1038 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1029
1039
1030
1040
1031 tags_paterns = OrderedDict((
1041 tags_paterns = OrderedDict((
1032 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1042 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1033 '<div class="metatag" tag="lang">\\2</div>')),
1043 '<div class="metatag" tag="lang">\\2</div>')),
1034
1044
1035 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1045 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1036 '<div class="metatag" tag="see">see: \\1 </div>')),
1046 '<div class="metatag" tag="see">see: \\1 </div>')),
1037
1047
1038 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1048 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1039 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1049 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1040
1050
1041 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1051 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1042 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1052 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1043
1053
1044 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1054 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1045 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1055 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1046
1056
1047 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1057 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1048 '<div class="metatag" tag="state \\1">\\1</div>')),
1058 '<div class="metatag" tag="state \\1">\\1</div>')),
1049
1059
1050 # label in grey
1060 # label in grey
1051 ('label', (re.compile(r'\[([a-z]+)\]'),
1061 ('label', (re.compile(r'\[([a-z]+)\]'),
1052 '<div class="metatag" tag="label">\\1</div>')),
1062 '<div class="metatag" tag="label">\\1</div>')),
1053
1063
1054 # generic catch all in grey
1064 # generic catch all in grey
1055 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1065 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1056 '<div class="metatag" tag="generic">\\1</div>')),
1066 '<div class="metatag" tag="generic">\\1</div>')),
1057 ))
1067 ))
1058
1068
1059
1069
1060 def extract_metatags(value):
1070 def extract_metatags(value):
1061 """
1071 """
1062 Extract supported meta-tags from given text value
1072 Extract supported meta-tags from given text value
1063 """
1073 """
1064 tags = []
1074 tags = []
1065 if not value:
1075 if not value:
1066 return tags, ''
1076 return tags, ''
1067
1077
1068 for key, val in tags_paterns.items():
1078 for key, val in tags_paterns.items():
1069 pat, replace_html = val
1079 pat, replace_html = val
1070 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1080 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1071 value = pat.sub('', value)
1081 value = pat.sub('', value)
1072
1082
1073 return tags, value
1083 return tags, value
1074
1084
1075
1085
1076 def style_metatag(tag_type, value):
1086 def style_metatag(tag_type, value):
1077 """
1087 """
1078 converts tags from value into html equivalent
1088 converts tags from value into html equivalent
1079 """
1089 """
1080 if not value:
1090 if not value:
1081 return ''
1091 return ''
1082
1092
1083 html_value = value
1093 html_value = value
1084 tag_data = tags_paterns.get(tag_type)
1094 tag_data = tags_paterns.get(tag_type)
1085 if tag_data:
1095 if tag_data:
1086 pat, replace_html = tag_data
1096 pat, replace_html = tag_data
1087 # convert to plain `unicode` instead of a markup tag to be used in
1097 # convert to plain `unicode` instead of a markup tag to be used in
1088 # regex expressions. safe_unicode doesn't work here
1098 # regex expressions. safe_unicode doesn't work here
1089 html_value = pat.sub(replace_html, value)
1099 html_value = pat.sub(replace_html, value)
1090
1100
1091 return html_value
1101 return html_value
1092
1102
1093
1103
1094 def bool2icon(value, show_at_false=True):
1104 def bool2icon(value, show_at_false=True):
1095 """
1105 """
1096 Returns boolean value of a given value, represented as html element with
1106 Returns boolean value of a given value, represented as html element with
1097 classes that will represent icons
1107 classes that will represent icons
1098
1108
1099 :param value: given value to convert to html node
1109 :param value: given value to convert to html node
1100 """
1110 """
1101
1111
1102 if value: # does bool conversion
1112 if value: # does bool conversion
1103 return HTML.tag('i', class_="icon-true", title='True')
1113 return HTML.tag('i', class_="icon-true", title='True')
1104 else: # not true as bool
1114 else: # not true as bool
1105 if show_at_false:
1115 if show_at_false:
1106 return HTML.tag('i', class_="icon-false", title='False')
1116 return HTML.tag('i', class_="icon-false", title='False')
1107 return HTML.tag('i')
1117 return HTML.tag('i')
1108
1118
1109
1119
1110 def b64(inp):
1120 def b64(inp):
1111 return base64.b64encode(inp)
1121 return base64.b64encode(inp)
1112
1122
1113 #==============================================================================
1123 #==============================================================================
1114 # PERMS
1124 # PERMS
1115 #==============================================================================
1125 #==============================================================================
1116 from rhodecode.lib.auth import (
1126 from rhodecode.lib.auth import (
1117 HasPermissionAny, HasPermissionAll,
1127 HasPermissionAny, HasPermissionAll,
1118 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1128 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1119 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1129 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1120 csrf_token_key, AuthUser)
1130 csrf_token_key, AuthUser)
1121
1131
1122
1132
1123 #==============================================================================
1133 #==============================================================================
1124 # GRAVATAR URL
1134 # GRAVATAR URL
1125 #==============================================================================
1135 #==============================================================================
1126 class InitialsGravatar(object):
1136 class InitialsGravatar(object):
1127 def __init__(self, email_address, first_name, last_name, size=30,
1137 def __init__(self, email_address, first_name, last_name, size=30,
1128 background=None, text_color='#fff'):
1138 background=None, text_color='#fff'):
1129 self.size = size
1139 self.size = size
1130 self.first_name = first_name
1140 self.first_name = first_name
1131 self.last_name = last_name
1141 self.last_name = last_name
1132 self.email_address = email_address
1142 self.email_address = email_address
1133 self.background = background or self.str2color(email_address)
1143 self.background = background or self.str2color(email_address)
1134 self.text_color = text_color
1144 self.text_color = text_color
1135
1145
1136 def get_color_bank(self):
1146 def get_color_bank(self):
1137 """
1147 """
1138 returns a predefined list of colors that gravatars can use.
1148 returns a predefined list of colors that gravatars can use.
1139 Those are randomized distinct colors that guarantee readability and
1149 Those are randomized distinct colors that guarantee readability and
1140 uniqueness.
1150 uniqueness.
1141
1151
1142 generated with: http://phrogz.net/css/distinct-colors.html
1152 generated with: http://phrogz.net/css/distinct-colors.html
1143 """
1153 """
1144 return [
1154 return [
1145 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1155 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1146 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1156 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1147 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1157 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1148 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1158 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1149 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1159 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1150 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1160 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1151 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1161 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1152 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1162 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1153 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1163 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1154 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1164 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1155 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1165 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1156 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1166 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1157 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1167 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1158 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1168 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1159 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1169 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1160 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1170 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1161 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1171 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1162 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1172 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1163 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1173 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1164 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1174 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1165 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1175 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1166 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1176 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1167 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1177 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1168 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1178 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1169 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1179 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1170 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1180 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1171 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1181 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1172 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1182 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1173 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1183 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1174 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1184 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1175 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1185 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1176 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1186 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1177 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1187 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1178 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1188 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1179 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1189 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1180 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1190 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1181 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1191 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1182 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1192 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1183 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1193 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1184 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1194 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1185 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1195 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1186 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1196 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1187 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1197 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1188 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1198 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1189 '#4f8c46', '#368dd9', '#5c0073'
1199 '#4f8c46', '#368dd9', '#5c0073'
1190 ]
1200 ]
1191
1201
1192 def rgb_to_hex_color(self, rgb_tuple):
1202 def rgb_to_hex_color(self, rgb_tuple):
1193 """
1203 """
1194 Converts an rgb_tuple passed to an hex color.
1204 Converts an rgb_tuple passed to an hex color.
1195
1205
1196 :param rgb_tuple: tuple with 3 ints represents rgb color space
1206 :param rgb_tuple: tuple with 3 ints represents rgb color space
1197 """
1207 """
1198 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1208 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1199
1209
1200 def email_to_int_list(self, email_str):
1210 def email_to_int_list(self, email_str):
1201 """
1211 """
1202 Get every byte of the hex digest value of email and turn it to integer.
1212 Get every byte of the hex digest value of email and turn it to integer.
1203 It's going to be always between 0-255
1213 It's going to be always between 0-255
1204 """
1214 """
1205 digest = md5_safe(email_str.lower())
1215 digest = md5_safe(email_str.lower())
1206 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1216 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1207
1217
1208 def pick_color_bank_index(self, email_str, color_bank):
1218 def pick_color_bank_index(self, email_str, color_bank):
1209 return self.email_to_int_list(email_str)[0] % len(color_bank)
1219 return self.email_to_int_list(email_str)[0] % len(color_bank)
1210
1220
1211 def str2color(self, email_str):
1221 def str2color(self, email_str):
1212 """
1222 """
1213 Tries to map in a stable algorithm an email to color
1223 Tries to map in a stable algorithm an email to color
1214
1224
1215 :param email_str:
1225 :param email_str:
1216 """
1226 """
1217 color_bank = self.get_color_bank()
1227 color_bank = self.get_color_bank()
1218 # pick position (module it's length so we always find it in the
1228 # pick position (module it's length so we always find it in the
1219 # bank even if it's smaller than 256 values
1229 # bank even if it's smaller than 256 values
1220 pos = self.pick_color_bank_index(email_str, color_bank)
1230 pos = self.pick_color_bank_index(email_str, color_bank)
1221 return color_bank[pos]
1231 return color_bank[pos]
1222
1232
1223 def normalize_email(self, email_address):
1233 def normalize_email(self, email_address):
1224 import unicodedata
1234 import unicodedata
1225 # default host used to fill in the fake/missing email
1235 # default host used to fill in the fake/missing email
1226 default_host = 'localhost'
1236 default_host = 'localhost'
1227
1237
1228 if not email_address:
1238 if not email_address:
1229 email_address = '%s@%s' % (User.DEFAULT_USER, default_host)
1239 email_address = '%s@%s' % (User.DEFAULT_USER, default_host)
1230
1240
1231 email_address = safe_unicode(email_address)
1241 email_address = safe_unicode(email_address)
1232
1242
1233 if u'@' not in email_address:
1243 if u'@' not in email_address:
1234 email_address = u'%s@%s' % (email_address, default_host)
1244 email_address = u'%s@%s' % (email_address, default_host)
1235
1245
1236 if email_address.endswith(u'@'):
1246 if email_address.endswith(u'@'):
1237 email_address = u'%s%s' % (email_address, default_host)
1247 email_address = u'%s%s' % (email_address, default_host)
1238
1248
1239 email_address = unicodedata.normalize('NFKD', email_address)\
1249 email_address = unicodedata.normalize('NFKD', email_address)\
1240 .encode('ascii', 'ignore')
1250 .encode('ascii', 'ignore')
1241 return email_address
1251 return email_address
1242
1252
1243 def get_initials(self):
1253 def get_initials(self):
1244 """
1254 """
1245 Returns 2 letter initials calculated based on the input.
1255 Returns 2 letter initials calculated based on the input.
1246 The algorithm picks first given email address, and takes first letter
1256 The algorithm picks first given email address, and takes first letter
1247 of part before @, and then the first letter of server name. In case
1257 of part before @, and then the first letter of server name. In case
1248 the part before @ is in a format of `somestring.somestring2` it replaces
1258 the part before @ is in a format of `somestring.somestring2` it replaces
1249 the server letter with first letter of somestring2
1259 the server letter with first letter of somestring2
1250
1260
1251 In case function was initialized with both first and lastname, this
1261 In case function was initialized with both first and lastname, this
1252 overrides the extraction from email by first letter of the first and
1262 overrides the extraction from email by first letter of the first and
1253 last name. We add special logic to that functionality, In case Full name
1263 last name. We add special logic to that functionality, In case Full name
1254 is compound, like Guido Von Rossum, we use last part of the last name
1264 is compound, like Guido Von Rossum, we use last part of the last name
1255 (Von Rossum) picking `R`.
1265 (Von Rossum) picking `R`.
1256
1266
1257 Function also normalizes the non-ascii characters to they ascii
1267 Function also normalizes the non-ascii characters to they ascii
1258 representation, eg Δ„ => A
1268 representation, eg Δ„ => A
1259 """
1269 """
1260 import unicodedata
1270 import unicodedata
1261 # replace non-ascii to ascii
1271 # replace non-ascii to ascii
1262 first_name = unicodedata.normalize(
1272 first_name = unicodedata.normalize(
1263 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1273 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1264 last_name = unicodedata.normalize(
1274 last_name = unicodedata.normalize(
1265 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1275 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1266
1276
1267 # do NFKD encoding, and also make sure email has proper format
1277 # do NFKD encoding, and also make sure email has proper format
1268 email_address = self.normalize_email(self.email_address)
1278 email_address = self.normalize_email(self.email_address)
1269
1279
1270 # first push the email initials
1280 # first push the email initials
1271 prefix, server = email_address.split('@', 1)
1281 prefix, server = email_address.split('@', 1)
1272
1282
1273 # check if prefix is maybe a 'first_name.last_name' syntax
1283 # check if prefix is maybe a 'first_name.last_name' syntax
1274 _dot_split = prefix.rsplit('.', 1)
1284 _dot_split = prefix.rsplit('.', 1)
1275 if len(_dot_split) == 2 and _dot_split[1]:
1285 if len(_dot_split) == 2 and _dot_split[1]:
1276 initials = [_dot_split[0][0], _dot_split[1][0]]
1286 initials = [_dot_split[0][0], _dot_split[1][0]]
1277 else:
1287 else:
1278 initials = [prefix[0], server[0]]
1288 initials = [prefix[0], server[0]]
1279
1289
1280 # then try to replace either first_name or last_name
1290 # then try to replace either first_name or last_name
1281 fn_letter = (first_name or " ")[0].strip()
1291 fn_letter = (first_name or " ")[0].strip()
1282 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1292 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1283
1293
1284 if fn_letter:
1294 if fn_letter:
1285 initials[0] = fn_letter
1295 initials[0] = fn_letter
1286
1296
1287 if ln_letter:
1297 if ln_letter:
1288 initials[1] = ln_letter
1298 initials[1] = ln_letter
1289
1299
1290 return ''.join(initials).upper()
1300 return ''.join(initials).upper()
1291
1301
1292 def get_img_data_by_type(self, font_family, img_type):
1302 def get_img_data_by_type(self, font_family, img_type):
1293 default_user = """
1303 default_user = """
1294 <svg xmlns="http://www.w3.org/2000/svg"
1304 <svg xmlns="http://www.w3.org/2000/svg"
1295 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1305 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1296 viewBox="-15 -10 439.165 429.164"
1306 viewBox="-15 -10 439.165 429.164"
1297
1307
1298 xml:space="preserve"
1308 xml:space="preserve"
1299 style="background:{background};" >
1309 style="background:{background};" >
1300
1310
1301 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1311 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1302 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1312 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1303 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1313 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1304 168.596,153.916,216.671,
1314 168.596,153.916,216.671,
1305 204.583,216.671z" fill="{text_color}"/>
1315 204.583,216.671z" fill="{text_color}"/>
1306 <path d="M407.164,374.717L360.88,
1316 <path d="M407.164,374.717L360.88,
1307 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1317 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1308 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1318 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1309 15.366-44.203,23.488-69.076,23.488c-24.877,
1319 15.366-44.203,23.488-69.076,23.488c-24.877,
1310 0-48.762-8.122-69.078-23.488
1320 0-48.762-8.122-69.078-23.488
1311 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1321 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1312 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1322 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1313 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1323 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1314 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1324 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1315 19.402-10.527 C409.699,390.129,
1325 19.402-10.527 C409.699,390.129,
1316 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1326 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1317 </svg>""".format(
1327 </svg>""".format(
1318 size=self.size,
1328 size=self.size,
1319 background='#979797', # @grey4
1329 background='#979797', # @grey4
1320 text_color=self.text_color,
1330 text_color=self.text_color,
1321 font_family=font_family)
1331 font_family=font_family)
1322
1332
1323 return {
1333 return {
1324 "default_user": default_user
1334 "default_user": default_user
1325 }[img_type]
1335 }[img_type]
1326
1336
1327 def get_img_data(self, svg_type=None):
1337 def get_img_data(self, svg_type=None):
1328 """
1338 """
1329 generates the svg metadata for image
1339 generates the svg metadata for image
1330 """
1340 """
1331 fonts = [
1341 fonts = [
1332 '-apple-system',
1342 '-apple-system',
1333 'BlinkMacSystemFont',
1343 'BlinkMacSystemFont',
1334 'Segoe UI',
1344 'Segoe UI',
1335 'Roboto',
1345 'Roboto',
1336 'Oxygen-Sans',
1346 'Oxygen-Sans',
1337 'Ubuntu',
1347 'Ubuntu',
1338 'Cantarell',
1348 'Cantarell',
1339 'Helvetica Neue',
1349 'Helvetica Neue',
1340 'sans-serif'
1350 'sans-serif'
1341 ]
1351 ]
1342 font_family = ','.join(fonts)
1352 font_family = ','.join(fonts)
1343 if svg_type:
1353 if svg_type:
1344 return self.get_img_data_by_type(font_family, svg_type)
1354 return self.get_img_data_by_type(font_family, svg_type)
1345
1355
1346 initials = self.get_initials()
1356 initials = self.get_initials()
1347 img_data = """
1357 img_data = """
1348 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1358 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1349 width="{size}" height="{size}"
1359 width="{size}" height="{size}"
1350 style="width: 100%; height: 100%; background-color: {background}"
1360 style="width: 100%; height: 100%; background-color: {background}"
1351 viewBox="0 0 {size} {size}">
1361 viewBox="0 0 {size} {size}">
1352 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1362 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1353 pointer-events="auto" fill="{text_color}"
1363 pointer-events="auto" fill="{text_color}"
1354 font-family="{font_family}"
1364 font-family="{font_family}"
1355 style="font-weight: 400; font-size: {f_size}px;">{text}
1365 style="font-weight: 400; font-size: {f_size}px;">{text}
1356 </text>
1366 </text>
1357 </svg>""".format(
1367 </svg>""".format(
1358 size=self.size,
1368 size=self.size,
1359 f_size=self.size/2.05, # scale the text inside the box nicely
1369 f_size=self.size/2.05, # scale the text inside the box nicely
1360 background=self.background,
1370 background=self.background,
1361 text_color=self.text_color,
1371 text_color=self.text_color,
1362 text=initials.upper(),
1372 text=initials.upper(),
1363 font_family=font_family)
1373 font_family=font_family)
1364
1374
1365 return img_data
1375 return img_data
1366
1376
1367 def generate_svg(self, svg_type=None):
1377 def generate_svg(self, svg_type=None):
1368 img_data = self.get_img_data(svg_type)
1378 img_data = self.get_img_data(svg_type)
1369 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1379 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1370
1380
1371
1381
1372 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1382 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1373
1383
1374 svg_type = None
1384 svg_type = None
1375 if email_address == User.DEFAULT_USER_EMAIL:
1385 if email_address == User.DEFAULT_USER_EMAIL:
1376 svg_type = 'default_user'
1386 svg_type = 'default_user'
1377
1387
1378 klass = InitialsGravatar(email_address, first_name, last_name, size)
1388 klass = InitialsGravatar(email_address, first_name, last_name, size)
1379
1389
1380 if store_on_disk:
1390 if store_on_disk:
1381 from rhodecode.apps.file_store import utils as store_utils
1391 from rhodecode.apps.file_store import utils as store_utils
1382 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1392 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1383 FileOverSizeException
1393 FileOverSizeException
1384 from rhodecode.model.db import Session
1394 from rhodecode.model.db import Session
1385
1395
1386 image_key = md5_safe(email_address.lower()
1396 image_key = md5_safe(email_address.lower()
1387 + first_name.lower() + last_name.lower())
1397 + first_name.lower() + last_name.lower())
1388
1398
1389 storage = store_utils.get_file_storage(request.registry.settings)
1399 storage = store_utils.get_file_storage(request.registry.settings)
1390 filename = '{}.svg'.format(image_key)
1400 filename = '{}.svg'.format(image_key)
1391 subdir = 'gravatars'
1401 subdir = 'gravatars'
1392 # since final name has a counter, we apply the 0
1402 # since final name has a counter, we apply the 0
1393 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1403 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1394 store_uid = os.path.join(subdir, uid)
1404 store_uid = os.path.join(subdir, uid)
1395
1405
1396 db_entry = FileStore.get_by_store_uid(store_uid)
1406 db_entry = FileStore.get_by_store_uid(store_uid)
1397 if db_entry:
1407 if db_entry:
1398 return request.route_path('download_file', fid=store_uid)
1408 return request.route_path('download_file', fid=store_uid)
1399
1409
1400 img_data = klass.get_img_data(svg_type=svg_type)
1410 img_data = klass.get_img_data(svg_type=svg_type)
1401 img_file = store_utils.bytes_to_file_obj(img_data)
1411 img_file = store_utils.bytes_to_file_obj(img_data)
1402
1412
1403 try:
1413 try:
1404 store_uid, metadata = storage.save_file(
1414 store_uid, metadata = storage.save_file(
1405 img_file, filename, directory=subdir,
1415 img_file, filename, directory=subdir,
1406 extensions=['.svg'], randomized_name=False)
1416 extensions=['.svg'], randomized_name=False)
1407 except (FileNotAllowedException, FileOverSizeException):
1417 except (FileNotAllowedException, FileOverSizeException):
1408 raise
1418 raise
1409
1419
1410 try:
1420 try:
1411 entry = FileStore.create(
1421 entry = FileStore.create(
1412 file_uid=store_uid, filename=metadata["filename"],
1422 file_uid=store_uid, filename=metadata["filename"],
1413 file_hash=metadata["sha256"], file_size=metadata["size"],
1423 file_hash=metadata["sha256"], file_size=metadata["size"],
1414 file_display_name=filename,
1424 file_display_name=filename,
1415 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1425 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1416 hidden=True, check_acl=False, user_id=1
1426 hidden=True, check_acl=False, user_id=1
1417 )
1427 )
1418 Session().add(entry)
1428 Session().add(entry)
1419 Session().commit()
1429 Session().commit()
1420 log.debug('Stored upload in DB as %s', entry)
1430 log.debug('Stored upload in DB as %s', entry)
1421 except Exception:
1431 except Exception:
1422 raise
1432 raise
1423
1433
1424 return request.route_path('download_file', fid=store_uid)
1434 return request.route_path('download_file', fid=store_uid)
1425
1435
1426 else:
1436 else:
1427 return klass.generate_svg(svg_type=svg_type)
1437 return klass.generate_svg(svg_type=svg_type)
1428
1438
1429
1439
1430 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1440 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1431 return safe_str(gravatar_url_tmpl)\
1441 return safe_str(gravatar_url_tmpl)\
1432 .replace('{email}', email_address) \
1442 .replace('{email}', email_address) \
1433 .replace('{md5email}', md5_safe(email_address.lower())) \
1443 .replace('{md5email}', md5_safe(email_address.lower())) \
1434 .replace('{netloc}', request.host) \
1444 .replace('{netloc}', request.host) \
1435 .replace('{scheme}', request.scheme) \
1445 .replace('{scheme}', request.scheme) \
1436 .replace('{size}', safe_str(size))
1446 .replace('{size}', safe_str(size))
1437
1447
1438
1448
1439 def gravatar_url(email_address, size=30, request=None):
1449 def gravatar_url(email_address, size=30, request=None):
1440 request = request or get_current_request()
1450 request = request or get_current_request()
1441 _use_gravatar = request.call_context.visual.use_gravatar
1451 _use_gravatar = request.call_context.visual.use_gravatar
1442
1452
1443 email_address = email_address or User.DEFAULT_USER_EMAIL
1453 email_address = email_address or User.DEFAULT_USER_EMAIL
1444 if isinstance(email_address, str):
1454 if isinstance(email_address, str):
1445 # hashlib crashes on unicode items
1455 # hashlib crashes on unicode items
1446 email_address = safe_str(email_address)
1456 email_address = safe_str(email_address)
1447
1457
1448 # empty email or default user
1458 # empty email or default user
1449 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1459 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1450 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1460 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1451
1461
1452 if _use_gravatar:
1462 if _use_gravatar:
1453 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1463 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1454 or User.DEFAULT_GRAVATAR_URL
1464 or User.DEFAULT_GRAVATAR_URL
1455 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1465 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1456
1466
1457 else:
1467 else:
1458 return initials_gravatar(request, email_address, '', '', size=size)
1468 return initials_gravatar(request, email_address, '', '', size=size)
1459
1469
1460
1470
1461 def breadcrumb_repo_link(repo):
1471 def breadcrumb_repo_link(repo):
1462 """
1472 """
1463 Makes a breadcrumbs path link to repo
1473 Makes a breadcrumbs path link to repo
1464
1474
1465 ex::
1475 ex::
1466 group >> subgroup >> repo
1476 group >> subgroup >> repo
1467
1477
1468 :param repo: a Repository instance
1478 :param repo: a Repository instance
1469 """
1479 """
1470
1480
1471 path = [
1481 path = [
1472 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1482 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1473 title='last change:{}'.format(format_date(group.last_commit_change)))
1483 title='last change:{}'.format(format_date(group.last_commit_change)))
1474 for group in repo.groups_with_parents
1484 for group in repo.groups_with_parents
1475 ] + [
1485 ] + [
1476 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1486 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1477 title='last change:{}'.format(format_date(repo.last_commit_change)))
1487 title='last change:{}'.format(format_date(repo.last_commit_change)))
1478 ]
1488 ]
1479
1489
1480 return literal(' &raquo; '.join(path))
1490 return literal(' &raquo; '.join(path))
1481
1491
1482
1492
1483 def breadcrumb_repo_group_link(repo_group):
1493 def breadcrumb_repo_group_link(repo_group):
1484 """
1494 """
1485 Makes a breadcrumbs path link to repo
1495 Makes a breadcrumbs path link to repo
1486
1496
1487 ex::
1497 ex::
1488 group >> subgroup
1498 group >> subgroup
1489
1499
1490 :param repo_group: a Repository Group instance
1500 :param repo_group: a Repository Group instance
1491 """
1501 """
1492
1502
1493 path = [
1503 path = [
1494 link_to(group.name,
1504 link_to(group.name,
1495 route_path('repo_group_home', repo_group_name=group.group_name),
1505 route_path('repo_group_home', repo_group_name=group.group_name),
1496 title='last change:{}'.format(format_date(group.last_commit_change)))
1506 title='last change:{}'.format(format_date(group.last_commit_change)))
1497 for group in repo_group.parents
1507 for group in repo_group.parents
1498 ] + [
1508 ] + [
1499 link_to(repo_group.name,
1509 link_to(repo_group.name,
1500 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1510 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1501 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1511 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1502 ]
1512 ]
1503
1513
1504 return literal(' &raquo; '.join(path))
1514 return literal(' &raquo; '.join(path))
1505
1515
1506
1516
1507 def format_byte_size_binary(file_size):
1517 def format_byte_size_binary(file_size):
1508 """
1518 """
1509 Formats file/folder sizes to standard.
1519 Formats file/folder sizes to standard.
1510 """
1520 """
1511 if file_size is None:
1521 if file_size is None:
1512 file_size = 0
1522 file_size = 0
1513
1523
1514 formatted_size = format_byte_size(file_size, binary=True)
1524 formatted_size = format_byte_size(file_size, binary=True)
1515 return formatted_size
1525 return formatted_size
1516
1526
1517
1527
1518 def urlify_text(text_, safe=True, **href_attrs):
1528 def urlify_text(text_, safe=True, **href_attrs):
1519 """
1529 """
1520 Extract urls from text and make html links out of them
1530 Extract urls from text and make html links out of them
1521 """
1531 """
1522
1532
1523 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1533 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1524 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1534 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1525
1535
1526 def url_func(match_obj):
1536 def url_func(match_obj):
1527 url_full = match_obj.groups()[0]
1537 url_full = match_obj.groups()[0]
1528 a_options = dict(href_attrs)
1538 a_options = dict(href_attrs)
1529 a_options['href'] = url_full
1539 a_options['href'] = url_full
1530 a_text = url_full
1540 a_text = url_full
1531 return HTML.tag("a", a_text, **a_options)
1541 return HTML.tag("a", a_text, **a_options)
1532
1542
1533 _new_text = url_pat.sub(url_func, text_)
1543 _new_text = url_pat.sub(url_func, text_)
1534
1544
1535 if safe:
1545 if safe:
1536 return literal(_new_text)
1546 return literal(_new_text)
1537 return _new_text
1547 return _new_text
1538
1548
1539
1549
1540 def urlify_commits(text_, repo_name):
1550 def urlify_commits(text_, repo_name):
1541 """
1551 """
1542 Extract commit ids from text and make link from them
1552 Extract commit ids from text and make link from them
1543
1553
1544 :param text_:
1554 :param text_:
1545 :param repo_name: repo name to build the URL with
1555 :param repo_name: repo name to build the URL with
1546 """
1556 """
1547
1557
1548 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1558 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1549
1559
1550 def url_func(match_obj):
1560 def url_func(match_obj):
1551 commit_id = match_obj.groups()[1]
1561 commit_id = match_obj.groups()[1]
1552 pref = match_obj.groups()[0]
1562 pref = match_obj.groups()[0]
1553 suf = match_obj.groups()[2]
1563 suf = match_obj.groups()[2]
1554
1564
1555 tmpl = (
1565 tmpl = (
1556 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1566 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1557 '%(commit_id)s</a>%(suf)s'
1567 '%(commit_id)s</a>%(suf)s'
1558 )
1568 )
1559 return tmpl % {
1569 return tmpl % {
1560 'pref': pref,
1570 'pref': pref,
1561 'cls': 'revision-link',
1571 'cls': 'revision-link',
1562 'url': route_url(
1572 'url': route_url(
1563 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1573 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1564 'commit_id': commit_id,
1574 'commit_id': commit_id,
1565 'suf': suf,
1575 'suf': suf,
1566 'hovercard_alt': 'Commit: {}'.format(commit_id),
1576 'hovercard_alt': 'Commit: {}'.format(commit_id),
1567 'hovercard_url': route_url(
1577 'hovercard_url': route_url(
1568 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1578 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1569 }
1579 }
1570
1580
1571 new_text = url_pat.sub(url_func, text_)
1581 new_text = url_pat.sub(url_func, text_)
1572
1582
1573 return new_text
1583 return new_text
1574
1584
1575
1585
1576 def _process_url_func(match_obj, repo_name, uid, entry,
1586 def _process_url_func(match_obj, repo_name, uid, entry,
1577 return_raw_data=False, link_format='html'):
1587 return_raw_data=False, link_format='html'):
1578 pref = ''
1588 pref = ''
1579 if match_obj.group().startswith(' '):
1589 if match_obj.group().startswith(' '):
1580 pref = ' '
1590 pref = ' '
1581
1591
1582 issue_id = ''.join(match_obj.groups())
1592 issue_id = ''.join(match_obj.groups())
1583
1593
1584 if link_format == 'html':
1594 if link_format == 'html':
1585 tmpl = (
1595 tmpl = (
1586 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1596 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1587 '%(issue-prefix)s%(id-repr)s'
1597 '%(issue-prefix)s%(id-repr)s'
1588 '</a>')
1598 '</a>')
1589 elif link_format == 'html+hovercard':
1599 elif link_format == 'html+hovercard':
1590 tmpl = (
1600 tmpl = (
1591 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1601 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1592 '%(issue-prefix)s%(id-repr)s'
1602 '%(issue-prefix)s%(id-repr)s'
1593 '</a>')
1603 '</a>')
1594 elif link_format in ['rst', 'rst+hovercard']:
1604 elif link_format in ['rst', 'rst+hovercard']:
1595 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1605 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1596 elif link_format in ['markdown', 'markdown+hovercard']:
1606 elif link_format in ['markdown', 'markdown+hovercard']:
1597 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1607 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1598 else:
1608 else:
1599 raise ValueError('Bad link_format:{}'.format(link_format))
1609 raise ValueError('Bad link_format:{}'.format(link_format))
1600
1610
1601 (repo_name_cleaned,
1611 (repo_name_cleaned,
1602 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1612 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1603
1613
1604 # variables replacement
1614 # variables replacement
1605 named_vars = {
1615 named_vars = {
1606 'id': issue_id,
1616 'id': issue_id,
1607 'repo': repo_name,
1617 'repo': repo_name,
1608 'repo_name': repo_name_cleaned,
1618 'repo_name': repo_name_cleaned,
1609 'group_name': parent_group_name,
1619 'group_name': parent_group_name,
1610 # set dummy keys so we always have them
1620 # set dummy keys so we always have them
1611 'hostname': '',
1621 'hostname': '',
1612 'netloc': '',
1622 'netloc': '',
1613 'scheme': ''
1623 'scheme': ''
1614 }
1624 }
1615
1625
1616 request = get_current_request()
1626 request = get_current_request()
1617 if request:
1627 if request:
1618 # exposes, hostname, netloc, scheme
1628 # exposes, hostname, netloc, scheme
1619 host_data = get_host_info(request)
1629 host_data = get_host_info(request)
1620 named_vars.update(host_data)
1630 named_vars.update(host_data)
1621
1631
1622 # named regex variables
1632 # named regex variables
1623 named_vars.update(match_obj.groupdict())
1633 named_vars.update(match_obj.groupdict())
1624 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1634 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1625 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1635 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1626 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1636 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1627
1637
1628 def quote_cleaner(input_str):
1638 def quote_cleaner(input_str):
1629 """Remove quotes as it's HTML"""
1639 """Remove quotes as it's HTML"""
1630 return input_str.replace('"', '')
1640 return input_str.replace('"', '')
1631
1641
1632 data = {
1642 data = {
1633 'pref': pref,
1643 'pref': pref,
1634 'cls': quote_cleaner('issue-tracker-link'),
1644 'cls': quote_cleaner('issue-tracker-link'),
1635 'url': quote_cleaner(_url),
1645 'url': quote_cleaner(_url),
1636 'id-repr': issue_id,
1646 'id-repr': issue_id,
1637 'issue-prefix': entry['pref'],
1647 'issue-prefix': entry['pref'],
1638 'serv': entry['url'],
1648 'serv': entry['url'],
1639 'title': bleach.clean(desc, strip=True),
1649 'title': bleach.clean(desc, strip=True),
1640 'hovercard_url': hovercard_url
1650 'hovercard_url': hovercard_url
1641 }
1651 }
1642
1652
1643 if return_raw_data:
1653 if return_raw_data:
1644 return {
1654 return {
1645 'id': issue_id,
1655 'id': issue_id,
1646 'url': _url
1656 'url': _url
1647 }
1657 }
1648 return tmpl % data
1658 return tmpl % data
1649
1659
1650
1660
1651 def get_active_pattern_entries(repo_name):
1661 def get_active_pattern_entries(repo_name):
1652 repo = None
1662 repo = None
1653 if repo_name:
1663 if repo_name:
1654 # Retrieving repo_name to avoid invalid repo_name to explode on
1664 # Retrieving repo_name to avoid invalid repo_name to explode on
1655 # IssueTrackerSettingsModel but still passing invalid name further down
1665 # IssueTrackerSettingsModel but still passing invalid name further down
1656 repo = Repository.get_by_repo_name(repo_name, cache=True)
1666 repo = Repository.get_by_repo_name(repo_name, cache=True)
1657
1667
1658 settings_model = IssueTrackerSettingsModel(repo=repo)
1668 settings_model = IssueTrackerSettingsModel(repo=repo)
1659 active_entries = settings_model.get_settings(cache=True)
1669 active_entries = settings_model.get_settings(cache=True)
1660 return active_entries
1670 return active_entries
1661
1671
1662
1672
1663 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1673 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1664
1674
1665 allowed_link_formats = [
1675 allowed_link_formats = [
1666 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1676 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1667
1677
1668 compile_cache = {
1678 compile_cache = {
1669
1679
1670 }
1680 }
1671
1681
1672
1682
1673 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1683 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1674
1684
1675 if link_format not in allowed_link_formats:
1685 if link_format not in allowed_link_formats:
1676 raise ValueError('Link format can be only one of:{} got {}'.format(
1686 raise ValueError('Link format can be only one of:{} got {}'.format(
1677 allowed_link_formats, link_format))
1687 allowed_link_formats, link_format))
1678 issues_data = []
1688 issues_data = []
1679 errors = []
1689 errors = []
1680 new_text = text_string
1690 new_text = text_string
1681
1691
1682 if active_entries is None:
1692 if active_entries is None:
1683 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1693 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1684 active_entries = get_active_pattern_entries(repo_name)
1694 active_entries = get_active_pattern_entries(repo_name)
1685
1695
1686 log.debug('Got %s pattern entries to process', len(active_entries))
1696 log.debug('Got %s pattern entries to process', len(active_entries))
1687
1697
1688 for uid, entry in active_entries.items():
1698 for uid, entry in active_entries.items():
1689
1699
1690 if not (entry['pat'] and entry['url']):
1700 if not (entry['pat'] and entry['url']):
1691 log.debug('skipping due to missing data')
1701 log.debug('skipping due to missing data')
1692 continue
1702 continue
1693
1703
1694 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1704 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1695 uid, entry['pat'], entry['url'], entry['pref'])
1705 uid, entry['pat'], entry['url'], entry['pref'])
1696
1706
1697 if entry.get('pat_compiled'):
1707 if entry.get('pat_compiled'):
1698 pattern = entry['pat_compiled']
1708 pattern = entry['pat_compiled']
1699 elif entry['pat'] in compile_cache:
1709 elif entry['pat'] in compile_cache:
1700 pattern = compile_cache[entry['pat']]
1710 pattern = compile_cache[entry['pat']]
1701 else:
1711 else:
1702 try:
1712 try:
1703 pattern = regex.compile(r'%s' % entry['pat'])
1713 pattern = regex.compile(r'%s' % entry['pat'])
1704 except regex.error as e:
1714 except regex.error as e:
1705 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1715 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1706 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1716 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1707 errors.append(regex_err)
1717 errors.append(regex_err)
1708 continue
1718 continue
1709 compile_cache[entry['pat']] = pattern
1719 compile_cache[entry['pat']] = pattern
1710
1720
1711 data_func = partial(
1721 data_func = partial(
1712 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1722 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1713 return_raw_data=True)
1723 return_raw_data=True)
1714
1724
1715 for match_obj in pattern.finditer(text_string):
1725 for match_obj in pattern.finditer(text_string):
1716 issues_data.append(data_func(match_obj))
1726 issues_data.append(data_func(match_obj))
1717
1727
1718 url_func = partial(
1728 url_func = partial(
1719 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1729 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1720 link_format=link_format)
1730 link_format=link_format)
1721
1731
1722 new_text = pattern.sub(url_func, new_text)
1732 new_text = pattern.sub(url_func, new_text)
1723 log.debug('processed prefix:uid `%s`', uid)
1733 log.debug('processed prefix:uid `%s`', uid)
1724
1734
1725 # finally use global replace, eg !123 -> pr-link, those will not catch
1735 # finally use global replace, eg !123 -> pr-link, those will not catch
1726 # if already similar pattern exists
1736 # if already similar pattern exists
1727 server_url = '${scheme}://${netloc}'
1737 server_url = '${scheme}://${netloc}'
1728 pr_entry = {
1738 pr_entry = {
1729 'pref': '!',
1739 'pref': '!',
1730 'url': server_url + '/_admin/pull-requests/${id}',
1740 'url': server_url + '/_admin/pull-requests/${id}',
1731 'desc': 'Pull Request !${id}',
1741 'desc': 'Pull Request !${id}',
1732 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1742 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1733 }
1743 }
1734 pr_url_func = partial(
1744 pr_url_func = partial(
1735 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1745 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1736 link_format=link_format+'+hovercard')
1746 link_format=link_format+'+hovercard')
1737 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1747 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1738 log.debug('processed !pr pattern')
1748 log.debug('processed !pr pattern')
1739
1749
1740 return new_text, issues_data, errors
1750 return new_text, issues_data, errors
1741
1751
1742
1752
1743 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1753 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1744 issues_container_callback=None, error_container=None):
1754 issues_container_callback=None, error_container=None):
1745 """
1755 """
1746 Parses given text message and makes proper links.
1756 Parses given text message and makes proper links.
1747 issues are linked to given issue-server, and rest is a commit link
1757 issues are linked to given issue-server, and rest is a commit link
1748 """
1758 """
1749
1759
1750 def escaper(_text):
1760 def escaper(_text):
1751 return _text.replace('<', '&lt;').replace('>', '&gt;')
1761 return _text.replace('<', '&lt;').replace('>', '&gt;')
1752
1762
1753 new_text = escaper(commit_text)
1763 new_text = escaper(commit_text)
1754
1764
1755 # extract http/https links and make them real urls
1765 # extract http/https links and make them real urls
1756 new_text = urlify_text(new_text, safe=False)
1766 new_text = urlify_text(new_text, safe=False)
1757
1767
1758 # urlify commits - extract commit ids and make link out of them, if we have
1768 # urlify commits - extract commit ids and make link out of them, if we have
1759 # the scope of repository present.
1769 # the scope of repository present.
1760 if repository:
1770 if repository:
1761 new_text = urlify_commits(new_text, repository)
1771 new_text = urlify_commits(new_text, repository)
1762
1772
1763 # process issue tracker patterns
1773 # process issue tracker patterns
1764 new_text, issues, errors = process_patterns(
1774 new_text, issues, errors = process_patterns(
1765 new_text, repository or '', active_entries=active_pattern_entries)
1775 new_text, repository or '', active_entries=active_pattern_entries)
1766
1776
1767 if issues_container_callback is not None:
1777 if issues_container_callback is not None:
1768 for issue in issues:
1778 for issue in issues:
1769 issues_container_callback(issue)
1779 issues_container_callback(issue)
1770
1780
1771 if error_container is not None:
1781 if error_container is not None:
1772 error_container.extend(errors)
1782 error_container.extend(errors)
1773
1783
1774 return literal(new_text)
1784 return literal(new_text)
1775
1785
1776
1786
1777 def render_binary(repo_name, file_obj):
1787 def render_binary(repo_name, file_obj):
1778 """
1788 """
1779 Choose how to render a binary file
1789 Choose how to render a binary file
1780 """
1790 """
1781
1791
1782 # unicode
1792 # unicode
1783 filename = file_obj.name
1793 filename = file_obj.name
1784
1794
1785 # images
1795 # images
1786 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1796 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1787 if fnmatch.fnmatch(filename, pat=ext):
1797 if fnmatch.fnmatch(filename, pat=ext):
1788 src = route_path(
1798 src = route_path(
1789 'repo_file_raw', repo_name=repo_name,
1799 'repo_file_raw', repo_name=repo_name,
1790 commit_id=file_obj.commit.raw_id,
1800 commit_id=file_obj.commit.raw_id,
1791 f_path=file_obj.path)
1801 f_path=file_obj.path)
1792
1802
1793 return literal(
1803 return literal(
1794 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1804 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1795
1805
1796
1806
1797 def renderer_from_filename(filename, exclude=None):
1807 def renderer_from_filename(filename, exclude=None):
1798 """
1808 """
1799 choose a renderer based on filename, this works only for text based files
1809 choose a renderer based on filename, this works only for text based files
1800 """
1810 """
1801
1811
1802 # ipython
1812 # ipython
1803 for ext in ['*.ipynb']:
1813 for ext in ['*.ipynb']:
1804 if fnmatch.fnmatch(filename, pat=ext):
1814 if fnmatch.fnmatch(filename, pat=ext):
1805 return 'jupyter'
1815 return 'jupyter'
1806
1816
1807 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1817 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1808 if is_markup:
1818 if is_markup:
1809 return is_markup
1819 return is_markup
1810 return None
1820 return None
1811
1821
1812
1822
1813 def render(source, renderer='rst', mentions=False, relative_urls=None,
1823 def render(source, renderer='rst', mentions=False, relative_urls=None,
1814 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1824 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1815
1825
1816 def maybe_convert_relative_links(html_source):
1826 def maybe_convert_relative_links(html_source):
1817 if relative_urls:
1827 if relative_urls:
1818 return relative_links(html_source, relative_urls)
1828 return relative_links(html_source, relative_urls)
1819 return html_source
1829 return html_source
1820
1830
1821 if renderer == 'plain':
1831 if renderer == 'plain':
1822 return literal(
1832 return literal(
1823 MarkupRenderer.plain(source, leading_newline=False))
1833 MarkupRenderer.plain(source, leading_newline=False))
1824
1834
1825 elif renderer == 'rst':
1835 elif renderer == 'rst':
1826 if repo_name:
1836 if repo_name:
1827 # process patterns on comments if we pass in repo name
1837 # process patterns on comments if we pass in repo name
1828 source, issues, errors = process_patterns(
1838 source, issues, errors = process_patterns(
1829 source, repo_name, link_format='rst',
1839 source, repo_name, link_format='rst',
1830 active_entries=active_pattern_entries)
1840 active_entries=active_pattern_entries)
1831 if issues_container_callback is not None:
1841 if issues_container_callback is not None:
1832 for issue in issues:
1842 for issue in issues:
1833 issues_container_callback(issue)
1843 issues_container_callback(issue)
1834
1844
1835 return literal(
1845 return literal(
1836 '<div class="rst-block">%s</div>' %
1846 '<div class="rst-block">%s</div>' %
1837 maybe_convert_relative_links(
1847 maybe_convert_relative_links(
1838 MarkupRenderer.rst(source, mentions=mentions)))
1848 MarkupRenderer.rst(source, mentions=mentions)))
1839
1849
1840 elif renderer == 'markdown':
1850 elif renderer == 'markdown':
1841 if repo_name:
1851 if repo_name:
1842 # process patterns on comments if we pass in repo name
1852 # process patterns on comments if we pass in repo name
1843 source, issues, errors = process_patterns(
1853 source, issues, errors = process_patterns(
1844 source, repo_name, link_format='markdown',
1854 source, repo_name, link_format='markdown',
1845 active_entries=active_pattern_entries)
1855 active_entries=active_pattern_entries)
1846 if issues_container_callback is not None:
1856 if issues_container_callback is not None:
1847 for issue in issues:
1857 for issue in issues:
1848 issues_container_callback(issue)
1858 issues_container_callback(issue)
1849
1859
1850
1860
1851 return literal(
1861 return literal(
1852 '<div class="markdown-block">%s</div>' %
1862 '<div class="markdown-block">%s</div>' %
1853 maybe_convert_relative_links(
1863 maybe_convert_relative_links(
1854 MarkupRenderer.markdown(source, flavored=True,
1864 MarkupRenderer.markdown(source, flavored=True,
1855 mentions=mentions)))
1865 mentions=mentions)))
1856
1866
1857 elif renderer == 'jupyter':
1867 elif renderer == 'jupyter':
1858 return literal(
1868 return literal(
1859 '<div class="ipynb">%s</div>' %
1869 '<div class="ipynb">%s</div>' %
1860 maybe_convert_relative_links(
1870 maybe_convert_relative_links(
1861 MarkupRenderer.jupyter(source)))
1871 MarkupRenderer.jupyter(source)))
1862
1872
1863 # None means just show the file-source
1873 # None means just show the file-source
1864 return None
1874 return None
1865
1875
1866
1876
1867 def commit_status(repo, commit_id):
1877 def commit_status(repo, commit_id):
1868 return ChangesetStatusModel().get_status(repo, commit_id)
1878 return ChangesetStatusModel().get_status(repo, commit_id)
1869
1879
1870
1880
1871 def commit_status_lbl(commit_status):
1881 def commit_status_lbl(commit_status):
1872 return dict(ChangesetStatus.STATUSES).get(commit_status)
1882 return dict(ChangesetStatus.STATUSES).get(commit_status)
1873
1883
1874
1884
1875 def commit_time(repo_name, commit_id):
1885 def commit_time(repo_name, commit_id):
1876 repo = Repository.get_by_repo_name(repo_name)
1886 repo = Repository.get_by_repo_name(repo_name)
1877 commit = repo.get_commit(commit_id=commit_id)
1887 commit = repo.get_commit(commit_id=commit_id)
1878 return commit.date
1888 return commit.date
1879
1889
1880
1890
1881 def get_permission_name(key):
1891 def get_permission_name(key):
1882 return dict(Permission.PERMS).get(key)
1892 return dict(Permission.PERMS).get(key)
1883
1893
1884
1894
1885 def journal_filter_help(request):
1895 def journal_filter_help(request):
1886 _ = request.translate
1896 _ = request.translate
1887 from rhodecode.lib.audit_logger import ACTIONS
1897 from rhodecode.lib.audit_logger import ACTIONS
1888 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1898 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1889
1899
1890 return _(
1900 return _(
1891 'Example filter terms:\n' +
1901 'Example filter terms:\n' +
1892 ' repository:vcs\n' +
1902 ' repository:vcs\n' +
1893 ' username:marcin\n' +
1903 ' username:marcin\n' +
1894 ' username:(NOT marcin)\n' +
1904 ' username:(NOT marcin)\n' +
1895 ' action:*push*\n' +
1905 ' action:*push*\n' +
1896 ' ip:127.0.0.1\n' +
1906 ' ip:127.0.0.1\n' +
1897 ' date:20120101\n' +
1907 ' date:20120101\n' +
1898 ' date:[20120101100000 TO 20120102]\n' +
1908 ' date:[20120101100000 TO 20120102]\n' +
1899 '\n' +
1909 '\n' +
1900 'Actions: {actions}\n' +
1910 'Actions: {actions}\n' +
1901 '\n' +
1911 '\n' +
1902 'Generate wildcards using \'*\' character:\n' +
1912 'Generate wildcards using \'*\' character:\n' +
1903 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1913 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1904 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1914 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1905 '\n' +
1915 '\n' +
1906 'Optional AND / OR operators in queries\n' +
1916 'Optional AND / OR operators in queries\n' +
1907 ' "repository:vcs OR repository:test"\n' +
1917 ' "repository:vcs OR repository:test"\n' +
1908 ' "username:test AND repository:test*"\n'
1918 ' "username:test AND repository:test*"\n'
1909 ).format(actions=actions)
1919 ).format(actions=actions)
1910
1920
1911
1921
1912 def not_mapped_error(repo_name):
1922 def not_mapped_error(repo_name):
1913 from rhodecode.translation import _
1923 from rhodecode.translation import _
1914 flash(_('%s repository is not mapped to db perhaps'
1924 flash(_('%s repository is not mapped to db perhaps'
1915 ' it was created or renamed from the filesystem'
1925 ' it was created or renamed from the filesystem'
1916 ' please run the application again'
1926 ' please run the application again'
1917 ' in order to rescan repositories') % repo_name, category='error')
1927 ' in order to rescan repositories') % repo_name, category='error')
1918
1928
1919
1929
1920 def ip_range(ip_addr):
1930 def ip_range(ip_addr):
1921 from rhodecode.model.db import UserIpMap
1931 from rhodecode.model.db import UserIpMap
1922 s, e = UserIpMap._get_ip_range(ip_addr)
1932 s, e = UserIpMap._get_ip_range(ip_addr)
1923 return '%s - %s' % (s, e)
1933 return '%s - %s' % (s, e)
1924
1934
1925
1935
1926 def form(url, method='post', needs_csrf_token=True, **attrs):
1936 def form(url, method='post', needs_csrf_token=True, **attrs):
1927 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1937 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1928 if method.lower() != 'get' and needs_csrf_token:
1938 if method.lower() != 'get' and needs_csrf_token:
1929 raise Exception(
1939 raise Exception(
1930 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1940 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1931 'CSRF token. If the endpoint does not require such token you can ' +
1941 'CSRF token. If the endpoint does not require such token you can ' +
1932 'explicitly set the parameter needs_csrf_token to false.')
1942 'explicitly set the parameter needs_csrf_token to false.')
1933
1943
1934 return insecure_form(url, method=method, **attrs)
1944 return insecure_form(url, method=method, **attrs)
1935
1945
1936
1946
1937 def secure_form(form_url, method="POST", multipart=False, **attrs):
1947 def secure_form(form_url, method="POST", multipart=False, **attrs):
1938 """Start a form tag that points the action to an url. This
1948 """Start a form tag that points the action to an url. This
1939 form tag will also include the hidden field containing
1949 form tag will also include the hidden field containing
1940 the auth token.
1950 the auth token.
1941
1951
1942 The url options should be given either as a string, or as a
1952 The url options should be given either as a string, or as a
1943 ``url()`` function. The method for the form defaults to POST.
1953 ``url()`` function. The method for the form defaults to POST.
1944
1954
1945 Options:
1955 Options:
1946
1956
1947 ``multipart``
1957 ``multipart``
1948 If set to True, the enctype is set to "multipart/form-data".
1958 If set to True, the enctype is set to "multipart/form-data".
1949 ``method``
1959 ``method``
1950 The method to use when submitting the form, usually either
1960 The method to use when submitting the form, usually either
1951 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1961 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1952 hidden input with name _method is added to simulate the verb
1962 hidden input with name _method is added to simulate the verb
1953 over POST.
1963 over POST.
1954
1964
1955 """
1965 """
1956
1966
1957 if 'request' in attrs:
1967 if 'request' in attrs:
1958 session = attrs['request'].session
1968 session = attrs['request'].session
1959 del attrs['request']
1969 del attrs['request']
1960 else:
1970 else:
1961 raise ValueError(
1971 raise ValueError(
1962 'Calling this form requires request= to be passed as argument')
1972 'Calling this form requires request= to be passed as argument')
1963
1973
1964 _form = insecure_form(form_url, method, multipart, **attrs)
1974 _form = insecure_form(form_url, method, multipart, **attrs)
1965 token = literal(
1975 token = literal(
1966 '<input type="hidden" name="{}" value="{}">'.format(
1976 '<input type="hidden" name="{}" value="{}">'.format(
1967 csrf_token_key, get_csrf_token(session)))
1977 csrf_token_key, get_csrf_token(session)))
1968
1978
1969 return literal("%s\n%s" % (_form, token))
1979 return literal("%s\n%s" % (_form, token))
1970
1980
1971
1981
1972 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1982 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1973 select_html = select(name, selected, options, **attrs)
1983 select_html = select(name, selected, options, **attrs)
1974
1984
1975 select2 = """
1985 select2 = """
1976 <script>
1986 <script>
1977 $(document).ready(function() {
1987 $(document).ready(function() {
1978 $('#%s').select2({
1988 $('#%s').select2({
1979 containerCssClass: 'drop-menu %s',
1989 containerCssClass: 'drop-menu %s',
1980 dropdownCssClass: 'drop-menu-dropdown',
1990 dropdownCssClass: 'drop-menu-dropdown',
1981 dropdownAutoWidth: true%s
1991 dropdownAutoWidth: true%s
1982 });
1992 });
1983 });
1993 });
1984 </script>
1994 </script>
1985 """
1995 """
1986
1996
1987 filter_option = """,
1997 filter_option = """,
1988 minimumResultsForSearch: -1
1998 minimumResultsForSearch: -1
1989 """
1999 """
1990 input_id = attrs.get('id') or name
2000 input_id = attrs.get('id') or name
1991 extra_classes = ' '.join(attrs.pop('extra_classes', []))
2001 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1992 filter_enabled = "" if enable_filter else filter_option
2002 filter_enabled = "" if enable_filter else filter_option
1993 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
2003 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1994
2004
1995 return literal(select_html+select_script)
2005 return literal(select_html+select_script)
1996
2006
1997
2007
1998 def get_visual_attr(tmpl_context_var, attr_name):
2008 def get_visual_attr(tmpl_context_var, attr_name):
1999 """
2009 """
2000 A safe way to get a variable from visual variable of template context
2010 A safe way to get a variable from visual variable of template context
2001
2011
2002 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2012 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
2003 :param attr_name: name of the attribute we fetch from the c.visual
2013 :param attr_name: name of the attribute we fetch from the c.visual
2004 """
2014 """
2005 visual = getattr(tmpl_context_var, 'visual', None)
2015 visual = getattr(tmpl_context_var, 'visual', None)
2006 if not visual:
2016 if not visual:
2007 return
2017 return
2008 else:
2018 else:
2009 return getattr(visual, attr_name, None)
2019 return getattr(visual, attr_name, None)
2010
2020
2011
2021
2012 def get_last_path_part(file_node):
2022 def get_last_path_part(file_node):
2013 if not file_node.path:
2023 if not file_node.path:
2014 return u'/'
2024 return u'/'
2015
2025
2016 path = safe_unicode(file_node.path.split('/')[-1])
2026 path = safe_unicode(file_node.path.split('/')[-1])
2017 return u'../' + path
2027 return u'../' + path
2018
2028
2019
2029
2020 def route_url(*args, **kwargs):
2030 def route_url(*args, **kwargs):
2021 """
2031 """
2022 Wrapper around pyramids `route_url` (fully qualified url) function.
2032 Wrapper around pyramids `route_url` (fully qualified url) function.
2023 """
2033 """
2024 req = get_current_request()
2034 req = get_current_request()
2025 return req.route_url(*args, **kwargs)
2035 return req.route_url(*args, **kwargs)
2026
2036
2027
2037
2028 def route_path(*args, **kwargs):
2038 def route_path(*args, **kwargs):
2029 """
2039 """
2030 Wrapper around pyramids `route_path` function.
2040 Wrapper around pyramids `route_path` function.
2031 """
2041 """
2032 req = get_current_request()
2042 req = get_current_request()
2033 return req.route_path(*args, **kwargs)
2043 return req.route_path(*args, **kwargs)
2034
2044
2035
2045
2036 def route_path_or_none(*args, **kwargs):
2046 def route_path_or_none(*args, **kwargs):
2037 try:
2047 try:
2038 return route_path(*args, **kwargs)
2048 return route_path(*args, **kwargs)
2039 except KeyError:
2049 except KeyError:
2040 return None
2050 return None
2041
2051
2042
2052
2043 def current_route_path(request, **kw):
2053 def current_route_path(request, **kw):
2044 new_args = request.GET.mixed()
2054 new_args = request.GET.mixed()
2045 new_args.update(kw)
2055 new_args.update(kw)
2046 return request.current_route_path(_query=new_args)
2056 return request.current_route_path(_query=new_args)
2047
2057
2048
2058
2049 def curl_api_example(method, args):
2059 def curl_api_example(method, args):
2050 args_json = json.dumps(OrderedDict([
2060 args_json = json.dumps(OrderedDict([
2051 ('id', 1),
2061 ('id', 1),
2052 ('auth_token', 'SECRET'),
2062 ('auth_token', 'SECRET'),
2053 ('method', method),
2063 ('method', method),
2054 ('args', args)
2064 ('args', args)
2055 ]))
2065 ]))
2056
2066
2057 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2067 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2058 api_url=route_url('apiv2'),
2068 api_url=route_url('apiv2'),
2059 args_json=args_json
2069 args_json=args_json
2060 )
2070 )
2061
2071
2062
2072
2063 def api_call_example(method, args):
2073 def api_call_example(method, args):
2064 """
2074 """
2065 Generates an API call example via CURL
2075 Generates an API call example via CURL
2066 """
2076 """
2067 curl_call = curl_api_example(method, args)
2077 curl_call = curl_api_example(method, args)
2068
2078
2069 return literal(
2079 return literal(
2070 curl_call +
2080 curl_call +
2071 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2081 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2072 "and needs to be of `api calls` role."
2082 "and needs to be of `api calls` role."
2073 .format(token_url=route_url('my_account_auth_tokens')))
2083 .format(token_url=route_url('my_account_auth_tokens')))
2074
2084
2075
2085
2076 def notification_description(notification, request):
2086 def notification_description(notification, request):
2077 """
2087 """
2078 Generate notification human readable description based on notification type
2088 Generate notification human readable description based on notification type
2079 """
2089 """
2080 from rhodecode.model.notification import NotificationModel
2090 from rhodecode.model.notification import NotificationModel
2081 return NotificationModel().make_description(
2091 return NotificationModel().make_description(
2082 notification, translate=request.translate)
2092 notification, translate=request.translate)
2083
2093
2084
2094
2085 def go_import_header(request, db_repo=None):
2095 def go_import_header(request, db_repo=None):
2086 """
2096 """
2087 Creates a header for go-import functionality in Go Lang
2097 Creates a header for go-import functionality in Go Lang
2088 """
2098 """
2089
2099
2090 if not db_repo:
2100 if not db_repo:
2091 return
2101 return
2092 if 'go-get' not in request.GET:
2102 if 'go-get' not in request.GET:
2093 return
2103 return
2094
2104
2095 clone_url = db_repo.clone_url()
2105 clone_url = db_repo.clone_url()
2096 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2106 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2097 # we have a repo and go-get flag,
2107 # we have a repo and go-get flag,
2098 return literal('<meta name="go-import" content="{} {} {}">'.format(
2108 return literal('<meta name="go-import" content="{} {} {}">'.format(
2099 prefix, db_repo.repo_type, clone_url))
2109 prefix, db_repo.repo_type, clone_url))
2100
2110
2101
2111
2102 def reviewer_as_json(*args, **kwargs):
2112 def reviewer_as_json(*args, **kwargs):
2103 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2113 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2104 return _reviewer_as_json(*args, **kwargs)
2114 return _reviewer_as_json(*args, **kwargs)
2105
2115
2106
2116
2107 def get_repo_view_type(request):
2117 def get_repo_view_type(request):
2108 route_name = request.matched_route.name
2118 route_name = request.matched_route.name
2109 route_to_view_type = {
2119 route_to_view_type = {
2110 'repo_changelog': 'commits',
2120 'repo_changelog': 'commits',
2111 'repo_commits': 'commits',
2121 'repo_commits': 'commits',
2112 'repo_files': 'files',
2122 'repo_files': 'files',
2113 'repo_summary': 'summary',
2123 'repo_summary': 'summary',
2114 'repo_commit': 'commit'
2124 'repo_commit': 'commit'
2115 }
2125 }
2116
2126
2117 return route_to_view_type.get(route_name)
2127 return route_to_view_type.get(route_name)
2118
2128
2119
2129
2120 def is_active(menu_entry, selected):
2130 def is_active(menu_entry, selected):
2121 """
2131 """
2122 Returns active class for selecting menus in templates
2132 Returns active class for selecting menus in templates
2123 <li class=${h.is_active('settings', current_active)}></li>
2133 <li class=${h.is_active('settings', current_active)}></li>
2124 """
2134 """
2125 if not isinstance(menu_entry, list):
2135 if not isinstance(menu_entry, list):
2126 menu_entry = [menu_entry]
2136 menu_entry = [menu_entry]
2127
2137
2128 if selected in menu_entry:
2138 if selected in menu_entry:
2129 return "active"
2139 return "active"
2130
2140
2131
2141
2132 class IssuesRegistry(object):
2142 class IssuesRegistry(object):
2133 """
2143 """
2134 issue_registry = IssuesRegistry()
2144 issue_registry = IssuesRegistry()
2135 some_func(issues_callback=issues_registry(...))
2145 some_func(issues_callback=issues_registry(...))
2136 """
2146 """
2137
2147
2138 def __init__(self):
2148 def __init__(self):
2139 self.issues = []
2149 self.issues = []
2140 self.unique_issues = collections.defaultdict(lambda: [])
2150 self.unique_issues = collections.defaultdict(lambda: [])
2141
2151
2142 def __call__(self, commit_dict=None):
2152 def __call__(self, commit_dict=None):
2143 def callback(issue):
2153 def callback(issue):
2144 if commit_dict and issue:
2154 if commit_dict and issue:
2145 issue['commit'] = commit_dict
2155 issue['commit'] = commit_dict
2146 self.issues.append(issue)
2156 self.issues.append(issue)
2147 self.unique_issues[issue['id']].append(issue)
2157 self.unique_issues[issue['id']].append(issue)
2148 return callback
2158 return callback
2149
2159
2150 def get_issues(self):
2160 def get_issues(self):
2151 return self.issues
2161 return self.issues
2152
2162
2153 @property
2163 @property
2154 def issues_unique_count(self):
2164 def issues_unique_count(self):
2155 return len(set(i['id'] for i in self.issues))
2165 return len(set(i['id'] for i in self.issues))
@@ -1,398 +1,398 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import io
20 import io
21 import shlex
21 import shlex
22
22
23 import math
23 import math
24 import re
24 import re
25 import os
25 import os
26 import datetime
26 import datetime
27 import logging
27 import logging
28 import queue
28 import queue
29 import subprocess
29 import subprocess
30
30
31
31
32 from dateutil.parser import parse
32 from dateutil.parser import parse
33 from pyramid.threadlocal import get_current_request
33 from pyramid.threadlocal import get_current_request
34 from pyramid.interfaces import IRoutesMapper
34 from pyramid.interfaces import IRoutesMapper
35 from pyramid.settings import asbool
35 from pyramid.settings import asbool
36 from pyramid.path import AssetResolver
36 from pyramid.path import AssetResolver
37 from threading import Thread
37 from threading import Thread
38
38
39 from rhodecode.config.jsroutes import generate_jsroutes_content
39 from rhodecode.config.jsroutes import generate_jsroutes_content
40 from rhodecode.lib.base import get_auth_user
40 from rhodecode.lib.base import get_auth_user
41
41
42 import rhodecode
42 import rhodecode
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 def add_renderer_globals(event):
48 def add_renderer_globals(event):
49 from rhodecode.lib import helpers
49 from rhodecode.lib import helpers
50
50
51 # TODO: When executed in pyramid view context the request is not available
51 # TODO: When executed in pyramid view context the request is not available
52 # in the event. Find a better solution to get the request.
52 # in the event. Find a better solution to get the request.
53 request = event['request'] or get_current_request()
53 request = event['request'] or get_current_request()
54
54
55 # Add Pyramid translation as '_' to context
55 # Add Pyramid translation as '_' to context
56 event['_'] = request.translate
56 event['_'] = request.translate
57 event['_ungettext'] = request.plularize
57 event['_ungettext'] = request.plularize
58 event['h'] = helpers
58 event['h'] = helpers
59
59
60
60
61 def set_user_lang(event):
61 def set_user_lang(event):
62 request = event.request
62 request = event.request
63 cur_user = getattr(request, 'user', None)
63 cur_user = getattr(request, 'user', None)
64
64
65 if cur_user:
65 if cur_user:
66 user_lang = cur_user.get_instance().user_data.get('language')
66 user_lang = cur_user.get_instance().user_data.get('language')
67 if user_lang:
67 if user_lang:
68 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
68 log.debug('lang: setting current user:%s language to: %s', cur_user, user_lang)
69 event.request._LOCALE_ = user_lang
69 event.request._LOCALE_ = user_lang
70
70
71
71
72 def update_celery_conf(event):
72 def update_celery_conf(event):
73 from rhodecode.lib.celerylib.loader import set_celery_conf
73 from rhodecode.lib.celerylib.loader import set_celery_conf
74 log.debug('Setting celery config from new request')
74 log.debug('Setting celery config from new request')
75 set_celery_conf(request=event.request, registry=event.request.registry)
75 set_celery_conf(request=event.request, registry=event.request.registry)
76
76
77
77
78 def add_request_user_context(event):
78 def add_request_user_context(event):
79 """
79 """
80 Adds auth user into request context
80 Adds auth user into request context
81 """
81 """
82 request = event.request
82 request = event.request
83 # access req_id as soon as possible
83 # access req_id as soon as possible
84 req_id = request.req_id
84 req_id = request.req_id
85
85
86 if hasattr(request, 'vcs_call'):
86 if hasattr(request, 'vcs_call'):
87 # skip vcs calls
87 # skip vcs calls
88 return
88 return
89
89
90 if hasattr(request, 'rpc_method'):
90 if hasattr(request, 'rpc_method'):
91 # skip api calls
91 # skip api calls
92 return
92 return
93
93
94 auth_user, auth_token = get_auth_user(request)
94 auth_user, auth_token = get_auth_user(request)
95 request.user = auth_user
95 request.user = auth_user
96 request.user_auth_token = auth_token
96 request.user_auth_token = auth_token
97 request.environ['rc_auth_user'] = auth_user
97 request.environ['rc_auth_user'] = auth_user
98 request.environ['rc_auth_user_id'] = auth_user.user_id
98 request.environ['rc_auth_user_id'] = auth_user.user_id
99 request.environ['rc_req_id'] = req_id
99 request.environ['rc_req_id'] = req_id
100
100
101
101
102 def reset_log_bucket(event):
102 def reset_log_bucket(event):
103 """
103 """
104 reset the log bucket on new request
104 reset the log bucket on new request
105 """
105 """
106 request = event.request
106 request = event.request
107 request.req_id_records_init()
107 request.req_id_records_init()
108
108
109
109
110 def scan_repositories_if_enabled(event):
110 def scan_repositories_if_enabled(event):
111 """
111 """
112 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
112 This is subscribed to the `pyramid.events.ApplicationCreated` event. It
113 does a repository scan if enabled in the settings.
113 does a repository scan if enabled in the settings.
114 """
114 """
115 settings = event.app.registry.settings
115 settings = event.app.registry.settings
116 vcs_server_enabled = settings['vcs.server.enable']
116 vcs_server_enabled = settings['vcs.server.enable']
117 import_on_startup = settings['startup.import_repos']
117 import_on_startup = settings['startup.import_repos']
118 if vcs_server_enabled and import_on_startup:
118 if vcs_server_enabled and import_on_startup:
119 from rhodecode.model.scm import ScmModel
119 from rhodecode.model.scm import ScmModel
120 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
120 from rhodecode.lib.utils import repo2db_mapper, get_rhodecode_base_path
121 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
121 repositories = ScmModel().repo_scan(get_rhodecode_base_path())
122 repo2db_mapper(repositories, remove_obsolete=False)
122 repo2db_mapper(repositories, remove_obsolete=False)
123
123
124
124
125 def write_metadata_if_needed(event):
125 def write_metadata_if_needed(event):
126 """
126 """
127 Writes upgrade metadata
127 Writes upgrade metadata
128 """
128 """
129 import rhodecode
129 import rhodecode
130 from rhodecode.lib import system_info
130 from rhodecode.lib import system_info
131 from rhodecode.lib import ext_json
131 from rhodecode.lib import ext_json
132
132
133 fname = '.rcmetadata.json'
133 fname = '.rcmetadata.json'
134 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
134 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
135 metadata_destination = os.path.join(ini_loc, fname)
135 metadata_destination = os.path.join(ini_loc, fname)
136
136
137 def get_update_age():
137 def get_update_age():
138 now = datetime.datetime.utcnow()
138 now = datetime.datetime.utcnow()
139
139
140 with open(metadata_destination, 'rb') as f:
140 with open(metadata_destination, 'rb') as f:
141 data = ext_json.json.loads(f.read())
141 data = ext_json.json.loads(f.read())
142 if 'created_on' in data:
142 if 'created_on' in data:
143 update_date = parse(data['created_on'])
143 update_date = parse(data['created_on'])
144 diff = now - update_date
144 diff = now - update_date
145 return diff.total_seconds() / 60.0
145 return diff.total_seconds() / 60.0
146
146
147 return 0
147 return 0
148
148
149 def write():
149 def write():
150 configuration = system_info.SysInfo(
150 configuration = system_info.SysInfo(
151 system_info.rhodecode_config)()['value']
151 system_info.rhodecode_config)()['value']
152 license_token = configuration['config']['license_token']
152 license_token = configuration['config']['license_token']
153
153
154 setup = dict(
154 setup = dict(
155 workers=configuration['config']['server:main'].get(
155 workers=configuration['config']['server:main'].get(
156 'workers', '?'),
156 'workers', '?'),
157 worker_type=configuration['config']['server:main'].get(
157 worker_type=configuration['config']['server:main'].get(
158 'worker_class', 'sync'),
158 'worker_class', 'sync'),
159 )
159 )
160 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
160 dbinfo = system_info.SysInfo(system_info.database_info)()['value']
161 del dbinfo['url']
161 del dbinfo['url']
162
162
163 metadata = dict(
163 metadata = dict(
164 desc='upgrade metadata info',
164 desc='upgrade metadata info',
165 license_token=license_token,
165 license_token=license_token,
166 created_on=datetime.datetime.utcnow().isoformat(),
166 created_on=datetime.datetime.utcnow().isoformat(),
167 usage=system_info.SysInfo(system_info.usage_info)()['value'],
167 usage=system_info.SysInfo(system_info.usage_info)()['value'],
168 platform=system_info.SysInfo(system_info.platform_type)()['value'],
168 platform=system_info.SysInfo(system_info.platform_type)()['value'],
169 database=dbinfo,
169 database=dbinfo,
170 cpu=system_info.SysInfo(system_info.cpu)()['value'],
170 cpu=system_info.SysInfo(system_info.cpu)()['value'],
171 memory=system_info.SysInfo(system_info.memory)()['value'],
171 memory=system_info.SysInfo(system_info.memory)()['value'],
172 setup=setup
172 setup=setup
173 )
173 )
174
174
175 with open(metadata_destination, 'wb') as f:
175 with open(metadata_destination, 'wb') as f:
176 f.write(ext_json.json.dumps(metadata))
176 f.write(ext_json.json.dumps(metadata))
177
177
178 settings = event.app.registry.settings
178 settings = event.app.registry.settings
179 if settings.get('metadata.skip'):
179 if settings.get('metadata.skip'):
180 return
180 return
181
181
182 # only write this every 24h, workers restart caused unwanted delays
182 # only write this every 24h, workers restart caused unwanted delays
183 try:
183 try:
184 age_in_min = get_update_age()
184 age_in_min = get_update_age()
185 except Exception:
185 except Exception:
186 age_in_min = 0
186 age_in_min = 0
187
187
188 if age_in_min > 60 * 60 * 24:
188 if age_in_min > 60 * 60 * 24:
189 return
189 return
190
190
191 try:
191 try:
192 write()
192 write()
193 except Exception:
193 except Exception:
194 pass
194 pass
195
195
196
196
197 def write_usage_data(event):
197 def write_usage_data(event):
198 import rhodecode
198 import rhodecode
199 from rhodecode.lib import system_info
199 from rhodecode.lib import system_info
200 from rhodecode.lib import ext_json
200 from rhodecode.lib import ext_json
201
201
202 settings = event.app.registry.settings
202 settings = event.app.registry.settings
203 instance_tag = settings.get('metadata.write_usage_tag')
203 instance_tag = settings.get('metadata.write_usage_tag')
204 if not settings.get('metadata.write_usage'):
204 if not settings.get('metadata.write_usage'):
205 return
205 return
206
206
207 def get_update_age(dest_file):
207 def get_update_age(dest_file):
208 now = datetime.datetime.utcnow()
208 now = datetime.datetime.utcnow()
209
209
210 with open(dest_file, 'rb') as f:
210 with open(dest_file, 'rb') as f:
211 data = ext_json.json.loads(f.read())
211 data = ext_json.json.loads(f.read())
212 if 'created_on' in data:
212 if 'created_on' in data:
213 update_date = parse(data['created_on'])
213 update_date = parse(data['created_on'])
214 diff = now - update_date
214 diff = now - update_date
215 return math.ceil(diff.total_seconds() / 60.0)
215 return math.ceil(diff.total_seconds() / 60.0)
216
216
217 return 0
217 return 0
218
218
219 utc_date = datetime.datetime.utcnow()
219 utc_date = datetime.datetime.utcnow()
220 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
220 hour_quarter = int(math.ceil((utc_date.hour + utc_date.minute/60.0) / 6.))
221 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
221 fname = '.rc_usage_{date.year}{date.month:02d}{date.day:02d}_{hour}.json'.format(
222 date=utc_date, hour=hour_quarter)
222 date=utc_date, hour=hour_quarter)
223 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
223 ini_loc = os.path.dirname(rhodecode.CONFIG.get('__file__'))
224
224
225 usage_dir = os.path.join(ini_loc, '.rcusage')
225 usage_dir = os.path.join(ini_loc, '.rcusage')
226 if not os.path.isdir(usage_dir):
226 if not os.path.isdir(usage_dir):
227 os.makedirs(usage_dir)
227 os.makedirs(usage_dir)
228 usage_metadata_destination = os.path.join(usage_dir, fname)
228 usage_metadata_destination = os.path.join(usage_dir, fname)
229
229
230 try:
230 try:
231 age_in_min = get_update_age(usage_metadata_destination)
231 age_in_min = get_update_age(usage_metadata_destination)
232 except Exception:
232 except Exception:
233 age_in_min = 0
233 age_in_min = 0
234
234
235 # write every 6th hour
235 # write every 6th hour
236 if age_in_min and age_in_min < 60 * 6:
236 if age_in_min and age_in_min < 60 * 6:
237 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
237 log.debug('Usage file created %s minutes ago, skipping (threshold: %s minutes)...',
238 age_in_min, 60 * 6)
238 age_in_min, 60 * 6)
239 return
239 return
240
240
241 def write(dest_file):
241 def write(dest_file):
242 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
242 configuration = system_info.SysInfo(system_info.rhodecode_config)()['value']
243 license_token = configuration['config']['license_token']
243 license_token = configuration['config']['license_token']
244
244
245 metadata = dict(
245 metadata = dict(
246 desc='Usage data',
246 desc='Usage data',
247 instance_tag=instance_tag,
247 instance_tag=instance_tag,
248 license_token=license_token,
248 license_token=license_token,
249 created_on=datetime.datetime.utcnow().isoformat(),
249 created_on=datetime.datetime.utcnow().isoformat(),
250 usage=system_info.SysInfo(system_info.usage_info)()['value'],
250 usage=system_info.SysInfo(system_info.usage_info)()['value'],
251 )
251 )
252
252
253 with open(dest_file, 'wb') as f:
253 with open(dest_file, 'wb') as f:
254 f.write(ext_json.json.dumps(metadata, indent=2, sort_keys=True))
254 f.write(ext_json.formatted_json(metadata))
255
255
256 try:
256 try:
257 log.debug('Writing usage file at: %s', usage_metadata_destination)
257 log.debug('Writing usage file at: %s', usage_metadata_destination)
258 write(usage_metadata_destination)
258 write(usage_metadata_destination)
259 except Exception:
259 except Exception:
260 pass
260 pass
261
261
262
262
263 def write_js_routes_if_enabled(event):
263 def write_js_routes_if_enabled(event):
264 registry = event.app.registry
264 registry = event.app.registry
265
265
266 mapper = registry.queryUtility(IRoutesMapper)
266 mapper = registry.queryUtility(IRoutesMapper)
267 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
267 _argument_prog = re.compile(r'\{(.*?)\}|:\((.*)\)')
268
268
269 def _extract_route_information(route):
269 def _extract_route_information(route):
270 """
270 """
271 Convert a route into tuple(name, path, args), eg:
271 Convert a route into tuple(name, path, args), eg:
272 ('show_user', '/profile/%(username)s', ['username'])
272 ('show_user', '/profile/%(username)s', ['username'])
273 """
273 """
274
274
275 routepath = route.pattern
275 routepath = route.pattern
276 pattern = route.pattern
276 pattern = route.pattern
277
277
278 def replace(matchobj):
278 def replace(matchobj):
279 if matchobj.group(1):
279 if matchobj.group(1):
280 return "%%(%s)s" % matchobj.group(1).split(':')[0]
280 return "%%(%s)s" % matchobj.group(1).split(':')[0]
281 else:
281 else:
282 return "%%(%s)s" % matchobj.group(2)
282 return "%%(%s)s" % matchobj.group(2)
283
283
284 routepath = _argument_prog.sub(replace, routepath)
284 routepath = _argument_prog.sub(replace, routepath)
285
285
286 if not routepath.startswith('/'):
286 if not routepath.startswith('/'):
287 routepath = '/'+routepath
287 routepath = '/'+routepath
288
288
289 return (
289 return (
290 route.name,
290 route.name,
291 routepath,
291 routepath,
292 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
292 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
293 for arg in _argument_prog.findall(pattern)]
293 for arg in _argument_prog.findall(pattern)]
294 )
294 )
295
295
296 def get_routes():
296 def get_routes():
297 # pyramid routes
297 # pyramid routes
298 for route in mapper.get_routes():
298 for route in mapper.get_routes():
299 if not route.name.startswith('__'):
299 if not route.name.startswith('__'):
300 yield _extract_route_information(route)
300 yield _extract_route_information(route)
301
301
302 if asbool(registry.settings.get('generate_js_files', 'false')):
302 if asbool(registry.settings.get('generate_js_files', 'false')):
303 static_path = AssetResolver().resolve('rhodecode:public').abspath()
303 static_path = AssetResolver().resolve('rhodecode:public').abspath()
304 jsroutes = get_routes()
304 jsroutes = get_routes()
305 jsroutes_file_content = generate_jsroutes_content(jsroutes)
305 jsroutes_file_content = generate_jsroutes_content(jsroutes)
306 jsroutes_file_path = os.path.join(
306 jsroutes_file_path = os.path.join(
307 static_path, 'js', 'rhodecode', 'routes.js')
307 static_path, 'js', 'rhodecode', 'routes.js')
308
308
309 try:
309 try:
310 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
310 with io.open(jsroutes_file_path, 'w', encoding='utf-8') as f:
311 f.write(jsroutes_file_content)
311 f.write(jsroutes_file_content)
312 except Exception:
312 except Exception:
313 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
313 log.exception('Failed to write routes.js into %s', jsroutes_file_path)
314
314
315
315
316 class Subscriber(object):
316 class Subscriber(object):
317 """
317 """
318 Base class for subscribers to the pyramid event system.
318 Base class for subscribers to the pyramid event system.
319 """
319 """
320 def __call__(self, event):
320 def __call__(self, event):
321 self.run(event)
321 self.run(event)
322
322
323 def run(self, event):
323 def run(self, event):
324 raise NotImplementedError('Subclass has to implement this.')
324 raise NotImplementedError('Subclass has to implement this.')
325
325
326
326
327 class AsyncSubscriber(Subscriber):
327 class AsyncSubscriber(Subscriber):
328 """
328 """
329 Subscriber that handles the execution of events in a separate task to not
329 Subscriber that handles the execution of events in a separate task to not
330 block the execution of the code which triggers the event. It puts the
330 block the execution of the code which triggers the event. It puts the
331 received events into a queue from which the worker process takes them in
331 received events into a queue from which the worker process takes them in
332 order.
332 order.
333 """
333 """
334 def __init__(self):
334 def __init__(self):
335 self._stop = False
335 self._stop = False
336 self._eventq = queue.Queue()
336 self._eventq = queue.Queue()
337 self._worker = self.create_worker()
337 self._worker = self.create_worker()
338 self._worker.start()
338 self._worker.start()
339
339
340 def __call__(self, event):
340 def __call__(self, event):
341 self._eventq.put(event)
341 self._eventq.put(event)
342
342
343 def create_worker(self):
343 def create_worker(self):
344 worker = Thread(target=self.do_work)
344 worker = Thread(target=self.do_work)
345 worker.daemon = True
345 worker.daemon = True
346 return worker
346 return worker
347
347
348 def stop_worker(self):
348 def stop_worker(self):
349 self._stop = False
349 self._stop = False
350 self._eventq.put(None)
350 self._eventq.put(None)
351 self._worker.join()
351 self._worker.join()
352
352
353 def do_work(self):
353 def do_work(self):
354 while not self._stop:
354 while not self._stop:
355 event = self._eventq.get()
355 event = self._eventq.get()
356 if event is not None:
356 if event is not None:
357 self.run(event)
357 self.run(event)
358
358
359
359
360 class AsyncSubprocessSubscriber(AsyncSubscriber):
360 class AsyncSubprocessSubscriber(AsyncSubscriber):
361 """
361 """
362 Subscriber that uses the subprocess module to execute a command if an
362 Subscriber that uses the subprocess module to execute a command if an
363 event is received. Events are handled asynchronously::
363 event is received. Events are handled asynchronously::
364
364
365 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
365 subscriber = AsyncSubprocessSubscriber('ls -la', timeout=10)
366 subscriber(dummyEvent) # running __call__(event)
366 subscriber(dummyEvent) # running __call__(event)
367
367
368 """
368 """
369
369
370 def __init__(self, cmd, timeout=None):
370 def __init__(self, cmd, timeout=None):
371 if not isinstance(cmd, (list, tuple)):
371 if not isinstance(cmd, (list, tuple)):
372 cmd = shlex.split(cmd)
372 cmd = shlex.split(cmd)
373 super(AsyncSubprocessSubscriber, self).__init__()
373 super(AsyncSubprocessSubscriber, self).__init__()
374 self._cmd = cmd
374 self._cmd = cmd
375 self._timeout = timeout
375 self._timeout = timeout
376
376
377 def run(self, event):
377 def run(self, event):
378 cmd = self._cmd
378 cmd = self._cmd
379 timeout = self._timeout
379 timeout = self._timeout
380 log.debug('Executing command %s.', cmd)
380 log.debug('Executing command %s.', cmd)
381
381
382 try:
382 try:
383 output = subprocess.check_output(
383 output = subprocess.check_output(
384 cmd, timeout=timeout, stderr=subprocess.STDOUT)
384 cmd, timeout=timeout, stderr=subprocess.STDOUT)
385 log.debug('Command finished %s', cmd)
385 log.debug('Command finished %s', cmd)
386 if output:
386 if output:
387 log.debug('Command output: %s', output)
387 log.debug('Command output: %s', output)
388 except subprocess.TimeoutExpired as e:
388 except subprocess.TimeoutExpired as e:
389 log.exception('Timeout while executing command.')
389 log.exception('Timeout while executing command.')
390 if e.output:
390 if e.output:
391 log.error('Command output: %s', e.output)
391 log.error('Command output: %s', e.output)
392 except subprocess.CalledProcessError as e:
392 except subprocess.CalledProcessError as e:
393 log.exception('Error while executing command.')
393 log.exception('Error while executing command.')
394 if e.output:
394 if e.output:
395 log.error('Command output: %s', e.output)
395 log.error('Command output: %s', e.output)
396 except Exception:
396 except Exception:
397 log.exception(
397 log.exception(
398 'Exception while executing command %s.', cmd)
398 'Exception while executing command %s.', cmd)
@@ -1,115 +1,115 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="/base/base.mako"/>
2 <%inherit file="/base/base.mako"/>
3 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="base" file="/base/base.mako"/>
4
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('Admin audit log entry')}
6 ${_('Admin audit log entry')}
7 %if c.rhodecode_name:
7 %if c.rhodecode_name:
8 &middot; ${h.branding(c.rhodecode_name)}
8 &middot; ${h.branding(c.rhodecode_name)}
9 %endif
9 %endif
10 </%def>
10 </%def>
11
11
12 <%def name="breadcrumbs_links()"></%def>
12 <%def name="breadcrumbs_links()"></%def>
13
13
14 <%def name="menu_bar_nav()">
14 <%def name="menu_bar_nav()">
15 ${self.menu_items(active='admin')}
15 ${self.menu_items(active='admin')}
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_subnav()">
18 <%def name="menu_bar_subnav()">
19 ${self.admin_menu(active='audit_logs')}
19 ${self.admin_menu(active='audit_logs')}
20 </%def>
20 </%def>
21
21
22 <%def name="main()">
22 <%def name="main()">
23 <div class="box">
23 <div class="box">
24
24
25 <div class="title">
25 <div class="title">
26 ${_('Audit long entry')} ${c.audit_log_entry.entry_id}
26 ${_('Audit long entry')} ${c.audit_log_entry.entry_id}
27 </div>
27 </div>
28
28
29 <div class="table">
29 <div class="table">
30 <div id="user_log">
30 <div id="user_log">
31 <table class="rctable audit-log">
31 <table class="rctable audit-log">
32 <tr>
32 <tr>
33 <td>
33 <td>
34 ${_('User')}:
34 ${_('User')}:
35 </td>
35 </td>
36 <td>
36 <td>
37 %if c.audit_log_entry.user is not None:
37 %if c.audit_log_entry.user is not None:
38 ${base.gravatar_with_user(c.audit_log_entry.user.email)}
38 ${base.gravatar_with_user(c.audit_log_entry.user.email)}
39 %else:
39 %else:
40 ${c.audit_log_entry.username}
40 ${c.audit_log_entry.username}
41 %endif
41 %endif
42 </td>
42 </td>
43 </tr>
43 </tr>
44 <tr>
44 <tr>
45 <td>
45 <td>
46 ${_('Date')}:
46 ${_('Date')}:
47 </td>
47 </td>
48 <td>
48 <td>
49 ${h.format_date(c.audit_log_entry.action_date)}
49 ${h.format_date(c.audit_log_entry.action_date)}
50 </td>
50 </td>
51 </tr>
51 </tr>
52 <tr>
52 <tr>
53 <td>
53 <td>
54 ${_('IP')}:
54 ${_('IP')}:
55 </td>
55 </td>
56 <td>
56 <td>
57 ${c.audit_log_entry.user_ip}
57 ${c.audit_log_entry.user_ip}
58 </td>
58 </td>
59 </tr>
59 </tr>
60
60
61 <tr>
61 <tr>
62 <td>
62 <td>
63 ${_('Action')}:
63 ${_('Action')}:
64 </td>
64 </td>
65 <td>
65 <td>
66 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_1:
66 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_1:
67 ${h.action_parser(request, l)[0]()}
67 ${h.action_parser(request, l)[0]()}
68 % else:
68 % else:
69 ${h.literal(c.audit_log_entry.action)}
69 ${h.literal(c.audit_log_entry.action)}
70 % endif
70 % endif
71
71
72 <div class="journal_action_params">
72 <div class="journal_action_params">
73 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_1:
73 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_1:
74 ${h.literal(h.action_parser(request, l)[1]())}
74 ${h.literal(h.action_parser(request, l)[1]())}
75 % endif
75 % endif
76 </div>
76 </div>
77 </td>
77 </td>
78 </tr>
78 </tr>
79 <tr>
79 <tr>
80 <td>
80 <td>
81 ${_('Action Data')}:
81 ${_('Action Data')}:
82 </td>
82 </td>
83 <td class="td-journalaction">
83 <td class="td-journalaction">
84 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_2:
84 % if c.audit_log_entry.version == c.audit_log_entry.VERSION_2:
85 <div>
85 <div>
86 <pre>${h.json.dumps(c.audit_log_entry.action_data, indent=4, sort_keys=True)}</pre>
86 <pre>${h.formatted_str_json(c.audit_log_entry.action_data)}</pre>
87 </div>
87 </div>
88 % else:
88 % else:
89 <pre title="${_('data not available for v1 entries type')}">-</pre>
89 <pre title="${_('data not available for v1 entries type')}">-</pre>
90 % endif
90 % endif
91 </td>
91 </td>
92 </tr>
92 </tr>
93 <tr>
93 <tr>
94 <td>
94 <td>
95 ${_('Repository')}:
95 ${_('Repository')}:
96 </td>
96 </td>
97 <td class="td-journalaction">
97 <td class="td-journalaction">
98 %if c.audit_log_entry.repository is not None:
98 %if c.audit_log_entry.repository is not None:
99 ${h.link_to(c.audit_log_entry.repository.repo_name, h.route_path('repo_summary',repo_name=c.audit_log_entry.repository.repo_name))}
99 ${h.link_to(c.audit_log_entry.repository.repo_name, h.route_path('repo_summary',repo_name=c.audit_log_entry.repository.repo_name))}
100 %else:
100 %else:
101 ${c.audit_log_entry.repository_name or '-'}
101 ${c.audit_log_entry.repository_name or '-'}
102 %endif
102 %endif
103 </td>
103 </td>
104 </tr>
104 </tr>
105
105
106 </table>
106 </table>
107
107
108 </div>
108 </div>
109 </div>
109 </div>
110 </div>
110 </div>
111
111
112 <script>
112 <script>
113 $('#j_filter').autoGrowInput();
113 $('#j_filter').autoGrowInput();
114 </script>
114 </script>
115 </%def>
115 </%def>
@@ -1,69 +1,69 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 %if c.audit_logs:
3 %if c.audit_logs:
4 <table class="rctable admin_log">
4 <table class="rctable admin_log">
5 <tr>
5 <tr>
6 <th>${_('Uid')}</th>
6 <th>${_('Uid')}</th>
7 <th>${_('Username')}</th>
7 <th>${_('Username')}</th>
8 <th>${_('Action')}</th>
8 <th>${_('Action')}</th>
9 <th>${_('Action Data')}</th>
9 <th>${_('Action Data')}</th>
10 <th>${_('Repository')}</th>
10 <th>${_('Repository')}</th>
11 <th>${_('Date')}</th>
11 <th>${_('Date')}</th>
12 <th>${_('IP')}</th>
12 <th>${_('IP')}</th>
13 </tr>
13 </tr>
14
14
15 %for cnt,l in enumerate(c.audit_logs):
15 %for cnt,l in enumerate(c.audit_logs):
16 <tr class="parity${cnt%2}">
16 <tr class="parity${cnt%2}">
17 <td class="td-col">
17 <td class="td-col">
18 <a href="${h.route_path('admin_audit_log_entry', audit_log_id=l.entry_id)}">${l.entry_id}</a>
18 <a href="${h.route_path('admin_audit_log_entry', audit_log_id=l.entry_id)}">${l.entry_id}</a>
19 </td>
19 </td>
20 <td class="td-user">
20 <td class="td-user">
21 %if l.user is not None:
21 %if l.user is not None:
22 ${base.gravatar_with_user(l.user.email)}
22 ${base.gravatar_with_user(l.user.email)}
23 %else:
23 %else:
24 ${l.username}
24 ${l.username}
25 %endif
25 %endif
26 </td>
26 </td>
27 <td class="td-journalaction">
27 <td class="td-journalaction">
28 % if l.version == l.VERSION_1:
28 % if l.version == l.VERSION_1:
29 ${h.action_parser(request, l)[0]()}
29 ${h.action_parser(request, l)[0]()}
30 % else:
30 % else:
31 ${h.literal(l.action)}
31 ${h.literal(l.action)}
32 % endif
32 % endif
33
33
34 <div class="journal_action_params">
34 <div class="journal_action_params">
35 % if l.version == l.VERSION_1:
35 % if l.version == l.VERSION_1:
36 ${h.literal(h.action_parser(request, l)[1]())}
36 ${h.literal(h.action_parser(request, l)[1]())}
37 % endif
37 % endif
38 </div>
38 </div>
39 </td>
39 </td>
40 <td>
40 <td>
41 % if l.version == l.VERSION_2:
41 % if l.version == l.VERSION_2:
42 <a href="#" onclick="$('#entry-'+${l.user_log_id}).toggle();return false">${_('toggle')}</a>
42 <a href="#" onclick="$('#entry-'+${l.user_log_id}).toggle();return false">${_('toggle')}</a>
43 <div id="entry-${l.user_log_id}" style="display: none">
43 <div id="entry-${l.user_log_id}" style="display: none">
44 <pre>${h.json.dumps(l.action_data, indent=4, sort_keys=True)}</pre>
44 <pre>${h.formatted_str_json(l.action_data)}</pre>
45 </div>
45 </div>
46 % else:
46 % else:
47 <pre title="${_('data not available for v1 entries type')}">-</pre>
47 <pre title="${_('data not available for v1 entries type')}">-</pre>
48 % endif
48 % endif
49 </td>
49 </td>
50 <td class="td-componentname">
50 <td class="td-componentname">
51 %if l.repository is not None:
51 %if l.repository is not None:
52 ${h.link_to(l.repository.repo_name, h.route_path('repo_summary',repo_name=l.repository.repo_name))}
52 ${h.link_to(l.repository.repo_name, h.route_path('repo_summary',repo_name=l.repository.repo_name))}
53 %else:
53 %else:
54 ${l.repository_name}
54 ${l.repository_name}
55 %endif
55 %endif
56 </td>
56 </td>
57
57
58 <td class="td-time">${h.format_date(l.action_date)}</td>
58 <td class="td-time">${h.format_date(l.action_date)}</td>
59 <td class="td-ip">${l.user_ip}</td>
59 <td class="td-ip">${l.user_ip}</td>
60 </tr>
60 </tr>
61 %endfor
61 %endfor
62 </table>
62 </table>
63
63
64 <div class="pagination-wh pagination-left">
64 <div class="pagination-wh pagination-left">
65 ${c.audit_logs.render()}
65 ${c.audit_logs.render()}
66 </div>
66 </div>
67 %else:
67 %else:
68 ${_('No actions yet')}
68 ${_('No actions yet')}
69 %endif No newline at end of file
69 %endif
@@ -1,328 +1,328 b''
1 ## snippet for displaying issue tracker settings
1 ## snippet for displaying issue tracker settings
2 ## usage:
2 ## usage:
3 ## <%namespace name="its" file="/base/issue_tracker_settings.mako"/>
3 ## <%namespace name="its" file="/base/issue_tracker_settings.mako"/>
4 ## ${its.issue_tracker_settings_table(patterns, form_url, delete_url)}
4 ## ${its.issue_tracker_settings_table(patterns, form_url, delete_url)}
5 ## ${its.issue_tracker_settings_test(test_url)}
5 ## ${its.issue_tracker_settings_test(test_url)}
6
6
7 <%def name="issue_tracker_settings_table(patterns, form_url, delete_url)">
7 <%def name="issue_tracker_settings_table(patterns, form_url, delete_url)">
8 <%
8 <%
9 # Name/desc, pattern, issue prefix
9 # Name/desc, pattern, issue prefix
10 examples = [
10 examples = [
11 (
11 (
12 ' ',
12 ' ',
13 ' ',
13 ' ',
14 ' ',
14 ' ',
15 ' '
15 ' '
16 ),
16 ),
17
17
18 (
18 (
19 'Tickets with #123 (Redmine etc)',
19 'Tickets with #123 (Redmine etc)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>\d+)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>\d+)',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
21 'https://myissueserver.com/${repo}/issue/${issue_id}',
22 ''
22 ''
23 ),
23 ),
24
24
25 (
25 (
26 'Redmine - Alternative',
26 'Redmine - Alternative',
27 '(?:issue-)(\d+)',
27 '(?:issue-)(\d+)',
28 'https://myissueserver.com/redmine/issue/${id}',
28 'https://myissueserver.com/redmine/issue/${id}',
29 ''
29 ''
30 ),
30 ),
31
31
32 (
32 (
33 'Redmine - Wiki',
33 'Redmine - Wiki',
34 '(?:wiki-)([a-zA-Z0-9]+)',
34 '(?:wiki-)([a-zA-Z0-9]+)',
35 'http://example.org/projects/${repo_name}/wiki/${id}',
35 'http://example.org/projects/${repo_name}/wiki/${id}',
36 'wiki-'
36 'wiki-'
37 ),
37 ),
38
38
39 (
39 (
40 'JIRA - All tickets',
40 'JIRA - All tickets',
41 # official JIRA ticket pattern
41 # official JIRA ticket pattern
42 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>[A-Z]{1,6}-(?:[1-9][0-9]{0,7}))',
42 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>[A-Z]{1,6}-(?:[1-9][0-9]{0,7}))',
43 'https://myjira.com/browse/${issue_id}',
43 'https://myjira.com/browse/${issue_id}',
44 ''
44 ''
45 ),
45 ),
46
46
47 (
47 (
48 'JIRA - Single project (JRA-XXXXXXXX)',
48 'JIRA - Single project (JRA-XXXXXXXX)',
49 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>JRA-(?:[1-9][0-9]{0,7}))',
49 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>JRA-(?:[1-9][0-9]{0,7}))',
50 'https://myjira.com/${issue_id}',
50 'https://myjira.com/${issue_id}',
51 ''
51 ''
52 ),
52 ),
53
53
54 (
54 (
55 'Confluence WIKI',
55 'Confluence WIKI',
56 '(?:conf-)([A-Z0-9]+)',
56 '(?:conf-)([A-Z0-9]+)',
57 'https://example.atlassian.net/display/wiki/${id}/${repo_name}',
57 'https://example.atlassian.net/display/wiki/${id}/${repo_name}',
58 'CONF-',
58 'CONF-',
59 ),
59 ),
60
60
61 (
61 (
62 'Pivotal Tracker',
62 'Pivotal Tracker',
63 '(?:pivot-)(?P<project_id>\d+)-(?P<story>\d+)',
63 '(?:pivot-)(?P<project_id>\d+)-(?P<story>\d+)',
64 'https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}',
64 'https://www.pivotaltracker.com/s/projects/${project_id}/stories/${story}',
65 'PIV-',
65 'PIV-',
66 ),
66 ),
67
67
68 (
68 (
69 'Trello',
69 'Trello',
70 '(?:trello-)(?P<card_id>[a-zA-Z0-9]+)',
70 '(?:trello-)(?P<card_id>[a-zA-Z0-9]+)',
71 'https://trello.com/example.com/${card_id}',
71 'https://trello.com/example.com/${card_id}',
72 'TRELLO-',
72 'TRELLO-',
73 ),
73 ),
74 ]
74 ]
75 %>
75 %>
76
76
77 <table class="rctable issuetracker">
77 <table class="rctable issuetracker">
78 <tr>
78 <tr>
79 <th>${_('Description')}</th>
79 <th>${_('Description')}</th>
80 <th>${_('Pattern')}</th>
80 <th>${_('Pattern')}</th>
81 <th>${_('Url')}</th>
81 <th>${_('Url')}</th>
82 <th>${_('Extra Prefix')}</th>
82 <th>${_('Extra Prefix')}</th>
83 <th ></th>
83 <th ></th>
84 </tr>
84 </tr>
85 % for name, pat, url, pref in examples:
85 % for name, pat, url, pref in examples:
86 <tr class="it-examples" style="${'' if loop.index == 0 else 'display:none'}">
86 <tr class="it-examples" style="${'' if loop.index == 0 else 'display:none'}">
87 <td class="td-issue-tracker-name issue-tracker-example">${name}</td>
87 <td class="td-issue-tracker-name issue-tracker-example">${name}</td>
88 <td class="td-regex issue-tracker-example">${pat}</td>
88 <td class="td-regex issue-tracker-example">${pat}</td>
89 <td class="td-url issue-tracker-example">${url}</td>
89 <td class="td-url issue-tracker-example">${url}</td>
90 <td class="td-prefix issue-tracker-example">${pref}</td>
90 <td class="td-prefix issue-tracker-example">${pref}</td>
91 <td>
91 <td>
92 % if loop.index == 0:
92 % if loop.index == 0:
93 <a href="#showMore" onclick="$('.it-examples').toggle(); return false">${_('show examples')}</a>
93 <a href="#showMore" onclick="$('.it-examples').toggle(); return false">${_('show examples')}</a>
94 % else:
94 % else:
95 <a href="#copyToInput" onclick="copyToInput(this, '${h.json.dumps(name)}', '${h.json.dumps(pat)}', '${h.json.dumps(url)}', '${h.json.dumps(pref)}'); return false">copy to input</a>
95 <a href="#copyToInput" onclick="copyToInput(this, '${h.str_json(name)}', '${h.str_json(pat)}', '${h.str_json(url)}', '${h.str_json(pref)}'); return false">copy to input</a>
96 % endif
96 % endif
97 </td>
97 </td>
98 </tr>
98 </tr>
99 % endfor
99 % endfor
100
100
101 %for uid, entry in patterns:
101 %for uid, entry in patterns:
102 <tr id="entry_${uid}">
102 <tr id="entry_${uid}">
103 <td class="td-issue-tracker-name issuetracker_desc">
103 <td class="td-issue-tracker-name issuetracker_desc">
104 <span class="entry">
104 <span class="entry">
105 ${entry.desc}
105 ${entry.desc}
106 </span>
106 </span>
107 <span class="edit">
107 <span class="edit">
108 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
108 ${h.text('new_pattern_description_'+uid, class_='medium-inline', value=entry.desc or '')}
109 </span>
109 </span>
110 </td>
110 </td>
111 <td class="td-issue-tracker-regex issuetracker_pat">
111 <td class="td-issue-tracker-regex issuetracker_pat">
112 <span class="entry">
112 <span class="entry">
113 ${entry.pat}
113 ${entry.pat}
114 </span>
114 </span>
115 <span class="edit">
115 <span class="edit">
116 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
116 ${h.text('new_pattern_pattern_'+uid, class_='medium-inline', value=entry.pat or '')}
117 </span>
117 </span>
118 </td>
118 </td>
119 <td class="td-url issuetracker_url">
119 <td class="td-url issuetracker_url">
120 <span class="entry">
120 <span class="entry">
121 ${entry.url}
121 ${entry.url}
122 </span>
122 </span>
123 <span class="edit">
123 <span class="edit">
124 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
124 ${h.text('new_pattern_url_'+uid, class_='medium-inline', value=entry.url or '')}
125 </span>
125 </span>
126 </td>
126 </td>
127 <td class="td-prefix issuetracker_pref">
127 <td class="td-prefix issuetracker_pref">
128 <span class="entry">
128 <span class="entry">
129 ${entry.pref}
129 ${entry.pref}
130 </span>
130 </span>
131 <span class="edit">
131 <span class="edit">
132 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
132 ${h.text('new_pattern_prefix_'+uid, class_='medium-inline', value=entry.pref or '')}
133 </span>
133 </span>
134 </td>
134 </td>
135 <td class="td-action">
135 <td class="td-action">
136 <div class="grid_edit">
136 <div class="grid_edit">
137 <span class="entry">
137 <span class="entry">
138 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
138 <a class="edit_issuetracker_entry" href="">${_('Edit')}</a>
139 </span>
139 </span>
140 <span class="edit">
140 <span class="edit">
141 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
141 <input id="uid_${uid}" name="uid" type="hidden" value="${uid}">
142 </span>
142 </span>
143 </div>
143 </div>
144 <div class="grid_delete">
144 <div class="grid_delete">
145 <span class="entry">
145 <span class="entry">
146 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
146 <a class="btn btn-link btn-danger delete_issuetracker_entry" data-desc="${entry.desc}" data-uid="${uid}">
147 ${_('Delete')}
147 ${_('Delete')}
148 </a>
148 </a>
149 </span>
149 </span>
150 <span class="edit">
150 <span class="edit">
151 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
151 <a class="btn btn-link btn-danger edit_issuetracker_cancel" data-uid="${uid}">${_('Cancel')}</a>
152 </span>
152 </span>
153 </div>
153 </div>
154 </td>
154 </td>
155 </tr>
155 </tr>
156 %endfor
156 %endfor
157 <tr id="last-row"></tr>
157 <tr id="last-row"></tr>
158 </table>
158 </table>
159 <p>
159 <p>
160 <a id="add_pattern" class="link">
160 <a id="add_pattern" class="link">
161 ${_('Add new')}
161 ${_('Add new')}
162 </a>
162 </a>
163 </p>
163 </p>
164
164
165 <script type="text/javascript">
165 <script type="text/javascript">
166 var newEntryLabel = $('label[for="new_entry"]');
166 var newEntryLabel = $('label[for="new_entry"]');
167
167
168 var resetEntry = function() {
168 var resetEntry = function() {
169 newEntryLabel.text("${_('New Entry')}:");
169 newEntryLabel.text("${_('New Entry')}:");
170 };
170 };
171
171
172 var delete_pattern = function(entry) {
172 var delete_pattern = function(entry) {
173 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
173 if (confirm("${_('Confirm to remove this pattern:')} "+$(entry).data('desc'))) {
174 $.ajax({
174 $.ajax({
175 type: "POST",
175 type: "POST",
176 url: "${delete_url}",
176 url: "${delete_url}",
177 data: {
177 data: {
178 'csrf_token': CSRF_TOKEN,
178 'csrf_token': CSRF_TOKEN,
179 'uid':$(entry).data('uid')
179 'uid':$(entry).data('uid')
180 },
180 },
181 success: function(){
181 success: function(){
182 window.location.reload();
182 window.location.reload();
183 },
183 },
184 error: function(data, textStatus, errorThrown){
184 error: function(data, textStatus, errorThrown){
185 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
185 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
186 }
186 }
187 });
187 });
188 }
188 }
189 };
189 };
190
190
191 $('.delete_issuetracker_entry').on('click', function(e){
191 $('.delete_issuetracker_entry').on('click', function(e){
192 e.preventDefault();
192 e.preventDefault();
193 delete_pattern(this);
193 delete_pattern(this);
194 });
194 });
195
195
196 $('.edit_issuetracker_entry').on('click', function(e){
196 $('.edit_issuetracker_entry').on('click', function(e){
197 e.preventDefault();
197 e.preventDefault();
198 $(this).parents('tr').addClass('editopen');
198 $(this).parents('tr').addClass('editopen');
199 });
199 });
200
200
201 $('.edit_issuetracker_cancel').on('click', function(e){
201 $('.edit_issuetracker_cancel').on('click', function(e){
202 e.preventDefault();
202 e.preventDefault();
203 $(this).parents('tr').removeClass('editopen');
203 $(this).parents('tr').removeClass('editopen');
204 // Reset to original value
204 // Reset to original value
205 var uid = $(this).data('uid');
205 var uid = $(this).data('uid');
206 $('#'+uid+' input').each(function(e) {
206 $('#'+uid+' input').each(function(e) {
207 this.value = this.defaultValue;
207 this.value = this.defaultValue;
208 });
208 });
209 });
209 });
210
210
211 $('input#reset').on('click', function(e) {
211 $('input#reset').on('click', function(e) {
212 resetEntry();
212 resetEntry();
213 });
213 });
214
214
215 $('#add_pattern').on('click', function(e) {
215 $('#add_pattern').on('click', function(e) {
216 addNewPatternInput();
216 addNewPatternInput();
217 });
217 });
218
218
219 var copied = false;
219 var copied = false;
220 copyToInput = function (elem, name, pat, url, pref) {
220 copyToInput = function (elem, name, pat, url, pref) {
221 if (copied === false) {
221 if (copied === false) {
222 addNewPatternInput();
222 addNewPatternInput();
223 copied = true;
223 copied = true;
224 }
224 }
225 $(elem).hide();
225 $(elem).hide();
226 var load = function(text){
226 var load = function(text){
227 return text.replace(/["]/g, "")
227 return text.replace(/["]/g, "")
228 };
228 };
229 $('#description_1').val(load(name));
229 $('#description_1').val(load(name));
230 $('#pattern_1').val(load(pat));
230 $('#pattern_1').val(load(pat));
231 $('#url_1').val(load(url));
231 $('#url_1').val(load(url));
232 $('#prefix_1').val(load(pref));
232 $('#prefix_1').val(load(pref));
233
233
234 }
234 }
235
235
236 </script>
236 </script>
237 </%def>
237 </%def>
238
238
239 <%def name="issue_tracker_new_row()">
239 <%def name="issue_tracker_new_row()">
240 <table id="add-row-tmpl" style="display: none;">
240 <table id="add-row-tmpl" style="display: none;">
241 <tbody>
241 <tbody>
242 <tr class="new_pattern">
242 <tr class="new_pattern">
243 <td class="td-issue-tracker-name issuetracker_desc">
243 <td class="td-issue-tracker-name issuetracker_desc">
244 <span class="entry">
244 <span class="entry">
245 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
245 <input class="medium-inline" id="description_##UUID##" name="new_pattern_description_##UUID##" value="##DESCRIPTION##" type="text">
246 </span>
246 </span>
247 </td>
247 </td>
248 <td class="td-issue-tracker-regex issuetracker_pat">
248 <td class="td-issue-tracker-regex issuetracker_pat">
249 <span class="entry">
249 <span class="entry">
250 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
250 <input class="medium-inline" id="pattern_##UUID##" name="new_pattern_pattern_##UUID##" placeholder="Pattern"
251 value="##PATTERN##" type="text">
251 value="##PATTERN##" type="text">
252 </span>
252 </span>
253 </td>
253 </td>
254 <td class="td-url issuetracker_url">
254 <td class="td-url issuetracker_url">
255 <span class="entry">
255 <span class="entry">
256 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
256 <input class="medium-inline" id="url_##UUID##" name="new_pattern_url_##UUID##" placeholder="Url" value="##URL##" type="text">
257 </span>
257 </span>
258 </td>
258 </td>
259 <td class="td-prefix issuetracker_pref">
259 <td class="td-prefix issuetracker_pref">
260 <span class="entry">
260 <span class="entry">
261 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
261 <input class="medium-inline" id="prefix_##UUID##" name="new_pattern_prefix_##UUID##" placeholder="Prefix" value="##PREFIX##" type="text">
262 </span>
262 </span>
263 </td>
263 </td>
264 <td class="td-action">
264 <td class="td-action">
265 </td>
265 </td>
266 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
266 <input id="uid_##UUID##" name="uid_##UUID##" type="hidden" value="">
267 </tr>
267 </tr>
268 </tbody>
268 </tbody>
269 </table>
269 </table>
270 </%def>
270 </%def>
271
271
272 <%def name="issue_tracker_settings_test(test_url)">
272 <%def name="issue_tracker_settings_test(test_url)">
273 <div class="form-vertical">
273 <div class="form-vertical">
274 <div class="fields">
274 <div class="fields">
275 <div class="field">
275 <div class="field">
276 <div class='textarea-full'>
276 <div class='textarea-full'>
277 <textarea id="test_pattern_data" rows="12">
277 <textarea id="test_pattern_data" rows="12">
278 This is an example text for testing issue tracker patterns.
278 This is an example text for testing issue tracker patterns.
279 This commit fixes ticket #451 and ticket #910, reference for JRA-401.
279 This commit fixes ticket #451 and ticket #910, reference for JRA-401.
280 The following tickets will get mentioned:
280 The following tickets will get mentioned:
281 #123
281 #123
282 #456 and PROJ-101
282 #456 and PROJ-101
283 JRA-123 and #123
283 JRA-123 and #123
284 PROJ-456
284 PROJ-456
285
285
286 [my artifact](http://something.com/JRA-1234-build.zip)
286 [my artifact](http://something.com/JRA-1234-build.zip)
287
287
288 - #1001
288 - #1001
289 - JRA-998
289 - JRA-998
290
290
291 Open a pull request !101 to contribute!
291 Open a pull request !101 to contribute!
292 Added tag v1.3.0 for commit 0f3b629be725
292 Added tag v1.3.0 for commit 0f3b629be725
293
293
294 Add a test pattern here and hit preview to see the link.
294 Add a test pattern here and hit preview to see the link.
295 </textarea>
295 </textarea>
296 </div>
296 </div>
297 </div>
297 </div>
298 </div>
298 </div>
299 <div class="test_pattern_preview">
299 <div class="test_pattern_preview">
300 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
300 <div id="test_pattern" class="btn btn-small" >${_('Preview')}</div>
301 <p>${_('Test Pattern Preview')}</p>
301 <p>${_('Test Pattern Preview')}</p>
302 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
302 <div id="test_pattern_result" style="white-space: pre-wrap"></div>
303 </div>
303 </div>
304 </div>
304 </div>
305
305
306 <script type="text/javascript">
306 <script type="text/javascript">
307 $('#test_pattern').on('click', function(e) {
307 $('#test_pattern').on('click', function(e) {
308 $.ajax({
308 $.ajax({
309 type: "POST",
309 type: "POST",
310 url: "${test_url}",
310 url: "${test_url}",
311 data: {
311 data: {
312 'test_text': $('#test_pattern_data').val(),
312 'test_text': $('#test_pattern_data').val(),
313 'csrf_token': CSRF_TOKEN
313 'csrf_token': CSRF_TOKEN
314 },
314 },
315 success: function(data){
315 success: function(data){
316 $('#test_pattern_result').html(data);
316 $('#test_pattern_result').html(data);
317 tooltipActivate();
317 tooltipActivate();
318 },
318 },
319 error: function(jqXHR, textStatus, errorThrown){
319 error: function(jqXHR, textStatus, errorThrown){
320 $('#test_pattern_result').html('Error: ' + errorThrown);
320 $('#test_pattern_result').html('Error: ' + errorThrown);
321 }
321 }
322 });
322 });
323 $('#test_pattern_result').show();
323 $('#test_pattern_result').show();
324 });
324 });
325 </script>
325 </script>
326 </%def>
326 </%def>
327
327
328
328
@@ -1,166 +1,166 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <!DOCTYPE html>
2 <!DOCTYPE html>
3
3
4 <%
4 <%
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 go_import_header = ''
6 go_import_header = ''
7 if hasattr(c, 'rhodecode_db_repo'):
7 if hasattr(c, 'rhodecode_db_repo'):
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_ref_name
9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_ref_name
10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
12
12
13 if getattr(c, 'repo_group', None):
13 if getattr(c, 'repo_group', None):
14 c.template_context['repo_group_id'] = c.repo_group.group_id
14 c.template_context['repo_group_id'] = c.repo_group.group_id
15 c.template_context['repo_group_name'] = c.repo_group.group_name
15 c.template_context['repo_group_name'] = c.repo_group.group_name
16
16
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 c.template_context['rhodecode_user']['user_id'] = c.rhodecode_user.user_id
18 c.template_context['rhodecode_user']['user_id'] = c.rhodecode_user.user_id
19 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
19 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
20 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
20 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
21 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
21 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
22 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
22 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
23 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
23 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
24
24
25 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
25 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
26 c.template_context['default_user'] = {
26 c.template_context['default_user'] = {
27 'username': h.DEFAULT_USER,
27 'username': h.DEFAULT_USER,
28 'user_id': 1
28 'user_id': 1
29 }
29 }
30 c.template_context['search_context'] = {
30 c.template_context['search_context'] = {
31 'repo_group_id': c.template_context.get('repo_group_id'),
31 'repo_group_id': c.template_context.get('repo_group_id'),
32 'repo_group_name': c.template_context.get('repo_group_name'),
32 'repo_group_name': c.template_context.get('repo_group_name'),
33 'repo_id': c.template_context.get('repo_id'),
33 'repo_id': c.template_context.get('repo_id'),
34 'repo_name': c.template_context.get('repo_name'),
34 'repo_name': c.template_context.get('repo_name'),
35 'repo_view_type': c.template_context.get('repo_view_type'),
35 'repo_view_type': c.template_context.get('repo_view_type'),
36 }
36 }
37
37
38 c.template_context['attachment_store'] = {
38 c.template_context['attachment_store'] = {
39 'max_file_size_mb': 10,
39 'max_file_size_mb': 10,
40 'image_ext': ["png", "jpg", "gif", "jpeg"]
40 'image_ext': ["png", "jpg", "gif", "jpeg"]
41 }
41 }
42
42
43 %>
43 %>
44 <html xmlns="http://www.w3.org/1999/xhtml">
44 <html xmlns="http://www.w3.org/1999/xhtml">
45 <head>
45 <head>
46 <title>${self.title()}</title>
46 <title>${self.title()}</title>
47 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
47 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
48
48
49 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
49 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
50
50
51 % if 'safari' in (request.user_agent or '').lower():
51 % if 'safari' in (request.user_agent or '').lower():
52 <meta name="referrer" content="origin">
52 <meta name="referrer" content="origin">
53 % else:
53 % else:
54 <meta name="referrer" content="origin-when-cross-origin">
54 <meta name="referrer" content="origin-when-cross-origin">
55 % endif
55 % endif
56
56
57 <%def name="robots()">
57 <%def name="robots()">
58 <meta name="robots" content="index, nofollow"/>
58 <meta name="robots" content="index, nofollow"/>
59 </%def>
59 </%def>
60 ${self.robots()}
60 ${self.robots()}
61 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
61 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
62 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
62 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
63 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
63 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
64
64
65 ## CSS definitions
65 ## CSS definitions
66 <%def name="css()">
66 <%def name="css()">
67 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
67 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
68 ## EXTRA FOR CSS
68 ## EXTRA FOR CSS
69 ${self.css_extra()}
69 ${self.css_extra()}
70 </%def>
70 </%def>
71 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
71 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
72 <%def name="css_extra()">
72 <%def name="css_extra()">
73 </%def>
73 </%def>
74
74
75 ${self.css()}
75 ${self.css()}
76
76
77 ## JAVASCRIPT
77 ## JAVASCRIPT
78 <%def name="js()">
78 <%def name="js()">
79
79
80 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
80 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
81 <script type="text/javascript">
81 <script type="text/javascript">
82 // register templateContext to pass template variables to JS
82 // register templateContext to pass template variables to JS
83 var templateContext = ${h.json.dumps(c.template_context)|n};
83 var templateContext = ${h.str_json(c.template_context)|n};
84
84
85 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
85 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
86 var APPLICATION_PLUGINS = [];
86 var APPLICATION_PLUGINS = [];
87 var ASSET_URL = "${h.asset('')}";
87 var ASSET_URL = "${h.asset('')}";
88 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
88 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
89 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
89 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
90
90
91 var APPENLIGHT = {
91 var APPENLIGHT = {
92 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
92 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
93 key: '${getattr(c, "appenlight_api_public_key", "")}',
93 key: '${getattr(c, "appenlight_api_public_key", "")}',
94 % if getattr(c, 'appenlight_server_url', None):
94 % if getattr(c, 'appenlight_server_url', None):
95 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
95 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
96 % endif
96 % endif
97 requestInfo: {
97 requestInfo: {
98 % if getattr(c, 'rhodecode_user', None):
98 % if getattr(c, 'rhodecode_user', None):
99 ip: '${c.rhodecode_user.ip_addr}',
99 ip: '${c.rhodecode_user.ip_addr}',
100 username: '${c.rhodecode_user.username}'
100 username: '${c.rhodecode_user.username}'
101 % endif
101 % endif
102 },
102 },
103 tags: {
103 tags: {
104 rhodecode_version: '${c.rhodecode_version}',
104 rhodecode_version: '${c.rhodecode_version}',
105 rhodecode_edition: '${c.rhodecode_edition}'
105 rhodecode_edition: '${c.rhodecode_edition}'
106 }
106 }
107 };
107 };
108
108
109 </script>
109 </script>
110 <%include file="/base/plugins_base.mako"/>
110 <%include file="/base/plugins_base.mako"/>
111 <!--[if lt IE 9]>
111 <!--[if lt IE 9]>
112 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
112 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
113 <![endif]-->
113 <![endif]-->
114 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
114 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
115 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
115 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
116 ## avoide escaping the %N
116 ## avoide escaping the %N
117 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
117 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
118 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
118 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
119
119
120
120
121 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
121 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
122 ${self.js_extra()}
122 ${self.js_extra()}
123
123
124 <script type="text/javascript">
124 <script type="text/javascript">
125 Rhodecode = (function() {
125 Rhodecode = (function() {
126 function _Rhodecode() {
126 function _Rhodecode() {
127 this.comments = new CommentsController();
127 this.comments = new CommentsController();
128 }
128 }
129 return new _Rhodecode();
129 return new _Rhodecode();
130 })();
130 })();
131
131
132 $(document).ready(function(){
132 $(document).ready(function(){
133 show_more_event();
133 show_more_event();
134 timeagoActivate();
134 timeagoActivate();
135 tooltipActivate();
135 tooltipActivate();
136 clipboardActivate();
136 clipboardActivate();
137 })
137 })
138 </script>
138 </script>
139
139
140 </%def>
140 </%def>
141
141
142 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
142 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
143 <%def name="js_extra()"></%def>
143 <%def name="js_extra()"></%def>
144 ${self.js()}
144 ${self.js()}
145
145
146 <%def name="head_extra()"></%def>
146 <%def name="head_extra()"></%def>
147 ${self.head_extra()}
147 ${self.head_extra()}
148 ## extra stuff
148 ## extra stuff
149 %if c.pre_code:
149 %if c.pre_code:
150 ${c.pre_code|n}
150 ${c.pre_code|n}
151 %endif
151 %endif
152 </head>
152 </head>
153 <body id="body">
153 <body id="body">
154 <noscript>
154 <noscript>
155 <div class="noscript-error">
155 <div class="noscript-error">
156 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
156 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
157 </div>
157 </div>
158 </noscript>
158 </noscript>
159
159
160 ${next.body()}
160 ${next.body()}
161 %if c.post_code:
161 %if c.post_code:
162 ${c.post_code|n}
162 ${c.post_code|n}
163 %endif
163 %endif
164 <rhodecode-app></rhodecode-app>
164 <rhodecode-app></rhodecode-app>
165 </body>
165 </body>
166 </html>
166 </html>
@@ -1,168 +1,168 b''
1 ## snippet for sidebar elements
1 ## snippet for sidebar elements
2 ## usage:
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 ## ${sidebar.comments_table()}
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
5 <%namespace name="base" file="/base/base.mako"/>
6
6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
7 <%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
8 <%
8 <%
9 if todo_comments:
9 if todo_comments:
10 cls_ = 'todos-content-table'
10 cls_ = 'todos-content-table'
11 def sorter(entry):
11 def sorter(entry):
12 user_id = entry.author.user_id
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
14 if user_id == c.rhodecode_user.user_id:
15 # own comments first
15 # own comments first
16 user_id = 0
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 elif draft_comments:
18 elif draft_comments:
19 cls_ = 'drafts-content-table'
19 cls_ = 'drafts-content-table'
20 def sorter(entry):
20 def sorter(entry):
21 return '{}'.format(str(entry.comment_id).zfill(10000))
21 return '{}'.format(str(entry.comment_id).zfill(10000))
22 else:
22 else:
23 cls_ = 'comments-content-table'
23 cls_ = 'comments-content-table'
24 def sorter(entry):
24 def sorter(entry):
25 return '{}'.format(str(entry.comment_id).zfill(10000))
25 return '{}'.format(str(entry.comment_id).zfill(10000))
26
26
27 existing_ids = existing_ids or []
27 existing_ids = existing_ids or []
28
28
29 %>
29 %>
30
30
31 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
31 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
32
32
33 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
33 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
34 <%
34 <%
35 display = ''
35 display = ''
36 _cls = ''
36 _cls = ''
37 ## Extra precaution to not show drafts in the sidebar for todo/comments
37 ## Extra precaution to not show drafts in the sidebar for todo/comments
38 if comment_obj.draft and not draft_comments:
38 if comment_obj.draft and not draft_comments:
39 continue
39 continue
40 %>
40 %>
41
41
42
42
43 <%
43 <%
44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
45 prev_comment_ver_index = 0
45 prev_comment_ver_index = 0
46 if loop_obj.previous:
46 if loop_obj.previous:
47 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
47 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
48
48
49 ver_info = None
49 ver_info = None
50 if getattr(c, 'versions', []):
50 if getattr(c, 'versions', []):
51 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
51 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
52 %>
52 %>
53 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
53 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
54 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
54 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
55 <%
55 <%
56 if (prev_comment_ver_index > comment_ver_index):
56 if (prev_comment_ver_index > comment_ver_index):
57 comments_ver_divider = comment_ver_index
57 comments_ver_divider = comment_ver_index
58 else:
58 else:
59 comments_ver_divider = None
59 comments_ver_divider = None
60 %>
60 %>
61
61
62 % if todo_comments:
62 % if todo_comments:
63 % if comment_obj.resolved:
63 % if comment_obj.resolved:
64 <% _cls = 'resolved-todo' %>
64 <% _cls = 'resolved-todo' %>
65 <% display = 'none' %>
65 <% display = 'none' %>
66 % endif
66 % endif
67 % else:
67 % else:
68 ## SKIP TODOs we display them in other area
68 ## SKIP TODOs we display them in other area
69 % if comment_obj.is_todo and not comment_obj.draft:
69 % if comment_obj.is_todo and not comment_obj.draft:
70 <% display = 'none' %>
70 <% display = 'none' %>
71 % endif
71 % endif
72 ## Skip outdated comments
72 ## Skip outdated comments
73 % if comment_obj.outdated:
73 % if comment_obj.outdated:
74 <% display = 'none' %>
74 <% display = 'none' %>
75 <% _cls = 'hidden-comment' %>
75 <% _cls = 'hidden-comment' %>
76 % endif
76 % endif
77 % endif
77 % endif
78
78
79 % if not todo_comments and comments_ver_divider:
79 % if not todo_comments and comments_ver_divider:
80 <tr class="old-comments-marker">
80 <tr class="old-comments-marker">
81 <td colspan="3">
81 <td colspan="3">
82 % if ver_info:
82 % if ver_info:
83 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
83 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
84 % else:
84 % else:
85 <code>v${comments_ver_divider}</code>
85 <code>v${comments_ver_divider}</code>
86 % endif
86 % endif
87 </td>
87 </td>
88 </tr>
88 </tr>
89
89
90 % endif
90 % endif
91
91
92 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
92 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
93 % if draft_comments:
93 % if draft_comments:
94 <td style="width: 15px;">
94 <td style="width: 15px;">
95 ${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
95 ${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
96 </td>
96 </td>
97 % endif
97 % endif
98 <td class="td-todo-number">
98 <td class="td-todo-number">
99 <%
99 <%
100 version_info = ''
100 version_info = ''
101 if is_pr:
101 if is_pr:
102 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
102 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
103 %>
103 %>
104 ## new comments, since refresh
104 ## new comments, since refresh
105 % if existing_ids and comment_obj.comment_id not in existing_ids:
105 % if existing_ids and comment_obj.comment_id not in existing_ids:
106 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
106 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
107 !
107 !
108 </div>
108 </div>
109 % endif
109 % endif
110
110
111 <%
111 <%
112 data = h.json.dumps({
112 data = h.str_json({
113 'comment_id': comment_obj.comment_id,
113 'comment_id': comment_obj.comment_id,
114 'version_info': version_info,
114 'version_info': version_info,
115 'file_name': comment_obj.f_path,
115 'file_name': comment_obj.f_path,
116 'line_no': comment_obj.line_no,
116 'line_no': comment_obj.line_no,
117 'outdated': comment_obj.outdated,
117 'outdated': comment_obj.outdated,
118 'inline': comment_obj.is_inline,
118 'inline': comment_obj.is_inline,
119 'is_todo': comment_obj.is_todo,
119 'is_todo': comment_obj.is_todo,
120 'created_on': h.format_date(comment_obj.created_on),
120 'created_on': h.format_date(comment_obj.created_on),
121 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
121 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
122 'review_status': (comment_obj.review_status or '')
122 'review_status': (comment_obj.review_status or '')
123 })
123 })
124
124
125 icon = ''
125 icon = ''
126
126
127 if comment_obj.outdated:
127 if comment_obj.outdated:
128 icon += ' icon-comment-toggle'
128 icon += ' icon-comment-toggle'
129 elif comment_obj.is_inline:
129 elif comment_obj.is_inline:
130 icon += ' icon-code'
130 icon += ' icon-code'
131 else:
131 else:
132 icon += ' icon-comment'
132 icon += ' icon-comment'
133
133
134 if comment_obj.draft:
134 if comment_obj.draft:
135 if comment_obj.is_todo:
135 if comment_obj.is_todo:
136 icon = 'icon-flag-filled icon-draft'
136 icon = 'icon-flag-filled icon-draft'
137 else:
137 else:
138 icon = 'icon-comment icon-draft'
138 icon = 'icon-comment icon-draft'
139
139
140 %>
140 %>
141
141
142 <i id="commentHovercard${comment_obj.comment_id}"
142 <i id="commentHovercard${comment_obj.comment_id}"
143 class="${icon} tooltip-hovercard"
143 class="${icon} tooltip-hovercard"
144 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
144 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
145 data-comment-json-b64='${h.b64(data)}'>
145 data-comment-json-b64='${h.b64(data)}'>
146 </i>
146 </i>
147
147
148 </td>
148 </td>
149
149
150 <td class="td-todo-gravatar">
150 <td class="td-todo-gravatar">
151 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
151 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
152 </td>
152 </td>
153 <td class="todo-comment-text-wrapper">
153 <td class="todo-comment-text-wrapper">
154 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
154 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
155 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
155 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
156 href="#comment-${comment_obj.comment_id}"
156 href="#comment-${comment_obj.comment_id}"
157 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
157 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
158
158
159 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
159 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
160 </a>
160 </a>
161 </div>
161 </div>
162 </td>
162 </td>
163 </tr>
163 </tr>
164 % endfor
164 % endfor
165
165
166 </table>
166 </table>
167
167
168 </%def> No newline at end of file
168 </%def>
@@ -1,557 +1,557 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%!
8 <%!
9 from rhodecode.lib import html_filters
9 from rhodecode.lib import html_filters
10 %>
10 %>
11
11
12
12
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
13 <%def name="comment_block(comment, inline=False, active_pattern_entries=None, is_new=False)">
14
14
15 <%
15 <%
16 from rhodecode.model.comment import CommentsModel
16 from rhodecode.model.comment import CommentsModel
17 comment_model = CommentsModel()
17 comment_model = CommentsModel()
18
18
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
19 comment_ver = comment.get_index_version(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
20 latest_ver = len(getattr(c, 'versions', []))
21 visible_for_user = True
21 visible_for_user = True
22 if comment.draft:
22 if comment.draft:
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
23 visible_for_user = comment.user_id == c.rhodecode_user.user_id
24 %>
24 %>
25
25
26 % if inline:
26 % if inline:
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
27 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
28 % else:
28 % else:
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
29 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
30 % endif
30 % endif
31
31
32 % if visible_for_user:
32 % if visible_for_user:
33 <div class="comment
33 <div class="comment
34 ${'comment-inline' if inline else 'comment-general'}
34 ${'comment-inline' if inline else 'comment-general'}
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
35 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
36 id="comment-${comment.comment_id}"
36 id="comment-${comment.comment_id}"
37 line="${comment.line_no}"
37 line="${comment.line_no}"
38 data-comment-id="${comment.comment_id}"
38 data-comment-id="${comment.comment_id}"
39 data-comment-type="${comment.comment_type}"
39 data-comment-type="${comment.comment_type}"
40 data-comment-draft=${h.json.dumps(comment.draft)}
40 data-comment-draft=${h.str_json(comment.draft)}
41 data-comment-renderer="${comment.renderer}"
41 data-comment-renderer="${comment.renderer}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
42 data-comment-text="${comment.text | html_filters.base64,n}"
43 data-comment-f-path="${comment.f_path}"
43 data-comment-f-path="${comment.f_path}"
44 data-comment-line-no="${comment.line_no}"
44 data-comment-line-no="${comment.line_no}"
45 data-comment-inline=${h.json.dumps(inline)}
45 data-comment-inline=${h.str_json(inline)}
46 style="${'display: none;' if outdated_at_ver else ''}">
46 style="${'display: none;' if outdated_at_ver else ''}">
47
47
48 <div class="meta">
48 <div class="meta">
49 <div class="comment-type-label">
49 <div class="comment-type-label">
50 % if comment.draft:
50 % if comment.draft:
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
51 <div class="tooltip comment-draft" title="${_('Draft comments are only visible to the author until submitted')}.">
52 DRAFT
52 DRAFT
53 </div>
53 </div>
54 % elif is_new:
54 % elif is_new:
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
55 <div class="tooltip comment-new" title="${_('This comment was added while you browsed this page')}.">
56 NEW
56 NEW
57 </div>
57 </div>
58 % endif
58 % endif
59
59
60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
60 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
61
61
62 ## TODO COMMENT
62 ## TODO COMMENT
63 % if comment.comment_type == 'todo':
63 % if comment.comment_type == 'todo':
64 % if comment.resolved:
64 % if comment.resolved:
65 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
65 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
66 <i class="icon-flag-filled"></i>
66 <i class="icon-flag-filled"></i>
67 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
67 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
68 </div>
68 </div>
69 % else:
69 % else:
70 <div class="resolved tooltip" style="display: none">
70 <div class="resolved tooltip" style="display: none">
71 <span>${comment.comment_type}</span>
71 <span>${comment.comment_type}</span>
72 </div>
72 </div>
73 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
73 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
74 <i class="icon-flag-filled"></i>
74 <i class="icon-flag-filled"></i>
75 ${comment.comment_type}
75 ${comment.comment_type}
76 </div>
76 </div>
77 % endif
77 % endif
78 ## NOTE COMMENT
78 ## NOTE COMMENT
79 % else:
79 % else:
80 ## RESOLVED NOTE
80 ## RESOLVED NOTE
81 % if comment.resolved_comment:
81 % if comment.resolved_comment:
82 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
82 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
83 fix
83 fix
84 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
84 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.str_json(comment.resolved_comment.outdated)})">
85 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
85 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
86 </a>
86 </a>
87 </div>
87 </div>
88 ## STATUS CHANGE NOTE
88 ## STATUS CHANGE NOTE
89 % elif not comment.is_inline and comment.status_change:
89 % elif not comment.is_inline and comment.status_change:
90 <%
90 <%
91 if comment.pull_request:
91 if comment.pull_request:
92 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
92 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
93 else:
93 else:
94 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
94 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
95 %>
95 %>
96
96
97 <i class="icon-circle review-status-${comment.review_status}"></i>
97 <i class="icon-circle review-status-${comment.review_status}"></i>
98 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
98 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
99 ${comment.review_status_lbl}
99 ${comment.review_status_lbl}
100 </div>
100 </div>
101 % else:
101 % else:
102 <div>
102 <div>
103 <i class="icon-comment"></i>
103 <i class="icon-comment"></i>
104 ${(comment.comment_type or 'note')}
104 ${(comment.comment_type or 'note')}
105 </div>
105 </div>
106 % endif
106 % endif
107 % endif
107 % endif
108
108
109 </div>
109 </div>
110 </div>
110 </div>
111 ## NOTE 0 and .. => because we disable it for now until UI ready
111 ## NOTE 0 and .. => because we disable it for now until UI ready
112 % if 0 and comment.status_change:
112 % if 0 and comment.status_change:
113 <div class="pull-left">
113 <div class="pull-left">
114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
114 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
115 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
115 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
116 ${'!{}'.format(comment.pull_request.pull_request_id)}
116 ${'!{}'.format(comment.pull_request.pull_request_id)}
117 </a>
117 </a>
118 </span>
118 </span>
119 </div>
119 </div>
120 % endif
120 % endif
121 ## Since only author can see drafts, we don't show it
121 ## Since only author can see drafts, we don't show it
122 % if not comment.draft:
122 % if not comment.draft:
123 <div class="author ${'author-inline' if inline else 'author-general'}">
123 <div class="author ${'author-inline' if inline else 'author-general'}">
124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
124 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
125 </div>
125 </div>
126 % endif
126 % endif
127
127
128 <div class="date">
128 <div class="date">
129 ${h.age_component(comment.modified_at, time_is_local=True)}
129 ${h.age_component(comment.modified_at, time_is_local=True)}
130 </div>
130 </div>
131
131
132 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
132 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
133 <span class="tag authortag tooltip" title="${_('Pull request author')}">
133 <span class="tag authortag tooltip" title="${_('Pull request author')}">
134 ${_('author')}
134 ${_('author')}
135 </span>
135 </span>
136 % endif
136 % endif
137
137
138 <%
138 <%
139 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
139 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
140 %>
140 %>
141
141
142 % if comment.history:
142 % if comment.history:
143 <div class="date">
143 <div class="date">
144
144
145 <input id="${comment_version_selector}" name="${comment_version_selector}"
145 <input id="${comment_version_selector}" name="${comment_version_selector}"
146 type="hidden"
146 type="hidden"
147 data-last-version="${comment.history[-1].version}">
147 data-last-version="${comment.history[-1].version}">
148
148
149 <script type="text/javascript">
149 <script type="text/javascript">
150
150
151 var preLoadVersionData = [
151 var preLoadVersionData = [
152 % for comment_history in comment.history:
152 % for comment_history in comment.history:
153 {
153 {
154 id: ${comment_history.comment_history_id},
154 id: ${comment_history.comment_history_id},
155 text: 'v${comment_history.version}',
155 text: 'v${comment_history.version}',
156 action: function () {
156 action: function () {
157 Rhodecode.comments.showVersion(
157 Rhodecode.comments.showVersion(
158 "${comment.comment_id}",
158 "${comment.comment_id}",
159 "${comment_history.comment_history_id}"
159 "${comment_history.comment_history_id}"
160 )
160 )
161 },
161 },
162 comment_version: "${comment_history.version}",
162 comment_version: "${comment_history.version}",
163 comment_author_username: "${comment_history.author.username}",
163 comment_author_username: "${comment_history.author.username}",
164 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
164 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
165 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
165 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
166 },
166 },
167 % endfor
167 % endfor
168 ]
168 ]
169 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
169 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
170
170
171 </script>
171 </script>
172
172
173 </div>
173 </div>
174 % else:
174 % else:
175 <div class="date" style="display: none">
175 <div class="date" style="display: none">
176 <input id="${comment_version_selector}" name="${comment_version_selector}"
176 <input id="${comment_version_selector}" name="${comment_version_selector}"
177 type="hidden"
177 type="hidden"
178 data-last-version="0">
178 data-last-version="0">
179 </div>
179 </div>
180 %endif
180 %endif
181
181
182 <div class="comment-links-block">
182 <div class="comment-links-block">
183
183
184 % if inline:
184 % if inline:
185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
185 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
186 % if outdated_at_ver:
186 % if outdated_at_ver:
187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
187 <strong class="comment-outdated-label">outdated</strong> <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
188 <code class="action-divider">|</code>
188 <code class="action-divider">|</code>
189 % elif comment_ver:
189 % elif comment_ver:
190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
190 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">${'v{}'.format(comment_ver)}</code>
191 <code class="action-divider">|</code>
191 <code class="action-divider">|</code>
192 % endif
192 % endif
193 </a>
193 </a>
194 % else:
194 % else:
195 % if comment_ver:
195 % if comment_ver:
196
196
197 % if comment.outdated:
197 % if comment.outdated:
198 <a class="pr-version"
198 <a class="pr-version"
199 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
199 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
200 >
200 >
201 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
201 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
202 </a>
202 </a>
203 <code class="action-divider">|</code>
203 <code class="action-divider">|</code>
204 % else:
204 % else:
205 <a class="tooltip pr-version"
205 <a class="tooltip pr-version"
206 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
206 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
207 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
207 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
208 >
208 >
209 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
209 <code class="pr-version-num">${'v{}'.format(comment_ver)}</code>
210 </a>
210 </a>
211 <code class="action-divider">|</code>
211 <code class="action-divider">|</code>
212 % endif
212 % endif
213
213
214 % endif
214 % endif
215 % endif
215 % endif
216
216
217 <details class="details-reset details-inline-block">
217 <details class="details-reset details-inline-block">
218 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
218 <summary class="noselect"><i class="icon-options cursor-pointer"></i></summary>
219 <details-menu class="details-dropdown">
219 <details-menu class="details-dropdown">
220
220
221 <div class="dropdown-item">
221 <div class="dropdown-item">
222 ${_('Comment')} #${comment.comment_id}
222 ${_('Comment')} #${comment.comment_id}
223 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
223 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${comment_model.get_url(comment,request, permalink=True, anchor='comment-{}'.format(comment.comment_id))}" title="${_('Copy permalink')}"></span>
224 </div>
224 </div>
225
225
226 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
226 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
227 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
227 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
228 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
228 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
229 ## permissions to delete
229 ## permissions to delete
230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
230 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
231 <div class="dropdown-divider"></div>
231 <div class="dropdown-divider"></div>
232 <div class="dropdown-item">
232 <div class="dropdown-item">
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
233 <a onclick="return Rhodecode.comments.editComment(this, '${comment.line_no}', '${comment.f_path}');" class="btn btn-link btn-sm edit-comment">${_('Edit')}</a>
234 </div>
234 </div>
235 <div class="dropdown-item">
235 <div class="dropdown-item">
236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
236 <a onclick="return Rhodecode.comments.deleteComment(this);" class="btn btn-link btn-sm btn-danger delete-comment">${_('Delete')}</a>
237 </div>
237 </div>
238 ## Only available in EE edition
238 ## Only available in EE edition
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
239 % if comment.draft and c.rhodecode_edition_id == 'EE':
240 <div class="dropdown-item">
240 <div class="dropdown-item">
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
241 <a onclick="return Rhodecode.comments.finalizeDrafts([${comment.comment_id}]);" class="btn btn-link btn-sm finalize-draft-comment">${_('Submit draft')}</a>
242 </div>
242 </div>
243 % endif
243 % endif
244 %else:
244 %else:
245 <div class="dropdown-divider"></div>
245 <div class="dropdown-divider"></div>
246 <div class="dropdown-item">
246 <div class="dropdown-item">
247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
247 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
248 </div>
248 </div>
249 <div class="dropdown-item">
249 <div class="dropdown-item">
250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
250 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
251 </div>
251 </div>
252 %endif
252 %endif
253 %else:
253 %else:
254 <div class="dropdown-divider"></div>
254 <div class="dropdown-divider"></div>
255 <div class="dropdown-item">
255 <div class="dropdown-item">
256 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
256 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
257 </div>
257 </div>
258 <div class="dropdown-item">
258 <div class="dropdown-item">
259 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
259 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
260 </div>
260 </div>
261 %endif
261 %endif
262 </details-menu>
262 </details-menu>
263 </details>
263 </details>
264
264
265 <code class="action-divider">|</code>
265 <code class="action-divider">|</code>
266 % if outdated_at_ver:
266 % if outdated_at_ver:
267 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
267 <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
268 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
268 <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
269 % else:
269 % else:
270 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
270 <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
271 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
271 <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
272 % endif
272 % endif
273
273
274 </div>
274 </div>
275 </div>
275 </div>
276 <div class="text">
276 <div class="text">
277 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
277 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
278 </div>
278 </div>
279
279
280 </div>
280 </div>
281 % endif
281 % endif
282 </%def>
282 </%def>
283
283
284 ## generate main comments
284 ## generate main comments
285 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
285 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
286 <%
286 <%
287 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
287 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
288 %>
288 %>
289
289
290 <div class="general-comments" id="comments">
290 <div class="general-comments" id="comments">
291 %for comment in comments:
291 %for comment in comments:
292 <div id="comment-tr-${comment.comment_id}">
292 <div id="comment-tr-${comment.comment_id}">
293 ## only render comments that are not from pull request, or from
293 ## only render comments that are not from pull request, or from
294 ## pull request and a status change
294 ## pull request and a status change
295 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
295 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
296 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
296 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
297 %endif
297 %endif
298 </div>
298 </div>
299 %endfor
299 %endfor
300 ## to anchor ajax comments
300 ## to anchor ajax comments
301 <div id="injected_page_comments"></div>
301 <div id="injected_page_comments"></div>
302 </div>
302 </div>
303 </%def>
303 </%def>
304
304
305
305
306 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
306 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
307
307
308 <div class="comments">
308 <div class="comments">
309 <%
309 <%
310 if is_pull_request:
310 if is_pull_request:
311 placeholder = _('Leave a comment on this Pull Request.')
311 placeholder = _('Leave a comment on this Pull Request.')
312 elif is_compare:
312 elif is_compare:
313 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
313 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
314 else:
314 else:
315 placeholder = _('Leave a comment on this Commit.')
315 placeholder = _('Leave a comment on this Commit.')
316 %>
316 %>
317
317
318 % if c.rhodecode_user.username != h.DEFAULT_USER:
318 % if c.rhodecode_user.username != h.DEFAULT_USER:
319 <div class="js-template" id="cb-comment-general-form-template">
319 <div class="js-template" id="cb-comment-general-form-template">
320 ## template generated for injection
320 ## template generated for injection
321 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
321 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
322 </div>
322 </div>
323
323
324 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
324 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
325 ## inject form here
325 ## inject form here
326 </div>
326 </div>
327 <script type="text/javascript">
327 <script type="text/javascript">
328 var resolvesCommentId = null;
328 var resolvesCommentId = null;
329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
329 var generalCommentForm = Rhodecode.comments.createGeneralComment(
330 'general', "${placeholder}", resolvesCommentId);
330 'general', "${placeholder}", resolvesCommentId);
331
331
332 // set custom success callback on rangeCommit
332 // set custom success callback on rangeCommit
333 % if is_compare:
333 % if is_compare:
334 generalCommentForm.setHandleFormSubmit(function(o) {
334 generalCommentForm.setHandleFormSubmit(function(o) {
335 var self = generalCommentForm;
335 var self = generalCommentForm;
336
336
337 var text = self.cm.getValue();
337 var text = self.cm.getValue();
338 var status = self.getCommentStatus();
338 var status = self.getCommentStatus();
339 var commentType = self.getCommentType();
339 var commentType = self.getCommentType();
340 var isDraft = self.getDraftState();
340 var isDraft = self.getDraftState();
341
341
342 if (text === "" && !status) {
342 if (text === "" && !status) {
343 return;
343 return;
344 }
344 }
345
345
346 // we can pick which commits we want to make the comment by
346 // we can pick which commits we want to make the comment by
347 // selecting them via click on preview pane, this will alter the hidden inputs
347 // selecting them via click on preview pane, this will alter the hidden inputs
348 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
348 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
349
349
350 var commitIds = [];
350 var commitIds = [];
351 $('#changeset_compare_view_content .compare_select').each(function(el) {
351 $('#changeset_compare_view_content .compare_select').each(function(el) {
352 var commitId = this.id.replace('row-', '');
352 var commitId = this.id.replace('row-', '');
353 if ($(this).hasClass('hl') || !cherryPicked) {
353 if ($(this).hasClass('hl') || !cherryPicked) {
354 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
354 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
355 commitIds.push(commitId);
355 commitIds.push(commitId);
356 } else {
356 } else {
357 $("input[data-commit-id='{0}']".format(commitId)).val('')
357 $("input[data-commit-id='{0}']".format(commitId)).val('')
358 }
358 }
359 });
359 });
360
360
361 self.setActionButtonsDisabled(true);
361 self.setActionButtonsDisabled(true);
362 self.cm.setOption("readOnly", true);
362 self.cm.setOption("readOnly", true);
363 var postData = {
363 var postData = {
364 'text': text,
364 'text': text,
365 'changeset_status': status,
365 'changeset_status': status,
366 'comment_type': commentType,
366 'comment_type': commentType,
367 'draft': isDraft,
367 'draft': isDraft,
368 'commit_ids': commitIds,
368 'commit_ids': commitIds,
369 'csrf_token': CSRF_TOKEN
369 'csrf_token': CSRF_TOKEN
370 };
370 };
371
371
372 var submitSuccessCallback = function(o) {
372 var submitSuccessCallback = function(o) {
373 location.reload(true);
373 location.reload(true);
374 };
374 };
375 var submitFailCallback = function(){
375 var submitFailCallback = function(){
376 self.resetCommentFormState(text)
376 self.resetCommentFormState(text)
377 };
377 };
378 self.submitAjaxPOST(
378 self.submitAjaxPOST(
379 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
379 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
380 });
380 });
381 % endif
381 % endif
382
382
383 </script>
383 </script>
384 % else:
384 % else:
385 ## form state when not logged in
385 ## form state when not logged in
386 <div class="comment-form ac">
386 <div class="comment-form ac">
387
387
388 <div class="comment-area">
388 <div class="comment-area">
389 <div class="comment-area-header">
389 <div class="comment-area-header">
390 <ul class="nav-links clearfix">
390 <ul class="nav-links clearfix">
391 <li class="active">
391 <li class="active">
392 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
392 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
393 </li>
393 </li>
394 <li class="">
394 <li class="">
395 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
395 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
396 </li>
396 </li>
397 </ul>
397 </ul>
398 </div>
398 </div>
399
399
400 <div class="comment-area-write" style="display: block;">
400 <div class="comment-area-write" style="display: block;">
401 <div id="edit-container">
401 <div id="edit-container">
402 <div style="padding: 20px 0px 0px 0;">
402 <div style="padding: 20px 0px 0px 0;">
403 ${_('You need to be logged in to leave comments.')}
403 ${_('You need to be logged in to leave comments.')}
404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
404 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
405 </div>
405 </div>
406 </div>
406 </div>
407 <div id="preview-container" class="clearfix" style="display: none;">
407 <div id="preview-container" class="clearfix" style="display: none;">
408 <div id="preview-box" class="preview-box"></div>
408 <div id="preview-box" class="preview-box"></div>
409 </div>
409 </div>
410 </div>
410 </div>
411
411
412 <div class="comment-area-footer">
412 <div class="comment-area-footer">
413 <div class="toolbar">
413 <div class="toolbar">
414 <div class="toolbar-text">
414 <div class="toolbar-text">
415 </div>
415 </div>
416 </div>
416 </div>
417 </div>
417 </div>
418 </div>
418 </div>
419
419
420 <div class="comment-footer">
420 <div class="comment-footer">
421 </div>
421 </div>
422
422
423 </div>
423 </div>
424 % endif
424 % endif
425
425
426 <script type="text/javascript">
426 <script type="text/javascript">
427 bindToggleButtons();
427 bindToggleButtons();
428 </script>
428 </script>
429 </div>
429 </div>
430 </%def>
430 </%def>
431
431
432
432
433 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
433 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
434
434
435 ## comment injected based on assumption that user is logged in
435 ## comment injected based on assumption that user is logged in
436 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
436 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
437
437
438 <div class="comment-area">
438 <div class="comment-area">
439 <div class="comment-area-header">
439 <div class="comment-area-header">
440 <div class="pull-left">
440 <div class="pull-left">
441 <ul class="nav-links clearfix">
441 <ul class="nav-links clearfix">
442 <li class="active">
442 <li class="active">
443 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
443 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
444 </li>
444 </li>
445 <li class="">
445 <li class="">
446 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
446 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
447 </li>
447 </li>
448 </ul>
448 </ul>
449 </div>
449 </div>
450 <div class="pull-right">
450 <div class="pull-right">
451 <span class="comment-area-text">${_('Mark as')}:</span>
451 <span class="comment-area-text">${_('Mark as')}:</span>
452 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
452 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
453 % for val in c.visual.comment_types:
453 % for val in c.visual.comment_types:
454 <option value="${val}">${val.upper()}</option>
454 <option value="${val}">${val.upper()}</option>
455 % endfor
455 % endfor
456 </select>
456 </select>
457 </div>
457 </div>
458 </div>
458 </div>
459
459
460 <div class="comment-area-write" style="display: block;">
460 <div class="comment-area-write" style="display: block;">
461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
461 <div id="edit-container_${lineno_id}" style="margin-top: -1px">
462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
462 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
463 </div>
463 </div>
464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
464 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
465 <div id="preview-box_${lineno_id}" class="preview-box"></div>
465 <div id="preview-box_${lineno_id}" class="preview-box"></div>
466 </div>
466 </div>
467 </div>
467 </div>
468
468
469 <div class="comment-area-footer comment-attachment-uploader">
469 <div class="comment-area-footer comment-attachment-uploader">
470 <div class="toolbar">
470 <div class="toolbar">
471
471
472 <div class="comment-attachment-text">
472 <div class="comment-attachment-text">
473 <div class="dropzone-text">
473 <div class="dropzone-text">
474 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
474 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
475 </div>
475 </div>
476 <div class="dropzone-upload" style="display:none">
476 <div class="dropzone-upload" style="display:none">
477 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
477 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
478 </div>
478 </div>
479 </div>
479 </div>
480
480
481 ## comments dropzone template, empty on purpose
481 ## comments dropzone template, empty on purpose
482 <div style="display: none" class="comment-attachment-uploader-template">
482 <div style="display: none" class="comment-attachment-uploader-template">
483 <div class="dz-file-preview" style="margin: 0">
483 <div class="dz-file-preview" style="margin: 0">
484 <div class="dz-error-message"></div>
484 <div class="dz-error-message"></div>
485 </div>
485 </div>
486 </div>
486 </div>
487
487
488 </div>
488 </div>
489 </div>
489 </div>
490 </div>
490 </div>
491
491
492 <div class="comment-footer">
492 <div class="comment-footer">
493
493
494 ## inject extra inputs into the form
494 ## inject extra inputs into the form
495 % if form_extras and isinstance(form_extras, (list, tuple)):
495 % if form_extras and isinstance(form_extras, (list, tuple)):
496 <div id="comment_form_extras">
496 <div id="comment_form_extras">
497 % for form_ex_el in form_extras:
497 % for form_ex_el in form_extras:
498 ${form_ex_el|n}
498 ${form_ex_el|n}
499 % endfor
499 % endfor
500 </div>
500 </div>
501 % endif
501 % endif
502
502
503 <div class="action-buttons">
503 <div class="action-buttons">
504 % if form_type != 'inline':
504 % if form_type != 'inline':
505 <div class="action-buttons-extra"></div>
505 <div class="action-buttons-extra"></div>
506 % endif
506 % endif
507
507
508 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
508 <input class="btn btn-success comment-button-input submit-comment-action" id="save_${lineno_id}" name="save" type="submit" value="${_('Add comment')}" data-is-draft=false onclick="$(this).addClass('submitter')">
509
509
510 % if form_type == 'inline':
510 % if form_type == 'inline':
511 % if c.rhodecode_edition_id == 'EE':
511 % if c.rhodecode_edition_id == 'EE':
512 ## Disable the button for CE, the "real" validation is in the backend code anyway
512 ## Disable the button for CE, the "real" validation is in the backend code anyway
513 <input class="btn btn-draft comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
513 <input class="btn btn-draft comment-button-input submit-draft-action" id="save_draft_${lineno_id}" name="save_draft" type="submit" value="${_('Add draft')}" data-is-draft=true onclick="$(this).addClass('submitter')">
514 % else:
514 % else:
515 <input class="btn btn-draft comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
515 <input class="btn btn-draft comment-button-input submit-draft-action disabled" disabled="disabled" type="submit" value="${_('Add draft')}" onclick="return false;" title="Draft comments only available in EE edition of RhodeCode">
516 % endif
516 % endif
517 % endif
517 % endif
518
518
519 % if review_statuses:
519 % if review_statuses:
520 <div class="comment-status-box">
520 <div class="comment-status-box">
521 <select id="change_status_${lineno_id}" name="changeset_status">
521 <select id="change_status_${lineno_id}" name="changeset_status">
522 <option></option> ## Placeholder
522 <option></option> ## Placeholder
523 % for status, lbl in review_statuses:
523 % for status, lbl in review_statuses:
524 <option value="${status}" data-status="${status}">${lbl}</option>
524 <option value="${status}" data-status="${status}">${lbl}</option>
525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
525 %if is_pull_request and change_status and status in ('approved', 'rejected'):
526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
526 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
527 %endif
527 %endif
528 % endfor
528 % endfor
529 </select>
529 </select>
530 </div>
530 </div>
531 % endif
531 % endif
532
532
533 ## inline for has a file, and line-number together with cancel hide button.
533 ## inline for has a file, and line-number together with cancel hide button.
534 % if form_type == 'inline':
534 % if form_type == 'inline':
535 <input type="hidden" name="f_path" value="{0}">
535 <input type="hidden" name="f_path" value="{0}">
536 <input type="hidden" name="line" value="${lineno_id}">
536 <input type="hidden" name="line" value="${lineno_id}">
537 <span style="opacity: 0.7" class="cursor-pointer cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
537 <span style="opacity: 0.7" class="cursor-pointer cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
538 ${_('dismiss')}
538 ${_('dismiss')}
539 </span>
539 </span>
540 % endif
540 % endif
541 </div>
541 </div>
542
542
543 <div class="toolbar-text">
543 <div class="toolbar-text">
544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
544 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
545 <span>${_('{} is supported.').format(renderer_url)|n}
545 <span>${_('{} is supported.').format(renderer_url)|n}
546
546
547 <i class="icon-info-circled tooltip-hovercard"
547 <i class="icon-info-circled tooltip-hovercard"
548 data-hovercard-alt="ALT"
548 data-hovercard-alt="ALT"
549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
549 data-hovercard-url="javascript:commentHelp('${c.visual.default_renderer.upper()}')"
550 data-comment-json-b64='${h.b64(h.json.dumps({}))}'></i>
550 data-comment-json-b64='${h.b64(h.str_json({}))}'></i>
551 </span>
551 </span>
552 </div>
552 </div>
553 </div>
553 </div>
554
554
555 </form>
555 </form>
556
556
557 </%def> No newline at end of file
557 </%def>
@@ -1,1404 +1,1404 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
2 <%namespace name="commentblock" file="/changeset/changeset_file_comment.mako"/>
3
3
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
4 <%def name="diff_line_anchor(commit, filename, line, type)"><%
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
5 return '%s_%s_%i' % (h.md5_safe(commit+filename), type, line)
6 %></%def>
6 %></%def>
7
7
8 <%def name="action_class(action)">
8 <%def name="action_class(action)">
9 <%
9 <%
10 return {
10 return {
11 '-': 'cb-deletion',
11 '-': 'cb-deletion',
12 '+': 'cb-addition',
12 '+': 'cb-addition',
13 ' ': 'cb-context',
13 ' ': 'cb-context',
14 }.get(action, 'cb-empty')
14 }.get(action, 'cb-empty')
15 %>
15 %>
16 </%def>
16 </%def>
17
17
18 <%def name="op_class(op_id)">
18 <%def name="op_class(op_id)">
19 <%
19 <%
20 return {
20 return {
21 DEL_FILENODE: 'deletion', # file deleted
21 DEL_FILENODE: 'deletion', # file deleted
22 BIN_FILENODE: 'warning' # binary diff hidden
22 BIN_FILENODE: 'warning' # binary diff hidden
23 }.get(op_id, 'addition')
23 }.get(op_id, 'addition')
24 %>
24 %>
25 </%def>
25 </%def>
26
26
27
27
28
28
29 <%def name="render_diffset(diffset, commit=None,
29 <%def name="render_diffset(diffset, commit=None,
30
30
31 # collapse all file diff entries when there are more than this amount of files in the diff
31 # collapse all file diff entries when there are more than this amount of files in the diff
32 collapse_when_files_over=20,
32 collapse_when_files_over=20,
33
33
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
34 # collapse lines in the diff when more than this amount of lines changed in the file diff
35 lines_changed_limit=500,
35 lines_changed_limit=500,
36
36
37 # add a ruler at to the output
37 # add a ruler at to the output
38 ruler_at_chars=0,
38 ruler_at_chars=0,
39
39
40 # show inline comments
40 # show inline comments
41 use_comments=False,
41 use_comments=False,
42
42
43 # disable new comments
43 # disable new comments
44 disable_new_comments=False,
44 disable_new_comments=False,
45
45
46 # special file-comments that were deleted in previous versions
46 # special file-comments that were deleted in previous versions
47 # it's used for showing outdated comments for deleted files in a PR
47 # it's used for showing outdated comments for deleted files in a PR
48 deleted_files_comments=None,
48 deleted_files_comments=None,
49
49
50 # for cache purpose
50 # for cache purpose
51 inline_comments=None,
51 inline_comments=None,
52
52
53 # additional menu for PRs
53 # additional menu for PRs
54 pull_request_menu=None,
54 pull_request_menu=None,
55
55
56 # show/hide todo next to comments
56 # show/hide todo next to comments
57 show_todos=True,
57 show_todos=True,
58
58
59 )">
59 )">
60
60
61 <%
61 <%
62 diffset_container_id = h.md5(diffset.target_ref)
62 diffset_container_id = h.md5(diffset.target_ref)
63 collapse_all = len(diffset.files) > collapse_when_files_over
63 collapse_all = len(diffset.files) > collapse_when_files_over
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
64 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
65 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
66 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
67 %>
67 %>
68
68
69 %if use_comments:
69 %if use_comments:
70
70
71 ## Template for injecting comments
71 ## Template for injecting comments
72 <div id="cb-comments-inline-container-template" class="js-template">
72 <div id="cb-comments-inline-container-template" class="js-template">
73 ${inline_comments_container([])}
73 ${inline_comments_container([])}
74 </div>
74 </div>
75
75
76 <div class="js-template" id="cb-comment-inline-form-template">
76 <div class="js-template" id="cb-comment-inline-form-template">
77 <div class="comment-inline-form ac">
77 <div class="comment-inline-form ac">
78 %if not c.rhodecode_user.is_default:
78 %if not c.rhodecode_user.is_default:
79 ## render template for inline comments
79 ## render template for inline comments
80 ${commentblock.comment_form(form_type='inline')}
80 ${commentblock.comment_form(form_type='inline')}
81 %endif
81 %endif
82 </div>
82 </div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86
86
87 %if c.user_session_attrs["diffmode"] == 'sideside':
87 %if c.user_session_attrs["diffmode"] == 'sideside':
88 <style>
88 <style>
89 .wrapper {
89 .wrapper {
90 max-width: 1600px !important;
90 max-width: 1600px !important;
91 }
91 }
92 </style>
92 </style>
93 %endif
93 %endif
94
94
95 %if ruler_at_chars:
95 %if ruler_at_chars:
96 <style>
96 <style>
97 .diff table.cb .cb-content:after {
97 .diff table.cb .cb-content:after {
98 content: "";
98 content: "";
99 border-left: 1px solid blue;
99 border-left: 1px solid blue;
100 position: absolute;
100 position: absolute;
101 top: 0;
101 top: 0;
102 height: 18px;
102 height: 18px;
103 opacity: .2;
103 opacity: .2;
104 z-index: 10;
104 z-index: 10;
105 //## +5 to account for diff action (+/-)
105 //## +5 to account for diff action (+/-)
106 left: ${ruler_at_chars + 5}ch;
106 left: ${ruler_at_chars + 5}ch;
107 </style>
107 </style>
108 %endif
108 %endif
109
109
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
110 <div class="diffset ${disable_new_comments and 'diffset-comments-disabled'}">
111
111
112 <div style="height: 20px; line-height: 20px">
112 <div style="height: 20px; line-height: 20px">
113 ## expand/collapse action
113 ## expand/collapse action
114 <div class="pull-left">
114 <div class="pull-left">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
115 <a class="${'collapsed' if collapse_all else ''}" href="#expand-files" onclick="toggleExpand(this, '${diffset_container_id}'); return false">
116 % if collapse_all:
116 % if collapse_all:
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
117 <i class="icon-plus-squared-alt icon-no-margin"></i>${_('Expand all files')}
118 % else:
118 % else:
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
119 <i class="icon-minus-squared-alt icon-no-margin"></i>${_('Collapse all files')}
120 % endif
120 % endif
121 </a>
121 </a>
122
122
123 </div>
123 </div>
124
124
125 ## todos
125 ## todos
126 % if show_todos and getattr(c, 'at_version', None):
126 % if show_todos and getattr(c, 'at_version', None):
127 <div class="pull-right">
127 <div class="pull-right">
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
128 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
129 ${_('not available in this view')}
129 ${_('not available in this view')}
130 </div>
130 </div>
131 % elif show_todos:
131 % elif show_todos:
132 <div class="pull-right">
132 <div class="pull-right">
133 <div class="comments-number" style="padding-left: 10px">
133 <div class="comments-number" style="padding-left: 10px">
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
134 % if hasattr(c, 'unresolved_comments') and hasattr(c, 'resolved_comments'):
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
135 <i class="icon-flag-filled" style="color: #949494">TODOs:</i>
136 % if c.unresolved_comments:
136 % if c.unresolved_comments:
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
137 <a href="#show-todos" onclick="$('#todo-box').toggle(); return false">
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
138 ${_('{} unresolved').format(len(c.unresolved_comments))}
139 </a>
139 </a>
140 % else:
140 % else:
141 ${_('0 unresolved')}
141 ${_('0 unresolved')}
142 % endif
142 % endif
143
143
144 ${_('{} Resolved').format(len(c.resolved_comments))}
144 ${_('{} Resolved').format(len(c.resolved_comments))}
145 % endif
145 % endif
146 </div>
146 </div>
147 </div>
147 </div>
148 % endif
148 % endif
149
149
150 ## ## comments
150 ## ## comments
151 ## <div class="pull-right">
151 ## <div class="pull-right">
152 ## <div class="comments-number" style="padding-left: 10px">
152 ## <div class="comments-number" style="padding-left: 10px">
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
153 ## % if hasattr(c, 'comments') and hasattr(c, 'inline_cnt'):
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
154 ## <i class="icon-comment" style="color: #949494">COMMENTS:</i>
155 ## % if c.comments:
155 ## % if c.comments:
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
156 ## <a href="#comments">${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))}</a>,
157 ## % else:
157 ## % else:
158 ## ${_('0 General')}
158 ## ${_('0 General')}
159 ## % endif
159 ## % endif
160 ##
160 ##
161 ## % if c.inline_cnt:
161 ## % if c.inline_cnt:
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
162 ## <a href="#" onclick="return Rhodecode.comments.nextComment();"
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
163 ## id="inline-comments-counter">${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(c.inline_cnt)}
164 ## </a>
164 ## </a>
165 ## % else:
165 ## % else:
166 ## ${_('0 Inline')}
166 ## ${_('0 Inline')}
167 ## % endif
167 ## % endif
168 ## % endif
168 ## % endif
169 ##
169 ##
170 ## % if pull_request_menu:
170 ## % if pull_request_menu:
171 ## <%
171 ## <%
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
172 ## outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
173 ## %>
173 ## %>
174 ##
174 ##
175 ## % if outdated_comm_count_ver:
175 ## % if outdated_comm_count_ver:
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
176 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
177 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
178 ## </a>
178 ## </a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
179 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
180 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
181 ## % else:
181 ## % else:
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
182 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
183 ## % endif
183 ## % endif
184 ##
184 ##
185 ## % endif
185 ## % endif
186 ##
186 ##
187 ## </div>
187 ## </div>
188 ## </div>
188 ## </div>
189
189
190 </div>
190 </div>
191
191
192 % if diffset.limited_diff:
192 % if diffset.limited_diff:
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
193 <div class="diffset-heading ${(diffset.limited_diff and 'diffset-heading-warning' or '')}">
194 <h2 class="clearinner">
194 <h2 class="clearinner">
195 ${_('The requested changes are too big and content was truncated.')}
195 ${_('The requested changes are too big and content was truncated.')}
196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
196 <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
197 </h2>
197 </h2>
198 </div>
198 </div>
199 % endif
199 % endif
200
200
201 <div id="todo-box">
201 <div id="todo-box">
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
202 % if hasattr(c, 'unresolved_comments') and c.unresolved_comments:
203 % for co in c.unresolved_comments:
203 % for co in c.unresolved_comments:
204 <a class="permalink" href="#comment-${co.comment_id}"
204 <a class="permalink" href="#comment-${co.comment_id}"
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
205 onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'))">
206 <i class="icon-flag-filled-red"></i>
206 <i class="icon-flag-filled-red"></i>
207 ${co.comment_id}</a>${('' if loop.last else ',')}
207 ${co.comment_id}</a>${('' if loop.last else ',')}
208 % endfor
208 % endfor
209 % endif
209 % endif
210 </div>
210 </div>
211 %if diffset.has_hidden_changes:
211 %if diffset.has_hidden_changes:
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
212 <p class="empty_data">${_('Some changes may be hidden')}</p>
213 %elif not diffset.files:
213 %elif not diffset.files:
214 <p class="empty_data">${_('No files')}</p>
214 <p class="empty_data">${_('No files')}</p>
215 %endif
215 %endif
216
216
217 <div class="filediffs">
217 <div class="filediffs">
218
218
219 ## initial value could be marked as False later on
219 ## initial value could be marked as False later on
220 <% over_lines_changed_limit = False %>
220 <% over_lines_changed_limit = False %>
221 %for i, filediff in enumerate(diffset.files):
221 %for i, filediff in enumerate(diffset.files):
222
222
223 %if filediff.source_file_path and filediff.target_file_path:
223 %if filediff.source_file_path and filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
224 %if filediff.source_file_path != filediff.target_file_path:
225 ## file was renamed, or copied
225 ## file was renamed, or copied
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
226 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
227 <%
227 <%
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
228 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> <del>{}</del>'.format(filediff.target_file_path, filediff.source_file_path))
229 final_path = filediff.target_file_path
229 final_path = filediff.target_file_path
230 %>
230 %>
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
231 %elif COPIED_FILENODE in filediff.patch['stats']['ops']:
232 <%
232 <%
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
233 final_file_name = h.literal(u'{} <i class="icon-angle-left"></i> {}'.format(filediff.target_file_path, filediff.source_file_path))
234 final_path = filediff.target_file_path
234 final_path = filediff.target_file_path
235 %>
235 %>
236 %endif
236 %endif
237 %else:
237 %else:
238 ## file was modified
238 ## file was modified
239 <%
239 <%
240 final_file_name = filediff.source_file_path
240 final_file_name = filediff.source_file_path
241 final_path = final_file_name
241 final_path = final_file_name
242 %>
242 %>
243 %endif
243 %endif
244 %else:
244 %else:
245 %if filediff.source_file_path:
245 %if filediff.source_file_path:
246 ## file was deleted
246 ## file was deleted
247 <%
247 <%
248 final_file_name = filediff.source_file_path
248 final_file_name = filediff.source_file_path
249 final_path = final_file_name
249 final_path = final_file_name
250 %>
250 %>
251 %else:
251 %else:
252 ## file was added
252 ## file was added
253 <%
253 <%
254 final_file_name = filediff.target_file_path
254 final_file_name = filediff.target_file_path
255 final_path = final_file_name
255 final_path = final_file_name
256 %>
256 %>
257 %endif
257 %endif
258 %endif
258 %endif
259
259
260 <%
260 <%
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
261 lines_changed = filediff.patch['stats']['added'] + filediff.patch['stats']['deleted']
262 over_lines_changed_limit = lines_changed > lines_changed_limit
262 over_lines_changed_limit = lines_changed > lines_changed_limit
263 %>
263 %>
264 ## anchor with support of sticky header
264 ## anchor with support of sticky header
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
265 <div class="anchor" id="a_${h.FID(filediff.raw_id, filediff.patch['filename'])}"></div>
266
266
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
267 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filediff)}" type="checkbox" onchange="updateSticky();">
268 <div
268 <div
269 class="filediff"
269 class="filediff"
270 data-f-path="${filediff.patch['filename']}"
270 data-f-path="${filediff.patch['filename']}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
271 data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}"
272 >
272 >
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
273 <label for="filediff-collapse-${id(filediff)}" class="filediff-heading">
274 <%
274 <%
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
275 file_comments = (get_inline_comments(inline_comments, filediff.patch['filename']) or {}).values()
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
276 total_file_comments = [_c for _c in h.itertools.chain.from_iterable(file_comments) if not (_c.outdated or _c.draft)]
277 %>
277 %>
278 <div class="filediff-collapse-indicator icon-"></div>
278 <div class="filediff-collapse-indicator icon-"></div>
279
279
280 ## Comments/Options PILL
280 ## Comments/Options PILL
281 <span class="pill-group pull-right">
281 <span class="pill-group pull-right">
282 <span class="pill" op="comments">
282 <span class="pill" op="comments">
283 <i class="icon-comment"></i> ${len(total_file_comments)}
283 <i class="icon-comment"></i> ${len(total_file_comments)}
284 </span>
284 </span>
285
285
286 <details class="details-reset details-inline-block">
286 <details class="details-reset details-inline-block">
287 <summary class="noselect">
287 <summary class="noselect">
288 <i class="pill icon-options cursor-pointer" op="options"></i>
288 <i class="pill icon-options cursor-pointer" op="options"></i>
289 </summary>
289 </summary>
290 <details-menu class="details-dropdown">
290 <details-menu class="details-dropdown">
291
291
292 <div class="dropdown-item">
292 <div class="dropdown-item">
293 <span>${final_path}</span>
293 <span>${final_path}</span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
294 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${final_path}" title="Copy file path"></span>
295 </div>
295 </div>
296
296
297 <div class="dropdown-divider"></div>
297 <div class="dropdown-divider"></div>
298
298
299 <div class="dropdown-item">
299 <div class="dropdown-item">
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
300 <% permalink = request.current_route_url(_anchor='a_{}'.format(h.FID(filediff.raw_id, filediff.patch['filename']))) %>
301 <a href="${permalink}">ΒΆ permalink</a>
301 <a href="${permalink}">ΒΆ permalink</a>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
302 <span class="pull-right icon-clipboard clipboard-action" data-clipboard-text="${permalink}" title="Copy permalink"></span>
303 </div>
303 </div>
304
304
305
305
306 </details-menu>
306 </details-menu>
307 </details>
307 </details>
308
308
309 </span>
309 </span>
310
310
311 ${diff_ops(final_file_name, filediff)}
311 ${diff_ops(final_file_name, filediff)}
312
312
313 </label>
313 </label>
314
314
315 ${diff_menu(filediff, use_comments=use_comments)}
315 ${diff_menu(filediff, use_comments=use_comments)}
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
316 <table id="file-${h.safeid(h.safe_unicode(filediff.patch['filename']))}" data-f-path="${filediff.patch['filename']}" data-anchor-id="${h.FID(filediff.raw_id, filediff.patch['filename'])}" class="code-visible-block cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
317
317
318 ## new/deleted/empty content case
318 ## new/deleted/empty content case
319 % if not filediff.hunks:
319 % if not filediff.hunks:
320 ## Comment container, on "fakes" hunk that contains all data to render comments
320 ## Comment container, on "fakes" hunk that contains all data to render comments
321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
321 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], filediff.hunk_ops, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
322 % endif
322 % endif
323
323
324 %if filediff.limited_diff:
324 %if filediff.limited_diff:
325 <tr class="cb-warning cb-collapser">
325 <tr class="cb-warning cb-collapser">
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
326 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
327 ${_('The requested commit or file is too big and content was truncated.')} <a href="${h.current_route_path(request, fulldiff=1)}" onclick="return confirm('${_("Showing a big diff might take some time and resources, continue?")}')">${_('Show full diff')}</a>
328 </td>
328 </td>
329 </tr>
329 </tr>
330 %else:
330 %else:
331 %if over_lines_changed_limit:
331 %if over_lines_changed_limit:
332 <tr class="cb-warning cb-collapser">
332 <tr class="cb-warning cb-collapser">
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
333 <td class="cb-text" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=6')}>
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
334 ${_('This diff has been collapsed as it changes many lines, (%i lines changed)' % lines_changed)}
335 <a href="#" class="cb-expand"
335 <a href="#" class="cb-expand"
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
336 onclick="$(this).closest('table').removeClass('cb-collapsed'); updateSticky(); return false;">${_('Show them')}
337 </a>
337 </a>
338 <a href="#" class="cb-collapse"
338 <a href="#" class="cb-collapse"
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
339 onclick="$(this).closest('table').addClass('cb-collapsed'); updateSticky(); return false;">${_('Hide them')}
340 </a>
340 </a>
341 </td>
341 </td>
342 </tr>
342 </tr>
343 %endif
343 %endif
344 %endif
344 %endif
345
345
346 % for hunk in filediff.hunks:
346 % for hunk in filediff.hunks:
347 <tr class="cb-hunk">
347 <tr class="cb-hunk">
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
348 <td ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=3' or '')}>
349 ## TODO: dan: add ajax loading of more context here
349 ## TODO: dan: add ajax loading of more context here
350 ## <a href="#">
350 ## <a href="#">
351 <i class="icon-more"></i>
351 <i class="icon-more"></i>
352 ## </a>
352 ## </a>
353 </td>
353 </td>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
354 <td ${(c.user_session_attrs["diffmode"] == 'sideside' and 'colspan=5' or '')}>
355 @@
355 @@
356 -${hunk.source_start},${hunk.source_length}
356 -${hunk.source_start},${hunk.source_length}
357 +${hunk.target_start},${hunk.target_length}
357 +${hunk.target_start},${hunk.target_length}
358 ${hunk.section_header}
358 ${hunk.section_header}
359 </td>
359 </td>
360 </tr>
360 </tr>
361
361
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
362 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
363 % endfor
363 % endfor
364
364
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
365 <% unmatched_comments = (inline_comments or {}).get(filediff.patch['filename'], {}) %>
366
366
367 ## outdated comments that do not fit into currently displayed lines
367 ## outdated comments that do not fit into currently displayed lines
368 % for lineno, comments in unmatched_comments.items():
368 % for lineno, comments in unmatched_comments.items():
369
369
370 %if c.user_session_attrs["diffmode"] == 'unified':
370 %if c.user_session_attrs["diffmode"] == 'unified':
371 % if loop.index == 0:
371 % if loop.index == 0:
372 <tr class="cb-hunk">
372 <tr class="cb-hunk">
373 <td colspan="3"></td>
373 <td colspan="3"></td>
374 <td>
374 <td>
375 <div>
375 <div>
376 ${_('Unmatched/outdated inline comments below')}
376 ${_('Unmatched/outdated inline comments below')}
377 </div>
377 </div>
378 </td>
378 </td>
379 </tr>
379 </tr>
380 % endif
380 % endif
381 <tr class="cb-line">
381 <tr class="cb-line">
382 <td class="cb-data cb-context"></td>
382 <td class="cb-data cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
383 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
384 <td class="cb-lineno cb-context"></td>
385 <td class="cb-content cb-context">
385 <td class="cb-content cb-context">
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
386 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
387 </td>
387 </td>
388 </tr>
388 </tr>
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
389 %elif c.user_session_attrs["diffmode"] == 'sideside':
390 % if loop.index == 0:
390 % if loop.index == 0:
391 <tr class="cb-comment-info">
391 <tr class="cb-comment-info">
392 <td colspan="2"></td>
392 <td colspan="2"></td>
393 <td class="cb-line">
393 <td class="cb-line">
394 <div>
394 <div>
395 ${_('Unmatched/outdated inline comments below')}
395 ${_('Unmatched/outdated inline comments below')}
396 </div>
396 </div>
397 </td>
397 </td>
398 <td colspan="2"></td>
398 <td colspan="2"></td>
399 <td class="cb-line">
399 <td class="cb-line">
400 <div>
400 <div>
401 ${_('Unmatched/outdated comments below')}
401 ${_('Unmatched/outdated comments below')}
402 </div>
402 </div>
403 </td>
403 </td>
404 </tr>
404 </tr>
405 % endif
405 % endif
406 <tr class="cb-line">
406 <tr class="cb-line">
407 <td class="cb-data cb-context"></td>
407 <td class="cb-data cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
408 <td class="cb-lineno cb-context"></td>
409 <td class="cb-content cb-context">
409 <td class="cb-content cb-context">
410 % if lineno.startswith('o'):
410 % if lineno.startswith('o'):
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
411 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
412 % endif
412 % endif
413 </td>
413 </td>
414
414
415 <td class="cb-data cb-context"></td>
415 <td class="cb-data cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
416 <td class="cb-lineno cb-context"></td>
417 <td class="cb-content cb-context">
417 <td class="cb-content cb-context">
418 % if lineno.startswith('n'):
418 % if lineno.startswith('n'):
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
419 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries)}
420 % endif
420 % endif
421 </td>
421 </td>
422 </tr>
422 </tr>
423 %endif
423 %endif
424
424
425 % endfor
425 % endfor
426
426
427 </table>
427 </table>
428 </div>
428 </div>
429 %endfor
429 %endfor
430
430
431 ## outdated comments that are made for a file that has been deleted
431 ## outdated comments that are made for a file that has been deleted
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
432 % for filename, comments_dict in (deleted_files_comments or {}).items():
433
433
434 <%
434 <%
435 display_state = 'display: none'
435 display_state = 'display: none'
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
436 open_comments_in_file = [x for x in comments_dict['comments'] if x.outdated is False]
437 if open_comments_in_file:
437 if open_comments_in_file:
438 display_state = ''
438 display_state = ''
439 fid = str(id(filename))
439 fid = str(id(filename))
440 %>
440 %>
441 <div class="filediffs filediff-outdated" style="${display_state}">
441 <div class="filediffs filediff-outdated" style="${display_state}">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
442 <input ${(collapse_all and 'checked' or '')} class="filediff-collapse-state collapse-${diffset_container_id}" id="filediff-collapse-${id(filename)}" type="checkbox" onchange="updateSticky();">
443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
443 <div class="filediff" data-f-path="${filename}" id="a_${h.FID(fid, filename)}">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
444 <label for="filediff-collapse-${id(filename)}" class="filediff-heading">
445 <div class="filediff-collapse-indicator icon-"></div>
445 <div class="filediff-collapse-indicator icon-"></div>
446
446
447 <span class="pill">
447 <span class="pill">
448 ## file was deleted
448 ## file was deleted
449 ${filename}
449 ${filename}
450 </span>
450 </span>
451 <span class="pill-group pull-left" >
451 <span class="pill-group pull-left" >
452 ## file op, doesn't need translation
452 ## file op, doesn't need translation
453 <span class="pill" op="removed">unresolved comments</span>
453 <span class="pill" op="removed">unresolved comments</span>
454 </span>
454 </span>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
455 <a class="pill filediff-anchor" href="#a_${h.FID(fid, filename)}">ΒΆ</a>
456 <span class="pill-group pull-right">
456 <span class="pill-group pull-right">
457 <span class="pill" op="deleted">
457 <span class="pill" op="deleted">
458 % if comments_dict['stats'] >0:
458 % if comments_dict['stats'] >0:
459 -${comments_dict['stats']}
459 -${comments_dict['stats']}
460 % else:
460 % else:
461 ${comments_dict['stats']}
461 ${comments_dict['stats']}
462 % endif
462 % endif
463 </span>
463 </span>
464 </span>
464 </span>
465 </label>
465 </label>
466
466
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
467 <table class="cb cb-diff-${c.user_session_attrs["diffmode"]} code-highlight ${(over_lines_changed_limit and 'cb-collapsed' or '')}">
468 <tr>
468 <tr>
469 % if c.user_session_attrs["diffmode"] == 'unified':
469 % if c.user_session_attrs["diffmode"] == 'unified':
470 <td></td>
470 <td></td>
471 %endif
471 %endif
472
472
473 <td></td>
473 <td></td>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
474 <td class="cb-text cb-${op_class(BIN_FILENODE)}" ${(c.user_session_attrs["diffmode"] == 'unified' and 'colspan=4' or 'colspan=5')}>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
475 <strong>${_('This file was removed from diff during updates to this pull-request.')}</strong><br/>
476 ${_('There are still outdated/unresolved comments attached to it.')}
476 ${_('There are still outdated/unresolved comments attached to it.')}
477 </td>
477 </td>
478 </tr>
478 </tr>
479 %if c.user_session_attrs["diffmode"] == 'unified':
479 %if c.user_session_attrs["diffmode"] == 'unified':
480 <tr class="cb-line">
480 <tr class="cb-line">
481 <td class="cb-data cb-context"></td>
481 <td class="cb-data cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
482 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
483 <td class="cb-lineno cb-context"></td>
484 <td class="cb-content cb-context">
484 <td class="cb-content cb-context">
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
485 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
486 </td>
486 </td>
487 </tr>
487 </tr>
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
488 %elif c.user_session_attrs["diffmode"] == 'sideside':
489 <tr class="cb-line">
489 <tr class="cb-line">
490 <td class="cb-data cb-context"></td>
490 <td class="cb-data cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
491 <td class="cb-lineno cb-context"></td>
492 <td class="cb-content cb-context"></td>
492 <td class="cb-content cb-context"></td>
493
493
494 <td class="cb-data cb-context"></td>
494 <td class="cb-data cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
495 <td class="cb-lineno cb-context"></td>
496 <td class="cb-content cb-context">
496 <td class="cb-content cb-context">
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
497 ${inline_comments_container(comments_dict['comments'], active_pattern_entries=active_pattern_entries)}
498 </td>
498 </td>
499 </tr>
499 </tr>
500 %endif
500 %endif
501 </table>
501 </table>
502 </div>
502 </div>
503 </div>
503 </div>
504 % endfor
504 % endfor
505
505
506 </div>
506 </div>
507 </div>
507 </div>
508 </%def>
508 </%def>
509
509
510 <%def name="diff_ops(file_name, filediff)">
510 <%def name="diff_ops(file_name, filediff)">
511 <%
511 <%
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
512 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
513 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE, COPIED_FILENODE
514 %>
514 %>
515 <span class="pill">
515 <span class="pill">
516 <i class="icon-file-text"></i>
516 <i class="icon-file-text"></i>
517 ${file_name}
517 ${file_name}
518 </span>
518 </span>
519
519
520 <span class="pill-group pull-right">
520 <span class="pill-group pull-right">
521
521
522 ## ops pills
522 ## ops pills
523 %if filediff.limited_diff:
523 %if filediff.limited_diff:
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
524 <span class="pill tooltip" op="limited" title="The stats for this diff are not complete">limited diff</span>
525 %endif
525 %endif
526
526
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
527 %if NEW_FILENODE in filediff.patch['stats']['ops']:
528 <span class="pill" op="created">created</span>
528 <span class="pill" op="created">created</span>
529 %if filediff['target_mode'].startswith('120'):
529 %if filediff['target_mode'].startswith('120'):
530 <span class="pill" op="symlink">symlink</span>
530 <span class="pill" op="symlink">symlink</span>
531 %else:
531 %else:
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
532 <span class="pill" op="mode">${nice_mode(filediff['target_mode'])}</span>
533 %endif
533 %endif
534 %endif
534 %endif
535
535
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
536 %if RENAMED_FILENODE in filediff.patch['stats']['ops']:
537 <span class="pill" op="renamed">renamed</span>
537 <span class="pill" op="renamed">renamed</span>
538 %endif
538 %endif
539
539
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
540 %if COPIED_FILENODE in filediff.patch['stats']['ops']:
541 <span class="pill" op="copied">copied</span>
541 <span class="pill" op="copied">copied</span>
542 %endif
542 %endif
543
543
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
544 %if DEL_FILENODE in filediff.patch['stats']['ops']:
545 <span class="pill" op="removed">removed</span>
545 <span class="pill" op="removed">removed</span>
546 %endif
546 %endif
547
547
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
548 %if CHMOD_FILENODE in filediff.patch['stats']['ops']:
549 <span class="pill" op="mode">
549 <span class="pill" op="mode">
550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
550 ${nice_mode(filediff['source_mode'])} ➑ ${nice_mode(filediff['target_mode'])}
551 </span>
551 </span>
552 %endif
552 %endif
553
553
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
554 %if BIN_FILENODE in filediff.patch['stats']['ops']:
555 <span class="pill" op="binary">binary</span>
555 <span class="pill" op="binary">binary</span>
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
556 %if MOD_FILENODE in filediff.patch['stats']['ops']:
557 <span class="pill" op="modified">modified</span>
557 <span class="pill" op="modified">modified</span>
558 %endif
558 %endif
559 %endif
559 %endif
560
560
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
561 <span class="pill" op="added">${('+' if filediff.patch['stats']['added'] else '')}${filediff.patch['stats']['added']}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
562 <span class="pill" op="deleted">${((h.safe_int(filediff.patch['stats']['deleted']) or 0) * -1)}</span>
563
563
564 </span>
564 </span>
565
565
566 </%def>
566 </%def>
567
567
568 <%def name="nice_mode(filemode)">
568 <%def name="nice_mode(filemode)">
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
569 ${(filemode.startswith('100') and filemode[3:] or filemode)}
570 </%def>
570 </%def>
571
571
572 <%def name="diff_menu(filediff, use_comments=False)">
572 <%def name="diff_menu(filediff, use_comments=False)">
573 <div class="filediff-menu">
573 <div class="filediff-menu">
574
574
575 %if filediff.diffset.source_ref:
575 %if filediff.diffset.source_ref:
576
576
577 ## FILE BEFORE CHANGES
577 ## FILE BEFORE CHANGES
578 %if filediff.operation in ['D', 'M']:
578 %if filediff.operation in ['D', 'M']:
579 <a
579 <a
580 class="tooltip"
580 class="tooltip"
581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
581 href="${h.route_path('repo_files',repo_name=filediff.diffset.target_repo_name,commit_id=filediff.diffset.source_ref,f_path=filediff.source_file_path)}"
582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
582 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
583 >
583 >
584 ${_('Show file before')}
584 ${_('Show file before')}
585 </a> |
585 </a> |
586 %else:
586 %else:
587 <span
587 <span
588 class="tooltip"
588 class="tooltip"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
589 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.source_ref[:12]})}"
590 >
590 >
591 ${_('Show file before')}
591 ${_('Show file before')}
592 </span> |
592 </span> |
593 %endif
593 %endif
594
594
595 ## FILE AFTER CHANGES
595 ## FILE AFTER CHANGES
596 %if filediff.operation in ['A', 'M']:
596 %if filediff.operation in ['A', 'M']:
597 <a
597 <a
598 class="tooltip"
598 class="tooltip"
599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
599 href="${h.route_path('repo_files',repo_name=filediff.diffset.source_repo_name,commit_id=filediff.diffset.target_ref,f_path=filediff.target_file_path)}"
600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
600 title="${h.tooltip(_('Show file at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
601 >
601 >
602 ${_('Show file after')}
602 ${_('Show file after')}
603 </a>
603 </a>
604 %else:
604 %else:
605 <span
605 <span
606 class="tooltip"
606 class="tooltip"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
607 title="${h.tooltip(_('File not present at commit: %(commit_id)s') % {'commit_id': filediff.diffset.target_ref[:12]})}"
608 >
608 >
609 ${_('Show file after')}
609 ${_('Show file after')}
610 </span>
610 </span>
611 %endif
611 %endif
612
612
613 % if use_comments:
613 % if use_comments:
614 |
614 |
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
615 <a href="#" onclick="Rhodecode.comments.toggleDiffComments(this);return toggleElement(this)"
616 data-toggle-on="${_('Hide comments')}"
616 data-toggle-on="${_('Hide comments')}"
617 data-toggle-off="${_('Show comments')}">
617 data-toggle-off="${_('Show comments')}">
618 <span class="hide-comment-button">${_('Hide comments')}</span>
618 <span class="hide-comment-button">${_('Hide comments')}</span>
619 </a>
619 </a>
620 % endif
620 % endif
621
621
622 %endif
622 %endif
623
623
624 </div>
624 </div>
625 </%def>
625 </%def>
626
626
627
627
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
628 <%def name="inline_comments_container(comments, active_pattern_entries=None, line_no='', f_path='')">
629
629
630 <div class="inline-comments">
630 <div class="inline-comments">
631 %for comment in comments:
631 %for comment in comments:
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
632 ${commentblock.comment_block(comment, inline=True, active_pattern_entries=active_pattern_entries)}
633 %endfor
633 %endfor
634
634
635 <%
635 <%
636 extra_class = ''
636 extra_class = ''
637 extra_style = ''
637 extra_style = ''
638
638
639 if comments and comments[-1].outdated_at_version(c.at_version_num):
639 if comments and comments[-1].outdated_at_version(c.at_version_num):
640 extra_class = ' comment-outdated'
640 extra_class = ' comment-outdated'
641 extra_style = 'display: none;'
641 extra_style = 'display: none;'
642
642
643 %>
643 %>
644
644
645 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
645 <div class="reply-thread-container-wrapper${extra_class}" style="${extra_style}">
646 <div class="reply-thread-container${extra_class}">
646 <div class="reply-thread-container${extra_class}">
647 <div class="reply-thread-gravatar">
647 <div class="reply-thread-gravatar">
648 % if c.rhodecode_user.username != h.DEFAULT_USER:
648 % if c.rhodecode_user.username != h.DEFAULT_USER:
649 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
649 ${base.gravatar(c.rhodecode_user.email, 20, tooltip=True, user=c.rhodecode_user)}
650 % endif
650 % endif
651 </div>
651 </div>
652
652
653 <div class="reply-thread-reply-button">
653 <div class="reply-thread-reply-button">
654 % if c.rhodecode_user.username != h.DEFAULT_USER:
654 % if c.rhodecode_user.username != h.DEFAULT_USER:
655 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
655 ## initial reply button, some JS logic can append here a FORM to leave a first comment.
656 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
656 <button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">Reply...</button>
657 % endif
657 % endif
658 </div>
658 </div>
659 ##% endif
659 ##% endif
660 <div class="reply-thread-last"></div>
660 <div class="reply-thread-last"></div>
661 </div>
661 </div>
662 </div>
662 </div>
663 </div>
663 </div>
664
664
665 </%def>
665 </%def>
666
666
667 <%!
667 <%!
668
668
669 def get_inline_comments(comments, filename):
669 def get_inline_comments(comments, filename):
670 if hasattr(filename, 'unicode_path'):
670 if hasattr(filename, 'unicode_path'):
671 filename = filename.unicode_path
671 filename = filename.unicode_path
672
672
673 if not isinstance(filename, str):
673 if not isinstance(filename, str):
674 return None
674 return None
675
675
676 if comments and filename in comments:
676 if comments and filename in comments:
677 return comments[filename]
677 return comments[filename]
678
678
679 return None
679 return None
680
680
681 def get_comments_for(diff_type, comments, filename, line_version, line_number):
681 def get_comments_for(diff_type, comments, filename, line_version, line_number):
682 if hasattr(filename, 'unicode_path'):
682 if hasattr(filename, 'unicode_path'):
683 filename = filename.unicode_path
683 filename = filename.unicode_path
684
684
685 if not isinstance(filename, str):
685 if not isinstance(filename, str):
686 return None
686 return None
687
687
688 file_comments = get_inline_comments(comments, filename)
688 file_comments = get_inline_comments(comments, filename)
689 if file_comments is None:
689 if file_comments is None:
690 return None
690 return None
691
691
692 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
692 line_key = '{}{}'.format(line_version, line_number) ## e.g o37, n12
693 if line_key in file_comments:
693 if line_key in file_comments:
694 data = file_comments.pop(line_key)
694 data = file_comments.pop(line_key)
695 return data
695 return data
696 %>
696 %>
697
697
698 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
698 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
699
699
700 <% chunk_count = 1 %>
700 <% chunk_count = 1 %>
701 %for loop_obj, item in h.looper(hunk.sideside):
701 %for loop_obj, item in h.looper(hunk.sideside):
702 <%
702 <%
703 line = item
703 line = item
704 i = loop_obj.index
704 i = loop_obj.index
705 prev_line = loop_obj.previous
705 prev_line = loop_obj.previous
706 old_line_anchor, new_line_anchor = None, None
706 old_line_anchor, new_line_anchor = None, None
707
707
708 if line.original.lineno:
708 if line.original.lineno:
709 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
709 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
710 if line.modified.lineno:
710 if line.modified.lineno:
711 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
711 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
712
712
713 line_action = line.modified.action or line.original.action
713 line_action = line.modified.action or line.original.action
714 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
714 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
715 %>
715 %>
716
716
717 <tr class="cb-line">
717 <tr class="cb-line">
718 <td class="cb-data ${action_class(line.original.action)}"
718 <td class="cb-data ${action_class(line.original.action)}"
719 data-line-no="${line.original.lineno}"
719 data-line-no="${line.original.lineno}"
720 >
720 >
721
721
722 <% line_old_comments, line_old_comments_no_drafts = None, None %>
722 <% line_old_comments, line_old_comments_no_drafts = None, None %>
723 %if line.original.get_comment_args:
723 %if line.original.get_comment_args:
724 <%
724 <%
725 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
725 line_old_comments = get_comments_for('side-by-side', inline_comments, *line.original.get_comment_args)
726 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
726 line_old_comments_no_drafts = [c for c in line_old_comments if not c.draft] if line_old_comments else []
727 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
727 has_outdated = any([x.outdated for x in line_old_comments_no_drafts])
728 %>
728 %>
729 %endif
729 %endif
730 %if line_old_comments_no_drafts:
730 %if line_old_comments_no_drafts:
731 % if has_outdated:
731 % if has_outdated:
732 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
732 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
733 % else:
733 % else:
734 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
734 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_old_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
735 % endif
735 % endif
736 %endif
736 %endif
737 </td>
737 </td>
738 <td class="cb-lineno ${action_class(line.original.action)}"
738 <td class="cb-lineno ${action_class(line.original.action)}"
739 data-line-no="${line.original.lineno}"
739 data-line-no="${line.original.lineno}"
740 %if old_line_anchor:
740 %if old_line_anchor:
741 id="${old_line_anchor}"
741 id="${old_line_anchor}"
742 %endif
742 %endif
743 >
743 >
744 %if line.original.lineno:
744 %if line.original.lineno:
745 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
745 <a name="${old_line_anchor}" href="#${old_line_anchor}">${line.original.lineno}</a>
746 %endif
746 %endif
747 </td>
747 </td>
748
748
749 <% line_no = 'o{}'.format(line.original.lineno) %>
749 <% line_no = 'o{}'.format(line.original.lineno) %>
750 <td class="cb-content ${action_class(line.original.action)}"
750 <td class="cb-content ${action_class(line.original.action)}"
751 data-line-no="${line_no}"
751 data-line-no="${line_no}"
752 >
752 >
753 %if use_comments and line.original.lineno:
753 %if use_comments and line.original.lineno:
754 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
754 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
755 %endif
755 %endif
756 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
756 <span class="cb-code"><span class="cb-action ${action_class(line.original.action)}"></span>${line.original.content or '' | n}</span>
757
757
758 %if use_comments and line.original.lineno and line_old_comments:
758 %if use_comments and line.original.lineno and line_old_comments:
759 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
759 ${inline_comments_container(line_old_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
760 %endif
760 %endif
761
761
762 </td>
762 </td>
763 <td class="cb-data ${action_class(line.modified.action)}"
763 <td class="cb-data ${action_class(line.modified.action)}"
764 data-line-no="${line.modified.lineno}"
764 data-line-no="${line.modified.lineno}"
765 >
765 >
766 <div>
766 <div>
767
767
768 <% line_new_comments, line_new_comments_no_drafts = None, None %>
768 <% line_new_comments, line_new_comments_no_drafts = None, None %>
769 %if line.modified.get_comment_args:
769 %if line.modified.get_comment_args:
770 <%
770 <%
771 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
771 line_new_comments = get_comments_for('side-by-side', inline_comments, *line.modified.get_comment_args)
772 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
772 line_new_comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
773 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
773 has_outdated = any([x.outdated for x in line_new_comments_no_drafts])
774 %>
774 %>
775 %endif
775 %endif
776
776
777 %if line_new_comments_no_drafts:
777 %if line_new_comments_no_drafts:
778 % if has_outdated:
778 % if has_outdated:
779 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
779 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
780 % else:
780 % else:
781 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
781 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(line_new_comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
782 % endif
782 % endif
783 %endif
783 %endif
784 </div>
784 </div>
785 </td>
785 </td>
786 <td class="cb-lineno ${action_class(line.modified.action)}"
786 <td class="cb-lineno ${action_class(line.modified.action)}"
787 data-line-no="${line.modified.lineno}"
787 data-line-no="${line.modified.lineno}"
788 %if new_line_anchor:
788 %if new_line_anchor:
789 id="${new_line_anchor}"
789 id="${new_line_anchor}"
790 %endif
790 %endif
791 >
791 >
792 %if line.modified.lineno:
792 %if line.modified.lineno:
793 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
793 <a name="${new_line_anchor}" href="#${new_line_anchor}">${line.modified.lineno}</a>
794 %endif
794 %endif
795 </td>
795 </td>
796
796
797 <% line_no = 'n{}'.format(line.modified.lineno) %>
797 <% line_no = 'n{}'.format(line.modified.lineno) %>
798 <td class="cb-content ${action_class(line.modified.action)}"
798 <td class="cb-content ${action_class(line.modified.action)}"
799 data-line-no="${line_no}"
799 data-line-no="${line_no}"
800 >
800 >
801 %if use_comments and line.modified.lineno:
801 %if use_comments and line.modified.lineno:
802 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
802 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
803 %endif
803 %endif
804 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
804 <span class="cb-code"><span class="cb-action ${action_class(line.modified.action)}"></span>${line.modified.content or '' | n}</span>
805 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
805 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
806 <div class="nav-chunk" style="visibility: hidden">
806 <div class="nav-chunk" style="visibility: hidden">
807 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
807 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
808 </div>
808 </div>
809 <% chunk_count +=1 %>
809 <% chunk_count +=1 %>
810 % endif
810 % endif
811 %if use_comments and line.modified.lineno and line_new_comments:
811 %if use_comments and line.modified.lineno and line_new_comments:
812 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
812 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
813 %endif
813 %endif
814
814
815 </td>
815 </td>
816 </tr>
816 </tr>
817 %endfor
817 %endfor
818 </%def>
818 </%def>
819
819
820
820
821 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
821 <%def name="render_hunk_lines_unified(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
822 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
822 %for old_line_no, new_line_no, action, content, comments_args in hunk.unified:
823
823
824 <%
824 <%
825 old_line_anchor, new_line_anchor = None, None
825 old_line_anchor, new_line_anchor = None, None
826 if old_line_no:
826 if old_line_no:
827 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
827 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, old_line_no, 'o')
828 if new_line_no:
828 if new_line_no:
829 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
829 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, new_line_no, 'n')
830 %>
830 %>
831 <tr class="cb-line">
831 <tr class="cb-line">
832 <td class="cb-data ${action_class(action)}">
832 <td class="cb-data ${action_class(action)}">
833 <div>
833 <div>
834
834
835 <% comments, comments_no_drafts = None, None %>
835 <% comments, comments_no_drafts = None, None %>
836 %if comments_args:
836 %if comments_args:
837 <%
837 <%
838 comments = get_comments_for('unified', inline_comments, *comments_args)
838 comments = get_comments_for('unified', inline_comments, *comments_args)
839 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
839 comments_no_drafts = [c for c in line_new_comments if not c.draft] if line_new_comments else []
840 has_outdated = any([x.outdated for x in comments_no_drafts])
840 has_outdated = any([x.outdated for x in comments_no_drafts])
841 %>
841 %>
842 %endif
842 %endif
843
843
844 % if comments_no_drafts:
844 % if comments_no_drafts:
845 % if has_outdated:
845 % if has_outdated:
846 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
846 <i class="tooltip toggle-comment-action icon-comment-toggle" title="${_('Comments including outdated: {}. Click here to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
847 % else:
847 % else:
848 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
848 <i class="tooltip toggle-comment-action icon-comment" title="${_('Comments: {}. Click to toggle them.').format(len(comments_no_drafts))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
849 % endif
849 % endif
850 % endif
850 % endif
851 </div>
851 </div>
852 </td>
852 </td>
853 <td class="cb-lineno ${action_class(action)}"
853 <td class="cb-lineno ${action_class(action)}"
854 data-line-no="${old_line_no}"
854 data-line-no="${old_line_no}"
855 %if old_line_anchor:
855 %if old_line_anchor:
856 id="${old_line_anchor}"
856 id="${old_line_anchor}"
857 %endif
857 %endif
858 >
858 >
859 %if old_line_anchor:
859 %if old_line_anchor:
860 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
860 <a name="${old_line_anchor}" href="#${old_line_anchor}">${old_line_no}</a>
861 %endif
861 %endif
862 </td>
862 </td>
863 <td class="cb-lineno ${action_class(action)}"
863 <td class="cb-lineno ${action_class(action)}"
864 data-line-no="${new_line_no}"
864 data-line-no="${new_line_no}"
865 %if new_line_anchor:
865 %if new_line_anchor:
866 id="${new_line_anchor}"
866 id="${new_line_anchor}"
867 %endif
867 %endif
868 >
868 >
869 %if new_line_anchor:
869 %if new_line_anchor:
870 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
870 <a name="${new_line_anchor}" href="#${new_line_anchor}">${new_line_no}</a>
871 %endif
871 %endif
872 </td>
872 </td>
873 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
873 <% line_no = '{}{}'.format(new_line_no and 'n' or 'o', new_line_no or old_line_no) %>
874 <td class="cb-content ${action_class(action)}"
874 <td class="cb-content ${action_class(action)}"
875 data-line-no="${line_no}"
875 data-line-no="${line_no}"
876 >
876 >
877 %if use_comments:
877 %if use_comments:
878 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
878 ${render_add_comment_button(line_no=line_no, f_path=filediff.patch['filename'])}
879 %endif
879 %endif
880 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
880 <span class="cb-code"><span class="cb-action ${action_class(action)}"></span> ${content or '' | n}</span>
881 %if use_comments and comments:
881 %if use_comments and comments:
882 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
882 ${inline_comments_container(comments, active_pattern_entries=active_pattern_entries, line_no=line_no, f_path=filediff.patch['filename'])}
883 %endif
883 %endif
884 </td>
884 </td>
885 </tr>
885 </tr>
886 %endfor
886 %endfor
887 </%def>
887 </%def>
888
888
889
889
890 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
890 <%def name="render_hunk_lines(filediff, diff_mode, hunk, use_comments, inline_comments, active_pattern_entries)">
891 % if diff_mode == 'unified':
891 % if diff_mode == 'unified':
892 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
892 ${render_hunk_lines_unified(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
893 % elif diff_mode == 'sideside':
893 % elif diff_mode == 'sideside':
894 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
894 ${render_hunk_lines_sideside(filediff, hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
895 % else:
895 % else:
896 <tr class="cb-line">
896 <tr class="cb-line">
897 <td>unknown diff mode</td>
897 <td>unknown diff mode</td>
898 </tr>
898 </tr>
899 % endif
899 % endif
900 </%def>file changes
900 </%def>file changes
901
901
902
902
903 <%def name="render_add_comment_button(line_no='', f_path='')">
903 <%def name="render_add_comment_button(line_no='', f_path='')">
904 % if not c.rhodecode_user.is_default:
904 % if not c.rhodecode_user.is_default:
905 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
905 <button class="btn btn-small btn-primary cb-comment-box-opener" onclick="return Rhodecode.comments.createComment(this, '${f_path}', '${line_no}', null)">
906 <span><i class="icon-comment"></i></span>
906 <span><i class="icon-comment"></i></span>
907 </button>
907 </button>
908 % endif
908 % endif
909 </%def>
909 </%def>
910
910
911 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
911 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
912 <% diffset_container_id = h.md5(diffset.target_ref) %>
912 <% diffset_container_id = h.md5(diffset.target_ref) %>
913
913
914 <div id="diff-file-sticky" class="diffset-menu clearinner">
914 <div id="diff-file-sticky" class="diffset-menu clearinner">
915 ## auto adjustable
915 ## auto adjustable
916 <div class="sidebar__inner">
916 <div class="sidebar__inner">
917 <div class="sidebar__bar">
917 <div class="sidebar__bar">
918 <div class="pull-right">
918 <div class="pull-right">
919
919
920 <div class="btn-group" style="margin-right: 5px;">
920 <div class="btn-group" style="margin-right: 5px;">
921 <a class="tooltip btn" onclick="scrollDown();return false" title="${_('Scroll to page bottom')}">
921 <a class="tooltip btn" onclick="scrollDown();return false" title="${_('Scroll to page bottom')}">
922 <i class="icon-arrow_down"></i>
922 <i class="icon-arrow_down"></i>
923 </a>
923 </a>
924 <a class="tooltip btn" onclick="scrollUp();return false" title="${_('Scroll to page top')}">
924 <a class="tooltip btn" onclick="scrollUp();return false" title="${_('Scroll to page top')}">
925 <i class="icon-arrow_up"></i>
925 <i class="icon-arrow_up"></i>
926 </a>
926 </a>
927 </div>
927 </div>
928
928
929 <div class="btn-group">
929 <div class="btn-group">
930 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
930 <a class="btn tooltip toggle-wide-diff" href="#toggle-wide-diff" onclick="toggleWideDiff(this); return false" title="${h.tooltip(_('Toggle wide diff'))}">
931 <i class="icon-wide-mode"></i>
931 <i class="icon-wide-mode"></i>
932 </a>
932 </a>
933 </div>
933 </div>
934 <div class="btn-group">
934 <div class="btn-group">
935
935
936 <a
936 <a
937 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
937 class="btn ${(c.user_session_attrs["diffmode"] == 'sideside' and 'btn-active')} tooltip"
938 title="${h.tooltip(_('View diff as side by side'))}"
938 title="${h.tooltip(_('View diff as side by side'))}"
939 href="${h.current_route_path(request, diffmode='sideside')}">
939 href="${h.current_route_path(request, diffmode='sideside')}">
940 <span>${_('Side by Side')}</span>
940 <span>${_('Side by Side')}</span>
941 </a>
941 </a>
942
942
943 <a
943 <a
944 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
944 class="btn ${(c.user_session_attrs["diffmode"] == 'unified' and 'btn-active')} tooltip"
945 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
945 title="${h.tooltip(_('View diff as unified'))}" href="${h.current_route_path(request, diffmode='unified')}">
946 <span>${_('Unified')}</span>
946 <span>${_('Unified')}</span>
947 </a>
947 </a>
948
948
949 % if range_diff_on is True:
949 % if range_diff_on is True:
950 <a
950 <a
951 title="${_('Turn off: Show the diff as commit range')}"
951 title="${_('Turn off: Show the diff as commit range')}"
952 class="btn btn-primary"
952 class="btn btn-primary"
953 href="${h.current_route_path(request, **{"range-diff":"0"})}">
953 href="${h.current_route_path(request, **{"range-diff":"0"})}">
954 <span>${_('Range Diff')}</span>
954 <span>${_('Range Diff')}</span>
955 </a>
955 </a>
956 % elif range_diff_on is False:
956 % elif range_diff_on is False:
957 <a
957 <a
958 title="${_('Show the diff as commit range')}"
958 title="${_('Show the diff as commit range')}"
959 class="btn"
959 class="btn"
960 href="${h.current_route_path(request, **{"range-diff":"1"})}">
960 href="${h.current_route_path(request, **{"range-diff":"1"})}">
961 <span>${_('Range Diff')}</span>
961 <span>${_('Range Diff')}</span>
962 </a>
962 </a>
963 % endif
963 % endif
964 </div>
964 </div>
965 <div class="btn-group">
965 <div class="btn-group">
966
966
967 <details class="details-reset details-inline-block">
967 <details class="details-reset details-inline-block">
968 <summary class="noselect btn">
968 <summary class="noselect btn">
969 <i class="icon-options cursor-pointer" op="options"></i>
969 <i class="icon-options cursor-pointer" op="options"></i>
970 </summary>
970 </summary>
971
971
972 <div>
972 <div>
973 <details-menu class="details-dropdown" style="top: 35px;">
973 <details-menu class="details-dropdown" style="top: 35px;">
974
974
975 <div class="dropdown-item">
975 <div class="dropdown-item">
976 <div style="padding: 2px 0px">
976 <div style="padding: 2px 0px">
977 % if request.GET.get('ignorews', '') == '1':
977 % if request.GET.get('ignorews', '') == '1':
978 <a href="${h.current_route_path(request, ignorews=0)}">${_('Show whitespace changes')}</a>
978 <a href="${h.current_route_path(request, ignorews=0)}">${_('Show whitespace changes')}</a>
979 % else:
979 % else:
980 <a href="${h.current_route_path(request, ignorews=1)}">${_('Hide whitespace changes')}</a>
980 <a href="${h.current_route_path(request, ignorews=1)}">${_('Hide whitespace changes')}</a>
981 % endif
981 % endif
982 </div>
982 </div>
983 </div>
983 </div>
984
984
985 <div class="dropdown-item">
985 <div class="dropdown-item">
986 <div style="padding: 2px 0px">
986 <div style="padding: 2px 0px">
987 % if request.GET.get('fullcontext', '') == '1':
987 % if request.GET.get('fullcontext', '') == '1':
988 <a href="${h.current_route_path(request, fullcontext=0)}">${_('Hide full context diff')}</a>
988 <a href="${h.current_route_path(request, fullcontext=0)}">${_('Hide full context diff')}</a>
989 % else:
989 % else:
990 <a href="${h.current_route_path(request, fullcontext=1)}">${_('Show full context diff')}</a>
990 <a href="${h.current_route_path(request, fullcontext=1)}">${_('Show full context diff')}</a>
991 % endif
991 % endif
992 </div>
992 </div>
993 </div>
993 </div>
994
994
995 </details-menu>
995 </details-menu>
996 </div>
996 </div>
997 </details>
997 </details>
998
998
999 </div>
999 </div>
1000 </div>
1000 </div>
1001 <div class="pull-left">
1001 <div class="pull-left">
1002 <div class="btn-group">
1002 <div class="btn-group">
1003 <div class="pull-left">
1003 <div class="pull-left">
1004 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
1004 ${h.hidden('file_filter_{}'.format(diffset_container_id))}
1005 </div>
1005 </div>
1006
1006
1007 </div>
1007 </div>
1008 </div>
1008 </div>
1009 </div>
1009 </div>
1010 <div class="fpath-placeholder pull-left">
1010 <div class="fpath-placeholder pull-left">
1011 <i class="icon-file-text"></i>
1011 <i class="icon-file-text"></i>
1012 <strong class="fpath-placeholder-text">
1012 <strong class="fpath-placeholder-text">
1013 Context file:
1013 Context file:
1014 </strong>
1014 </strong>
1015 </div>
1015 </div>
1016 <div class="pull-right noselect">
1016 <div class="pull-right noselect">
1017 %if commit:
1017 %if commit:
1018 <span>
1018 <span>
1019 <code>${h.show_id(commit)}</code>
1019 <code>${h.show_id(commit)}</code>
1020 </span>
1020 </span>
1021 %elif pull_request_menu and pull_request_menu.get('pull_request'):
1021 %elif pull_request_menu and pull_request_menu.get('pull_request'):
1022 <span>
1022 <span>
1023 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
1023 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
1024 </span>
1024 </span>
1025 %endif
1025 %endif
1026 % if commit or pull_request_menu:
1026 % if commit or pull_request_menu:
1027 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
1027 <span class="tooltip" title="Navigate to previous or next change inside files." id="diff_nav">Loading diff...:</span>
1028 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
1028 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
1029 <i class="icon-angle-up"></i>
1029 <i class="icon-angle-up"></i>
1030 </span>
1030 </span>
1031 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
1031 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
1032 <i class="icon-angle-down"></i>
1032 <i class="icon-angle-down"></i>
1033 </span>
1033 </span>
1034 % endif
1034 % endif
1035 </div>
1035 </div>
1036 <div class="sidebar_inner_shadow"></div>
1036 <div class="sidebar_inner_shadow"></div>
1037 </div>
1037 </div>
1038 </div>
1038 </div>
1039
1039
1040 % if diffset:
1040 % if diffset:
1041 %if diffset.limited_diff:
1041 %if diffset.limited_diff:
1042 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
1042 <% file_placeholder = _ungettext('%(num)s file changed', '%(num)s files changed', diffset.changed_files) % {'num': diffset.changed_files} %>
1043 %else:
1043 %else:
1044 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1044 <% file_placeholder = h.literal(_ungettext('%(num)s file changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>', '%(num)s files changed: <span class="op-added">%(linesadd)s inserted</span>, <span class="op-deleted">%(linesdel)s deleted</span>',
1045 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1045 diffset.changed_files) % {'num': diffset.changed_files, 'linesadd': diffset.lines_added, 'linesdel': diffset.lines_deleted}) %>
1046
1046
1047 %endif
1047 %endif
1048 ## case on range-diff placeholder needs to be updated
1048 ## case on range-diff placeholder needs to be updated
1049 % if range_diff_on is True:
1049 % if range_diff_on is True:
1050 <% file_placeholder = _('Disabled on range diff') %>
1050 <% file_placeholder = _('Disabled on range diff') %>
1051 % endif
1051 % endif
1052
1052
1053 <script type="text/javascript">
1053 <script type="text/javascript">
1054 var feedFilesOptions = function (query, initialData) {
1054 var feedFilesOptions = function (query, initialData) {
1055 var data = {results: []};
1055 var data = {results: []};
1056 var isQuery = typeof query.term !== 'undefined';
1056 var isQuery = typeof query.term !== 'undefined';
1057
1057
1058 var section = _gettext('Changed files');
1058 var section = _gettext('Changed files');
1059 var filteredData = [];
1059 var filteredData = [];
1060
1060
1061 //filter results
1061 //filter results
1062 $.each(initialData.results, function (idx, value) {
1062 $.each(initialData.results, function (idx, value) {
1063
1063
1064 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1064 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
1065 filteredData.push({
1065 filteredData.push({
1066 'id': this.id,
1066 'id': this.id,
1067 'text': this.text,
1067 'text': this.text,
1068 "ops": this.ops,
1068 "ops": this.ops,
1069 })
1069 })
1070 }
1070 }
1071
1071
1072 });
1072 });
1073
1073
1074 data.results = filteredData;
1074 data.results = filteredData;
1075
1075
1076 query.callback(data);
1076 query.callback(data);
1077 };
1077 };
1078
1078
1079 var selectionFormatter = function(data, escapeMarkup) {
1079 var selectionFormatter = function(data, escapeMarkup) {
1080 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1080 var container = '<div class="filelist" style="padding-right:100px">{0}</div>';
1081 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1081 var tmpl = '<div><strong>{0}</strong></div>'.format(escapeMarkup(data['text']));
1082 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1082 var pill = '<div class="pill-group" style="position: absolute; top:7px; right: 0">' +
1083 '<span class="pill" op="added">{0}</span>' +
1083 '<span class="pill" op="added">{0}</span>' +
1084 '<span class="pill" op="deleted">{1}</span>' +
1084 '<span class="pill" op="deleted">{1}</span>' +
1085 '</div>'
1085 '</div>'
1086 ;
1086 ;
1087 var added = data['ops']['added'];
1087 var added = data['ops']['added'];
1088 if (added === 0) {
1088 if (added === 0) {
1089 // don't show +0
1089 // don't show +0
1090 added = 0;
1090 added = 0;
1091 } else {
1091 } else {
1092 added = '+' + added;
1092 added = '+' + added;
1093 }
1093 }
1094
1094
1095 var deleted = -1*data['ops']['deleted'];
1095 var deleted = -1*data['ops']['deleted'];
1096
1096
1097 tmpl += pill.format(added, deleted);
1097 tmpl += pill.format(added, deleted);
1098 return container.format(tmpl);
1098 return container.format(tmpl);
1099 };
1099 };
1100 var formatFileResult = function(result, container, query, escapeMarkup) {
1100 var formatFileResult = function(result, container, query, escapeMarkup) {
1101 return selectionFormatter(result, escapeMarkup);
1101 return selectionFormatter(result, escapeMarkup);
1102 };
1102 };
1103
1103
1104 var formatSelection = function (data, container) {
1104 var formatSelection = function (data, container) {
1105 return '${file_placeholder}'
1105 return '${file_placeholder}'
1106 };
1106 };
1107
1107
1108 if (window.preloadFileFilterData === undefined) {
1108 if (window.preloadFileFilterData === undefined) {
1109 window.preloadFileFilterData = {}
1109 window.preloadFileFilterData = {}
1110 }
1110 }
1111
1111
1112 preloadFileFilterData["${diffset_container_id}"] = {
1112 preloadFileFilterData["${diffset_container_id}"] = {
1113 results: [
1113 results: [
1114 % for filediff in diffset.files:
1114 % for filediff in diffset.files:
1115 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1115 {id:"a_${h.FID(filediff.raw_id, filediff.patch['filename'])}",
1116 text:"${filediff.patch['filename']}",
1116 text:"${filediff.patch['filename']}",
1117 ops:${h.json.dumps(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1117 ops:${h.str_json(filediff.patch['stats'])|n}}${('' if loop.last else ',')}
1118 % endfor
1118 % endfor
1119 ]
1119 ]
1120 };
1120 };
1121
1121
1122 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1122 var diffFileFilterId = "#file_filter_" + "${diffset_container_id}";
1123 var diffFileFilter = $(diffFileFilterId).select2({
1123 var diffFileFilter = $(diffFileFilterId).select2({
1124 'dropdownAutoWidth': true,
1124 'dropdownAutoWidth': true,
1125 'width': 'auto',
1125 'width': 'auto',
1126
1126
1127 containerCssClass: "drop-menu",
1127 containerCssClass: "drop-menu",
1128 dropdownCssClass: "drop-menu-dropdown",
1128 dropdownCssClass: "drop-menu-dropdown",
1129 data: preloadFileFilterData["${diffset_container_id}"],
1129 data: preloadFileFilterData["${diffset_container_id}"],
1130 query: function(query) {
1130 query: function(query) {
1131 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1131 feedFilesOptions(query, preloadFileFilterData["${diffset_container_id}"]);
1132 },
1132 },
1133 initSelection: function(element, callback) {
1133 initSelection: function(element, callback) {
1134 callback({'init': true});
1134 callback({'init': true});
1135 },
1135 },
1136 formatResult: formatFileResult,
1136 formatResult: formatFileResult,
1137 formatSelection: formatSelection
1137 formatSelection: formatSelection
1138 });
1138 });
1139
1139
1140 % if range_diff_on is True:
1140 % if range_diff_on is True:
1141 diffFileFilter.select2("enable", false);
1141 diffFileFilter.select2("enable", false);
1142 % endif
1142 % endif
1143
1143
1144 $(diffFileFilterId).on('select2-selecting', function (e) {
1144 $(diffFileFilterId).on('select2-selecting', function (e) {
1145 var idSelector = e.choice.id;
1145 var idSelector = e.choice.id;
1146
1146
1147 // expand the container if we quick-select the field
1147 // expand the container if we quick-select the field
1148 $('#'+idSelector).next().prop('checked', false);
1148 $('#'+idSelector).next().prop('checked', false);
1149 // hide the mast as we later do preventDefault()
1149 // hide the mast as we later do preventDefault()
1150 $("#select2-drop-mask").click();
1150 $("#select2-drop-mask").click();
1151
1151
1152 window.location.hash = '#'+idSelector;
1152 window.location.hash = '#'+idSelector;
1153 updateSticky();
1153 updateSticky();
1154
1154
1155 e.preventDefault();
1155 e.preventDefault();
1156 });
1156 });
1157
1157
1158 diffNavText = 'diff navigation:'
1158 diffNavText = 'diff navigation:'
1159
1159
1160 getCurrentChunk = function () {
1160 getCurrentChunk = function () {
1161
1161
1162 var chunksAll = $('.nav-chunk').filter(function () {
1162 var chunksAll = $('.nav-chunk').filter(function () {
1163 return $(this).parents('.filediff').prev().get(0).checked !== true
1163 return $(this).parents('.filediff').prev().get(0).checked !== true
1164 })
1164 })
1165 var chunkSelected = $('.nav-chunk.selected');
1165 var chunkSelected = $('.nav-chunk.selected');
1166 var initial = false;
1166 var initial = false;
1167
1167
1168 if (chunkSelected.length === 0) {
1168 if (chunkSelected.length === 0) {
1169 // no initial chunk selected, we pick first
1169 // no initial chunk selected, we pick first
1170 chunkSelected = $(chunksAll.get(0));
1170 chunkSelected = $(chunksAll.get(0));
1171 var initial = true;
1171 var initial = true;
1172 }
1172 }
1173
1173
1174 return {
1174 return {
1175 'all': chunksAll,
1175 'all': chunksAll,
1176 'selected': chunkSelected,
1176 'selected': chunkSelected,
1177 'initial': initial,
1177 'initial': initial,
1178 }
1178 }
1179 }
1179 }
1180
1180
1181 animateDiffNavText = function () {
1181 animateDiffNavText = function () {
1182 var $diffNav = $('#diff_nav')
1182 var $diffNav = $('#diff_nav')
1183
1183
1184 var callback = function () {
1184 var callback = function () {
1185 $diffNav.animate({'opacity': 1.00}, 200)
1185 $diffNav.animate({'opacity': 1.00}, 200)
1186 };
1186 };
1187 $diffNav.animate({'opacity': 0.15}, 200, callback);
1187 $diffNav.animate({'opacity': 0.15}, 200, callback);
1188 }
1188 }
1189
1189
1190 scrollToChunk = function (moveBy) {
1190 scrollToChunk = function (moveBy) {
1191 var chunk = getCurrentChunk();
1191 var chunk = getCurrentChunk();
1192 var all = chunk.all
1192 var all = chunk.all
1193 var selected = chunk.selected
1193 var selected = chunk.selected
1194
1194
1195 var curPos = all.index(selected);
1195 var curPos = all.index(selected);
1196 var newPos = curPos;
1196 var newPos = curPos;
1197 if (!chunk.initial) {
1197 if (!chunk.initial) {
1198 var newPos = curPos + moveBy;
1198 var newPos = curPos + moveBy;
1199 }
1199 }
1200
1200
1201 var curElem = all.get(newPos);
1201 var curElem = all.get(newPos);
1202
1202
1203 if (curElem === undefined) {
1203 if (curElem === undefined) {
1204 // end or back
1204 // end or back
1205 $('#diff_nav').html('no next diff element:')
1205 $('#diff_nav').html('no next diff element:')
1206 animateDiffNavText()
1206 animateDiffNavText()
1207 return
1207 return
1208 } else if (newPos < 0) {
1208 } else if (newPos < 0) {
1209 $('#diff_nav').html('no previous diff element:')
1209 $('#diff_nav').html('no previous diff element:')
1210 animateDiffNavText()
1210 animateDiffNavText()
1211 return
1211 return
1212 } else {
1212 } else {
1213 $('#diff_nav').html(diffNavText)
1213 $('#diff_nav').html(diffNavText)
1214 }
1214 }
1215
1215
1216 curElem = $(curElem)
1216 curElem = $(curElem)
1217 var offset = 100;
1217 var offset = 100;
1218 $(window).scrollTop(curElem.position().top - offset);
1218 $(window).scrollTop(curElem.position().top - offset);
1219
1219
1220 //clear selection
1220 //clear selection
1221 all.removeClass('selected')
1221 all.removeClass('selected')
1222 curElem.addClass('selected')
1222 curElem.addClass('selected')
1223 }
1223 }
1224
1224
1225 scrollToPrevChunk = function () {
1225 scrollToPrevChunk = function () {
1226 scrollToChunk(-1)
1226 scrollToChunk(-1)
1227 }
1227 }
1228 scrollToNextChunk = function () {
1228 scrollToNextChunk = function () {
1229 scrollToChunk(1)
1229 scrollToChunk(1)
1230 }
1230 }
1231
1231
1232 </script>
1232 </script>
1233 % endif
1233 % endif
1234
1234
1235 <script type="text/javascript">
1235 <script type="text/javascript">
1236 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1236 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1237
1237
1238 $(document).ready(function () {
1238 $(document).ready(function () {
1239
1239
1240 var contextPrefix = _gettext('Context file: ');
1240 var contextPrefix = _gettext('Context file: ');
1241 ## sticky sidebar
1241 ## sticky sidebar
1242 var sidebarElement = document.getElementById('diff-file-sticky');
1242 var sidebarElement = document.getElementById('diff-file-sticky');
1243 sidebar = new StickySidebar(sidebarElement, {
1243 sidebar = new StickySidebar(sidebarElement, {
1244 topSpacing: 0,
1244 topSpacing: 0,
1245 bottomSpacing: 0,
1245 bottomSpacing: 0,
1246 innerWrapperSelector: '.sidebar__inner'
1246 innerWrapperSelector: '.sidebar__inner'
1247 });
1247 });
1248 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1248 sidebarElement.addEventListener('affixed.static.stickySidebar', function () {
1249 // reset our file so it's not holding new value
1249 // reset our file so it's not holding new value
1250 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1250 $('.fpath-placeholder-text').html(contextPrefix + ' - ')
1251 });
1251 });
1252
1252
1253 updateSticky = function () {
1253 updateSticky = function () {
1254 sidebar.updateSticky();
1254 sidebar.updateSticky();
1255 Waypoint.refreshAll();
1255 Waypoint.refreshAll();
1256 };
1256 };
1257
1257
1258 var animateText = function (fPath, anchorId) {
1258 var animateText = function (fPath, anchorId) {
1259 fPath = Select2.util.escapeMarkup(fPath);
1259 fPath = Select2.util.escapeMarkup(fPath);
1260 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1260 $('.fpath-placeholder-text').html(contextPrefix + '<a href="#a_' + anchorId + '">' + fPath + '</a>')
1261 };
1261 };
1262
1262
1263 ## dynamic file waypoints
1263 ## dynamic file waypoints
1264 var setFPathInfo = function(fPath, anchorId){
1264 var setFPathInfo = function(fPath, anchorId){
1265 animateText(fPath, anchorId)
1265 animateText(fPath, anchorId)
1266 };
1266 };
1267
1267
1268 var codeBlock = $('.filediff');
1268 var codeBlock = $('.filediff');
1269
1269
1270 // forward waypoint
1270 // forward waypoint
1271 codeBlock.waypoint(
1271 codeBlock.waypoint(
1272 function(direction) {
1272 function(direction) {
1273 if (direction === "down"){
1273 if (direction === "down"){
1274 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1274 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1275 }
1275 }
1276 }, {
1276 }, {
1277 offset: function () {
1277 offset: function () {
1278 return 70;
1278 return 70;
1279 },
1279 },
1280 context: '.fpath-placeholder'
1280 context: '.fpath-placeholder'
1281 }
1281 }
1282 );
1282 );
1283
1283
1284 // backward waypoint
1284 // backward waypoint
1285 codeBlock.waypoint(
1285 codeBlock.waypoint(
1286 function(direction) {
1286 function(direction) {
1287 if (direction === "up"){
1287 if (direction === "up"){
1288 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1288 setFPathInfo($(this.element).data('fPath'), $(this.element).data('anchorId'))
1289 }
1289 }
1290 }, {
1290 }, {
1291 offset: function () {
1291 offset: function () {
1292 return -this.element.clientHeight + 90;
1292 return -this.element.clientHeight + 90;
1293 },
1293 },
1294 context: '.fpath-placeholder'
1294 context: '.fpath-placeholder'
1295 }
1295 }
1296 );
1296 );
1297
1297
1298 toggleWideDiff = function (el) {
1298 toggleWideDiff = function (el) {
1299 updateSticky();
1299 updateSticky();
1300 var wide = Rhodecode.comments.toggleWideMode(this);
1300 var wide = Rhodecode.comments.toggleWideMode(this);
1301 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1301 storeUserSessionAttr('rc_user_session_attr.wide_diff_mode', wide);
1302 if (wide === true) {
1302 if (wide === true) {
1303 $(el).addClass('btn-active');
1303 $(el).addClass('btn-active');
1304 } else {
1304 } else {
1305 $(el).removeClass('btn-active');
1305 $(el).removeClass('btn-active');
1306 }
1306 }
1307 return null;
1307 return null;
1308 };
1308 };
1309
1309
1310 toggleExpand = function (el, diffsetEl) {
1310 toggleExpand = function (el, diffsetEl) {
1311 var el = $(el);
1311 var el = $(el);
1312 if (el.hasClass('collapsed')) {
1312 if (el.hasClass('collapsed')) {
1313 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1313 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', false);
1314 el.removeClass('collapsed');
1314 el.removeClass('collapsed');
1315 el.html(
1315 el.html(
1316 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1316 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1317 _gettext('Collapse all files'));
1317 _gettext('Collapse all files'));
1318 }
1318 }
1319 else {
1319 else {
1320 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1320 $('.filediff-collapse-state.collapse-{0}'.format(diffsetEl)).prop('checked', true);
1321 el.addClass('collapsed');
1321 el.addClass('collapsed');
1322 el.html(
1322 el.html(
1323 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1323 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1324 _gettext('Expand all files'));
1324 _gettext('Expand all files'));
1325 }
1325 }
1326 updateSticky()
1326 updateSticky()
1327 };
1327 };
1328
1328
1329 toggleCommitExpand = function (el) {
1329 toggleCommitExpand = function (el) {
1330 var $el = $(el);
1330 var $el = $(el);
1331 var commits = $el.data('toggleCommitsCnt');
1331 var commits = $el.data('toggleCommitsCnt');
1332 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1332 var collapseMsg = _ngettext('Collapse {0} commit', 'Collapse {0} commits', commits).format(commits);
1333 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1333 var expandMsg = _ngettext('Expand {0} commit', 'Expand {0} commits', commits).format(commits);
1334
1334
1335 if ($el.hasClass('collapsed')) {
1335 if ($el.hasClass('collapsed')) {
1336 $('.compare_select').show();
1336 $('.compare_select').show();
1337 $('.compare_select_hidden').hide();
1337 $('.compare_select_hidden').hide();
1338
1338
1339 $el.removeClass('collapsed');
1339 $el.removeClass('collapsed');
1340 $el.html(
1340 $el.html(
1341 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1341 '<i class="icon-minus-squared-alt icon-no-margin"></i>' +
1342 collapseMsg);
1342 collapseMsg);
1343 }
1343 }
1344 else {
1344 else {
1345 $('.compare_select').hide();
1345 $('.compare_select').hide();
1346 $('.compare_select_hidden').show();
1346 $('.compare_select_hidden').show();
1347 $el.addClass('collapsed');
1347 $el.addClass('collapsed');
1348 $el.html(
1348 $el.html(
1349 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1349 '<i class="icon-plus-squared-alt icon-no-margin"></i>' +
1350 expandMsg);
1350 expandMsg);
1351 }
1351 }
1352 updateSticky();
1352 updateSticky();
1353 };
1353 };
1354
1354
1355 // get stored diff mode and pre-enable it
1355 // get stored diff mode and pre-enable it
1356 if (templateContext.session_attrs.wide_diff_mode === "true") {
1356 if (templateContext.session_attrs.wide_diff_mode === "true") {
1357 Rhodecode.comments.toggleWideMode(null);
1357 Rhodecode.comments.toggleWideMode(null);
1358 $('.toggle-wide-diff').addClass('btn-active');
1358 $('.toggle-wide-diff').addClass('btn-active');
1359 updateSticky();
1359 updateSticky();
1360 }
1360 }
1361
1361
1362 // DIFF NAV //
1362 // DIFF NAV //
1363
1363
1364 // element to detect scroll direction of
1364 // element to detect scroll direction of
1365 var $window = $(window);
1365 var $window = $(window);
1366
1366
1367 // initialize last scroll position
1367 // initialize last scroll position
1368 var lastScrollY = $window.scrollTop();
1368 var lastScrollY = $window.scrollTop();
1369
1369
1370 $window.on('resize scrollstop', {latency: 350}, function () {
1370 $window.on('resize scrollstop', {latency: 350}, function () {
1371 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1371 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1372
1372
1373 // get current scroll position
1373 // get current scroll position
1374 var currentScrollY = $window.scrollTop();
1374 var currentScrollY = $window.scrollTop();
1375
1375
1376 // determine current scroll direction
1376 // determine current scroll direction
1377 if (currentScrollY > lastScrollY) {
1377 if (currentScrollY > lastScrollY) {
1378 var y = 'down'
1378 var y = 'down'
1379 } else if (currentScrollY !== lastScrollY) {
1379 } else if (currentScrollY !== lastScrollY) {
1380 var y = 'up';
1380 var y = 'up';
1381 }
1381 }
1382
1382
1383 var pos = -1; // by default we use last element in viewport
1383 var pos = -1; // by default we use last element in viewport
1384 if (y === 'down') {
1384 if (y === 'down') {
1385 pos = -1;
1385 pos = -1;
1386 } else if (y === 'up') {
1386 } else if (y === 'up') {
1387 pos = 0;
1387 pos = 0;
1388 }
1388 }
1389
1389
1390 if (visibleChunks.length > 0) {
1390 if (visibleChunks.length > 0) {
1391 $('.nav-chunk').removeClass('selected');
1391 $('.nav-chunk').removeClass('selected');
1392 $(visibleChunks.get(pos)).addClass('selected');
1392 $(visibleChunks.get(pos)).addClass('selected');
1393 }
1393 }
1394
1394
1395 // update last scroll position to current position
1395 // update last scroll position to current position
1396 lastScrollY = currentScrollY;
1396 lastScrollY = currentScrollY;
1397
1397
1398 });
1398 });
1399 $('#diff_nav').html(diffNavText);
1399 $('#diff_nav').html(diffNavText);
1400
1400
1401 });
1401 });
1402 </script>
1402 </script>
1403
1403
1404 </%def>
1404 </%def>
@@ -1,82 +1,82 b''
1
1
2 <div class="pull-request-wrap">
2 <div class="pull-request-wrap">
3
3
4 % if c.pr_merge_possible:
4 % if c.pr_merge_possible:
5 <h2 class="merge-status">
5 <h2 class="merge-status">
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
6 <span class="merge-icon success"><i class="icon-ok"></i></span>
7 ${_('This pull request can be merged automatically.')}
7 ${_('This pull request can be merged automatically.')}
8 </h2>
8 </h2>
9 % else:
9 % else:
10 <h2 class="merge-status">
10 <h2 class="merge-status">
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
11 <span class="merge-icon warning"><i class="icon-false"></i></span>
12 ${_('Merge is not currently possible because of below failed checks.')}
12 ${_('Merge is not currently possible because of below failed checks.')}
13 </h2>
13 </h2>
14 % endif
14 % endif
15
15
16 % if c.pr_merge_errors.items():
16 % if c.pr_merge_errors.items():
17 <ul>
17 <ul>
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
18 % for pr_check_key, pr_check_details in c.pr_merge_errors.items():
19 <% pr_check_type = pr_check_details['error_type'] %>
19 <% pr_check_type = pr_check_details['error_type'] %>
20 <li>
20 <li>
21 <div class="merge-message ${pr_check_type}" data-role="merge-message">
21 <div class="merge-message ${pr_check_type}" data-role="merge-message">
22 <span style="white-space: pre-line">- ${pr_check_details['message']}</span>
22 <span style="white-space: pre-line">- ${pr_check_details['message']}</span>
23 % if pr_check_key == 'todo':
23 % if pr_check_key == 'todo':
24 % for co in pr_check_details['details']:
24 % for co in pr_check_details['details']:
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.json.dumps(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
25 <a class="permalink" href="#comment-${co.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${co.comment_id}'), 0, ${h.str_json(co.outdated)})"> #${co.comment_id}</a>${'' if loop.last else ','}
26 % endfor
26 % endfor
27 % endif
27 % endif
28 </div>
28 </div>
29 </li>
29 </li>
30 % endfor
30 % endfor
31 </ul>
31 </ul>
32 % endif
32 % endif
33
33
34 <div class="pull-request-merge-actions">
34 <div class="pull-request-merge-actions">
35 % if c.allowed_to_merge:
35 % if c.allowed_to_merge:
36 ## Merge info, show only if all errors are taken care of
36 ## Merge info, show only if all errors are taken care of
37 % if not c.pr_merge_errors and c.pr_merge_info:
37 % if not c.pr_merge_errors and c.pr_merge_info:
38 <div class="pull-request-merge-info">
38 <div class="pull-request-merge-info">
39 <ul>
39 <ul>
40 % for pr_merge_key, pr_merge_details in c.pr_merge_info.items():
40 % for pr_merge_key, pr_merge_details in c.pr_merge_info.items():
41 <li>
41 <li>
42 - ${pr_merge_details['message']}
42 - ${pr_merge_details['message']}
43 </li>
43 </li>
44 % endfor
44 % endfor
45 </ul>
45 </ul>
46 </div>
46 </div>
47 % endif
47 % endif
48
48
49 <div>
49 <div>
50 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', request=request)}
50 ${h.secure_form(h.route_path('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form', request=request)}
51 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
51 <% merge_disabled = ' disabled' if c.pr_merge_possible is False else '' %>
52
52
53 % if c.allowed_to_close:
53 % if c.allowed_to_close:
54 ## close PR action, injected later next to COMMENT button
54 ## close PR action, injected later next to COMMENT button
55 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
55 % if c.pull_request_review_status == c.REVIEW_STATUS_APPROVED:
56 <a id="close-pull-request-action" class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
56 <a id="close-pull-request-action" class="btn btn-approved-status" href="#close-as-approved" onclick="closePullRequest('${c.REVIEW_STATUS_APPROVED}'); return false;">
57 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
57 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_APPROVED))}
58 </a>
58 </a>
59 % else:
59 % else:
60 <a id="close-pull-request-action" class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
60 <a id="close-pull-request-action" class="btn btn-rejected-status" href="#close-as-rejected" onclick="closePullRequest('${c.REVIEW_STATUS_REJECTED}'); return false;">
61 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
61 ${_('Close with status {}').format(h.commit_status_lbl(c.REVIEW_STATUS_REJECTED))}
62 </a>
62 </a>
63 % endif
63 % endif
64 % endif
64 % endif
65
65
66 <input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
66 <input type="submit" id="merge_pull_request" value="${_('Merge and close Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
67 ${h.end_form()}
67 ${h.end_form()}
68
68
69 <div class="pull-request-merge-refresh">
69 <div class="pull-request-merge-refresh">
70 <a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
70 <a href="#refreshChecks" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
71 </div>
71 </div>
72
72
73 </div>
73 </div>
74 % elif c.rhodecode_user.username != h.DEFAULT_USER:
74 % elif c.rhodecode_user.username != h.DEFAULT_USER:
75 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
75 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
76 <input type="submit" value="${_('Merge and close Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
76 <input type="submit" value="${_('Merge and close Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
77 % else:
77 % else:
78 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
78 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
79 % endif
79 % endif
80 </div>
80 </div>
81
81
82 </div>
82 </div>
@@ -1,140 +1,139 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 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 import pytest
21 import pytest
22 import urllib.parse
22 import urllib.parse
23 import mock
23 import mock
24 import simplejson as json
25
24
26 from rhodecode.lib.vcs.backends.base import Config
25 from rhodecode.lib.vcs.backends.base import Config
27 from rhodecode.tests.lib.middleware import mock_scm_app
26 from rhodecode.tests.lib.middleware import mock_scm_app
28 import rhodecode.lib.middleware.simplegit as simplegit
27 import rhodecode.lib.middleware.simplegit as simplegit
29
28
30
29
31 def get_environ(url, request_method):
30 def get_environ(url, request_method):
32 """Construct a minimum WSGI environ based on the URL."""
31 """Construct a minimum WSGI environ based on the URL."""
33 parsed_url = urllib.parse.urlparse(url)
32 parsed_url = urllib.parse.urlparse(url)
34 environ = {
33 environ = {
35 'PATH_INFO': parsed_url.path,
34 'PATH_INFO': parsed_url.path,
36 'QUERY_STRING': parsed_url.query,
35 'QUERY_STRING': parsed_url.query,
37 'REQUEST_METHOD': request_method,
36 'REQUEST_METHOD': request_method,
38 }
37 }
39
38
40 return environ
39 return environ
41
40
42
41
43 @pytest.mark.parametrize(
42 @pytest.mark.parametrize(
44 'url, expected_action, request_method',
43 'url, expected_action, request_method',
45 [
44 [
46 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
45 ('/foo/bar/info/refs?service=git-upload-pack', 'pull', 'GET'),
47 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
46 ('/foo/bar/info/refs?service=git-receive-pack', 'push', 'GET'),
48 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
47 ('/foo/bar/git-upload-pack', 'pull', 'GET'),
49 ('/foo/bar/git-receive-pack', 'push', 'GET'),
48 ('/foo/bar/git-receive-pack', 'push', 'GET'),
50 # Edge case: missing data for info/refs
49 # Edge case: missing data for info/refs
51 ('/foo/info/refs?service=', 'pull', 'GET'),
50 ('/foo/info/refs?service=', 'pull', 'GET'),
52 ('/foo/info/refs', 'pull', 'GET'),
51 ('/foo/info/refs', 'pull', 'GET'),
53 # Edge case: git command comes with service argument
52 # Edge case: git command comes with service argument
54 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
53 ('/foo/git-upload-pack?service=git-receive-pack', 'pull', 'GET'),
55 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
54 ('/foo/git-receive-pack?service=git-upload-pack', 'push', 'GET'),
56 # Edge case: repo name conflicts with git commands
55 # Edge case: repo name conflicts with git commands
57 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
56 ('/git-receive-pack/git-upload-pack', 'pull', 'GET'),
58 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
57 ('/git-receive-pack/git-receive-pack', 'push', 'GET'),
59 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
58 ('/git-upload-pack/git-upload-pack', 'pull', 'GET'),
60 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
59 ('/git-upload-pack/git-receive-pack', 'push', 'GET'),
61 ('/foo/git-receive-pack', 'push', 'GET'),
60 ('/foo/git-receive-pack', 'push', 'GET'),
62 # Edge case: not a smart protocol url
61 # Edge case: not a smart protocol url
63 ('/foo/bar', 'pull', 'GET'),
62 ('/foo/bar', 'pull', 'GET'),
64 # GIT LFS cases, batch
63 # GIT LFS cases, batch
65 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
64 ('/foo/bar/info/lfs/objects/batch', 'push', 'GET'),
66 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
65 ('/foo/bar/info/lfs/objects/batch', 'pull', 'POST'),
67 # GIT LFS oid, dl/upl
66 # GIT LFS oid, dl/upl
68 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
67 ('/foo/bar/info/lfs/abcdeabcde', 'pull', 'GET'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
68 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'PUT'),
70 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
69 ('/foo/bar/info/lfs/abcdeabcde', 'push', 'POST'),
71 # Edge case: repo name conflicts with git commands
70 # Edge case: repo name conflicts with git commands
72 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
71 ('/info/lfs/info/lfs/objects/batch', 'push', 'GET'),
73 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
72 ('/info/lfs/info/lfs/objects/batch', 'pull', 'POST'),
74
73
75 ])
74 ])
76 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
75 def test_get_action(url, expected_action, request_method, baseapp, request_stub):
77 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
76 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
78 registry=request_stub.registry)
77 registry=request_stub.registry)
79 assert expected_action == app._get_action(get_environ(url, request_method))
78 assert expected_action == app._get_action(get_environ(url, request_method))
80
79
81
80
82 @pytest.mark.parametrize(
81 @pytest.mark.parametrize(
83 'url, expected_repo_name, request_method',
82 'url, expected_repo_name, request_method',
84 [
83 [
85 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
84 ('/foo/info/refs?service=git-upload-pack', 'foo', 'GET'),
86 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
85 ('/foo/bar/info/refs?service=git-receive-pack', 'foo/bar', 'GET'),
87 ('/foo/git-upload-pack', 'foo', 'GET'),
86 ('/foo/git-upload-pack', 'foo', 'GET'),
88 ('/foo/git-receive-pack', 'foo', 'GET'),
87 ('/foo/git-receive-pack', 'foo', 'GET'),
89 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
88 ('/foo/bar/git-upload-pack', 'foo/bar', 'GET'),
90 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
89 ('/foo/bar/git-receive-pack', 'foo/bar', 'GET'),
91
90
92 # GIT LFS cases, batch
91 # GIT LFS cases, batch
93 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
92 ('/foo/bar/info/lfs/objects/batch', 'foo/bar', 'GET'),
94 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
93 ('/example-git/info/lfs/objects/batch', 'example-git', 'POST'),
95 # GIT LFS oid, dl/upl
94 # GIT LFS oid, dl/upl
96 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
95 ('/foo/info/lfs/abcdeabcde', 'foo', 'GET'),
97 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
96 ('/foo/bar/info/lfs/abcdeabcde', 'foo/bar', 'PUT'),
98 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
97 ('/my-git-repo/info/lfs/abcdeabcde', 'my-git-repo', 'POST'),
99 # Edge case: repo name conflicts with git commands
98 # Edge case: repo name conflicts with git commands
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
99 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'GET'),
101 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
100 ('/info/lfs/info/lfs/objects/batch', 'info/lfs', 'POST'),
102
101
103 ])
102 ])
104 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
103 def test_get_repository_name(url, expected_repo_name, request_method, baseapp, request_stub):
105 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
104 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
106 registry=request_stub.registry)
105 registry=request_stub.registry)
107 assert expected_repo_name == app._get_repository_name(
106 assert expected_repo_name == app._get_repository_name(
108 get_environ(url, request_method))
107 get_environ(url, request_method))
109
108
110
109
111 def test_get_config(user_util, baseapp, request_stub):
110 def test_get_config(user_util, baseapp, request_stub):
112 repo = user_util.create_repo(repo_type='git')
111 repo = user_util.create_repo(repo_type='git')
113 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
112 app = simplegit.SimpleGit(config={'auth_ret_code': '', 'base_path': ''},
114 registry=request_stub.registry)
113 registry=request_stub.registry)
115 extras = {'foo': 'FOO', 'bar': 'BAR'}
114 extras = {'foo': 'FOO', 'bar': 'BAR'}
116
115
117 # We copy the extras as the method below will change the contents.
116 # We copy the extras as the method below will change the contents.
118 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
117 git_config = app._create_config(dict(extras), repo_name=repo.repo_name)
119
118
120 expected_config = dict(extras)
119 expected_config = dict(extras)
121 expected_config.update({
120 expected_config.update({
122 'git_update_server_info': False,
121 'git_update_server_info': False,
123 'git_lfs_enabled': False,
122 'git_lfs_enabled': False,
124 'git_lfs_store_path': git_config['git_lfs_store_path'],
123 'git_lfs_store_path': git_config['git_lfs_store_path'],
125 'git_lfs_http_scheme': 'http'
124 'git_lfs_http_scheme': 'http'
126 })
125 })
127
126
128 assert git_config == expected_config
127 assert git_config == expected_config
129
128
130
129
131 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
130 def test_create_wsgi_app_uses_scm_app_from_simplevcs(baseapp, request_stub):
132 config = {
131 config = {
133 'auth_ret_code': '',
132 'auth_ret_code': '',
134 'base_path': '',
133 'base_path': '',
135 'vcs.scm_app_implementation':
134 'vcs.scm_app_implementation':
136 'rhodecode.tests.lib.middleware.mock_scm_app',
135 'rhodecode.tests.lib.middleware.mock_scm_app',
137 }
136 }
138 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
137 app = simplegit.SimpleGit(config=config, registry=request_stub.registry)
139 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
138 wsgi_app = app._create_wsgi_app('/tmp/test', 'test_repo', {})
140 assert wsgi_app is mock_scm_app.mock_git_wsgi
139 assert wsgi_app is mock_scm_app.mock_git_wsgi
General Comments 0
You need to be logged in to leave comments. Login now