##// END OF EJS Templates
users: enable full edit mode for super admins....
super-admin -
r4740:9d84ba0c default
parent child Browse files
Show More
@@ -1,1318 +1,1322 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.ext_json import 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
315 c.edit_mode = self.request.POST.get('edit') == '1'
314 form_result = {}
316 form_result = {}
315 old_values = c.user.get_api_data()
317 old_values = c.user.get_api_data()
316 try:
318 try:
317 form_result = _form.to_python(dict(self.request.POST))
319 form_result = _form.to_python(dict(self.request.POST))
318 skip_attrs = ['extern_name']
320 skip_attrs = ['extern_name']
319 # TODO: plugin should define if username can be updated
321 # TODO: plugin should define if username can be updated
320 if c.extern_type != "rhodecode":
322
323 if c.extern_type != "rhodecode" and not c.edit_mode:
321 # forbid updating username for external accounts
324 # forbid updating username for external accounts
322 skip_attrs.append('username')
325 skip_attrs.append('username')
323
326
324 UserModel().update_user(
327 UserModel().update_user(
325 user_id, skip_attrs=skip_attrs, **form_result)
328 user_id, skip_attrs=skip_attrs, **form_result)
326
329
327 audit_logger.store_web(
330 audit_logger.store_web(
328 'user.edit', action_data={'old_data': old_values},
331 'user.edit', action_data={'old_data': old_values},
329 user=c.rhodecode_user)
332 user=c.rhodecode_user)
330
333
331 Session().commit()
334 Session().commit()
332 h.flash(_('User updated successfully'), category='success')
335 h.flash(_('User updated successfully'), category='success')
333 except formencode.Invalid as errors:
336 except formencode.Invalid as errors:
334 data = render(
337 data = render(
335 'rhodecode:templates/admin/users/user_edit.mako',
338 'rhodecode:templates/admin/users/user_edit.mako',
336 self._get_template_context(c), self.request)
339 self._get_template_context(c), self.request)
337 html = formencode.htmlfill.render(
340 html = formencode.htmlfill.render(
338 data,
341 data,
339 defaults=errors.value,
342 defaults=errors.value,
340 errors=errors.error_dict or {},
343 errors=errors.error_dict or {},
341 prefix_error=False,
344 prefix_error=False,
342 encoding="UTF-8",
345 encoding="UTF-8",
343 force_defaults=False
346 force_defaults=False
344 )
347 )
345 return Response(html)
348 return Response(html)
346 except UserCreationError as e:
349 except UserCreationError as e:
347 h.flash(e, 'error')
350 h.flash(e, 'error')
348 except Exception:
351 except Exception:
349 log.exception("Exception updating user")
352 log.exception("Exception updating user")
350 h.flash(_('Error occurred during update of user %s')
353 h.flash(_('Error occurred during update of user %s')
351 % form_result.get('username'), category='error')
354 % form_result.get('username'), category='error')
352 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
355 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
353
356
354 @LoginRequired()
357 @LoginRequired()
355 @HasPermissionAllDecorator('hg.admin')
358 @HasPermissionAllDecorator('hg.admin')
356 @CSRFRequired()
359 @CSRFRequired()
357 def user_delete(self):
360 def user_delete(self):
358 _ = self.request.translate
361 _ = self.request.translate
359 c = self.load_default_context()
362 c = self.load_default_context()
360 c.user = self.db_user
363 c.user = self.db_user
361
364
362 _repos = c.user.repositories
365 _repos = c.user.repositories
363 _repo_groups = c.user.repository_groups
366 _repo_groups = c.user.repository_groups
364 _user_groups = c.user.user_groups
367 _user_groups = c.user.user_groups
365 _pull_requests = c.user.user_pull_requests
368 _pull_requests = c.user.user_pull_requests
366 _artifacts = c.user.artifacts
369 _artifacts = c.user.artifacts
367
370
368 handle_repos = None
371 handle_repos = None
369 handle_repo_groups = None
372 handle_repo_groups = None
370 handle_user_groups = None
373 handle_user_groups = None
371 handle_pull_requests = None
374 handle_pull_requests = None
372 handle_artifacts = None
375 handle_artifacts = None
373
376
374 # 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
375 def set_handle_flash_repos():
378 def set_handle_flash_repos():
376 handle = handle_repos
379 handle = handle_repos
377 if handle == 'detach':
380 if handle == 'detach':
378 h.flash(_('Detached %s repositories') % len(_repos),
381 h.flash(_('Detached %s repositories') % len(_repos),
379 category='success')
382 category='success')
380 elif handle == 'delete':
383 elif handle == 'delete':
381 h.flash(_('Deleted %s repositories') % len(_repos),
384 h.flash(_('Deleted %s repositories') % len(_repos),
382 category='success')
385 category='success')
383
386
384 def set_handle_flash_repo_groups():
387 def set_handle_flash_repo_groups():
385 handle = handle_repo_groups
388 handle = handle_repo_groups
386 if handle == 'detach':
389 if handle == 'detach':
387 h.flash(_('Detached %s repository groups') % len(_repo_groups),
390 h.flash(_('Detached %s repository groups') % len(_repo_groups),
388 category='success')
391 category='success')
389 elif handle == 'delete':
392 elif handle == 'delete':
390 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
393 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
391 category='success')
394 category='success')
392
395
393 def set_handle_flash_user_groups():
396 def set_handle_flash_user_groups():
394 handle = handle_user_groups
397 handle = handle_user_groups
395 if handle == 'detach':
398 if handle == 'detach':
396 h.flash(_('Detached %s user groups') % len(_user_groups),
399 h.flash(_('Detached %s user groups') % len(_user_groups),
397 category='success')
400 category='success')
398 elif handle == 'delete':
401 elif handle == 'delete':
399 h.flash(_('Deleted %s user groups') % len(_user_groups),
402 h.flash(_('Deleted %s user groups') % len(_user_groups),
400 category='success')
403 category='success')
401
404
402 def set_handle_flash_pull_requests():
405 def set_handle_flash_pull_requests():
403 handle = handle_pull_requests
406 handle = handle_pull_requests
404 if handle == 'detach':
407 if handle == 'detach':
405 h.flash(_('Detached %s pull requests') % len(_pull_requests),
408 h.flash(_('Detached %s pull requests') % len(_pull_requests),
406 category='success')
409 category='success')
407 elif handle == 'delete':
410 elif handle == 'delete':
408 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
411 h.flash(_('Deleted %s pull requests') % len(_pull_requests),
409 category='success')
412 category='success')
410
413
411 def set_handle_flash_artifacts():
414 def set_handle_flash_artifacts():
412 handle = handle_artifacts
415 handle = handle_artifacts
413 if handle == 'detach':
416 if handle == 'detach':
414 h.flash(_('Detached %s artifacts') % len(_artifacts),
417 h.flash(_('Detached %s artifacts') % len(_artifacts),
415 category='success')
418 category='success')
416 elif handle == 'delete':
419 elif handle == 'delete':
417 h.flash(_('Deleted %s artifacts') % len(_artifacts),
420 h.flash(_('Deleted %s artifacts') % len(_artifacts),
418 category='success')
421 category='success')
419
422
420 handle_user = User.get_first_super_admin()
423 handle_user = User.get_first_super_admin()
421 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'))
422 if handle_user_id:
425 if handle_user_id:
423 # NOTE(marcink): we get new owner for objects...
426 # NOTE(marcink): we get new owner for objects...
424 handle_user = User.get_or_404(handle_user_id)
427 handle_user = User.get_or_404(handle_user_id)
425
428
426 if _repos and self.request.POST.get('user_repos'):
429 if _repos and self.request.POST.get('user_repos'):
427 handle_repos = self.request.POST['user_repos']
430 handle_repos = self.request.POST['user_repos']
428
431
429 if _repo_groups and self.request.POST.get('user_repo_groups'):
432 if _repo_groups and self.request.POST.get('user_repo_groups'):
430 handle_repo_groups = self.request.POST['user_repo_groups']
433 handle_repo_groups = self.request.POST['user_repo_groups']
431
434
432 if _user_groups and self.request.POST.get('user_user_groups'):
435 if _user_groups and self.request.POST.get('user_user_groups'):
433 handle_user_groups = self.request.POST['user_user_groups']
436 handle_user_groups = self.request.POST['user_user_groups']
434
437
435 if _pull_requests and self.request.POST.get('user_pull_requests'):
438 if _pull_requests and self.request.POST.get('user_pull_requests'):
436 handle_pull_requests = self.request.POST['user_pull_requests']
439 handle_pull_requests = self.request.POST['user_pull_requests']
437
440
438 if _artifacts and self.request.POST.get('user_artifacts'):
441 if _artifacts and self.request.POST.get('user_artifacts'):
439 handle_artifacts = self.request.POST['user_artifacts']
442 handle_artifacts = self.request.POST['user_artifacts']
440
443
441 old_values = c.user.get_api_data()
444 old_values = c.user.get_api_data()
442
445
443 try:
446 try:
444
447
445 UserModel().delete(
448 UserModel().delete(
446 c.user,
449 c.user,
447 handle_repos=handle_repos,
450 handle_repos=handle_repos,
448 handle_repo_groups=handle_repo_groups,
451 handle_repo_groups=handle_repo_groups,
449 handle_user_groups=handle_user_groups,
452 handle_user_groups=handle_user_groups,
450 handle_pull_requests=handle_pull_requests,
453 handle_pull_requests=handle_pull_requests,
451 handle_artifacts=handle_artifacts,
454 handle_artifacts=handle_artifacts,
452 handle_new_owner=handle_user
455 handle_new_owner=handle_user
453 )
456 )
454
457
455 audit_logger.store_web(
458 audit_logger.store_web(
456 'user.delete', action_data={'old_data': old_values},
459 'user.delete', action_data={'old_data': old_values},
457 user=c.rhodecode_user)
460 user=c.rhodecode_user)
458
461
459 Session().commit()
462 Session().commit()
460 set_handle_flash_repos()
463 set_handle_flash_repos()
461 set_handle_flash_repo_groups()
464 set_handle_flash_repo_groups()
462 set_handle_flash_user_groups()
465 set_handle_flash_user_groups()
463 set_handle_flash_pull_requests()
466 set_handle_flash_pull_requests()
464 set_handle_flash_artifacts()
467 set_handle_flash_artifacts()
465 username = h.escape(old_values['username'])
468 username = h.escape(old_values['username'])
466 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
469 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
467 except (UserOwnsReposException, UserOwnsRepoGroupsException,
470 except (UserOwnsReposException, UserOwnsRepoGroupsException,
468 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
471 UserOwnsUserGroupsException, UserOwnsPullRequestsException,
469 UserOwnsArtifactsException, DefaultUserException) as e:
472 UserOwnsArtifactsException, DefaultUserException) as e:
470 h.flash(e, category='warning')
473 h.flash(e, category='warning')
471 except Exception:
474 except Exception:
472 log.exception("Exception during deletion of user")
475 log.exception("Exception during deletion of user")
473 h.flash(_('An error occurred during deletion of user'),
476 h.flash(_('An error occurred during deletion of user'),
474 category='error')
477 category='error')
475 raise HTTPFound(h.route_path('users'))
478 raise HTTPFound(h.route_path('users'))
476
479
477 @LoginRequired()
480 @LoginRequired()
478 @HasPermissionAllDecorator('hg.admin')
481 @HasPermissionAllDecorator('hg.admin')
479 def user_edit(self):
482 def user_edit(self):
480 _ = self.request.translate
483 _ = self.request.translate
481 c = self.load_default_context()
484 c = self.load_default_context()
482 c.user = self.db_user
485 c.user = self.db_user
483
486
484 c.active = 'profile'
487 c.active = 'profile'
485 c.extern_type = c.user.extern_type
488 c.extern_type = c.user.extern_type
486 c.extern_name = c.user.extern_name
489 c.extern_name = c.user.extern_name
487 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'
488
492
489 defaults = c.user.get_dict()
493 defaults = c.user.get_dict()
490 defaults.update({'language': c.user.user_data.get('language')})
494 defaults.update({'language': c.user.user_data.get('language')})
491
495
492 data = render(
496 data = render(
493 'rhodecode:templates/admin/users/user_edit.mako',
497 'rhodecode:templates/admin/users/user_edit.mako',
494 self._get_template_context(c), self.request)
498 self._get_template_context(c), self.request)
495 html = formencode.htmlfill.render(
499 html = formencode.htmlfill.render(
496 data,
500 data,
497 defaults=defaults,
501 defaults=defaults,
498 encoding="UTF-8",
502 encoding="UTF-8",
499 force_defaults=False
503 force_defaults=False
500 )
504 )
501 return Response(html)
505 return Response(html)
502
506
503 @LoginRequired()
507 @LoginRequired()
504 @HasPermissionAllDecorator('hg.admin')
508 @HasPermissionAllDecorator('hg.admin')
505 def user_edit_advanced(self):
509 def user_edit_advanced(self):
506 _ = self.request.translate
510 _ = self.request.translate
507 c = self.load_default_context()
511 c = self.load_default_context()
508
512
509 user_id = self.db_user_id
513 user_id = self.db_user_id
510 c.user = self.db_user
514 c.user = self.db_user
511
515
512 c.detach_user = User.get_first_super_admin()
516 c.detach_user = User.get_first_super_admin()
513 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'))
514 if detach_user_id:
518 if detach_user_id:
515 c.detach_user = User.get_or_404(detach_user_id)
519 c.detach_user = User.get_or_404(detach_user_id)
516
520
517 c.active = 'advanced'
521 c.active = 'advanced'
518 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)
519 c.personal_repo_group_name = RepoGroupModel()\
523 c.personal_repo_group_name = RepoGroupModel()\
520 .get_personal_group_name(c.user)
524 .get_personal_group_name(c.user)
521
525
522 c.user_to_review_rules = sorted(
526 c.user_to_review_rules = sorted(
523 (x.user for x in c.user.user_review_rules),
527 (x.user for x in c.user.user_review_rules),
524 key=lambda u: u.username.lower())
528 key=lambda u: u.username.lower())
525
529
526 defaults = c.user.get_dict()
530 defaults = c.user.get_dict()
527
531
528 # 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
529 # reviewer.
533 # reviewer.
530 has_review = len(c.user.reviewer_pull_requests)
534 has_review = len(c.user.reviewer_pull_requests)
531 c.can_delete_user = not has_review
535 c.can_delete_user = not has_review
532 c.can_delete_user_message = ''
536 c.can_delete_user_message = ''
533 inactive_link = h.link_to(
537 inactive_link = h.link_to(
534 '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'))
535 if has_review == 1:
539 if has_review == 1:
536 c.can_delete_user_message = h.literal(_(
540 c.can_delete_user_message = h.literal(_(
537 'The user participates as reviewer in {} pull request and '
541 'The user participates as reviewer in {} pull request and '
538 'cannot be deleted. \nYou can set the user to '
542 'cannot be deleted. \nYou can set the user to '
539 '"{}" instead of deleting it.').format(
543 '"{}" instead of deleting it.').format(
540 has_review, inactive_link))
544 has_review, inactive_link))
541 elif has_review:
545 elif has_review:
542 c.can_delete_user_message = h.literal(_(
546 c.can_delete_user_message = h.literal(_(
543 'The user participates as reviewer in {} pull requests and '
547 'The user participates as reviewer in {} pull requests and '
544 'cannot be deleted. \nYou can set the user to '
548 'cannot be deleted. \nYou can set the user to '
545 '"{}" instead of deleting it.').format(
549 '"{}" instead of deleting it.').format(
546 has_review, inactive_link))
550 has_review, inactive_link))
547
551
548 data = render(
552 data = render(
549 'rhodecode:templates/admin/users/user_edit.mako',
553 'rhodecode:templates/admin/users/user_edit.mako',
550 self._get_template_context(c), self.request)
554 self._get_template_context(c), self.request)
551 html = formencode.htmlfill.render(
555 html = formencode.htmlfill.render(
552 data,
556 data,
553 defaults=defaults,
557 defaults=defaults,
554 encoding="UTF-8",
558 encoding="UTF-8",
555 force_defaults=False
559 force_defaults=False
556 )
560 )
557 return Response(html)
561 return Response(html)
558
562
559 @LoginRequired()
563 @LoginRequired()
560 @HasPermissionAllDecorator('hg.admin')
564 @HasPermissionAllDecorator('hg.admin')
561 def user_edit_global_perms(self):
565 def user_edit_global_perms(self):
562 _ = self.request.translate
566 _ = self.request.translate
563 c = self.load_default_context()
567 c = self.load_default_context()
564 c.user = self.db_user
568 c.user = self.db_user
565
569
566 c.active = 'global_perms'
570 c.active = 'global_perms'
567
571
568 c.default_user = User.get_default_user()
572 c.default_user = User.get_default_user()
569 defaults = c.user.get_dict()
573 defaults = c.user.get_dict()
570 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
574 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
571 defaults.update(c.default_user.get_default_perms())
575 defaults.update(c.default_user.get_default_perms())
572 defaults.update(c.user.get_default_perms())
576 defaults.update(c.user.get_default_perms())
573
577
574 data = render(
578 data = render(
575 'rhodecode:templates/admin/users/user_edit.mako',
579 'rhodecode:templates/admin/users/user_edit.mako',
576 self._get_template_context(c), self.request)
580 self._get_template_context(c), self.request)
577 html = formencode.htmlfill.render(
581 html = formencode.htmlfill.render(
578 data,
582 data,
579 defaults=defaults,
583 defaults=defaults,
580 encoding="UTF-8",
584 encoding="UTF-8",
581 force_defaults=False
585 force_defaults=False
582 )
586 )
583 return Response(html)
587 return Response(html)
584
588
585 @LoginRequired()
589 @LoginRequired()
586 @HasPermissionAllDecorator('hg.admin')
590 @HasPermissionAllDecorator('hg.admin')
587 @CSRFRequired()
591 @CSRFRequired()
588 def user_edit_global_perms_update(self):
592 def user_edit_global_perms_update(self):
589 _ = self.request.translate
593 _ = self.request.translate
590 c = self.load_default_context()
594 c = self.load_default_context()
591
595
592 user_id = self.db_user_id
596 user_id = self.db_user_id
593 c.user = self.db_user
597 c.user = self.db_user
594
598
595 c.active = 'global_perms'
599 c.active = 'global_perms'
596 try:
600 try:
597 # first stage that verifies the checkbox
601 # first stage that verifies the checkbox
598 _form = UserIndividualPermissionsForm(self.request.translate)
602 _form = UserIndividualPermissionsForm(self.request.translate)
599 form_result = _form.to_python(dict(self.request.POST))
603 form_result = _form.to_python(dict(self.request.POST))
600 inherit_perms = form_result['inherit_default_permissions']
604 inherit_perms = form_result['inherit_default_permissions']
601 c.user.inherit_default_permissions = inherit_perms
605 c.user.inherit_default_permissions = inherit_perms
602 Session().add(c.user)
606 Session().add(c.user)
603
607
604 if not inherit_perms:
608 if not inherit_perms:
605 # only update the individual ones if we un check the flag
609 # only update the individual ones if we un check the flag
606 _form = UserPermissionsForm(
610 _form = UserPermissionsForm(
607 self.request.translate,
611 self.request.translate,
608 [x[0] for x in c.repo_create_choices],
612 [x[0] for x in c.repo_create_choices],
609 [x[0] for x in c.repo_create_on_write_choices],
613 [x[0] for x in c.repo_create_on_write_choices],
610 [x[0] for x in c.repo_group_create_choices],
614 [x[0] for x in c.repo_group_create_choices],
611 [x[0] for x in c.user_group_create_choices],
615 [x[0] for x in c.user_group_create_choices],
612 [x[0] for x in c.fork_choices],
616 [x[0] for x in c.fork_choices],
613 [x[0] for x in c.inherit_default_permission_choices])()
617 [x[0] for x in c.inherit_default_permission_choices])()
614
618
615 form_result = _form.to_python(dict(self.request.POST))
619 form_result = _form.to_python(dict(self.request.POST))
616 form_result.update({'perm_user_id': c.user.user_id})
620 form_result.update({'perm_user_id': c.user.user_id})
617
621
618 PermissionModel().update_user_permissions(form_result)
622 PermissionModel().update_user_permissions(form_result)
619
623
620 # TODO(marcink): implement global permissions
624 # TODO(marcink): implement global permissions
621 # audit_log.store_web('user.edit.permissions')
625 # audit_log.store_web('user.edit.permissions')
622
626
623 Session().commit()
627 Session().commit()
624
628
625 h.flash(_('User global permissions updated successfully'),
629 h.flash(_('User global permissions updated successfully'),
626 category='success')
630 category='success')
627
631
628 except formencode.Invalid as errors:
632 except formencode.Invalid as errors:
629 data = render(
633 data = render(
630 'rhodecode:templates/admin/users/user_edit.mako',
634 'rhodecode:templates/admin/users/user_edit.mako',
631 self._get_template_context(c), self.request)
635 self._get_template_context(c), self.request)
632 html = formencode.htmlfill.render(
636 html = formencode.htmlfill.render(
633 data,
637 data,
634 defaults=errors.value,
638 defaults=errors.value,
635 errors=errors.error_dict or {},
639 errors=errors.error_dict or {},
636 prefix_error=False,
640 prefix_error=False,
637 encoding="UTF-8",
641 encoding="UTF-8",
638 force_defaults=False
642 force_defaults=False
639 )
643 )
640 return Response(html)
644 return Response(html)
641 except Exception:
645 except Exception:
642 log.exception("Exception during permissions saving")
646 log.exception("Exception during permissions saving")
643 h.flash(_('An error occurred during permissions saving'),
647 h.flash(_('An error occurred during permissions saving'),
644 category='error')
648 category='error')
645
649
646 affected_user_ids = [user_id]
650 affected_user_ids = [user_id]
647 PermissionModel().trigger_permission_flush(affected_user_ids)
651 PermissionModel().trigger_permission_flush(affected_user_ids)
648 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))
649
653
650 @LoginRequired()
654 @LoginRequired()
651 @HasPermissionAllDecorator('hg.admin')
655 @HasPermissionAllDecorator('hg.admin')
652 @CSRFRequired()
656 @CSRFRequired()
653 def user_enable_force_password_reset(self):
657 def user_enable_force_password_reset(self):
654 _ = self.request.translate
658 _ = self.request.translate
655 c = self.load_default_context()
659 c = self.load_default_context()
656
660
657 user_id = self.db_user_id
661 user_id = self.db_user_id
658 c.user = self.db_user
662 c.user = self.db_user
659
663
660 try:
664 try:
661 c.user.update_userdata(force_password_change=True)
665 c.user.update_userdata(force_password_change=True)
662
666
663 msg = _('Force password change enabled for user')
667 msg = _('Force password change enabled for user')
664 audit_logger.store_web('user.edit.password_reset.enabled',
668 audit_logger.store_web('user.edit.password_reset.enabled',
665 user=c.rhodecode_user)
669 user=c.rhodecode_user)
666
670
667 Session().commit()
671 Session().commit()
668 h.flash(msg, category='success')
672 h.flash(msg, category='success')
669 except Exception:
673 except Exception:
670 log.exception("Exception during password reset for user")
674 log.exception("Exception during password reset for user")
671 h.flash(_('An error occurred during password reset for user'),
675 h.flash(_('An error occurred during password reset for user'),
672 category='error')
676 category='error')
673
677
674 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))
675
679
676 @LoginRequired()
680 @LoginRequired()
677 @HasPermissionAllDecorator('hg.admin')
681 @HasPermissionAllDecorator('hg.admin')
678 @CSRFRequired()
682 @CSRFRequired()
679 def user_disable_force_password_reset(self):
683 def user_disable_force_password_reset(self):
680 _ = self.request.translate
684 _ = self.request.translate
681 c = self.load_default_context()
685 c = self.load_default_context()
682
686
683 user_id = self.db_user_id
687 user_id = self.db_user_id
684 c.user = self.db_user
688 c.user = self.db_user
685
689
686 try:
690 try:
687 c.user.update_userdata(force_password_change=False)
691 c.user.update_userdata(force_password_change=False)
688
692
689 msg = _('Force password change disabled for user')
693 msg = _('Force password change disabled for user')
690 audit_logger.store_web(
694 audit_logger.store_web(
691 'user.edit.password_reset.disabled',
695 'user.edit.password_reset.disabled',
692 user=c.rhodecode_user)
696 user=c.rhodecode_user)
693
697
694 Session().commit()
698 Session().commit()
695 h.flash(msg, category='success')
699 h.flash(msg, category='success')
696 except Exception:
700 except Exception:
697 log.exception("Exception during password reset for user")
701 log.exception("Exception during password reset for user")
698 h.flash(_('An error occurred during password reset for user'),
702 h.flash(_('An error occurred during password reset for user'),
699 category='error')
703 category='error')
700
704
701 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))
702
706
703 @LoginRequired()
707 @LoginRequired()
704 @HasPermissionAllDecorator('hg.admin')
708 @HasPermissionAllDecorator('hg.admin')
705 @CSRFRequired()
709 @CSRFRequired()
706 def user_notice_dismiss(self):
710 def user_notice_dismiss(self):
707 _ = self.request.translate
711 _ = self.request.translate
708 c = self.load_default_context()
712 c = self.load_default_context()
709
713
710 user_id = self.db_user_id
714 user_id = self.db_user_id
711 c.user = self.db_user
715 c.user = self.db_user
712 user_notice_id = safe_int(self.request.POST.get('notice_id'))
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
713 notice = UserNotice().query()\
717 notice = UserNotice().query()\
714 .filter(UserNotice.user_id == user_id)\
718 .filter(UserNotice.user_id == user_id)\
715 .filter(UserNotice.user_notice_id == user_notice_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
716 .scalar()
720 .scalar()
717 read = False
721 read = False
718 if notice:
722 if notice:
719 notice.notice_read = True
723 notice.notice_read = True
720 Session().add(notice)
724 Session().add(notice)
721 Session().commit()
725 Session().commit()
722 read = True
726 read = True
723
727
724 return {'notice': user_notice_id, 'read': read}
728 return {'notice': user_notice_id, 'read': read}
725
729
726 @LoginRequired()
730 @LoginRequired()
727 @HasPermissionAllDecorator('hg.admin')
731 @HasPermissionAllDecorator('hg.admin')
728 @CSRFRequired()
732 @CSRFRequired()
729 def user_create_personal_repo_group(self):
733 def user_create_personal_repo_group(self):
730 """
734 """
731 Create personal repository group for this user
735 Create personal repository group for this user
732 """
736 """
733 from rhodecode.model.repo_group import RepoGroupModel
737 from rhodecode.model.repo_group import RepoGroupModel
734
738
735 _ = self.request.translate
739 _ = self.request.translate
736 c = self.load_default_context()
740 c = self.load_default_context()
737
741
738 user_id = self.db_user_id
742 user_id = self.db_user_id
739 c.user = self.db_user
743 c.user = self.db_user
740
744
741 personal_repo_group = RepoGroup.get_user_personal_repo_group(
745 personal_repo_group = RepoGroup.get_user_personal_repo_group(
742 c.user.user_id)
746 c.user.user_id)
743 if personal_repo_group:
747 if personal_repo_group:
744 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))
745
749
746 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
750 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
747 named_personal_group = RepoGroup.get_by_group_name(
751 named_personal_group = RepoGroup.get_by_group_name(
748 personal_repo_group_name)
752 personal_repo_group_name)
749 try:
753 try:
750
754
751 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:
752 # migrate the same named group, and mark it as personal
756 # migrate the same named group, and mark it as personal
753 named_personal_group.personal = True
757 named_personal_group.personal = True
754 Session().add(named_personal_group)
758 Session().add(named_personal_group)
755 Session().commit()
759 Session().commit()
756 msg = _('Linked repository group `%s` as personal' % (
760 msg = _('Linked repository group `%s` as personal' % (
757 personal_repo_group_name,))
761 personal_repo_group_name,))
758 h.flash(msg, category='success')
762 h.flash(msg, category='success')
759 elif not named_personal_group:
763 elif not named_personal_group:
760 RepoGroupModel().create_personal_repo_group(c.user)
764 RepoGroupModel().create_personal_repo_group(c.user)
761
765
762 msg = _('Created repository group `%s`' % (
766 msg = _('Created repository group `%s`' % (
763 personal_repo_group_name,))
767 personal_repo_group_name,))
764 h.flash(msg, category='success')
768 h.flash(msg, category='success')
765 else:
769 else:
766 msg = _('Repository group `%s` is already taken' % (
770 msg = _('Repository group `%s` is already taken' % (
767 personal_repo_group_name,))
771 personal_repo_group_name,))
768 h.flash(msg, category='warning')
772 h.flash(msg, category='warning')
769 except Exception:
773 except Exception:
770 log.exception("Exception during repository group creation")
774 log.exception("Exception during repository group creation")
771 msg = _(
775 msg = _(
772 'An error occurred during repository group creation for user')
776 'An error occurred during repository group creation for user')
773 h.flash(msg, category='error')
777 h.flash(msg, category='error')
774 Session().rollback()
778 Session().rollback()
775
779
776 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))
777
781
778 @LoginRequired()
782 @LoginRequired()
779 @HasPermissionAllDecorator('hg.admin')
783 @HasPermissionAllDecorator('hg.admin')
780 def auth_tokens(self):
784 def auth_tokens(self):
781 _ = self.request.translate
785 _ = self.request.translate
782 c = self.load_default_context()
786 c = self.load_default_context()
783 c.user = self.db_user
787 c.user = self.db_user
784
788
785 c.active = 'auth_tokens'
789 c.active = 'auth_tokens'
786
790
787 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
791 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
788 c.role_values = [
792 c.role_values = [
789 (x, AuthTokenModel.cls._get_role_name(x))
793 (x, AuthTokenModel.cls._get_role_name(x))
790 for x in AuthTokenModel.cls.ROLES]
794 for x in AuthTokenModel.cls.ROLES]
791 c.role_options = [(c.role_values, _("Role"))]
795 c.role_options = [(c.role_values, _("Role"))]
792 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
796 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
793 c.user.user_id, show_expired=True)
797 c.user.user_id, show_expired=True)
794 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
798 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
795 return self._get_template_context(c)
799 return self._get_template_context(c)
796
800
797 @LoginRequired()
801 @LoginRequired()
798 @HasPermissionAllDecorator('hg.admin')
802 @HasPermissionAllDecorator('hg.admin')
799 def auth_tokens_view(self):
803 def auth_tokens_view(self):
800 _ = self.request.translate
804 _ = self.request.translate
801 c = self.load_default_context()
805 c = self.load_default_context()
802 c.user = self.db_user
806 c.user = self.db_user
803
807
804 auth_token_id = self.request.POST.get('auth_token_id')
808 auth_token_id = self.request.POST.get('auth_token_id')
805
809
806 if auth_token_id:
810 if auth_token_id:
807 token = UserApiKeys.get_or_404(auth_token_id)
811 token = UserApiKeys.get_or_404(auth_token_id)
808
812
809 return {
813 return {
810 'auth_token': token.api_key
814 'auth_token': token.api_key
811 }
815 }
812
816
813 def maybe_attach_token_scope(self, token):
817 def maybe_attach_token_scope(self, token):
814 # implemented in EE edition
818 # implemented in EE edition
815 pass
819 pass
816
820
817 @LoginRequired()
821 @LoginRequired()
818 @HasPermissionAllDecorator('hg.admin')
822 @HasPermissionAllDecorator('hg.admin')
819 @CSRFRequired()
823 @CSRFRequired()
820 def auth_tokens_add(self):
824 def auth_tokens_add(self):
821 _ = self.request.translate
825 _ = self.request.translate
822 c = self.load_default_context()
826 c = self.load_default_context()
823
827
824 user_id = self.db_user_id
828 user_id = self.db_user_id
825 c.user = self.db_user
829 c.user = self.db_user
826
830
827 user_data = c.user.get_api_data()
831 user_data = c.user.get_api_data()
828 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
832 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
829 description = self.request.POST.get('description')
833 description = self.request.POST.get('description')
830 role = self.request.POST.get('role')
834 role = self.request.POST.get('role')
831
835
832 token = UserModel().add_auth_token(
836 token = UserModel().add_auth_token(
833 user=c.user.user_id,
837 user=c.user.user_id,
834 lifetime_minutes=lifetime, role=role, description=description,
838 lifetime_minutes=lifetime, role=role, description=description,
835 scope_callback=self.maybe_attach_token_scope)
839 scope_callback=self.maybe_attach_token_scope)
836 token_data = token.get_api_data()
840 token_data = token.get_api_data()
837
841
838 audit_logger.store_web(
842 audit_logger.store_web(
839 'user.edit.token.add', action_data={
843 'user.edit.token.add', action_data={
840 'data': {'token': token_data, 'user': user_data}},
844 'data': {'token': token_data, 'user': user_data}},
841 user=self._rhodecode_user, )
845 user=self._rhodecode_user, )
842 Session().commit()
846 Session().commit()
843
847
844 h.flash(_("Auth token successfully created"), category='success')
848 h.flash(_("Auth token successfully created"), category='success')
845 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))
846
850
847 @LoginRequired()
851 @LoginRequired()
848 @HasPermissionAllDecorator('hg.admin')
852 @HasPermissionAllDecorator('hg.admin')
849 @CSRFRequired()
853 @CSRFRequired()
850 def auth_tokens_delete(self):
854 def auth_tokens_delete(self):
851 _ = self.request.translate
855 _ = self.request.translate
852 c = self.load_default_context()
856 c = self.load_default_context()
853
857
854 user_id = self.db_user_id
858 user_id = self.db_user_id
855 c.user = self.db_user
859 c.user = self.db_user
856
860
857 user_data = c.user.get_api_data()
861 user_data = c.user.get_api_data()
858
862
859 del_auth_token = self.request.POST.get('del_auth_token')
863 del_auth_token = self.request.POST.get('del_auth_token')
860
864
861 if del_auth_token:
865 if del_auth_token:
862 token = UserApiKeys.get_or_404(del_auth_token)
866 token = UserApiKeys.get_or_404(del_auth_token)
863 token_data = token.get_api_data()
867 token_data = token.get_api_data()
864
868
865 AuthTokenModel().delete(del_auth_token, c.user.user_id)
869 AuthTokenModel().delete(del_auth_token, c.user.user_id)
866 audit_logger.store_web(
870 audit_logger.store_web(
867 'user.edit.token.delete', action_data={
871 'user.edit.token.delete', action_data={
868 'data': {'token': token_data, 'user': user_data}},
872 'data': {'token': token_data, 'user': user_data}},
869 user=self._rhodecode_user,)
873 user=self._rhodecode_user,)
870 Session().commit()
874 Session().commit()
871 h.flash(_("Auth token successfully deleted"), category='success')
875 h.flash(_("Auth token successfully deleted"), category='success')
872
876
873 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))
874
878
875 @LoginRequired()
879 @LoginRequired()
876 @HasPermissionAllDecorator('hg.admin')
880 @HasPermissionAllDecorator('hg.admin')
877 def ssh_keys(self):
881 def ssh_keys(self):
878 _ = self.request.translate
882 _ = self.request.translate
879 c = self.load_default_context()
883 c = self.load_default_context()
880 c.user = self.db_user
884 c.user = self.db_user
881
885
882 c.active = 'ssh_keys'
886 c.active = 'ssh_keys'
883 c.default_key = self.request.GET.get('default_key')
887 c.default_key = self.request.GET.get('default_key')
884 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)
885 return self._get_template_context(c)
889 return self._get_template_context(c)
886
890
887 @LoginRequired()
891 @LoginRequired()
888 @HasPermissionAllDecorator('hg.admin')
892 @HasPermissionAllDecorator('hg.admin')
889 def ssh_keys_generate_keypair(self):
893 def ssh_keys_generate_keypair(self):
890 _ = self.request.translate
894 _ = self.request.translate
891 c = self.load_default_context()
895 c = self.load_default_context()
892
896
893 c.user = self.db_user
897 c.user = self.db_user
894
898
895 c.active = 'ssh_keys_generate'
899 c.active = 'ssh_keys_generate'
896 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
897 private_format = self.request.GET.get('private_format') \
901 private_format = self.request.GET.get('private_format') \
898 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
899 c.private, c.public = SshKeyModel().generate_keypair(
903 c.private, c.public = SshKeyModel().generate_keypair(
900 comment=comment, private_format=private_format)
904 comment=comment, private_format=private_format)
901
905
902 return self._get_template_context(c)
906 return self._get_template_context(c)
903
907
904 @LoginRequired()
908 @LoginRequired()
905 @HasPermissionAllDecorator('hg.admin')
909 @HasPermissionAllDecorator('hg.admin')
906 @CSRFRequired()
910 @CSRFRequired()
907 def ssh_keys_add(self):
911 def ssh_keys_add(self):
908 _ = self.request.translate
912 _ = self.request.translate
909 c = self.load_default_context()
913 c = self.load_default_context()
910
914
911 user_id = self.db_user_id
915 user_id = self.db_user_id
912 c.user = self.db_user
916 c.user = self.db_user
913
917
914 user_data = c.user.get_api_data()
918 user_data = c.user.get_api_data()
915 key_data = self.request.POST.get('key_data')
919 key_data = self.request.POST.get('key_data')
916 description = self.request.POST.get('description')
920 description = self.request.POST.get('description')
917
921
918 fingerprint = 'unknown'
922 fingerprint = 'unknown'
919 try:
923 try:
920 if not key_data:
924 if not key_data:
921 raise ValueError('Please add a valid public key')
925 raise ValueError('Please add a valid public key')
922
926
923 key = SshKeyModel().parse_key(key_data.strip())
927 key = SshKeyModel().parse_key(key_data.strip())
924 fingerprint = key.hash_md5()
928 fingerprint = key.hash_md5()
925
929
926 ssh_key = SshKeyModel().create(
930 ssh_key = SshKeyModel().create(
927 c.user.user_id, fingerprint, key.keydata, description)
931 c.user.user_id, fingerprint, key.keydata, description)
928 ssh_key_data = ssh_key.get_api_data()
932 ssh_key_data = ssh_key.get_api_data()
929
933
930 audit_logger.store_web(
934 audit_logger.store_web(
931 'user.edit.ssh_key.add', action_data={
935 'user.edit.ssh_key.add', action_data={
932 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
936 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
933 user=self._rhodecode_user, )
937 user=self._rhodecode_user, )
934 Session().commit()
938 Session().commit()
935
939
936 # Trigger an event on change of keys.
940 # Trigger an event on change of keys.
937 trigger(SshKeyFileChangeEvent(), self.request.registry)
941 trigger(SshKeyFileChangeEvent(), self.request.registry)
938
942
939 h.flash(_("Ssh Key successfully created"), category='success')
943 h.flash(_("Ssh Key successfully created"), category='success')
940
944
941 except IntegrityError:
945 except IntegrityError:
942 log.exception("Exception during ssh key saving")
946 log.exception("Exception during ssh key saving")
943 err = 'Such key with fingerprint `{}` already exists, ' \
947 err = 'Such key with fingerprint `{}` already exists, ' \
944 'please use a different one'.format(fingerprint)
948 'please use a different one'.format(fingerprint)
945 h.flash(_('An error occurred during ssh key saving: {}').format(err),
949 h.flash(_('An error occurred during ssh key saving: {}').format(err),
946 category='error')
950 category='error')
947 except Exception as e:
951 except Exception as e:
948 log.exception("Exception during ssh key saving")
952 log.exception("Exception during ssh key saving")
949 h.flash(_('An error occurred during ssh key saving: {}').format(e),
953 h.flash(_('An error occurred during ssh key saving: {}').format(e),
950 category='error')
954 category='error')
951
955
952 return HTTPFound(
956 return HTTPFound(
953 h.route_path('edit_user_ssh_keys', user_id=user_id))
957 h.route_path('edit_user_ssh_keys', user_id=user_id))
954
958
955 @LoginRequired()
959 @LoginRequired()
956 @HasPermissionAllDecorator('hg.admin')
960 @HasPermissionAllDecorator('hg.admin')
957 @CSRFRequired()
961 @CSRFRequired()
958 def ssh_keys_delete(self):
962 def ssh_keys_delete(self):
959 _ = self.request.translate
963 _ = self.request.translate
960 c = self.load_default_context()
964 c = self.load_default_context()
961
965
962 user_id = self.db_user_id
966 user_id = self.db_user_id
963 c.user = self.db_user
967 c.user = self.db_user
964
968
965 user_data = c.user.get_api_data()
969 user_data = c.user.get_api_data()
966
970
967 del_ssh_key = self.request.POST.get('del_ssh_key')
971 del_ssh_key = self.request.POST.get('del_ssh_key')
968
972
969 if del_ssh_key:
973 if del_ssh_key:
970 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
974 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
971 ssh_key_data = ssh_key.get_api_data()
975 ssh_key_data = ssh_key.get_api_data()
972
976
973 SshKeyModel().delete(del_ssh_key, c.user.user_id)
977 SshKeyModel().delete(del_ssh_key, c.user.user_id)
974 audit_logger.store_web(
978 audit_logger.store_web(
975 'user.edit.ssh_key.delete', action_data={
979 'user.edit.ssh_key.delete', action_data={
976 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
980 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
977 user=self._rhodecode_user,)
981 user=self._rhodecode_user,)
978 Session().commit()
982 Session().commit()
979 # Trigger an event on change of keys.
983 # Trigger an event on change of keys.
980 trigger(SshKeyFileChangeEvent(), self.request.registry)
984 trigger(SshKeyFileChangeEvent(), self.request.registry)
981 h.flash(_("Ssh key successfully deleted"), category='success')
985 h.flash(_("Ssh key successfully deleted"), category='success')
982
986
983 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))
984
988
985 @LoginRequired()
989 @LoginRequired()
986 @HasPermissionAllDecorator('hg.admin')
990 @HasPermissionAllDecorator('hg.admin')
987 def emails(self):
991 def emails(self):
988 _ = self.request.translate
992 _ = self.request.translate
989 c = self.load_default_context()
993 c = self.load_default_context()
990 c.user = self.db_user
994 c.user = self.db_user
991
995
992 c.active = 'emails'
996 c.active = 'emails'
993 c.user_email_map = UserEmailMap.query() \
997 c.user_email_map = UserEmailMap.query() \
994 .filter(UserEmailMap.user == c.user).all()
998 .filter(UserEmailMap.user == c.user).all()
995
999
996 return self._get_template_context(c)
1000 return self._get_template_context(c)
997
1001
998 @LoginRequired()
1002 @LoginRequired()
999 @HasPermissionAllDecorator('hg.admin')
1003 @HasPermissionAllDecorator('hg.admin')
1000 @CSRFRequired()
1004 @CSRFRequired()
1001 def emails_add(self):
1005 def emails_add(self):
1002 _ = self.request.translate
1006 _ = self.request.translate
1003 c = self.load_default_context()
1007 c = self.load_default_context()
1004
1008
1005 user_id = self.db_user_id
1009 user_id = self.db_user_id
1006 c.user = self.db_user
1010 c.user = self.db_user
1007
1011
1008 email = self.request.POST.get('new_email')
1012 email = self.request.POST.get('new_email')
1009 user_data = c.user.get_api_data()
1013 user_data = c.user.get_api_data()
1010 try:
1014 try:
1011
1015
1012 form = UserExtraEmailForm(self.request.translate)()
1016 form = UserExtraEmailForm(self.request.translate)()
1013 data = form.to_python({'email': email})
1017 data = form.to_python({'email': email})
1014 email = data['email']
1018 email = data['email']
1015
1019
1016 UserModel().add_extra_email(c.user.user_id, email)
1020 UserModel().add_extra_email(c.user.user_id, email)
1017 audit_logger.store_web(
1021 audit_logger.store_web(
1018 'user.edit.email.add',
1022 'user.edit.email.add',
1019 action_data={'email': email, 'user': user_data},
1023 action_data={'email': email, 'user': user_data},
1020 user=self._rhodecode_user)
1024 user=self._rhodecode_user)
1021 Session().commit()
1025 Session().commit()
1022 h.flash(_("Added new email address `%s` for user account") % email,
1026 h.flash(_("Added new email address `%s` for user account") % email,
1023 category='success')
1027 category='success')
1024 except formencode.Invalid as error:
1028 except formencode.Invalid as error:
1025 h.flash(h.escape(error.error_dict['email']), category='error')
1029 h.flash(h.escape(error.error_dict['email']), category='error')
1026 except IntegrityError:
1030 except IntegrityError:
1027 log.warning("Email %s already exists", email)
1031 log.warning("Email %s already exists", email)
1028 h.flash(_('Email `{}` is already registered for another user.').format(email),
1032 h.flash(_('Email `{}` is already registered for another user.').format(email),
1029 category='error')
1033 category='error')
1030 except Exception:
1034 except Exception:
1031 log.exception("Exception during email saving")
1035 log.exception("Exception during email saving")
1032 h.flash(_('An error occurred during email saving'),
1036 h.flash(_('An error occurred during email saving'),
1033 category='error')
1037 category='error')
1034 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))
1035
1039
1036 @LoginRequired()
1040 @LoginRequired()
1037 @HasPermissionAllDecorator('hg.admin')
1041 @HasPermissionAllDecorator('hg.admin')
1038 @CSRFRequired()
1042 @CSRFRequired()
1039 def emails_delete(self):
1043 def emails_delete(self):
1040 _ = self.request.translate
1044 _ = self.request.translate
1041 c = self.load_default_context()
1045 c = self.load_default_context()
1042
1046
1043 user_id = self.db_user_id
1047 user_id = self.db_user_id
1044 c.user = self.db_user
1048 c.user = self.db_user
1045
1049
1046 email_id = self.request.POST.get('del_email_id')
1050 email_id = self.request.POST.get('del_email_id')
1047 user_model = UserModel()
1051 user_model = UserModel()
1048
1052
1049 email = UserEmailMap.query().get(email_id).email
1053 email = UserEmailMap.query().get(email_id).email
1050 user_data = c.user.get_api_data()
1054 user_data = c.user.get_api_data()
1051 user_model.delete_extra_email(c.user.user_id, email_id)
1055 user_model.delete_extra_email(c.user.user_id, email_id)
1052 audit_logger.store_web(
1056 audit_logger.store_web(
1053 'user.edit.email.delete',
1057 'user.edit.email.delete',
1054 action_data={'email': email, 'user': user_data},
1058 action_data={'email': email, 'user': user_data},
1055 user=self._rhodecode_user)
1059 user=self._rhodecode_user)
1056 Session().commit()
1060 Session().commit()
1057 h.flash(_("Removed email address from user account"),
1061 h.flash(_("Removed email address from user account"),
1058 category='success')
1062 category='success')
1059 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))
1060
1064
1061 @LoginRequired()
1065 @LoginRequired()
1062 @HasPermissionAllDecorator('hg.admin')
1066 @HasPermissionAllDecorator('hg.admin')
1063 def ips(self):
1067 def ips(self):
1064 _ = self.request.translate
1068 _ = self.request.translate
1065 c = self.load_default_context()
1069 c = self.load_default_context()
1066 c.user = self.db_user
1070 c.user = self.db_user
1067
1071
1068 c.active = 'ips'
1072 c.active = 'ips'
1069 c.user_ip_map = UserIpMap.query() \
1073 c.user_ip_map = UserIpMap.query() \
1070 .filter(UserIpMap.user == c.user).all()
1074 .filter(UserIpMap.user == c.user).all()
1071
1075
1072 c.inherit_default_ips = c.user.inherit_default_permissions
1076 c.inherit_default_ips = c.user.inherit_default_permissions
1073 c.default_user_ip_map = UserIpMap.query() \
1077 c.default_user_ip_map = UserIpMap.query() \
1074 .filter(UserIpMap.user == User.get_default_user()).all()
1078 .filter(UserIpMap.user == User.get_default_user()).all()
1075
1079
1076 return self._get_template_context(c)
1080 return self._get_template_context(c)
1077
1081
1078 @LoginRequired()
1082 @LoginRequired()
1079 @HasPermissionAllDecorator('hg.admin')
1083 @HasPermissionAllDecorator('hg.admin')
1080 @CSRFRequired()
1084 @CSRFRequired()
1081 # 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
1082 # edit their IP white list
1086 # edit their IP white list
1083 def ips_add(self):
1087 def ips_add(self):
1084 _ = self.request.translate
1088 _ = self.request.translate
1085 c = self.load_default_context()
1089 c = self.load_default_context()
1086
1090
1087 user_id = self.db_user_id
1091 user_id = self.db_user_id
1088 c.user = self.db_user
1092 c.user = self.db_user
1089
1093
1090 user_model = UserModel()
1094 user_model = UserModel()
1091 desc = self.request.POST.get('description')
1095 desc = self.request.POST.get('description')
1092 try:
1096 try:
1093 ip_list = user_model.parse_ip_range(
1097 ip_list = user_model.parse_ip_range(
1094 self.request.POST.get('new_ip'))
1098 self.request.POST.get('new_ip'))
1095 except Exception as e:
1099 except Exception as e:
1096 ip_list = []
1100 ip_list = []
1097 log.exception("Exception during ip saving")
1101 log.exception("Exception during ip saving")
1098 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1102 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1099 category='error')
1103 category='error')
1100 added = []
1104 added = []
1101 user_data = c.user.get_api_data()
1105 user_data = c.user.get_api_data()
1102 for ip in ip_list:
1106 for ip in ip_list:
1103 try:
1107 try:
1104 form = UserExtraIpForm(self.request.translate)()
1108 form = UserExtraIpForm(self.request.translate)()
1105 data = form.to_python({'ip': ip})
1109 data = form.to_python({'ip': ip})
1106 ip = data['ip']
1110 ip = data['ip']
1107
1111
1108 user_model.add_extra_ip(c.user.user_id, ip, desc)
1112 user_model.add_extra_ip(c.user.user_id, ip, desc)
1109 audit_logger.store_web(
1113 audit_logger.store_web(
1110 'user.edit.ip.add',
1114 'user.edit.ip.add',
1111 action_data={'ip': ip, 'user': user_data},
1115 action_data={'ip': ip, 'user': user_data},
1112 user=self._rhodecode_user)
1116 user=self._rhodecode_user)
1113 Session().commit()
1117 Session().commit()
1114 added.append(ip)
1118 added.append(ip)
1115 except formencode.Invalid as error:
1119 except formencode.Invalid as error:
1116 msg = error.error_dict['ip']
1120 msg = error.error_dict['ip']
1117 h.flash(msg, category='error')
1121 h.flash(msg, category='error')
1118 except Exception:
1122 except Exception:
1119 log.exception("Exception during ip saving")
1123 log.exception("Exception during ip saving")
1120 h.flash(_('An error occurred during ip saving'),
1124 h.flash(_('An error occurred during ip saving'),
1121 category='error')
1125 category='error')
1122 if added:
1126 if added:
1123 h.flash(
1127 h.flash(
1124 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1128 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1125 category='success')
1129 category='success')
1126 if 'default_user' in self.request.POST:
1130 if 'default_user' in self.request.POST:
1127 # 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
1128 raise HTTPFound(h.route_path('admin_permissions_ips'))
1132 raise HTTPFound(h.route_path('admin_permissions_ips'))
1129 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))
1130
1134
1131 @LoginRequired()
1135 @LoginRequired()
1132 @HasPermissionAllDecorator('hg.admin')
1136 @HasPermissionAllDecorator('hg.admin')
1133 @CSRFRequired()
1137 @CSRFRequired()
1134 # 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
1135 # edit their IP white list
1139 # edit their IP white list
1136 def ips_delete(self):
1140 def ips_delete(self):
1137 _ = self.request.translate
1141 _ = self.request.translate
1138 c = self.load_default_context()
1142 c = self.load_default_context()
1139
1143
1140 user_id = self.db_user_id
1144 user_id = self.db_user_id
1141 c.user = self.db_user
1145 c.user = self.db_user
1142
1146
1143 ip_id = self.request.POST.get('del_ip_id')
1147 ip_id = self.request.POST.get('del_ip_id')
1144 user_model = UserModel()
1148 user_model = UserModel()
1145 user_data = c.user.get_api_data()
1149 user_data = c.user.get_api_data()
1146 ip = UserIpMap.query().get(ip_id).ip_addr
1150 ip = UserIpMap.query().get(ip_id).ip_addr
1147 user_model.delete_extra_ip(c.user.user_id, ip_id)
1151 user_model.delete_extra_ip(c.user.user_id, ip_id)
1148 audit_logger.store_web(
1152 audit_logger.store_web(
1149 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1153 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1150 user=self._rhodecode_user)
1154 user=self._rhodecode_user)
1151 Session().commit()
1155 Session().commit()
1152 h.flash(_("Removed ip address from user whitelist"), category='success')
1156 h.flash(_("Removed ip address from user whitelist"), category='success')
1153
1157
1154 if 'default_user' in self.request.POST:
1158 if 'default_user' in self.request.POST:
1155 # 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
1156 raise HTTPFound(h.route_path('admin_permissions_ips'))
1160 raise HTTPFound(h.route_path('admin_permissions_ips'))
1157 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))
1158
1162
1159 @LoginRequired()
1163 @LoginRequired()
1160 @HasPermissionAllDecorator('hg.admin')
1164 @HasPermissionAllDecorator('hg.admin')
1161 def groups_management(self):
1165 def groups_management(self):
1162 c = self.load_default_context()
1166 c = self.load_default_context()
1163 c.user = self.db_user
1167 c.user = self.db_user
1164 c.data = c.user.group_member
1168 c.data = c.user.group_member
1165
1169
1166 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1170 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1167 for group in c.user.group_member]
1171 for group in c.user.group_member]
1168 c.groups = json.dumps(groups)
1172 c.groups = json.dumps(groups)
1169 c.active = 'groups'
1173 c.active = 'groups'
1170
1174
1171 return self._get_template_context(c)
1175 return self._get_template_context(c)
1172
1176
1173 @LoginRequired()
1177 @LoginRequired()
1174 @HasPermissionAllDecorator('hg.admin')
1178 @HasPermissionAllDecorator('hg.admin')
1175 @CSRFRequired()
1179 @CSRFRequired()
1176 def groups_management_updates(self):
1180 def groups_management_updates(self):
1177 _ = self.request.translate
1181 _ = self.request.translate
1178 c = self.load_default_context()
1182 c = self.load_default_context()
1179
1183
1180 user_id = self.db_user_id
1184 user_id = self.db_user_id
1181 c.user = self.db_user
1185 c.user = self.db_user
1182
1186
1183 user_groups = set(self.request.POST.getall('users_group_id'))
1187 user_groups = set(self.request.POST.getall('users_group_id'))
1184 user_groups_objects = []
1188 user_groups_objects = []
1185
1189
1186 for ugid in user_groups:
1190 for ugid in user_groups:
1187 user_groups_objects.append(
1191 user_groups_objects.append(
1188 UserGroupModel().get_group(safe_int(ugid)))
1192 UserGroupModel().get_group(safe_int(ugid)))
1189 user_group_model = UserGroupModel()
1193 user_group_model = UserGroupModel()
1190 added_to_groups, removed_from_groups = \
1194 added_to_groups, removed_from_groups = \
1191 user_group_model.change_groups(c.user, user_groups_objects)
1195 user_group_model.change_groups(c.user, user_groups_objects)
1192
1196
1193 user_data = c.user.get_api_data()
1197 user_data = c.user.get_api_data()
1194 for user_group_id in added_to_groups:
1198 for user_group_id in added_to_groups:
1195 user_group = UserGroup.get(user_group_id)
1199 user_group = UserGroup.get(user_group_id)
1196 old_values = user_group.get_api_data()
1200 old_values = user_group.get_api_data()
1197 audit_logger.store_web(
1201 audit_logger.store_web(
1198 'user_group.edit.member.add',
1202 'user_group.edit.member.add',
1199 action_data={'user': user_data, 'old_data': old_values},
1203 action_data={'user': user_data, 'old_data': old_values},
1200 user=self._rhodecode_user)
1204 user=self._rhodecode_user)
1201
1205
1202 for user_group_id in removed_from_groups:
1206 for user_group_id in removed_from_groups:
1203 user_group = UserGroup.get(user_group_id)
1207 user_group = UserGroup.get(user_group_id)
1204 old_values = user_group.get_api_data()
1208 old_values = user_group.get_api_data()
1205 audit_logger.store_web(
1209 audit_logger.store_web(
1206 'user_group.edit.member.delete',
1210 'user_group.edit.member.delete',
1207 action_data={'user': user_data, 'old_data': old_values},
1211 action_data={'user': user_data, 'old_data': old_values},
1208 user=self._rhodecode_user)
1212 user=self._rhodecode_user)
1209
1213
1210 Session().commit()
1214 Session().commit()
1211 c.active = 'user_groups_management'
1215 c.active = 'user_groups_management'
1212 h.flash(_("Groups successfully changed"), category='success')
1216 h.flash(_("Groups successfully changed"), category='success')
1213
1217
1214 return HTTPFound(h.route_path(
1218 return HTTPFound(h.route_path(
1215 'edit_user_groups_management', user_id=user_id))
1219 'edit_user_groups_management', user_id=user_id))
1216
1220
1217 @LoginRequired()
1221 @LoginRequired()
1218 @HasPermissionAllDecorator('hg.admin')
1222 @HasPermissionAllDecorator('hg.admin')
1219 def user_audit_logs(self):
1223 def user_audit_logs(self):
1220 _ = self.request.translate
1224 _ = self.request.translate
1221 c = self.load_default_context()
1225 c = self.load_default_context()
1222 c.user = self.db_user
1226 c.user = self.db_user
1223
1227
1224 c.active = 'audit'
1228 c.active = 'audit'
1225
1229
1226 p = safe_int(self.request.GET.get('page', 1), 1)
1230 p = safe_int(self.request.GET.get('page', 1), 1)
1227
1231
1228 filter_term = self.request.GET.get('filter')
1232 filter_term = self.request.GET.get('filter')
1229 user_log = UserModel().get_user_log(c.user, filter_term)
1233 user_log = UserModel().get_user_log(c.user, filter_term)
1230
1234
1231 def url_generator(page_num):
1235 def url_generator(page_num):
1232 query_params = {
1236 query_params = {
1233 'page': page_num
1237 'page': page_num
1234 }
1238 }
1235 if filter_term:
1239 if filter_term:
1236 query_params['filter'] = filter_term
1240 query_params['filter'] = filter_term
1237 return self.request.current_route_path(_query=query_params)
1241 return self.request.current_route_path(_query=query_params)
1238
1242
1239 c.audit_logs = SqlPage(
1243 c.audit_logs = SqlPage(
1240 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)
1241 c.filter_term = filter_term
1245 c.filter_term = filter_term
1242 return self._get_template_context(c)
1246 return self._get_template_context(c)
1243
1247
1244 @LoginRequired()
1248 @LoginRequired()
1245 @HasPermissionAllDecorator('hg.admin')
1249 @HasPermissionAllDecorator('hg.admin')
1246 def user_audit_logs_download(self):
1250 def user_audit_logs_download(self):
1247 _ = self.request.translate
1251 _ = self.request.translate
1248 c = self.load_default_context()
1252 c = self.load_default_context()
1249 c.user = self.db_user
1253 c.user = self.db_user
1250
1254
1251 user_log = UserModel().get_user_log(c.user, filter_term=None)
1255 user_log = UserModel().get_user_log(c.user, filter_term=None)
1252
1256
1253 audit_log_data = {}
1257 audit_log_data = {}
1254 for entry in user_log:
1258 for entry in user_log:
1255 audit_log_data[entry.user_log_id] = entry.get_dict()
1259 audit_log_data[entry.user_log_id] = entry.get_dict()
1256
1260
1257 response = Response(json.dumps(audit_log_data, indent=4))
1261 response = Response(json.dumps(audit_log_data, indent=4))
1258 response.content_disposition = str(
1262 response.content_disposition = str(
1259 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1263 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1260 response.content_type = 'application/json'
1264 response.content_type = 'application/json'
1261
1265
1262 return response
1266 return response
1263
1267
1264 @LoginRequired()
1268 @LoginRequired()
1265 @HasPermissionAllDecorator('hg.admin')
1269 @HasPermissionAllDecorator('hg.admin')
1266 def user_perms_summary(self):
1270 def user_perms_summary(self):
1267 _ = self.request.translate
1271 _ = self.request.translate
1268 c = self.load_default_context()
1272 c = self.load_default_context()
1269 c.user = self.db_user
1273 c.user = self.db_user
1270
1274
1271 c.active = 'perms_summary'
1275 c.active = 'perms_summary'
1272 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1276 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1273
1277
1274 return self._get_template_context(c)
1278 return self._get_template_context(c)
1275
1279
1276 @LoginRequired()
1280 @LoginRequired()
1277 @HasPermissionAllDecorator('hg.admin')
1281 @HasPermissionAllDecorator('hg.admin')
1278 def user_perms_summary_json(self):
1282 def user_perms_summary_json(self):
1279 self.load_default_context()
1283 self.load_default_context()
1280 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1284 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1281
1285
1282 return perm_user.permissions
1286 return perm_user.permissions
1283
1287
1284 @LoginRequired()
1288 @LoginRequired()
1285 @HasPermissionAllDecorator('hg.admin')
1289 @HasPermissionAllDecorator('hg.admin')
1286 def user_caches(self):
1290 def user_caches(self):
1287 _ = self.request.translate
1291 _ = self.request.translate
1288 c = self.load_default_context()
1292 c = self.load_default_context()
1289 c.user = self.db_user
1293 c.user = self.db_user
1290
1294
1291 c.active = 'caches'
1295 c.active = 'caches'
1292 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1296 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1293
1297
1294 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1298 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1295 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1299 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1296 c.backend = c.region.backend
1300 c.backend = c.region.backend
1297 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1301 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1298
1302
1299 return self._get_template_context(c)
1303 return self._get_template_context(c)
1300
1304
1301 @LoginRequired()
1305 @LoginRequired()
1302 @HasPermissionAllDecorator('hg.admin')
1306 @HasPermissionAllDecorator('hg.admin')
1303 @CSRFRequired()
1307 @CSRFRequired()
1304 def user_caches_update(self):
1308 def user_caches_update(self):
1305 _ = self.request.translate
1309 _ = self.request.translate
1306 c = self.load_default_context()
1310 c = self.load_default_context()
1307 c.user = self.db_user
1311 c.user = self.db_user
1308
1312
1309 c.active = 'caches'
1313 c.active = 'caches'
1310 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1314 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1311
1315
1312 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1316 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1313 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1317 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1314
1318
1315 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1319 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1316
1320
1317 return HTTPFound(h.route_path(
1321 return HTTPFound(h.route_path(
1318 'edit_user_caches', user_id=c.user.user_id))
1322 'edit_user_caches', user_id=c.user.user_id))
@@ -1,390 +1,390 b''
1 import sys
1 import sys
2 import threading
2 import threading
3 import weakref
3 import weakref
4 from base64 import b64encode
4 from base64 import b64encode
5 from logging import getLogger
5 from logging import getLogger
6 from os import urandom
6 from os import urandom
7
7
8 from redis import StrictRedis
8 from redis import StrictRedis
9
9
10 __version__ = '3.7.0'
10 __version__ = '3.7.0'
11
11
12 loggers = {
12 loggers = {
13 k: getLogger("rhodecode." + ".".join((__name__, k)))
13 k: getLogger("rhodecode." + ".".join((__name__, k)))
14 for k in [
14 for k in [
15 "acquire",
15 "acquire",
16 "refresh.thread.start",
16 "refresh.thread.start",
17 "refresh.thread.stop",
17 "refresh.thread.stop",
18 "refresh.thread.exit",
18 "refresh.thread.exit",
19 "refresh.start",
19 "refresh.start",
20 "refresh.shutdown",
20 "refresh.shutdown",
21 "refresh.exit",
21 "refresh.exit",
22 "release",
22 "release",
23 ]
23 ]
24 }
24 }
25
25
26 PY3 = sys.version_info[0] == 3
26 PY3 = sys.version_info[0] == 3
27
27
28 if PY3:
28 if PY3:
29 text_type = str
29 text_type = str
30 binary_type = bytes
30 binary_type = bytes
31 else:
31 else:
32 text_type = unicode # noqa
32 text_type = unicode # noqa
33 binary_type = str
33 binary_type = str
34
34
35
35
36 # Check if the id match. If not, return an error code.
36 # Check if the id match. If not, return an error code.
37 UNLOCK_SCRIPT = b"""
37 UNLOCK_SCRIPT = b"""
38 if redis.call("get", KEYS[1]) ~= ARGV[1] then
38 if redis.call("get", KEYS[1]) ~= ARGV[1] then
39 return 1
39 return 1
40 else
40 else
41 redis.call("del", KEYS[2])
41 redis.call("del", KEYS[2])
42 redis.call("lpush", KEYS[2], 1)
42 redis.call("lpush", KEYS[2], 1)
43 redis.call("pexpire", KEYS[2], ARGV[2])
43 redis.call("pexpire", KEYS[2], ARGV[2])
44 redis.call("del", KEYS[1])
44 redis.call("del", KEYS[1])
45 return 0
45 return 0
46 end
46 end
47 """
47 """
48
48
49 # Covers both cases when key doesn't exist and doesn't equal to lock's id
49 # Covers both cases when key doesn't exist and doesn't equal to lock's id
50 EXTEND_SCRIPT = b"""
50 EXTEND_SCRIPT = b"""
51 if redis.call("get", KEYS[1]) ~= ARGV[1] then
51 if redis.call("get", KEYS[1]) ~= ARGV[1] then
52 return 1
52 return 1
53 elseif redis.call("ttl", KEYS[1]) < 0 then
53 elseif redis.call("ttl", KEYS[1]) < 0 then
54 return 2
54 return 2
55 else
55 else
56 redis.call("expire", KEYS[1], ARGV[2])
56 redis.call("expire", KEYS[1], ARGV[2])
57 return 0
57 return 0
58 end
58 end
59 """
59 """
60
60
61 RESET_SCRIPT = b"""
61 RESET_SCRIPT = b"""
62 redis.call('del', KEYS[2])
62 redis.call('del', KEYS[2])
63 redis.call('lpush', KEYS[2], 1)
63 redis.call('lpush', KEYS[2], 1)
64 redis.call('pexpire', KEYS[2], ARGV[2])
64 redis.call('pexpire', KEYS[2], ARGV[2])
65 return redis.call('del', KEYS[1])
65 return redis.call('del', KEYS[1])
66 """
66 """
67
67
68 RESET_ALL_SCRIPT = b"""
68 RESET_ALL_SCRIPT = b"""
69 local locks = redis.call('keys', 'lock:*')
69 local locks = redis.call('keys', 'lock:*')
70 local signal
70 local signal
71 for _, lock in pairs(locks) do
71 for _, lock in pairs(locks) do
72 signal = 'lock-signal:' .. string.sub(lock, 6)
72 signal = 'lock-signal:' .. string.sub(lock, 6)
73 redis.call('del', signal)
73 redis.call('del', signal)
74 redis.call('lpush', signal, 1)
74 redis.call('lpush', signal, 1)
75 redis.call('expire', signal, 1)
75 redis.call('expire', signal, 1)
76 redis.call('del', lock)
76 redis.call('del', lock)
77 end
77 end
78 return #locks
78 return #locks
79 """
79 """
80
80
81
81
82 class AlreadyAcquired(RuntimeError):
82 class AlreadyAcquired(RuntimeError):
83 pass
83 pass
84
84
85
85
86 class NotAcquired(RuntimeError):
86 class NotAcquired(RuntimeError):
87 pass
87 pass
88
88
89
89
90 class AlreadyStarted(RuntimeError):
90 class AlreadyStarted(RuntimeError):
91 pass
91 pass
92
92
93
93
94 class TimeoutNotUsable(RuntimeError):
94 class TimeoutNotUsable(RuntimeError):
95 pass
95 pass
96
96
97
97
98 class InvalidTimeout(RuntimeError):
98 class InvalidTimeout(RuntimeError):
99 pass
99 pass
100
100
101
101
102 class TimeoutTooLarge(RuntimeError):
102 class TimeoutTooLarge(RuntimeError):
103 pass
103 pass
104
104
105
105
106 class NotExpirable(RuntimeError):
106 class NotExpirable(RuntimeError):
107 pass
107 pass
108
108
109
109
110 class Lock(object):
110 class Lock(object):
111 """
111 """
112 A Lock context manager implemented via redis SETNX/BLPOP.
112 A Lock context manager implemented via redis SETNX/BLPOP.
113 """
113 """
114 unlock_script = None
114 unlock_script = None
115 extend_script = None
115 extend_script = None
116 reset_script = None
116 reset_script = None
117 reset_all_script = None
117 reset_all_script = None
118
118
119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
119 def __init__(self, redis_client, name, expire=None, id=None, auto_renewal=False, strict=True, signal_expire=1000):
120 """
120 """
121 :param redis_client:
121 :param redis_client:
122 An instance of :class:`~StrictRedis`.
122 An instance of :class:`~StrictRedis`.
123 :param name:
123 :param name:
124 The name (redis key) the lock should have.
124 The name (redis key) the lock should have.
125 :param expire:
125 :param expire:
126 The lock expiry time in seconds. If left at the default (None)
126 The lock expiry time in seconds. If left at the default (None)
127 the lock will not expire.
127 the lock will not expire.
128 :param id:
128 :param id:
129 The ID (redis value) the lock should have. A random value is
129 The ID (redis value) the lock should have. A random value is
130 generated when left at the default.
130 generated when left at the default.
131
131
132 Note that if you specify this then the lock is marked as "held". Acquires
132 Note that if you specify this then the lock is marked as "held". Acquires
133 won't be possible.
133 won't be possible.
134 :param auto_renewal:
134 :param auto_renewal:
135 If set to ``True``, Lock will automatically renew the lock so that it
135 If set to ``True``, Lock will automatically renew the lock so that it
136 doesn't expire for as long as the lock is held (acquire() called
136 doesn't expire for as long as the lock is held (acquire() called
137 or running in a context manager).
137 or running in a context manager).
138
138
139 Implementation note: Renewal will happen using a daemon thread with
139 Implementation note: Renewal will happen using a daemon thread with
140 an interval of ``expire*2/3``. If wishing to use a different renewal
140 an interval of ``expire*2/3``. If wishing to use a different renewal
141 time, subclass Lock, call ``super().__init__()`` then set
141 time, subclass Lock, call ``super().__init__()`` then set
142 ``self._lock_renewal_interval`` to your desired interval.
142 ``self._lock_renewal_interval`` to your desired interval.
143 :param strict:
143 :param strict:
144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
144 If set ``True`` then the ``redis_client`` needs to be an instance of ``redis.StrictRedis``.
145 :param signal_expire:
145 :param signal_expire:
146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
146 Advanced option to override signal list expiration in milliseconds. Increase it for very slow clients. Default: ``1000``.
147 """
147 """
148 if strict and not isinstance(redis_client, StrictRedis):
148 if strict and not isinstance(redis_client, StrictRedis):
149 raise ValueError("redis_client must be instance of StrictRedis. "
149 raise ValueError("redis_client must be instance of StrictRedis. "
150 "Use strict=False if you know what you're doing.")
150 "Use strict=False if you know what you're doing.")
151 if auto_renewal and expire is None:
151 if auto_renewal and expire is None:
152 raise ValueError("Expire may not be None when auto_renewal is set")
152 raise ValueError("Expire may not be None when auto_renewal is set")
153
153
154 self._client = redis_client
154 self._client = redis_client
155
155
156 if expire:
156 if expire:
157 expire = int(expire)
157 expire = int(expire)
158 if expire < 0:
158 if expire < 0:
159 raise ValueError("A negative expire is not acceptable.")
159 raise ValueError("A negative expire is not acceptable.")
160 else:
160 else:
161 expire = None
161 expire = None
162 self._expire = expire
162 self._expire = expire
163
163
164 self._signal_expire = signal_expire
164 self._signal_expire = signal_expire
165 if id is None:
165 if id is None:
166 self._id = b64encode(urandom(18)).decode('ascii')
166 self._id = b64encode(urandom(18)).decode('ascii')
167 elif isinstance(id, binary_type):
167 elif isinstance(id, binary_type):
168 try:
168 try:
169 self._id = id.decode('ascii')
169 self._id = id.decode('ascii')
170 except UnicodeDecodeError:
170 except UnicodeDecodeError:
171 self._id = b64encode(id).decode('ascii')
171 self._id = b64encode(id).decode('ascii')
172 elif isinstance(id, text_type):
172 elif isinstance(id, text_type):
173 self._id = id
173 self._id = id
174 else:
174 else:
175 raise TypeError("Incorrect type for `id`. Must be bytes/str not %s." % type(id))
175 raise TypeError("Incorrect type for `id`. Must be bytes/str not %s." % type(id))
176 self._name = 'lock:' + name
176 self._name = 'lock:' + name
177 self._signal = 'lock-signal:' + name
177 self._signal = 'lock-signal:' + name
178 self._lock_renewal_interval = (float(expire) * 2 / 3
178 self._lock_renewal_interval = (float(expire) * 2 / 3
179 if auto_renewal
179 if auto_renewal
180 else None)
180 else None)
181 self._lock_renewal_thread = None
181 self._lock_renewal_thread = None
182
182
183 self.register_scripts(redis_client)
183 self.register_scripts(redis_client)
184
184
185 @classmethod
185 @classmethod
186 def register_scripts(cls, redis_client):
186 def register_scripts(cls, redis_client):
187 global reset_all_script
187 global reset_all_script
188 if reset_all_script is None:
188 if reset_all_script is None:
189 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
189 reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
190 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
190 cls.unlock_script = redis_client.register_script(UNLOCK_SCRIPT)
191 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
191 cls.extend_script = redis_client.register_script(EXTEND_SCRIPT)
192 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
192 cls.reset_script = redis_client.register_script(RESET_SCRIPT)
193 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
193 cls.reset_all_script = redis_client.register_script(RESET_ALL_SCRIPT)
194
194
195 @property
195 @property
196 def _held(self):
196 def _held(self):
197 return self.id == self.get_owner_id()
197 return self.id == self.get_owner_id()
198
198
199 def reset(self):
199 def reset(self):
200 """
200 """
201 Forcibly deletes the lock. Use this with care.
201 Forcibly deletes the lock. Use this with care.
202 """
202 """
203 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
203 self.reset_script(client=self._client, keys=(self._name, self._signal), args=(self.id, self._signal_expire))
204
204
205 @property
205 @property
206 def id(self):
206 def id(self):
207 return self._id
207 return self._id
208
208
209 def get_owner_id(self):
209 def get_owner_id(self):
210 owner_id = self._client.get(self._name)
210 owner_id = self._client.get(self._name)
211 if isinstance(owner_id, binary_type):
211 if isinstance(owner_id, binary_type):
212 owner_id = owner_id.decode('ascii', 'replace')
212 owner_id = owner_id.decode('ascii', 'replace')
213 return owner_id
213 return owner_id
214
214
215 def acquire(self, blocking=True, timeout=None):
215 def acquire(self, blocking=True, timeout=None):
216 """
216 """
217 :param blocking:
217 :param blocking:
218 Boolean value specifying whether lock should be blocking or not.
218 Boolean value specifying whether lock should be blocking or not.
219 :param timeout:
219 :param timeout:
220 An integer value specifying the maximum number of seconds to block.
220 An integer value specifying the maximum number of seconds to block.
221 """
221 """
222 logger = loggers["acquire"]
222 logger = loggers["acquire"]
223
223
224 logger.debug("Getting acquire on %r ...", self._name)
224 logger.debug("Getting blocking: %s acquire on %r ...", blocking, self._name)
225
225
226 if self._held:
226 if self._held:
227 owner_id = self.get_owner_id()
227 owner_id = self.get_owner_id()
228 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
228 raise AlreadyAcquired("Already acquired from this Lock instance. Lock id: {}".format(owner_id))
229
229
230 if not blocking and timeout is not None:
230 if not blocking and timeout is not None:
231 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
231 raise TimeoutNotUsable("Timeout cannot be used if blocking=False")
232
232
233 if timeout:
233 if timeout:
234 timeout = int(timeout)
234 timeout = int(timeout)
235 if timeout < 0:
235 if timeout < 0:
236 raise InvalidTimeout("Timeout (%d) cannot be less than or equal to 0" % timeout)
236 raise InvalidTimeout("Timeout (%d) cannot be less than or equal to 0" % timeout)
237
237
238 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
238 if self._expire and not self._lock_renewal_interval and timeout > self._expire:
239 raise TimeoutTooLarge("Timeout (%d) cannot be greater than expire (%d)" % (timeout, self._expire))
239 raise TimeoutTooLarge("Timeout (%d) cannot be greater than expire (%d)" % (timeout, self._expire))
240
240
241 busy = True
241 busy = True
242 blpop_timeout = timeout or self._expire or 0
242 blpop_timeout = timeout or self._expire or 0
243 timed_out = False
243 timed_out = False
244 while busy:
244 while busy:
245 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
245 busy = not self._client.set(self._name, self._id, nx=True, ex=self._expire)
246 if busy:
246 if busy:
247 if timed_out:
247 if timed_out:
248 return False
248 return False
249 elif blocking:
249 elif blocking:
250 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
250 timed_out = not self._client.blpop(self._signal, blpop_timeout) and timeout
251 else:
251 else:
252 logger.warning("Failed to get %r.", self._name)
252 logger.warning("Failed to get %r.", self._name)
253 return False
253 return False
254
254
255 logger.info("Got lock for %r.", self._name)
255 logger.info("Got lock for %r.", self._name)
256 if self._lock_renewal_interval is not None:
256 if self._lock_renewal_interval is not None:
257 self._start_lock_renewer()
257 self._start_lock_renewer()
258 return True
258 return True
259
259
260 def extend(self, expire=None):
260 def extend(self, expire=None):
261 """Extends expiration time of the lock.
261 """Extends expiration time of the lock.
262
262
263 :param expire:
263 :param expire:
264 New expiration time. If ``None`` - `expire` provided during
264 New expiration time. If ``None`` - `expire` provided during
265 lock initialization will be taken.
265 lock initialization will be taken.
266 """
266 """
267 if expire:
267 if expire:
268 expire = int(expire)
268 expire = int(expire)
269 if expire < 0:
269 if expire < 0:
270 raise ValueError("A negative expire is not acceptable.")
270 raise ValueError("A negative expire is not acceptable.")
271 elif self._expire is not None:
271 elif self._expire is not None:
272 expire = self._expire
272 expire = self._expire
273 else:
273 else:
274 raise TypeError(
274 raise TypeError(
275 "To extend a lock 'expire' must be provided as an "
275 "To extend a lock 'expire' must be provided as an "
276 "argument to extend() method or at initialization time."
276 "argument to extend() method or at initialization time."
277 )
277 )
278
278
279 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
279 error = self.extend_script(client=self._client, keys=(self._name, self._signal), args=(self._id, expire))
280 if error == 1:
280 if error == 1:
281 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
281 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
282 elif error == 2:
282 elif error == 2:
283 raise NotExpirable("Lock %s has no assigned expiration time" % self._name)
283 raise NotExpirable("Lock %s has no assigned expiration time" % self._name)
284 elif error:
284 elif error:
285 raise RuntimeError("Unsupported error code %s from EXTEND script" % error)
285 raise RuntimeError("Unsupported error code %s from EXTEND script" % error)
286
286
287 @staticmethod
287 @staticmethod
288 def _lock_renewer(lockref, interval, stop):
288 def _lock_renewer(lockref, interval, stop):
289 """
289 """
290 Renew the lock key in redis every `interval` seconds for as long
290 Renew the lock key in redis every `interval` seconds for as long
291 as `self._lock_renewal_thread.should_exit` is False.
291 as `self._lock_renewal_thread.should_exit` is False.
292 """
292 """
293 while not stop.wait(timeout=interval):
293 while not stop.wait(timeout=interval):
294 loggers["refresh.thread.start"].debug("Refreshing lock")
294 loggers["refresh.thread.start"].debug("Refreshing lock")
295 lock = lockref()
295 lock = lockref()
296 if lock is None:
296 if lock is None:
297 loggers["refresh.thread.stop"].debug(
297 loggers["refresh.thread.stop"].debug(
298 "The lock no longer exists, stopping lock refreshing"
298 "The lock no longer exists, stopping lock refreshing"
299 )
299 )
300 break
300 break
301 lock.extend(expire=lock._expire)
301 lock.extend(expire=lock._expire)
302 del lock
302 del lock
303 loggers["refresh.thread.exit"].debug("Exit requested, stopping lock refreshing")
303 loggers["refresh.thread.exit"].debug("Exit requested, stopping lock refreshing")
304
304
305 def _start_lock_renewer(self):
305 def _start_lock_renewer(self):
306 """
306 """
307 Starts the lock refresher thread.
307 Starts the lock refresher thread.
308 """
308 """
309 if self._lock_renewal_thread is not None:
309 if self._lock_renewal_thread is not None:
310 raise AlreadyStarted("Lock refresh thread already started")
310 raise AlreadyStarted("Lock refresh thread already started")
311
311
312 loggers["refresh.start"].debug(
312 loggers["refresh.start"].debug(
313 "Starting thread to refresh lock every %s seconds",
313 "Starting thread to refresh lock every %s seconds",
314 self._lock_renewal_interval
314 self._lock_renewal_interval
315 )
315 )
316 self._lock_renewal_stop = threading.Event()
316 self._lock_renewal_stop = threading.Event()
317 self._lock_renewal_thread = threading.Thread(
317 self._lock_renewal_thread = threading.Thread(
318 group=None,
318 group=None,
319 target=self._lock_renewer,
319 target=self._lock_renewer,
320 kwargs={'lockref': weakref.ref(self),
320 kwargs={'lockref': weakref.ref(self),
321 'interval': self._lock_renewal_interval,
321 'interval': self._lock_renewal_interval,
322 'stop': self._lock_renewal_stop}
322 'stop': self._lock_renewal_stop}
323 )
323 )
324 self._lock_renewal_thread.setDaemon(True)
324 self._lock_renewal_thread.setDaemon(True)
325 self._lock_renewal_thread.start()
325 self._lock_renewal_thread.start()
326
326
327 def _stop_lock_renewer(self):
327 def _stop_lock_renewer(self):
328 """
328 """
329 Stop the lock renewer.
329 Stop the lock renewer.
330
330
331 This signals the renewal thread and waits for its exit.
331 This signals the renewal thread and waits for its exit.
332 """
332 """
333 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
333 if self._lock_renewal_thread is None or not self._lock_renewal_thread.is_alive():
334 return
334 return
335 loggers["refresh.shutdown"].debug("Signalling the lock refresher to stop")
335 loggers["refresh.shutdown"].debug("Signalling the lock refresher to stop")
336 self._lock_renewal_stop.set()
336 self._lock_renewal_stop.set()
337 self._lock_renewal_thread.join()
337 self._lock_renewal_thread.join()
338 self._lock_renewal_thread = None
338 self._lock_renewal_thread = None
339 loggers["refresh.exit"].debug("Lock refresher has stopped")
339 loggers["refresh.exit"].debug("Lock refresher has stopped")
340
340
341 def __enter__(self):
341 def __enter__(self):
342 acquired = self.acquire(blocking=True)
342 acquired = self.acquire(blocking=True)
343 assert acquired, "Lock wasn't acquired, but blocking=True"
343 assert acquired, "Lock wasn't acquired, but blocking=True"
344 return self
344 return self
345
345
346 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
346 def __exit__(self, exc_type=None, exc_value=None, traceback=None):
347 self.release()
347 self.release()
348
348
349 def release(self):
349 def release(self):
350 """Releases the lock, that was acquired with the same object.
350 """Releases the lock, that was acquired with the same object.
351
351
352 .. note::
352 .. note::
353
353
354 If you want to release a lock that you acquired in a different place you have two choices:
354 If you want to release a lock that you acquired in a different place you have two choices:
355
355
356 * Use ``Lock("name", id=id_from_other_place).release()``
356 * Use ``Lock("name", id=id_from_other_place).release()``
357 * Use ``Lock("name").reset()``
357 * Use ``Lock("name").reset()``
358 """
358 """
359 if self._lock_renewal_thread is not None:
359 if self._lock_renewal_thread is not None:
360 self._stop_lock_renewer()
360 self._stop_lock_renewer()
361 loggers["release"].debug("Releasing %r.", self._name)
361 loggers["release"].debug("Releasing %r.", self._name)
362 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
362 error = self.unlock_script(client=self._client, keys=(self._name, self._signal), args=(self._id, self._signal_expire))
363 if error == 1:
363 if error == 1:
364 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
364 raise NotAcquired("Lock %s is not acquired or it already expired." % self._name)
365 elif error:
365 elif error:
366 raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
366 raise RuntimeError("Unsupported error code %s from EXTEND script." % error)
367
367
368 def locked(self):
368 def locked(self):
369 """
369 """
370 Return true if the lock is acquired.
370 Return true if the lock is acquired.
371
371
372 Checks that lock with same name already exists. This method returns true, even if
372 Checks that lock with same name already exists. This method returns true, even if
373 lock have another id.
373 lock have another id.
374 """
374 """
375 return self._client.exists(self._name) == 1
375 return self._client.exists(self._name) == 1
376
376
377
377
378 reset_all_script = None
378 reset_all_script = None
379
379
380
380
381 def reset_all(redis_client):
381 def reset_all(redis_client):
382 """
382 """
383 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
383 Forcibly deletes all locks if its remains (like a crash reason). Use this with care.
384
384
385 :param redis_client:
385 :param redis_client:
386 An instance of :class:`~StrictRedis`.
386 An instance of :class:`~StrictRedis`.
387 """
387 """
388 Lock.register_scripts(redis_client)
388 Lock.register_scripts(redis_client)
389
389
390 reset_all_script(client=redis_client) # noqa
390 reset_all_script(client=redis_client) # noqa
@@ -1,2148 +1,2147 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 import StringIO
33 import StringIO
34 import textwrap
34 import textwrap
35 import urllib
35 import urllib
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 pyramid import compat
50 from pyramid import compat
51 from datetime import datetime
51 from datetime import datetime
52 from functools import partial
52 from functools import partial
53 from pygments.formatters.html import HtmlFormatter
53 from pygments.formatters.html import HtmlFormatter
54 from pygments.lexers import (
54 from pygments.lexers import (
55 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
55 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
56
56
57 from pyramid.threadlocal import get_current_request
57 from pyramid.threadlocal import get_current_request
58 from tempita import looper
58 from tempita import looper
59 from webhelpers2.html import literal, HTML, escape
59 from webhelpers2.html import literal, HTML, escape
60 from webhelpers2.html._autolink import _auto_link_urls
60 from webhelpers2.html._autolink import _auto_link_urls
61 from webhelpers2.html.tools import (
61 from webhelpers2.html.tools import (
62 button_to, highlight, js_obfuscate, strip_links, strip_tags)
62 button_to, highlight, js_obfuscate, strip_links, strip_tags)
63
63
64 from webhelpers2.text import (
64 from webhelpers2.text import (
65 chop_at, collapse, convert_accented_entities,
65 chop_at, collapse, convert_accented_entities,
66 convert_misc_entities, lchop, plural, rchop, remove_formatting,
66 convert_misc_entities, lchop, plural, rchop, remove_formatting,
67 replace_whitespace, urlify, truncate, wrap_paragraphs)
67 replace_whitespace, urlify, truncate, wrap_paragraphs)
68 from webhelpers2.date import time_ago_in_words
68 from webhelpers2.date import time_ago_in_words
69
69
70 from webhelpers2.html.tags import (
70 from webhelpers2.html.tags import (
71 _input, NotGiven, _make_safe_id_component as safeid,
71 _input, NotGiven, _make_safe_id_component as safeid,
72 form as insecure_form,
72 form as insecure_form,
73 auto_discovery_link, checkbox, end_form, file,
73 auto_discovery_link, checkbox, end_form, file,
74 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
74 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
75 select as raw_select, stylesheet_link, submit, text, password, textarea,
75 select as raw_select, stylesheet_link, submit, text, password, textarea,
76 ul, radio, Options)
76 ul, radio, Options)
77
77
78 from webhelpers2.number import format_byte_size
78 from webhelpers2.number import format_byte_size
79
79
80 from rhodecode.lib.action_parser import action_parser
80 from rhodecode.lib.action_parser import action_parser
81 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
81 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
82 from rhodecode.lib.ext_json import json
82 from rhodecode.lib.ext_json import json
83 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
83 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
84 from rhodecode.lib.utils2 import (
84 from rhodecode.lib.utils2 import (
85 str2bool, safe_unicode, safe_str,
85 str2bool, safe_unicode, safe_str,
86 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
86 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
87 AttributeDict, safe_int, md5, md5_safe, get_host_info)
87 AttributeDict, safe_int, md5, md5_safe, get_host_info)
88 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
88 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
89 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
89 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
90 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
90 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
91 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
91 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
92 from rhodecode.lib.index.search_utils import get_matching_line_offsets
92 from rhodecode.lib.index.search_utils import get_matching_line_offsets
93 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
93 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
94 from rhodecode.model.changeset_status import ChangesetStatusModel
94 from rhodecode.model.changeset_status import ChangesetStatusModel
95 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
95 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
96 from rhodecode.model.repo_group import RepoGroupModel
96 from rhodecode.model.repo_group import RepoGroupModel
97 from rhodecode.model.settings import IssueTrackerSettingsModel
97 from rhodecode.model.settings import IssueTrackerSettingsModel
98
98
99
99
100 log = logging.getLogger(__name__)
100 log = logging.getLogger(__name__)
101
101
102
102
103 DEFAULT_USER = User.DEFAULT_USER
103 DEFAULT_USER = User.DEFAULT_USER
104 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
104 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
105
105
106
106
107 def asset(path, ver=None, **kwargs):
107 def asset(path, ver=None, **kwargs):
108 """
108 """
109 Helper to generate a static asset file path for rhodecode assets
109 Helper to generate a static asset file path for rhodecode assets
110
110
111 eg. h.asset('images/image.png', ver='3923')
111 eg. h.asset('images/image.png', ver='3923')
112
112
113 :param path: path of asset
113 :param path: path of asset
114 :param ver: optional version query param to append as ?ver=
114 :param ver: optional version query param to append as ?ver=
115 """
115 """
116 request = get_current_request()
116 request = get_current_request()
117 query = {}
117 query = {}
118 query.update(kwargs)
118 query.update(kwargs)
119 if ver:
119 if ver:
120 query = {'ver': ver}
120 query = {'ver': ver}
121 return request.static_path(
121 return request.static_path(
122 'rhodecode:public/{}'.format(path), _query=query)
122 'rhodecode:public/{}'.format(path), _query=query)
123
123
124
124
125 default_html_escape_table = {
125 default_html_escape_table = {
126 ord('&'): u'&amp;',
126 ord('&'): u'&amp;',
127 ord('<'): u'&lt;',
127 ord('<'): u'&lt;',
128 ord('>'): u'&gt;',
128 ord('>'): u'&gt;',
129 ord('"'): u'&quot;',
129 ord('"'): u'&quot;',
130 ord("'"): u'&#39;',
130 ord("'"): u'&#39;',
131 }
131 }
132
132
133
133
134 def html_escape(text, html_escape_table=default_html_escape_table):
134 def html_escape(text, html_escape_table=default_html_escape_table):
135 """Produce entities within text."""
135 """Produce entities within text."""
136 return text.translate(html_escape_table)
136 return text.translate(html_escape_table)
137
137
138
138
139 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
139 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
140 """
140 """
141 Truncate string ``s`` at the first occurrence of ``sub``.
141 Truncate string ``s`` at the first occurrence of ``sub``.
142
142
143 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
143 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
144 """
144 """
145 suffix_if_chopped = suffix_if_chopped or ''
145 suffix_if_chopped = suffix_if_chopped or ''
146 pos = s.find(sub)
146 pos = s.find(sub)
147 if pos == -1:
147 if pos == -1:
148 return s
148 return s
149
149
150 if inclusive:
150 if inclusive:
151 pos += len(sub)
151 pos += len(sub)
152
152
153 chopped = s[:pos]
153 chopped = s[:pos]
154 left = s[pos:].strip()
154 left = s[pos:].strip()
155
155
156 if left and suffix_if_chopped:
156 if left and suffix_if_chopped:
157 chopped += suffix_if_chopped
157 chopped += suffix_if_chopped
158
158
159 return chopped
159 return chopped
160
160
161
161
162 def shorter(text, size=20, prefix=False):
162 def shorter(text, size=20, prefix=False):
163 postfix = '...'
163 postfix = '...'
164 if len(text) > size:
164 if len(text) > size:
165 if prefix:
165 if prefix:
166 # shorten in front
166 # shorten in front
167 return postfix + text[-(size - len(postfix)):]
167 return postfix + text[-(size - len(postfix)):]
168 else:
168 else:
169 return text[:size - len(postfix)] + postfix
169 return text[:size - len(postfix)] + postfix
170 return text
170 return text
171
171
172
172
173 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
173 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
174 """
174 """
175 Reset button
175 Reset button
176 """
176 """
177 return _input(type, name, value, id, attrs)
177 return _input(type, name, value, id, attrs)
178
178
179
179
180 def select(name, selected_values, options, id=NotGiven, **attrs):
180 def select(name, selected_values, options, id=NotGiven, **attrs):
181
181
182 if isinstance(options, (list, tuple)):
182 if isinstance(options, (list, tuple)):
183 options_iter = options
183 options_iter = options
184 # Handle old value,label lists ... where value also can be value,label lists
184 # Handle old value,label lists ... where value also can be value,label lists
185 options = Options()
185 options = Options()
186 for opt in options_iter:
186 for opt in options_iter:
187 if isinstance(opt, tuple) and len(opt) == 2:
187 if isinstance(opt, tuple) and len(opt) == 2:
188 value, label = opt
188 value, label = opt
189 elif isinstance(opt, basestring):
189 elif isinstance(opt, basestring):
190 value = label = opt
190 value = label = opt
191 else:
191 else:
192 raise ValueError('invalid select option type %r' % type(opt))
192 raise ValueError('invalid select option type %r' % type(opt))
193
193
194 if isinstance(value, (list, tuple)):
194 if isinstance(value, (list, tuple)):
195 option_group = options.add_optgroup(label)
195 option_group = options.add_optgroup(label)
196 for opt2 in value:
196 for opt2 in value:
197 if isinstance(opt2, tuple) and len(opt2) == 2:
197 if isinstance(opt2, tuple) and len(opt2) == 2:
198 group_value, group_label = opt2
198 group_value, group_label = opt2
199 elif isinstance(opt2, basestring):
199 elif isinstance(opt2, basestring):
200 group_value = group_label = opt2
200 group_value = group_label = opt2
201 else:
201 else:
202 raise ValueError('invalid select option type %r' % type(opt2))
202 raise ValueError('invalid select option type %r' % type(opt2))
203
203
204 option_group.add_option(group_label, group_value)
204 option_group.add_option(group_label, group_value)
205 else:
205 else:
206 options.add_option(label, value)
206 options.add_option(label, value)
207
207
208 return raw_select(name, selected_values, options, id=id, **attrs)
208 return raw_select(name, selected_values, options, id=id, **attrs)
209
209
210
210
211 def branding(name, length=40):
211 def branding(name, length=40):
212 return truncate(name, length, indicator="")
212 return truncate(name, length, indicator="")
213
213
214
214
215 def FID(raw_id, path):
215 def FID(raw_id, path):
216 """
216 """
217 Creates a unique ID for filenode based on it's hash of path and commit
217 Creates a unique ID for filenode based on it's hash of path and commit
218 it's safe to use in urls
218 it's safe to use in urls
219
219
220 :param raw_id:
220 :param raw_id:
221 :param path:
221 :param path:
222 """
222 """
223
223
224 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
224 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
225
225
226
226
227 class _GetError(object):
227 class _GetError(object):
228 """Get error from form_errors, and represent it as span wrapped error
228 """Get error from form_errors, and represent it as span wrapped error
229 message
229 message
230
230
231 :param field_name: field to fetch errors for
231 :param field_name: field to fetch errors for
232 :param form_errors: form errors dict
232 :param form_errors: form errors dict
233 """
233 """
234
234
235 def __call__(self, field_name, form_errors):
235 def __call__(self, field_name, form_errors):
236 tmpl = """<span class="error_msg">%s</span>"""
236 tmpl = """<span class="error_msg">%s</span>"""
237 if form_errors and field_name in form_errors:
237 if form_errors and field_name in form_errors:
238 return literal(tmpl % form_errors.get(field_name))
238 return literal(tmpl % form_errors.get(field_name))
239
239
240
240
241 get_error = _GetError()
241 get_error = _GetError()
242
242
243
243
244 class _ToolTip(object):
244 class _ToolTip(object):
245
245
246 def __call__(self, tooltip_title, trim_at=50):
246 def __call__(self, tooltip_title, trim_at=50):
247 """
247 """
248 Special function just to wrap our text into nice formatted
248 Special function just to wrap our text into nice formatted
249 autowrapped text
249 autowrapped text
250
250
251 :param tooltip_title:
251 :param tooltip_title:
252 """
252 """
253 tooltip_title = escape(tooltip_title)
253 tooltip_title = escape(tooltip_title)
254 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
254 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
255 return tooltip_title
255 return tooltip_title
256
256
257
257
258 tooltip = _ToolTip()
258 tooltip = _ToolTip()
259
259
260 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
260 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
261
261
262
262
263 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
263 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
264 limit_items=False, linkify_last_item=False, hide_last_item=False,
264 limit_items=False, linkify_last_item=False, hide_last_item=False,
265 copy_path_icon=True):
265 copy_path_icon=True):
266 if isinstance(file_path, str):
266 if isinstance(file_path, str):
267 file_path = safe_unicode(file_path)
267 file_path = safe_unicode(file_path)
268
268
269 if at_ref:
269 if at_ref:
270 route_qry = {'at': at_ref}
270 route_qry = {'at': at_ref}
271 default_landing_ref = at_ref or landing_ref_name or commit_id
271 default_landing_ref = at_ref or landing_ref_name or commit_id
272 else:
272 else:
273 route_qry = None
273 route_qry = None
274 default_landing_ref = commit_id
274 default_landing_ref = commit_id
275
275
276 # first segment is a `HOME` link to repo files root location
276 # first segment is a `HOME` link to repo files root location
277 root_name = literal(u'<i class="icon-home"></i>')
277 root_name = literal(u'<i class="icon-home"></i>')
278
278
279 url_segments = [
279 url_segments = [
280 link_to(
280 link_to(
281 root_name,
281 root_name,
282 repo_files_by_ref_url(
282 repo_files_by_ref_url(
283 repo_name,
283 repo_name,
284 repo_type,
284 repo_type,
285 f_path=None, # None here is a special case for SVN repos,
285 f_path=None, # None here is a special case for SVN repos,
286 # that won't prefix with a ref
286 # that won't prefix with a ref
287 ref_name=default_landing_ref,
287 ref_name=default_landing_ref,
288 commit_id=commit_id,
288 commit_id=commit_id,
289 query=route_qry
289 query=route_qry
290 )
290 )
291 )]
291 )]
292
292
293 path_segments = file_path.split('/')
293 path_segments = file_path.split('/')
294 last_cnt = len(path_segments) - 1
294 last_cnt = len(path_segments) - 1
295 for cnt, segment in enumerate(path_segments):
295 for cnt, segment in enumerate(path_segments):
296 if not segment:
296 if not segment:
297 continue
297 continue
298 segment_html = escape(segment)
298 segment_html = escape(segment)
299
299
300 last_item = cnt == last_cnt
300 last_item = cnt == last_cnt
301
301
302 if last_item and hide_last_item:
302 if last_item and hide_last_item:
303 # iterate over and hide last element
303 # iterate over and hide last element
304 continue
304 continue
305
305
306 if last_item and linkify_last_item is False:
306 if last_item and linkify_last_item is False:
307 # plain version
307 # plain version
308 url_segments.append(segment_html)
308 url_segments.append(segment_html)
309 else:
309 else:
310 url_segments.append(
310 url_segments.append(
311 link_to(
311 link_to(
312 segment_html,
312 segment_html,
313 repo_files_by_ref_url(
313 repo_files_by_ref_url(
314 repo_name,
314 repo_name,
315 repo_type,
315 repo_type,
316 f_path='/'.join(path_segments[:cnt + 1]),
316 f_path='/'.join(path_segments[:cnt + 1]),
317 ref_name=default_landing_ref,
317 ref_name=default_landing_ref,
318 commit_id=commit_id,
318 commit_id=commit_id,
319 query=route_qry
319 query=route_qry
320 ),
320 ),
321 ))
321 ))
322
322
323 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
323 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
324 if limit_items and len(limited_url_segments) < len(url_segments):
324 if limit_items and len(limited_url_segments) < len(url_segments):
325 url_segments = limited_url_segments
325 url_segments = limited_url_segments
326
326
327 full_path = file_path
327 full_path = file_path
328 if copy_path_icon:
328 if copy_path_icon:
329 icon = files_icon.format(escape(full_path))
329 icon = files_icon.format(escape(full_path))
330 else:
330 else:
331 icon = ''
331 icon = ''
332
332
333 if file_path == '':
333 if file_path == '':
334 return root_name
334 return root_name
335 else:
335 else:
336 return literal(' / '.join(url_segments) + icon)
336 return literal(' / '.join(url_segments) + icon)
337
337
338
338
339 def files_url_data(request):
339 def files_url_data(request):
340 matchdict = request.matchdict
340 matchdict = request.matchdict
341
341
342 if 'f_path' not in matchdict:
342 if 'f_path' not in matchdict:
343 matchdict['f_path'] = ''
343 matchdict['f_path'] = ''
344
344
345 if 'commit_id' not in matchdict:
345 if 'commit_id' not in matchdict:
346 matchdict['commit_id'] = 'tip'
346 matchdict['commit_id'] = 'tip'
347
347
348 return json.dumps(matchdict)
348 return json.dumps(matchdict)
349
349
350
350
351 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
351 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
352 _is_svn = is_svn(db_repo_type)
352 _is_svn = is_svn(db_repo_type)
353 final_f_path = f_path
353 final_f_path = f_path
354
354
355 if _is_svn:
355 if _is_svn:
356 """
356 """
357 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
357 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
358 actually commit_id followed by the ref_name. This should be done only in case
358 actually commit_id followed by the ref_name. This should be done only in case
359 This is a initial landing url, without additional paths.
359 This is a initial landing url, without additional paths.
360
360
361 like: /1000/tags/1.0.0/?at=tags/1.0.0
361 like: /1000/tags/1.0.0/?at=tags/1.0.0
362 """
362 """
363
363
364 if ref_name and ref_name != 'tip':
364 if ref_name and ref_name != 'tip':
365 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
365 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
366 # for SVN we only do this magic prefix if it's root, .eg landing revision
366 # for SVN we only do this magic prefix if it's root, .eg landing revision
367 # of files link. If we are in the tree we don't need this since we traverse the url
367 # of files link. If we are in the tree we don't need this since we traverse the url
368 # that has everything stored
368 # that has everything stored
369 if f_path in ['', '/']:
369 if f_path in ['', '/']:
370 final_f_path = '/'.join([ref_name, f_path])
370 final_f_path = '/'.join([ref_name, f_path])
371
371
372 # SVN always needs a commit_id explicitly, without a named REF
372 # SVN always needs a commit_id explicitly, without a named REF
373 default_commit_id = commit_id
373 default_commit_id = commit_id
374 else:
374 else:
375 """
375 """
376 For git and mercurial we construct a new URL using the names instead of commit_id
376 For git and mercurial we construct a new URL using the names instead of commit_id
377 like: /master/some_path?at=master
377 like: /master/some_path?at=master
378 """
378 """
379 # We currently do not support branches with slashes
379 # We currently do not support branches with slashes
380 if '/' in ref_name:
380 if '/' in ref_name:
381 default_commit_id = commit_id
381 default_commit_id = commit_id
382 else:
382 else:
383 default_commit_id = ref_name
383 default_commit_id = ref_name
384
384
385 # sometimes we pass f_path as None, to indicate explicit no prefix,
385 # sometimes we pass f_path as None, to indicate explicit no prefix,
386 # we translate it to string to not have None
386 # we translate it to string to not have None
387 final_f_path = final_f_path or ''
387 final_f_path = final_f_path or ''
388
388
389 files_url = route_path(
389 files_url = route_path(
390 'repo_files',
390 'repo_files',
391 repo_name=db_repo_name,
391 repo_name=db_repo_name,
392 commit_id=default_commit_id,
392 commit_id=default_commit_id,
393 f_path=final_f_path,
393 f_path=final_f_path,
394 _query=query
394 _query=query
395 )
395 )
396 return files_url
396 return files_url
397
397
398
398
399 def code_highlight(code, lexer, formatter, use_hl_filter=False):
399 def code_highlight(code, lexer, formatter, use_hl_filter=False):
400 """
400 """
401 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
401 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
402
402
403 If ``outfile`` is given and a valid file object (an object
403 If ``outfile`` is given and a valid file object (an object
404 with a ``write`` method), the result will be written to it, otherwise
404 with a ``write`` method), the result will be written to it, otherwise
405 it is returned as a string.
405 it is returned as a string.
406 """
406 """
407 if use_hl_filter:
407 if use_hl_filter:
408 # add HL filter
408 # add HL filter
409 from rhodecode.lib.index import search_utils
409 from rhodecode.lib.index import search_utils
410 lexer.add_filter(search_utils.ElasticSearchHLFilter())
410 lexer.add_filter(search_utils.ElasticSearchHLFilter())
411 return pygments.format(pygments.lex(code, lexer), formatter)
411 return pygments.format(pygments.lex(code, lexer), formatter)
412
412
413
413
414 class CodeHtmlFormatter(HtmlFormatter):
414 class CodeHtmlFormatter(HtmlFormatter):
415 """
415 """
416 My code Html Formatter for source codes
416 My code Html Formatter for source codes
417 """
417 """
418
418
419 def wrap(self, source, outfile):
419 def wrap(self, source, outfile):
420 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
420 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
421
421
422 def _wrap_code(self, source):
422 def _wrap_code(self, source):
423 for cnt, it in enumerate(source):
423 for cnt, it in enumerate(source):
424 i, t = it
424 i, t = it
425 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
425 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
426 yield i, t
426 yield i, t
427
427
428 def _wrap_tablelinenos(self, inner):
428 def _wrap_tablelinenos(self, inner):
429 dummyoutfile = StringIO.StringIO()
429 dummyoutfile = StringIO.StringIO()
430 lncount = 0
430 lncount = 0
431 for t, line in inner:
431 for t, line in inner:
432 if t:
432 if t:
433 lncount += 1
433 lncount += 1
434 dummyoutfile.write(line)
434 dummyoutfile.write(line)
435
435
436 fl = self.linenostart
436 fl = self.linenostart
437 mw = len(str(lncount + fl - 1))
437 mw = len(str(lncount + fl - 1))
438 sp = self.linenospecial
438 sp = self.linenospecial
439 st = self.linenostep
439 st = self.linenostep
440 la = self.lineanchors
440 la = self.lineanchors
441 aln = self.anchorlinenos
441 aln = self.anchorlinenos
442 nocls = self.noclasses
442 nocls = self.noclasses
443 if sp:
443 if sp:
444 lines = []
444 lines = []
445
445
446 for i in range(fl, fl + lncount):
446 for i in range(fl, fl + lncount):
447 if i % st == 0:
447 if i % st == 0:
448 if i % sp == 0:
448 if i % sp == 0:
449 if aln:
449 if aln:
450 lines.append('<a href="#%s%d" class="special">%*d</a>' %
450 lines.append('<a href="#%s%d" class="special">%*d</a>' %
451 (la, i, mw, i))
451 (la, i, mw, i))
452 else:
452 else:
453 lines.append('<span class="special">%*d</span>' % (mw, i))
453 lines.append('<span class="special">%*d</span>' % (mw, i))
454 else:
454 else:
455 if aln:
455 if aln:
456 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
456 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
457 else:
457 else:
458 lines.append('%*d' % (mw, i))
458 lines.append('%*d' % (mw, i))
459 else:
459 else:
460 lines.append('')
460 lines.append('')
461 ls = '\n'.join(lines)
461 ls = '\n'.join(lines)
462 else:
462 else:
463 lines = []
463 lines = []
464 for i in range(fl, fl + lncount):
464 for i in range(fl, fl + lncount):
465 if i % st == 0:
465 if i % st == 0:
466 if aln:
466 if aln:
467 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))
468 else:
468 else:
469 lines.append('%*d' % (mw, i))
469 lines.append('%*d' % (mw, i))
470 else:
470 else:
471 lines.append('')
471 lines.append('')
472 ls = '\n'.join(lines)
472 ls = '\n'.join(lines)
473
473
474 # in case you wonder about the seemingly redundant <div> here: since the
474 # in case you wonder about the seemingly redundant <div> here: since the
475 # content in the other cell also is wrapped in a div, some browsers in
475 # content in the other cell also is wrapped in a div, some browsers in
476 # some configurations seem to mess up the formatting...
476 # some configurations seem to mess up the formatting...
477 if nocls:
477 if nocls:
478 yield 0, ('<table class="%stable">' % self.cssclass +
478 yield 0, ('<table class="%stable">' % self.cssclass +
479 '<tr><td><div class="linenodiv" '
479 '<tr><td><div class="linenodiv" '
480 'style="background-color: #f0f0f0; padding-right: 10px">'
480 'style="background-color: #f0f0f0; padding-right: 10px">'
481 '<pre style="line-height: 125%">' +
481 '<pre style="line-height: 125%">' +
482 ls + '</pre></div></td><td id="hlcode" class="code">')
482 ls + '</pre></div></td><td id="hlcode" class="code">')
483 else:
483 else:
484 yield 0, ('<table class="%stable">' % self.cssclass +
484 yield 0, ('<table class="%stable">' % self.cssclass +
485 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
485 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
486 ls + '</pre></div></td><td id="hlcode" class="code">')
486 ls + '</pre></div></td><td id="hlcode" class="code">')
487 yield 0, dummyoutfile.getvalue()
487 yield 0, dummyoutfile.getvalue()
488 yield 0, '</td></tr></table>'
488 yield 0, '</td></tr></table>'
489
489
490
490
491 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
491 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
492 def __init__(self, **kw):
492 def __init__(self, **kw):
493 # only show these line numbers if set
493 # only show these line numbers if set
494 self.only_lines = kw.pop('only_line_numbers', [])
494 self.only_lines = kw.pop('only_line_numbers', [])
495 self.query_terms = kw.pop('query_terms', [])
495 self.query_terms = kw.pop('query_terms', [])
496 self.max_lines = kw.pop('max_lines', 5)
496 self.max_lines = kw.pop('max_lines', 5)
497 self.line_context = kw.pop('line_context', 3)
497 self.line_context = kw.pop('line_context', 3)
498 self.url = kw.pop('url', None)
498 self.url = kw.pop('url', None)
499
499
500 super(CodeHtmlFormatter, self).__init__(**kw)
500 super(CodeHtmlFormatter, self).__init__(**kw)
501
501
502 def _wrap_code(self, source):
502 def _wrap_code(self, source):
503 for cnt, it in enumerate(source):
503 for cnt, it in enumerate(source):
504 i, t = it
504 i, t = it
505 t = '<pre>%s</pre>' % t
505 t = '<pre>%s</pre>' % t
506 yield i, t
506 yield i, t
507
507
508 def _wrap_tablelinenos(self, inner):
508 def _wrap_tablelinenos(self, inner):
509 yield 0, '<table class="code-highlight %stable">' % self.cssclass
509 yield 0, '<table class="code-highlight %stable">' % self.cssclass
510
510
511 last_shown_line_number = 0
511 last_shown_line_number = 0
512 current_line_number = 1
512 current_line_number = 1
513
513
514 for t, line in inner:
514 for t, line in inner:
515 if not t:
515 if not t:
516 yield t, line
516 yield t, line
517 continue
517 continue
518
518
519 if current_line_number in self.only_lines:
519 if current_line_number in self.only_lines:
520 if last_shown_line_number + 1 != current_line_number:
520 if last_shown_line_number + 1 != current_line_number:
521 yield 0, '<tr>'
521 yield 0, '<tr>'
522 yield 0, '<td class="line">...</td>'
522 yield 0, '<td class="line">...</td>'
523 yield 0, '<td id="hlcode" class="code"></td>'
523 yield 0, '<td id="hlcode" class="code"></td>'
524 yield 0, '</tr>'
524 yield 0, '</tr>'
525
525
526 yield 0, '<tr>'
526 yield 0, '<tr>'
527 if self.url:
527 if self.url:
528 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
528 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
529 self.url, current_line_number, current_line_number)
529 self.url, current_line_number, current_line_number)
530 else:
530 else:
531 yield 0, '<td class="line"><a href="">%i</a></td>' % (
531 yield 0, '<td class="line"><a href="">%i</a></td>' % (
532 current_line_number)
532 current_line_number)
533 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
533 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
534 yield 0, '</tr>'
534 yield 0, '</tr>'
535
535
536 last_shown_line_number = current_line_number
536 last_shown_line_number = current_line_number
537
537
538 current_line_number += 1
538 current_line_number += 1
539
539
540 yield 0, '</table>'
540 yield 0, '</table>'
541
541
542
542
543 def hsv_to_rgb(h, s, v):
543 def hsv_to_rgb(h, s, v):
544 """ Convert hsv color values to rgb """
544 """ Convert hsv color values to rgb """
545
545
546 if s == 0.0:
546 if s == 0.0:
547 return v, v, v
547 return v, v, v
548 i = int(h * 6.0) # XXX assume int() truncates!
548 i = int(h * 6.0) # XXX assume int() truncates!
549 f = (h * 6.0) - i
549 f = (h * 6.0) - i
550 p = v * (1.0 - s)
550 p = v * (1.0 - s)
551 q = v * (1.0 - s * f)
551 q = v * (1.0 - s * f)
552 t = v * (1.0 - s * (1.0 - f))
552 t = v * (1.0 - s * (1.0 - f))
553 i = i % 6
553 i = i % 6
554 if i == 0:
554 if i == 0:
555 return v, t, p
555 return v, t, p
556 if i == 1:
556 if i == 1:
557 return q, v, p
557 return q, v, p
558 if i == 2:
558 if i == 2:
559 return p, v, t
559 return p, v, t
560 if i == 3:
560 if i == 3:
561 return p, q, v
561 return p, q, v
562 if i == 4:
562 if i == 4:
563 return t, p, v
563 return t, p, v
564 if i == 5:
564 if i == 5:
565 return v, p, q
565 return v, p, q
566
566
567
567
568 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
568 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
569 """
569 """
570 Generator for getting n of evenly distributed colors using
570 Generator for getting n of evenly distributed colors using
571 hsv color and golden ratio. It always return same order of colors
571 hsv color and golden ratio. It always return same order of colors
572
572
573 :param n: number of colors to generate
573 :param n: number of colors to generate
574 :param saturation: saturation of returned colors
574 :param saturation: saturation of returned colors
575 :param lightness: lightness of returned colors
575 :param lightness: lightness of returned colors
576 :returns: RGB tuple
576 :returns: RGB tuple
577 """
577 """
578
578
579 golden_ratio = 0.618033988749895
579 golden_ratio = 0.618033988749895
580 h = 0.22717784590367374
580 h = 0.22717784590367374
581
581
582 for _ in xrange(n):
582 for _ in xrange(n):
583 h += golden_ratio
583 h += golden_ratio
584 h %= 1
584 h %= 1
585 HSV_tuple = [h, saturation, lightness]
585 HSV_tuple = [h, saturation, lightness]
586 RGB_tuple = hsv_to_rgb(*HSV_tuple)
586 RGB_tuple = hsv_to_rgb(*HSV_tuple)
587 yield map(lambda x: str(int(x * 256)), RGB_tuple)
587 yield map(lambda x: str(int(x * 256)), RGB_tuple)
588
588
589
589
590 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
590 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
591 """
591 """
592 Returns a function which when called with an argument returns a unique
592 Returns a function which when called with an argument returns a unique
593 color for that argument, eg.
593 color for that argument, eg.
594
594
595 :param n: number of colors to generate
595 :param n: number of colors to generate
596 :param saturation: saturation of returned colors
596 :param saturation: saturation of returned colors
597 :param lightness: lightness of returned colors
597 :param lightness: lightness of returned colors
598 :returns: css RGB string
598 :returns: css RGB string
599
599
600 >>> color_hash = color_hasher()
600 >>> color_hash = color_hasher()
601 >>> color_hash('hello')
601 >>> color_hash('hello')
602 'rgb(34, 12, 59)'
602 'rgb(34, 12, 59)'
603 >>> color_hash('hello')
603 >>> color_hash('hello')
604 'rgb(34, 12, 59)'
604 'rgb(34, 12, 59)'
605 >>> color_hash('other')
605 >>> color_hash('other')
606 'rgb(90, 224, 159)'
606 'rgb(90, 224, 159)'
607 """
607 """
608
608
609 color_dict = {}
609 color_dict = {}
610 cgenerator = unique_color_generator(
610 cgenerator = unique_color_generator(
611 saturation=saturation, lightness=lightness)
611 saturation=saturation, lightness=lightness)
612
612
613 def get_color_string(thing):
613 def get_color_string(thing):
614 if thing in color_dict:
614 if thing in color_dict:
615 col = color_dict[thing]
615 col = color_dict[thing]
616 else:
616 else:
617 col = color_dict[thing] = cgenerator.next()
617 col = color_dict[thing] = cgenerator.next()
618 return "rgb(%s)" % (', '.join(col))
618 return "rgb(%s)" % (', '.join(col))
619
619
620 return get_color_string
620 return get_color_string
621
621
622
622
623 def get_lexer_safe(mimetype=None, filepath=None):
623 def get_lexer_safe(mimetype=None, filepath=None):
624 """
624 """
625 Tries to return a relevant pygments lexer using mimetype/filepath name,
625 Tries to return a relevant pygments lexer using mimetype/filepath name,
626 defaulting to plain text if none could be found
626 defaulting to plain text if none could be found
627 """
627 """
628 lexer = None
628 lexer = None
629 try:
629 try:
630 if mimetype:
630 if mimetype:
631 lexer = get_lexer_for_mimetype(mimetype)
631 lexer = get_lexer_for_mimetype(mimetype)
632 if not lexer:
632 if not lexer:
633 lexer = get_lexer_for_filename(filepath)
633 lexer = get_lexer_for_filename(filepath)
634 except pygments.util.ClassNotFound:
634 except pygments.util.ClassNotFound:
635 pass
635 pass
636
636
637 if not lexer:
637 if not lexer:
638 lexer = get_lexer_by_name('text')
638 lexer = get_lexer_by_name('text')
639
639
640 return lexer
640 return lexer
641
641
642
642
643 def get_lexer_for_filenode(filenode):
643 def get_lexer_for_filenode(filenode):
644 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
644 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
645 return lexer
645 return lexer
646
646
647
647
648 def pygmentize(filenode, **kwargs):
648 def pygmentize(filenode, **kwargs):
649 """
649 """
650 pygmentize function using pygments
650 pygmentize function using pygments
651
651
652 :param filenode:
652 :param filenode:
653 """
653 """
654 lexer = get_lexer_for_filenode(filenode)
654 lexer = get_lexer_for_filenode(filenode)
655 return literal(code_highlight(filenode.content, lexer,
655 return literal(code_highlight(filenode.content, lexer,
656 CodeHtmlFormatter(**kwargs)))
656 CodeHtmlFormatter(**kwargs)))
657
657
658
658
659 def is_following_repo(repo_name, user_id):
659 def is_following_repo(repo_name, user_id):
660 from rhodecode.model.scm import ScmModel
660 from rhodecode.model.scm import ScmModel
661 return ScmModel().is_following_repo(repo_name, user_id)
661 return ScmModel().is_following_repo(repo_name, user_id)
662
662
663
663
664 class _Message(object):
664 class _Message(object):
665 """A message returned by ``Flash.pop_messages()``.
665 """A message returned by ``Flash.pop_messages()``.
666
666
667 Converting the message to a string returns the message text. Instances
667 Converting the message to a string returns the message text. Instances
668 also have the following attributes:
668 also have the following attributes:
669
669
670 * ``message``: the message text.
670 * ``message``: the message text.
671 * ``category``: the category specified when the message was created.
671 * ``category``: the category specified when the message was created.
672 """
672 """
673
673
674 def __init__(self, category, message, sub_data=None):
674 def __init__(self, category, message, sub_data=None):
675 self.category = category
675 self.category = category
676 self.message = message
676 self.message = message
677 self.sub_data = sub_data or {}
677 self.sub_data = sub_data or {}
678
678
679 def __str__(self):
679 def __str__(self):
680 return self.message
680 return self.message
681
681
682 __unicode__ = __str__
682 __unicode__ = __str__
683
683
684 def __html__(self):
684 def __html__(self):
685 return escape(safe_unicode(self.message))
685 return escape(safe_unicode(self.message))
686
686
687
687
688 class Flash(object):
688 class Flash(object):
689 # List of allowed categories. If None, allow any category.
689 # List of allowed categories. If None, allow any category.
690 categories = ["warning", "notice", "error", "success"]
690 categories = ["warning", "notice", "error", "success"]
691
691
692 # Default category if none is specified.
692 # Default category if none is specified.
693 default_category = "notice"
693 default_category = "notice"
694
694
695 def __init__(self, session_key="flash", categories=None,
695 def __init__(self, session_key="flash", categories=None,
696 default_category=None):
696 default_category=None):
697 """
697 """
698 Instantiate a ``Flash`` object.
698 Instantiate a ``Flash`` object.
699
699
700 ``session_key`` is the key to save the messages under in the user's
700 ``session_key`` is the key to save the messages under in the user's
701 session.
701 session.
702
702
703 ``categories`` is an optional list which overrides the default list
703 ``categories`` is an optional list which overrides the default list
704 of categories.
704 of categories.
705
705
706 ``default_category`` overrides the default category used for messages
706 ``default_category`` overrides the default category used for messages
707 when none is specified.
707 when none is specified.
708 """
708 """
709 self.session_key = session_key
709 self.session_key = session_key
710 if categories is not None:
710 if categories is not None:
711 self.categories = categories
711 self.categories = categories
712 if default_category is not None:
712 if default_category is not None:
713 self.default_category = default_category
713 self.default_category = default_category
714 if self.categories and self.default_category not in self.categories:
714 if self.categories and self.default_category not in self.categories:
715 raise ValueError(
715 raise ValueError(
716 "unrecognized default category %r" % (self.default_category,))
716 "unrecognized default category %r" % (self.default_category,))
717
717
718 def pop_messages(self, session=None, request=None):
718 def pop_messages(self, session=None, request=None):
719 """
719 """
720 Return all accumulated messages and delete them from the session.
720 Return all accumulated messages and delete them from the session.
721
721
722 The return value is a list of ``Message`` objects.
722 The return value is a list of ``Message`` objects.
723 """
723 """
724 messages = []
724 messages = []
725
725
726 if not session:
726 if not session:
727 if not request:
727 if not request:
728 request = get_current_request()
728 request = get_current_request()
729 session = request.session
729 session = request.session
730
730
731 # Pop the 'old' pylons flash messages. They are tuples of the form
731 # Pop the 'old' pylons flash messages. They are tuples of the form
732 # (category, message)
732 # (category, message)
733 for cat, msg in session.pop(self.session_key, []):
733 for cat, msg in session.pop(self.session_key, []):
734 messages.append(_Message(cat, msg))
734 messages.append(_Message(cat, msg))
735
735
736 # Pop the 'new' pyramid flash messages for each category as list
736 # Pop the 'new' pyramid flash messages for each category as list
737 # of strings.
737 # of strings.
738 for cat in self.categories:
738 for cat in self.categories:
739 for msg in session.pop_flash(queue=cat):
739 for msg in session.pop_flash(queue=cat):
740 sub_data = {}
740 sub_data = {}
741 if hasattr(msg, 'rsplit'):
741 if hasattr(msg, 'rsplit'):
742 flash_data = msg.rsplit('|DELIM|', 1)
742 flash_data = msg.rsplit('|DELIM|', 1)
743 org_message = flash_data[0]
743 org_message = flash_data[0]
744 if len(flash_data) > 1:
744 if len(flash_data) > 1:
745 sub_data = json.loads(flash_data[1])
745 sub_data = json.loads(flash_data[1])
746 else:
746 else:
747 org_message = msg
747 org_message = msg
748
748
749 messages.append(_Message(cat, org_message, sub_data=sub_data))
749 messages.append(_Message(cat, org_message, sub_data=sub_data))
750
750
751 # Map messages from the default queue to the 'notice' category.
751 # Map messages from the default queue to the 'notice' category.
752 for msg in session.pop_flash():
752 for msg in session.pop_flash():
753 messages.append(_Message('notice', msg))
753 messages.append(_Message('notice', msg))
754
754
755 session.save()
755 session.save()
756 return messages
756 return messages
757
757
758 def json_alerts(self, session=None, request=None):
758 def json_alerts(self, session=None, request=None):
759 payloads = []
759 payloads = []
760 messages = flash.pop_messages(session=session, request=request) or []
760 messages = flash.pop_messages(session=session, request=request) or []
761 for message in messages:
761 for message in messages:
762 payloads.append({
762 payloads.append({
763 'message': {
763 'message': {
764 'message': u'{}'.format(message.message),
764 'message': u'{}'.format(message.message),
765 'level': message.category,
765 'level': message.category,
766 'force': True,
766 'force': True,
767 'subdata': message.sub_data
767 'subdata': message.sub_data
768 }
768 }
769 })
769 })
770 return json.dumps(payloads)
770 return json.dumps(payloads)
771
771
772 def __call__(self, message, category=None, ignore_duplicate=True,
772 def __call__(self, message, category=None, ignore_duplicate=True,
773 session=None, request=None):
773 session=None, request=None):
774
774
775 if not session:
775 if not session:
776 if not request:
776 if not request:
777 request = get_current_request()
777 request = get_current_request()
778 session = request.session
778 session = request.session
779
779
780 session.flash(
780 session.flash(
781 message, queue=category, allow_duplicate=not ignore_duplicate)
781 message, queue=category, allow_duplicate=not ignore_duplicate)
782
782
783
783
784 flash = Flash()
784 flash = Flash()
785
785
786 #==============================================================================
786 #==============================================================================
787 # SCM FILTERS available via h.
787 # SCM FILTERS available via h.
788 #==============================================================================
788 #==============================================================================
789 from rhodecode.lib.vcs.utils import author_name, author_email
789 from rhodecode.lib.vcs.utils import author_name, author_email
790 from rhodecode.lib.utils2 import age, age_from_seconds
790 from rhodecode.lib.utils2 import age, age_from_seconds
791 from rhodecode.model.db import User, ChangesetStatus
791 from rhodecode.model.db import User, ChangesetStatus
792
792
793
793
794 email = author_email
794 email = author_email
795
795
796
796
797 def capitalize(raw_text):
797 def capitalize(raw_text):
798 return raw_text.capitalize()
798 return raw_text.capitalize()
799
799
800
800
801 def short_id(long_id):
801 def short_id(long_id):
802 return long_id[:12]
802 return long_id[:12]
803
803
804
804
805 def hide_credentials(url):
805 def hide_credentials(url):
806 from rhodecode.lib.utils2 import credentials_filter
806 from rhodecode.lib.utils2 import credentials_filter
807 return credentials_filter(url)
807 return credentials_filter(url)
808
808
809
809
810 import pytz
810 import pytz
811 import tzlocal
811 import tzlocal
812 local_timezone = tzlocal.get_localzone()
812 local_timezone = tzlocal.get_localzone()
813
813
814
814
815 def get_timezone(datetime_iso, time_is_local=False):
815 def get_timezone(datetime_iso, time_is_local=False):
816 tzinfo = '+00:00'
816 tzinfo = '+00:00'
817
817
818 # detect if we have a timezone info, otherwise, add it
818 # detect if we have a timezone info, otherwise, add it
819 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
820 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 force_timezone = os.environ.get('RC_TIMEZONE', '')
821 if force_timezone:
821 if force_timezone:
822 force_timezone = pytz.timezone(force_timezone)
822 force_timezone = pytz.timezone(force_timezone)
823 timezone = force_timezone or local_timezone
823 timezone = force_timezone or local_timezone
824 offset = timezone.localize(datetime_iso).strftime('%z')
824 offset = timezone.localize(datetime_iso).strftime('%z')
825 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
825 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
826 return tzinfo
826 return tzinfo
827
827
828
828
829 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
829 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
830 title = value or format_date(datetime_iso)
830 title = value or format_date(datetime_iso)
831 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
831 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
832
832
833 return literal(
833 return literal(
834 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
834 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
835 cls='tooltip' if tooltip else '',
835 cls='tooltip' if tooltip else '',
836 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
836 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
837 title=title, dt=datetime_iso, tzinfo=tzinfo
837 title=title, dt=datetime_iso, tzinfo=tzinfo
838 ))
838 ))
839
839
840
840
841 def _shorten_commit_id(commit_id, commit_len=None):
841 def _shorten_commit_id(commit_id, commit_len=None):
842 if commit_len is None:
842 if commit_len is None:
843 request = get_current_request()
843 request = get_current_request()
844 commit_len = request.call_context.visual.show_sha_length
844 commit_len = request.call_context.visual.show_sha_length
845 return commit_id[:commit_len]
845 return commit_id[:commit_len]
846
846
847
847
848 def show_id(commit, show_idx=None, commit_len=None):
848 def show_id(commit, show_idx=None, commit_len=None):
849 """
849 """
850 Configurable function that shows ID
850 Configurable function that shows ID
851 by default it's r123:fffeeefffeee
851 by default it's r123:fffeeefffeee
852
852
853 :param commit: commit instance
853 :param commit: commit instance
854 """
854 """
855 if show_idx is None:
855 if show_idx is None:
856 request = get_current_request()
856 request = get_current_request()
857 show_idx = request.call_context.visual.show_revision_number
857 show_idx = request.call_context.visual.show_revision_number
858
858
859 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
859 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
860 if show_idx:
860 if show_idx:
861 return 'r%s:%s' % (commit.idx, raw_id)
861 return 'r%s:%s' % (commit.idx, raw_id)
862 else:
862 else:
863 return '%s' % (raw_id, )
863 return '%s' % (raw_id, )
864
864
865
865
866 def format_date(date):
866 def format_date(date):
867 """
867 """
868 use a standardized formatting for dates used in RhodeCode
868 use a standardized formatting for dates used in RhodeCode
869
869
870 :param date: date/datetime object
870 :param date: date/datetime object
871 :return: formatted date
871 :return: formatted date
872 """
872 """
873
873
874 if date:
874 if date:
875 _fmt = "%a, %d %b %Y %H:%M:%S"
875 _fmt = "%a, %d %b %Y %H:%M:%S"
876 return safe_unicode(date.strftime(_fmt))
876 return safe_unicode(date.strftime(_fmt))
877
877
878 return u""
878 return u""
879
879
880
880
881 class _RepoChecker(object):
881 class _RepoChecker(object):
882
882
883 def __init__(self, backend_alias):
883 def __init__(self, backend_alias):
884 self._backend_alias = backend_alias
884 self._backend_alias = backend_alias
885
885
886 def __call__(self, repository):
886 def __call__(self, repository):
887 if hasattr(repository, 'alias'):
887 if hasattr(repository, 'alias'):
888 _type = repository.alias
888 _type = repository.alias
889 elif hasattr(repository, 'repo_type'):
889 elif hasattr(repository, 'repo_type'):
890 _type = repository.repo_type
890 _type = repository.repo_type
891 else:
891 else:
892 _type = repository
892 _type = repository
893 return _type == self._backend_alias
893 return _type == self._backend_alias
894
894
895
895
896 is_git = _RepoChecker('git')
896 is_git = _RepoChecker('git')
897 is_hg = _RepoChecker('hg')
897 is_hg = _RepoChecker('hg')
898 is_svn = _RepoChecker('svn')
898 is_svn = _RepoChecker('svn')
899
899
900
900
901 def get_repo_type_by_name(repo_name):
901 def get_repo_type_by_name(repo_name):
902 repo = Repository.get_by_repo_name(repo_name)
902 repo = Repository.get_by_repo_name(repo_name)
903 if repo:
903 if repo:
904 return repo.repo_type
904 return repo.repo_type
905
905
906
906
907 def is_svn_without_proxy(repository):
907 def is_svn_without_proxy(repository):
908 if is_svn(repository):
908 if is_svn(repository):
909 from rhodecode.model.settings import VcsSettingsModel
909 from rhodecode.model.settings import VcsSettingsModel
910 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
910 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
911 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
911 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
912 return False
912 return False
913
913
914
914
915 def discover_user(author):
915 def discover_user(author):
916 """
916 """
917 Tries to discover RhodeCode User based on the author string. Author string
917 Tries to discover RhodeCode User based on the author string. Author string
918 is typically `FirstName LastName <email@address.com>`
918 is typically `FirstName LastName <email@address.com>`
919 """
919 """
920
920
921 # if author is already an instance use it for extraction
921 # if author is already an instance use it for extraction
922 if isinstance(author, User):
922 if isinstance(author, User):
923 return author
923 return author
924
924
925 # Valid email in the attribute passed, see if they're in the system
925 # Valid email in the attribute passed, see if they're in the system
926 _email = author_email(author)
926 _email = author_email(author)
927 if _email != '':
927 if _email != '':
928 user = User.get_by_email(_email, case_insensitive=True, cache=True)
928 user = User.get_by_email(_email, case_insensitive=True, cache=True)
929 if user is not None:
929 if user is not None:
930 return user
930 return user
931
931
932 # Maybe it's a username, we try to extract it and fetch by username ?
932 # Maybe it's a username, we try to extract it and fetch by username ?
933 _author = author_name(author)
933 _author = author_name(author)
934 user = User.get_by_username(_author, case_insensitive=True, cache=True)
934 user = User.get_by_username(_author, case_insensitive=True, cache=True)
935 if user is not None:
935 if user is not None:
936 return user
936 return user
937
937
938 return None
938 return None
939
939
940
940
941 def email_or_none(author):
941 def email_or_none(author):
942 # extract email from the commit string
942 # extract email from the commit string
943 _email = author_email(author)
943 _email = author_email(author)
944
944
945 # If we have an email, use it, otherwise
945 # If we have an email, use it, otherwise
946 # see if it contains a username we can get an email from
946 # see if it contains a username we can get an email from
947 if _email != '':
947 if _email != '':
948 return _email
948 return _email
949 else:
949 else:
950 user = User.get_by_username(
950 user = User.get_by_username(
951 author_name(author), case_insensitive=True, cache=True)
951 author_name(author), case_insensitive=True, cache=True)
952
952
953 if user is not None:
953 if user is not None:
954 return user.email
954 return user.email
955
955
956 # No valid email, not a valid user in the system, none!
956 # No valid email, not a valid user in the system, none!
957 return None
957 return None
958
958
959
959
960 def link_to_user(author, length=0, **kwargs):
960 def link_to_user(author, length=0, **kwargs):
961 user = discover_user(author)
961 user = discover_user(author)
962 # user can be None, but if we have it already it means we can re-use it
962 # user can be None, but if we have it already it means we can re-use it
963 # in the person() function, so we save 1 intensive-query
963 # in the person() function, so we save 1 intensive-query
964 if user:
964 if user:
965 author = user
965 author = user
966
966
967 display_person = person(author, 'username_or_name_or_email')
967 display_person = person(author, 'username_or_name_or_email')
968 if length:
968 if length:
969 display_person = shorter(display_person, length)
969 display_person = shorter(display_person, length)
970
970
971 if user and user.username != user.DEFAULT_USER:
971 if user and user.username != user.DEFAULT_USER:
972 return link_to(
972 return link_to(
973 escape(display_person),
973 escape(display_person),
974 route_path('user_profile', username=user.username),
974 route_path('user_profile', username=user.username),
975 **kwargs)
975 **kwargs)
976 else:
976 else:
977 return escape(display_person)
977 return escape(display_person)
978
978
979
979
980 def link_to_group(users_group_name, **kwargs):
980 def link_to_group(users_group_name, **kwargs):
981 return link_to(
981 return link_to(
982 escape(users_group_name),
982 escape(users_group_name),
983 route_path('user_group_profile', user_group_name=users_group_name),
983 route_path('user_group_profile', user_group_name=users_group_name),
984 **kwargs)
984 **kwargs)
985
985
986
986
987 def person(author, show_attr="username_and_name"):
987 def person(author, show_attr="username_and_name"):
988 user = discover_user(author)
988 user = discover_user(author)
989 if user:
989 if user:
990 return getattr(user, show_attr)
990 return getattr(user, show_attr)
991 else:
991 else:
992 _author = author_name(author)
992 _author = author_name(author)
993 _email = email(author)
993 _email = email(author)
994 return _author or _email
994 return _author or _email
995
995
996
996
997 def author_string(email):
997 def author_string(email):
998 if email:
998 if email:
999 user = User.get_by_email(email, case_insensitive=True, cache=True)
999 user = User.get_by_email(email, case_insensitive=True, cache=True)
1000 if user:
1000 if user:
1001 if user.first_name or user.last_name:
1001 if user.first_name or user.last_name:
1002 return '%s %s &lt;%s&gt;' % (
1002 return '%s %s &lt;%s&gt;' % (
1003 user.first_name, user.last_name, email)
1003 user.first_name, user.last_name, email)
1004 else:
1004 else:
1005 return email
1005 return email
1006 else:
1006 else:
1007 return email
1007 return email
1008 else:
1008 else:
1009 return None
1009 return None
1010
1010
1011
1011
1012 def person_by_id(id_, show_attr="username_and_name"):
1012 def person_by_id(id_, show_attr="username_and_name"):
1013 # attr to return from fetched user
1013 # attr to return from fetched user
1014 person_getter = lambda usr: getattr(usr, show_attr)
1014 person_getter = lambda usr: getattr(usr, show_attr)
1015
1015
1016 #maybe it's an ID ?
1016 #maybe it's an ID ?
1017 if str(id_).isdigit() or isinstance(id_, int):
1017 if str(id_).isdigit() or isinstance(id_, int):
1018 id_ = int(id_)
1018 id_ = int(id_)
1019 user = User.get(id_)
1019 user = User.get(id_)
1020 if user is not None:
1020 if user is not None:
1021 return person_getter(user)
1021 return person_getter(user)
1022 return id_
1022 return id_
1023
1023
1024
1024
1025 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1025 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1026 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1026 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1027 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1027 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1028
1028
1029
1029
1030 tags_paterns = OrderedDict((
1030 tags_paterns = OrderedDict((
1031 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1031 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1032 '<div class="metatag" tag="lang">\\2</div>')),
1032 '<div class="metatag" tag="lang">\\2</div>')),
1033
1033
1034 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1035 '<div class="metatag" tag="see">see: \\1 </div>')),
1035 '<div class="metatag" tag="see">see: \\1 </div>')),
1036
1036
1037 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1037 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1038 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1038 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1039
1039
1040 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1040 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1041 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1041 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1042
1042
1043 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1043 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1044 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1044 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1045
1045
1046 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1046 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1047 '<div class="metatag" tag="state \\1">\\1</div>')),
1047 '<div class="metatag" tag="state \\1">\\1</div>')),
1048
1048
1049 # label in grey
1049 # label in grey
1050 ('label', (re.compile(r'\[([a-z]+)\]'),
1050 ('label', (re.compile(r'\[([a-z]+)\]'),
1051 '<div class="metatag" tag="label">\\1</div>')),
1051 '<div class="metatag" tag="label">\\1</div>')),
1052
1052
1053 # generic catch all in grey
1053 # generic catch all in grey
1054 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1054 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1055 '<div class="metatag" tag="generic">\\1</div>')),
1055 '<div class="metatag" tag="generic">\\1</div>')),
1056 ))
1056 ))
1057
1057
1058
1058
1059 def extract_metatags(value):
1059 def extract_metatags(value):
1060 """
1060 """
1061 Extract supported meta-tags from given text value
1061 Extract supported meta-tags from given text value
1062 """
1062 """
1063 tags = []
1063 tags = []
1064 if not value:
1064 if not value:
1065 return tags, ''
1065 return tags, ''
1066
1066
1067 for key, val in tags_paterns.items():
1067 for key, val in tags_paterns.items():
1068 pat, replace_html = val
1068 pat, replace_html = val
1069 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1069 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1070 value = pat.sub('', value)
1070 value = pat.sub('', value)
1071
1071
1072 return tags, value
1072 return tags, value
1073
1073
1074
1074
1075 def style_metatag(tag_type, value):
1075 def style_metatag(tag_type, value):
1076 """
1076 """
1077 converts tags from value into html equivalent
1077 converts tags from value into html equivalent
1078 """
1078 """
1079 if not value:
1079 if not value:
1080 return ''
1080 return ''
1081
1081
1082 html_value = value
1082 html_value = value
1083 tag_data = tags_paterns.get(tag_type)
1083 tag_data = tags_paterns.get(tag_type)
1084 if tag_data:
1084 if tag_data:
1085 pat, replace_html = tag_data
1085 pat, replace_html = tag_data
1086 # convert to plain `unicode` instead of a markup tag to be used in
1086 # convert to plain `unicode` instead of a markup tag to be used in
1087 # regex expressions. safe_unicode doesn't work here
1087 # regex expressions. safe_unicode doesn't work here
1088 html_value = pat.sub(replace_html, unicode(value))
1088 html_value = pat.sub(replace_html, unicode(value))
1089
1089
1090 return html_value
1090 return html_value
1091
1091
1092
1092
1093 def bool2icon(value, show_at_false=True):
1093 def bool2icon(value, show_at_false=True):
1094 """
1094 """
1095 Returns boolean value of a given value, represented as html element with
1095 Returns boolean value of a given value, represented as html element with
1096 classes that will represent icons
1096 classes that will represent icons
1097
1097
1098 :param value: given value to convert to html node
1098 :param value: given value to convert to html node
1099 """
1099 """
1100
1100
1101 if value: # does bool conversion
1101 if value: # does bool conversion
1102 return HTML.tag('i', class_="icon-true", title='True')
1102 return HTML.tag('i', class_="icon-true", title='True')
1103 else: # not true as bool
1103 else: # not true as bool
1104 if show_at_false:
1104 if show_at_false:
1105 return HTML.tag('i', class_="icon-false", title='False')
1105 return HTML.tag('i', class_="icon-false", title='False')
1106 return HTML.tag('i')
1106 return HTML.tag('i')
1107
1107
1108
1108
1109 def b64(inp):
1109 def b64(inp):
1110 return base64.b64encode(inp)
1110 return base64.b64encode(inp)
1111
1111
1112 #==============================================================================
1112 #==============================================================================
1113 # PERMS
1113 # PERMS
1114 #==============================================================================
1114 #==============================================================================
1115 from rhodecode.lib.auth import (
1115 from rhodecode.lib.auth import (
1116 HasPermissionAny, HasPermissionAll,
1116 HasPermissionAny, HasPermissionAll,
1117 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1117 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1118 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1118 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1119 csrf_token_key, AuthUser)
1119 csrf_token_key, AuthUser)
1120
1120
1121
1121
1122 #==============================================================================
1122 #==============================================================================
1123 # GRAVATAR URL
1123 # GRAVATAR URL
1124 #==============================================================================
1124 #==============================================================================
1125 class InitialsGravatar(object):
1125 class InitialsGravatar(object):
1126 def __init__(self, email_address, first_name, last_name, size=30,
1126 def __init__(self, email_address, first_name, last_name, size=30,
1127 background=None, text_color='#fff'):
1127 background=None, text_color='#fff'):
1128 self.size = size
1128 self.size = size
1129 self.first_name = first_name
1129 self.first_name = first_name
1130 self.last_name = last_name
1130 self.last_name = last_name
1131 self.email_address = email_address
1131 self.email_address = email_address
1132 self.background = background or self.str2color(email_address)
1132 self.background = background or self.str2color(email_address)
1133 self.text_color = text_color
1133 self.text_color = text_color
1134
1134
1135 def get_color_bank(self):
1135 def get_color_bank(self):
1136 """
1136 """
1137 returns a predefined list of colors that gravatars can use.
1137 returns a predefined list of colors that gravatars can use.
1138 Those are randomized distinct colors that guarantee readability and
1138 Those are randomized distinct colors that guarantee readability and
1139 uniqueness.
1139 uniqueness.
1140
1140
1141 generated with: http://phrogz.net/css/distinct-colors.html
1141 generated with: http://phrogz.net/css/distinct-colors.html
1142 """
1142 """
1143 return [
1143 return [
1144 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1144 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1145 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1145 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1146 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1146 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1147 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1147 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1148 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1148 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1149 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1149 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1150 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1150 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1151 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1151 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1152 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1152 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1153 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1153 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1154 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1154 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1155 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1155 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1156 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1156 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1157 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1157 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1158 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1158 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1159 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1159 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1160 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1160 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1161 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1161 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1162 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1162 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1163 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1163 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1164 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1164 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1165 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1165 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1166 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1166 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1167 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1167 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1168 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1168 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1169 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1169 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1170 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1170 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1171 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1171 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1172 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1172 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1173 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1173 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1174 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1174 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1175 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1175 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1176 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1176 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1177 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1177 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1178 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1178 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1179 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1179 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1180 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1180 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1181 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1181 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1182 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1182 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1183 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1183 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1184 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1184 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1185 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1185 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1186 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1186 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1187 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1187 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1188 '#4f8c46', '#368dd9', '#5c0073'
1188 '#4f8c46', '#368dd9', '#5c0073'
1189 ]
1189 ]
1190
1190
1191 def rgb_to_hex_color(self, rgb_tuple):
1191 def rgb_to_hex_color(self, rgb_tuple):
1192 """
1192 """
1193 Converts an rgb_tuple passed to an hex color.
1193 Converts an rgb_tuple passed to an hex color.
1194
1194
1195 :param rgb_tuple: tuple with 3 ints represents rgb color space
1195 :param rgb_tuple: tuple with 3 ints represents rgb color space
1196 """
1196 """
1197 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1197 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1198
1198
1199 def email_to_int_list(self, email_str):
1199 def email_to_int_list(self, email_str):
1200 """
1200 """
1201 Get every byte of the hex digest value of email and turn it to integer.
1201 Get every byte of the hex digest value of email and turn it to integer.
1202 It's going to be always between 0-255
1202 It's going to be always between 0-255
1203 """
1203 """
1204 digest = md5_safe(email_str.lower())
1204 digest = md5_safe(email_str.lower())
1205 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1205 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1206
1206
1207 def pick_color_bank_index(self, email_str, color_bank):
1207 def pick_color_bank_index(self, email_str, color_bank):
1208 return self.email_to_int_list(email_str)[0] % len(color_bank)
1208 return self.email_to_int_list(email_str)[0] % len(color_bank)
1209
1209
1210 def str2color(self, email_str):
1210 def str2color(self, email_str):
1211 """
1211 """
1212 Tries to map in a stable algorithm an email to color
1212 Tries to map in a stable algorithm an email to color
1213
1213
1214 :param email_str:
1214 :param email_str:
1215 """
1215 """
1216 color_bank = self.get_color_bank()
1216 color_bank = self.get_color_bank()
1217 # pick position (module it's length so we always find it in the
1217 # pick position (module it's length so we always find it in the
1218 # bank even if it's smaller than 256 values
1218 # bank even if it's smaller than 256 values
1219 pos = self.pick_color_bank_index(email_str, color_bank)
1219 pos = self.pick_color_bank_index(email_str, color_bank)
1220 return color_bank[pos]
1220 return color_bank[pos]
1221
1221
1222 def normalize_email(self, email_address):
1222 def normalize_email(self, email_address):
1223 import unicodedata
1223 import unicodedata
1224 # default host used to fill in the fake/missing email
1224 # default host used to fill in the fake/missing email
1225 default_host = u'localhost'
1225 default_host = u'localhost'
1226
1226
1227 if not email_address:
1227 if not email_address:
1228 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1228 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1229
1229
1230 email_address = safe_unicode(email_address)
1230 email_address = safe_unicode(email_address)
1231
1231
1232 if u'@' not in email_address:
1232 if u'@' not in email_address:
1233 email_address = u'%s@%s' % (email_address, default_host)
1233 email_address = u'%s@%s' % (email_address, default_host)
1234
1234
1235 if email_address.endswith(u'@'):
1235 if email_address.endswith(u'@'):
1236 email_address = u'%s%s' % (email_address, default_host)
1236 email_address = u'%s%s' % (email_address, default_host)
1237
1237
1238 email_address = unicodedata.normalize('NFKD', email_address)\
1238 email_address = unicodedata.normalize('NFKD', email_address)\
1239 .encode('ascii', 'ignore')
1239 .encode('ascii', 'ignore')
1240 return email_address
1240 return email_address
1241
1241
1242 def get_initials(self):
1242 def get_initials(self):
1243 """
1243 """
1244 Returns 2 letter initials calculated based on the input.
1244 Returns 2 letter initials calculated based on the input.
1245 The algorithm picks first given email address, and takes first letter
1245 The algorithm picks first given email address, and takes first letter
1246 of part before @, and then the first letter of server name. In case
1246 of part before @, and then the first letter of server name. In case
1247 the part before @ is in a format of `somestring.somestring2` it replaces
1247 the part before @ is in a format of `somestring.somestring2` it replaces
1248 the server letter with first letter of somestring2
1248 the server letter with first letter of somestring2
1249
1249
1250 In case function was initialized with both first and lastname, this
1250 In case function was initialized with both first and lastname, this
1251 overrides the extraction from email by first letter of the first and
1251 overrides the extraction from email by first letter of the first and
1252 last name. We add special logic to that functionality, In case Full name
1252 last name. We add special logic to that functionality, In case Full name
1253 is compound, like Guido Von Rossum, we use last part of the last name
1253 is compound, like Guido Von Rossum, we use last part of the last name
1254 (Von Rossum) picking `R`.
1254 (Von Rossum) picking `R`.
1255
1255
1256 Function also normalizes the non-ascii characters to they ascii
1256 Function also normalizes the non-ascii characters to they ascii
1257 representation, eg Δ„ => A
1257 representation, eg Δ„ => A
1258 """
1258 """
1259 import unicodedata
1259 import unicodedata
1260 # replace non-ascii to ascii
1260 # replace non-ascii to ascii
1261 first_name = unicodedata.normalize(
1261 first_name = unicodedata.normalize(
1262 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1262 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1263 last_name = unicodedata.normalize(
1263 last_name = unicodedata.normalize(
1264 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1264 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1265
1265
1266 # do NFKD encoding, and also make sure email has proper format
1266 # do NFKD encoding, and also make sure email has proper format
1267 email_address = self.normalize_email(self.email_address)
1267 email_address = self.normalize_email(self.email_address)
1268
1268
1269 # first push the email initials
1269 # first push the email initials
1270 prefix, server = email_address.split('@', 1)
1270 prefix, server = email_address.split('@', 1)
1271
1271
1272 # check if prefix is maybe a 'first_name.last_name' syntax
1272 # check if prefix is maybe a 'first_name.last_name' syntax
1273 _dot_split = prefix.rsplit('.', 1)
1273 _dot_split = prefix.rsplit('.', 1)
1274 if len(_dot_split) == 2 and _dot_split[1]:
1274 if len(_dot_split) == 2 and _dot_split[1]:
1275 initials = [_dot_split[0][0], _dot_split[1][0]]
1275 initials = [_dot_split[0][0], _dot_split[1][0]]
1276 else:
1276 else:
1277 initials = [prefix[0], server[0]]
1277 initials = [prefix[0], server[0]]
1278
1278
1279 # then try to replace either first_name or last_name
1279 # then try to replace either first_name or last_name
1280 fn_letter = (first_name or " ")[0].strip()
1280 fn_letter = (first_name or " ")[0].strip()
1281 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1281 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1282
1282
1283 if fn_letter:
1283 if fn_letter:
1284 initials[0] = fn_letter
1284 initials[0] = fn_letter
1285
1285
1286 if ln_letter:
1286 if ln_letter:
1287 initials[1] = ln_letter
1287 initials[1] = ln_letter
1288
1288
1289 return ''.join(initials).upper()
1289 return ''.join(initials).upper()
1290
1290
1291 def get_img_data_by_type(self, font_family, img_type):
1291 def get_img_data_by_type(self, font_family, img_type):
1292 default_user = """
1292 default_user = """
1293 <svg xmlns="http://www.w3.org/2000/svg"
1293 <svg xmlns="http://www.w3.org/2000/svg"
1294 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1294 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1295 viewBox="-15 -10 439.165 429.164"
1295 viewBox="-15 -10 439.165 429.164"
1296
1296
1297 xml:space="preserve"
1297 xml:space="preserve"
1298 style="background:{background};" >
1298 style="background:{background};" >
1299
1299
1300 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1300 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1301 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1301 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1302 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1302 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1303 168.596,153.916,216.671,
1303 168.596,153.916,216.671,
1304 204.583,216.671z" fill="{text_color}"/>
1304 204.583,216.671z" fill="{text_color}"/>
1305 <path d="M407.164,374.717L360.88,
1305 <path d="M407.164,374.717L360.88,
1306 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1306 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1307 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1307 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1308 15.366-44.203,23.488-69.076,23.488c-24.877,
1308 15.366-44.203,23.488-69.076,23.488c-24.877,
1309 0-48.762-8.122-69.078-23.488
1309 0-48.762-8.122-69.078-23.488
1310 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1310 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1311 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1311 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1312 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1312 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1313 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1313 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1314 19.402-10.527 C409.699,390.129,
1314 19.402-10.527 C409.699,390.129,
1315 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1315 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1316 </svg>""".format(
1316 </svg>""".format(
1317 size=self.size,
1317 size=self.size,
1318 background='#979797', # @grey4
1318 background='#979797', # @grey4
1319 text_color=self.text_color,
1319 text_color=self.text_color,
1320 font_family=font_family)
1320 font_family=font_family)
1321
1321
1322 return {
1322 return {
1323 "default_user": default_user
1323 "default_user": default_user
1324 }[img_type]
1324 }[img_type]
1325
1325
1326 def get_img_data(self, svg_type=None):
1326 def get_img_data(self, svg_type=None):
1327 """
1327 """
1328 generates the svg metadata for image
1328 generates the svg metadata for image
1329 """
1329 """
1330 fonts = [
1330 fonts = [
1331 '-apple-system',
1331 '-apple-system',
1332 'BlinkMacSystemFont',
1332 'BlinkMacSystemFont',
1333 'Segoe UI',
1333 'Segoe UI',
1334 'Roboto',
1334 'Roboto',
1335 'Oxygen-Sans',
1335 'Oxygen-Sans',
1336 'Ubuntu',
1336 'Ubuntu',
1337 'Cantarell',
1337 'Cantarell',
1338 'Helvetica Neue',
1338 'Helvetica Neue',
1339 'sans-serif'
1339 'sans-serif'
1340 ]
1340 ]
1341 font_family = ','.join(fonts)
1341 font_family = ','.join(fonts)
1342 if svg_type:
1342 if svg_type:
1343 return self.get_img_data_by_type(font_family, svg_type)
1343 return self.get_img_data_by_type(font_family, svg_type)
1344
1344
1345 initials = self.get_initials()
1345 initials = self.get_initials()
1346 img_data = """
1346 img_data = """
1347 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1347 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1348 width="{size}" height="{size}"
1348 width="{size}" height="{size}"
1349 style="width: 100%; height: 100%; background-color: {background}"
1349 style="width: 100%; height: 100%; background-color: {background}"
1350 viewBox="0 0 {size} {size}">
1350 viewBox="0 0 {size} {size}">
1351 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1351 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1352 pointer-events="auto" fill="{text_color}"
1352 pointer-events="auto" fill="{text_color}"
1353 font-family="{font_family}"
1353 font-family="{font_family}"
1354 style="font-weight: 400; font-size: {f_size}px;">{text}
1354 style="font-weight: 400; font-size: {f_size}px;">{text}
1355 </text>
1355 </text>
1356 </svg>""".format(
1356 </svg>""".format(
1357 size=self.size,
1357 size=self.size,
1358 f_size=self.size/2.05, # scale the text inside the box nicely
1358 f_size=self.size/2.05, # scale the text inside the box nicely
1359 background=self.background,
1359 background=self.background,
1360 text_color=self.text_color,
1360 text_color=self.text_color,
1361 text=initials.upper(),
1361 text=initials.upper(),
1362 font_family=font_family)
1362 font_family=font_family)
1363
1363
1364 return img_data
1364 return img_data
1365
1365
1366 def generate_svg(self, svg_type=None):
1366 def generate_svg(self, svg_type=None):
1367 img_data = self.get_img_data(svg_type)
1367 img_data = self.get_img_data(svg_type)
1368 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1368 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1369
1369
1370
1370
1371 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1371 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1372
1372
1373 svg_type = None
1373 svg_type = None
1374 if email_address == User.DEFAULT_USER_EMAIL:
1374 if email_address == User.DEFAULT_USER_EMAIL:
1375 svg_type = 'default_user'
1375 svg_type = 'default_user'
1376
1376
1377 klass = InitialsGravatar(email_address, first_name, last_name, size)
1377 klass = InitialsGravatar(email_address, first_name, last_name, size)
1378
1378
1379 if store_on_disk:
1379 if store_on_disk:
1380 from rhodecode.apps.file_store import utils as store_utils
1380 from rhodecode.apps.file_store import utils as store_utils
1381 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1381 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1382 FileOverSizeException
1382 FileOverSizeException
1383 from rhodecode.model.db import Session
1383 from rhodecode.model.db import Session
1384
1384
1385 image_key = md5_safe(email_address.lower()
1385 image_key = md5_safe(email_address.lower()
1386 + first_name.lower() + last_name.lower())
1386 + first_name.lower() + last_name.lower())
1387
1387
1388 storage = store_utils.get_file_storage(request.registry.settings)
1388 storage = store_utils.get_file_storage(request.registry.settings)
1389 filename = '{}.svg'.format(image_key)
1389 filename = '{}.svg'.format(image_key)
1390 subdir = 'gravatars'
1390 subdir = 'gravatars'
1391 # since final name has a counter, we apply the 0
1391 # since final name has a counter, we apply the 0
1392 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1392 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1393 store_uid = os.path.join(subdir, uid)
1393 store_uid = os.path.join(subdir, uid)
1394
1394
1395 db_entry = FileStore.get_by_store_uid(store_uid)
1395 db_entry = FileStore.get_by_store_uid(store_uid)
1396 if db_entry:
1396 if db_entry:
1397 return request.route_path('download_file', fid=store_uid)
1397 return request.route_path('download_file', fid=store_uid)
1398
1398
1399 img_data = klass.get_img_data(svg_type=svg_type)
1399 img_data = klass.get_img_data(svg_type=svg_type)
1400 img_file = store_utils.bytes_to_file_obj(img_data)
1400 img_file = store_utils.bytes_to_file_obj(img_data)
1401
1401
1402 try:
1402 try:
1403 store_uid, metadata = storage.save_file(
1403 store_uid, metadata = storage.save_file(
1404 img_file, filename, directory=subdir,
1404 img_file, filename, directory=subdir,
1405 extensions=['.svg'], randomized_name=False)
1405 extensions=['.svg'], randomized_name=False)
1406 except (FileNotAllowedException, FileOverSizeException):
1406 except (FileNotAllowedException, FileOverSizeException):
1407 raise
1407 raise
1408
1408
1409 try:
1409 try:
1410 entry = FileStore.create(
1410 entry = FileStore.create(
1411 file_uid=store_uid, filename=metadata["filename"],
1411 file_uid=store_uid, filename=metadata["filename"],
1412 file_hash=metadata["sha256"], file_size=metadata["size"],
1412 file_hash=metadata["sha256"], file_size=metadata["size"],
1413 file_display_name=filename,
1413 file_display_name=filename,
1414 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1414 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1415 hidden=True, check_acl=False, user_id=1
1415 hidden=True, check_acl=False, user_id=1
1416 )
1416 )
1417 Session().add(entry)
1417 Session().add(entry)
1418 Session().commit()
1418 Session().commit()
1419 log.debug('Stored upload in DB as %s', entry)
1419 log.debug('Stored upload in DB as %s', entry)
1420 except Exception:
1420 except Exception:
1421 raise
1421 raise
1422
1422
1423 return request.route_path('download_file', fid=store_uid)
1423 return request.route_path('download_file', fid=store_uid)
1424
1424
1425 else:
1425 else:
1426 return klass.generate_svg(svg_type=svg_type)
1426 return klass.generate_svg(svg_type=svg_type)
1427
1427
1428
1428
1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1429 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1430 return safe_str(gravatar_url_tmpl)\
1430 return safe_str(gravatar_url_tmpl)\
1431 .replace('{email}', email_address) \
1431 .replace('{email}', email_address) \
1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1432 .replace('{md5email}', md5_safe(email_address.lower())) \
1433 .replace('{netloc}', request.host) \
1433 .replace('{netloc}', request.host) \
1434 .replace('{scheme}', request.scheme) \
1434 .replace('{scheme}', request.scheme) \
1435 .replace('{size}', safe_str(size))
1435 .replace('{size}', safe_str(size))
1436
1436
1437
1437
1438 def gravatar_url(email_address, size=30, request=None):
1438 def gravatar_url(email_address, size=30, request=None):
1439 request = request or get_current_request()
1439 request = request or get_current_request()
1440 _use_gravatar = request.call_context.visual.use_gravatar
1440 _use_gravatar = request.call_context.visual.use_gravatar
1441
1441
1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1442 email_address = email_address or User.DEFAULT_USER_EMAIL
1443 if isinstance(email_address, unicode):
1443 if isinstance(email_address, unicode):
1444 # hashlib crashes on unicode items
1444 # hashlib crashes on unicode items
1445 email_address = safe_str(email_address)
1445 email_address = safe_str(email_address)
1446
1446
1447 # empty email or default user
1447 # empty email or default user
1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1448 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1449 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1450
1450
1451 if _use_gravatar:
1451 if _use_gravatar:
1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1452 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1453 or User.DEFAULT_GRAVATAR_URL
1453 or User.DEFAULT_GRAVATAR_URL
1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1454 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1455
1455
1456 else:
1456 else:
1457 return initials_gravatar(request, email_address, '', '', size=size)
1457 return initials_gravatar(request, email_address, '', '', size=size)
1458
1458
1459
1459
1460 def breadcrumb_repo_link(repo):
1460 def breadcrumb_repo_link(repo):
1461 """
1461 """
1462 Makes a breadcrumbs path link to repo
1462 Makes a breadcrumbs path link to repo
1463
1463
1464 ex::
1464 ex::
1465 group >> subgroup >> repo
1465 group >> subgroup >> repo
1466
1466
1467 :param repo: a Repository instance
1467 :param repo: a Repository instance
1468 """
1468 """
1469
1469
1470 path = [
1470 path = [
1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1471 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1472 title='last change:{}'.format(format_date(group.last_commit_change)))
1473 for group in repo.groups_with_parents
1473 for group in repo.groups_with_parents
1474 ] + [
1474 ] + [
1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1475 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1476 title='last change:{}'.format(format_date(repo.last_commit_change)))
1477 ]
1477 ]
1478
1478
1479 return literal(' &raquo; '.join(path))
1479 return literal(' &raquo; '.join(path))
1480
1480
1481
1481
1482 def breadcrumb_repo_group_link(repo_group):
1482 def breadcrumb_repo_group_link(repo_group):
1483 """
1483 """
1484 Makes a breadcrumbs path link to repo
1484 Makes a breadcrumbs path link to repo
1485
1485
1486 ex::
1486 ex::
1487 group >> subgroup
1487 group >> subgroup
1488
1488
1489 :param repo_group: a Repository Group instance
1489 :param repo_group: a Repository Group instance
1490 """
1490 """
1491
1491
1492 path = [
1492 path = [
1493 link_to(group.name,
1493 link_to(group.name,
1494 route_path('repo_group_home', repo_group_name=group.group_name),
1494 route_path('repo_group_home', repo_group_name=group.group_name),
1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1495 title='last change:{}'.format(format_date(group.last_commit_change)))
1496 for group in repo_group.parents
1496 for group in repo_group.parents
1497 ] + [
1497 ] + [
1498 link_to(repo_group.name,
1498 link_to(repo_group.name,
1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1499 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1500 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1501 ]
1501 ]
1502
1502
1503 return literal(' &raquo; '.join(path))
1503 return literal(' &raquo; '.join(path))
1504
1504
1505
1505
1506 def format_byte_size_binary(file_size):
1506 def format_byte_size_binary(file_size):
1507 """
1507 """
1508 Formats file/folder sizes to standard.
1508 Formats file/folder sizes to standard.
1509 """
1509 """
1510 if file_size is None:
1510 if file_size is None:
1511 file_size = 0
1511 file_size = 0
1512
1512
1513 formatted_size = format_byte_size(file_size, binary=True)
1513 formatted_size = format_byte_size(file_size, binary=True)
1514 return formatted_size
1514 return formatted_size
1515
1515
1516
1516
1517 def urlify_text(text_, safe=True, **href_attrs):
1517 def urlify_text(text_, safe=True, **href_attrs):
1518 """
1518 """
1519 Extract urls from text and make html links out of them
1519 Extract urls from text and make html links out of them
1520 """
1520 """
1521
1521
1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1522 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1523 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1523 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1524
1524
1525 def url_func(match_obj):
1525 def url_func(match_obj):
1526 url_full = match_obj.groups()[0]
1526 url_full = match_obj.groups()[0]
1527 a_options = dict(href_attrs)
1527 a_options = dict(href_attrs)
1528 a_options['href'] = url_full
1528 a_options['href'] = url_full
1529 a_text = url_full
1529 a_text = url_full
1530 return HTML.tag("a", a_text, **a_options)
1530 return HTML.tag("a", a_text, **a_options)
1531
1531
1532 _new_text = url_pat.sub(url_func, text_)
1532 _new_text = url_pat.sub(url_func, text_)
1533
1533
1534 if safe:
1534 if safe:
1535 return literal(_new_text)
1535 return literal(_new_text)
1536 return _new_text
1536 return _new_text
1537
1537
1538
1538
1539 def urlify_commits(text_, repo_name):
1539 def urlify_commits(text_, repo_name):
1540 """
1540 """
1541 Extract commit ids from text and make link from them
1541 Extract commit ids from text and make link from them
1542
1542
1543 :param text_:
1543 :param text_:
1544 :param repo_name: repo name to build the URL with
1544 :param repo_name: repo name to build the URL with
1545 """
1545 """
1546
1546
1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1547 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1548
1548
1549 def url_func(match_obj):
1549 def url_func(match_obj):
1550 commit_id = match_obj.groups()[1]
1550 commit_id = match_obj.groups()[1]
1551 pref = match_obj.groups()[0]
1551 pref = match_obj.groups()[0]
1552 suf = match_obj.groups()[2]
1552 suf = match_obj.groups()[2]
1553
1553
1554 tmpl = (
1554 tmpl = (
1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1555 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1556 '%(commit_id)s</a>%(suf)s'
1556 '%(commit_id)s</a>%(suf)s'
1557 )
1557 )
1558 return tmpl % {
1558 return tmpl % {
1559 'pref': pref,
1559 'pref': pref,
1560 'cls': 'revision-link',
1560 'cls': 'revision-link',
1561 'url': route_url(
1561 'url': route_url(
1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1562 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1563 'commit_id': commit_id,
1563 'commit_id': commit_id,
1564 'suf': suf,
1564 'suf': suf,
1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1565 'hovercard_alt': 'Commit: {}'.format(commit_id),
1566 'hovercard_url': route_url(
1566 'hovercard_url': route_url(
1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1567 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1568 }
1568 }
1569
1569
1570 new_text = url_pat.sub(url_func, text_)
1570 new_text = url_pat.sub(url_func, text_)
1571
1571
1572 return new_text
1572 return new_text
1573
1573
1574
1574
1575 def _process_url_func(match_obj, repo_name, uid, entry,
1575 def _process_url_func(match_obj, repo_name, uid, entry,
1576 return_raw_data=False, link_format='html'):
1576 return_raw_data=False, link_format='html'):
1577 pref = ''
1577 pref = ''
1578 if match_obj.group().startswith(' '):
1578 if match_obj.group().startswith(' '):
1579 pref = ' '
1579 pref = ' '
1580
1580
1581 issue_id = ''.join(match_obj.groups())
1581 issue_id = ''.join(match_obj.groups())
1582
1582
1583 if link_format == 'html':
1583 if link_format == 'html':
1584 tmpl = (
1584 tmpl = (
1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1585 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1586 '%(issue-prefix)s%(id-repr)s'
1586 '%(issue-prefix)s%(id-repr)s'
1587 '</a>')
1587 '</a>')
1588 elif link_format == 'html+hovercard':
1588 elif link_format == 'html+hovercard':
1589 tmpl = (
1589 tmpl = (
1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1590 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1591 '%(issue-prefix)s%(id-repr)s'
1591 '%(issue-prefix)s%(id-repr)s'
1592 '</a>')
1592 '</a>')
1593 elif link_format in ['rst', 'rst+hovercard']:
1593 elif link_format in ['rst', 'rst+hovercard']:
1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1594 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1595 elif link_format in ['markdown', 'markdown+hovercard']:
1595 elif link_format in ['markdown', 'markdown+hovercard']:
1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1596 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1597 else:
1597 else:
1598 raise ValueError('Bad link_format:{}'.format(link_format))
1598 raise ValueError('Bad link_format:{}'.format(link_format))
1599
1599
1600 (repo_name_cleaned,
1600 (repo_name_cleaned,
1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1601 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1602
1602
1603 # variables replacement
1603 # variables replacement
1604 named_vars = {
1604 named_vars = {
1605 'id': issue_id,
1605 'id': issue_id,
1606 'repo': repo_name,
1606 'repo': repo_name,
1607 'repo_name': repo_name_cleaned,
1607 'repo_name': repo_name_cleaned,
1608 'group_name': parent_group_name,
1608 'group_name': parent_group_name,
1609 # set dummy keys so we always have them
1609 # set dummy keys so we always have them
1610 'hostname': '',
1610 'hostname': '',
1611 'netloc': '',
1611 'netloc': '',
1612 'scheme': ''
1612 'scheme': ''
1613 }
1613 }
1614
1614
1615 request = get_current_request()
1615 request = get_current_request()
1616 if request:
1616 if request:
1617 # exposes, hostname, netloc, scheme
1617 # exposes, hostname, netloc, scheme
1618 host_data = get_host_info(request)
1618 host_data = get_host_info(request)
1619 named_vars.update(host_data)
1619 named_vars.update(host_data)
1620
1620
1621 # named regex variables
1621 # named regex variables
1622 named_vars.update(match_obj.groupdict())
1622 named_vars.update(match_obj.groupdict())
1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1623 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1624 desc = string.Template(escape(entry['desc'])).safe_substitute(**named_vars)
1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1625 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1626
1626
1627 def quote_cleaner(input_str):
1627 def quote_cleaner(input_str):
1628 """Remove quotes as it's HTML"""
1628 """Remove quotes as it's HTML"""
1629 return input_str.replace('"', '')
1629 return input_str.replace('"', '')
1630
1630
1631 data = {
1631 data = {
1632 'pref': pref,
1632 'pref': pref,
1633 'cls': quote_cleaner('issue-tracker-link'),
1633 'cls': quote_cleaner('issue-tracker-link'),
1634 'url': quote_cleaner(_url),
1634 'url': quote_cleaner(_url),
1635 'id-repr': issue_id,
1635 'id-repr': issue_id,
1636 'issue-prefix': entry['pref'],
1636 'issue-prefix': entry['pref'],
1637 'serv': entry['url'],
1637 'serv': entry['url'],
1638 'title': bleach.clean(desc, strip=True),
1638 'title': bleach.clean(desc, strip=True),
1639 'hovercard_url': hovercard_url
1639 'hovercard_url': hovercard_url
1640 }
1640 }
1641
1641
1642 if return_raw_data:
1642 if return_raw_data:
1643 return {
1643 return {
1644 'id': issue_id,
1644 'id': issue_id,
1645 'url': _url
1645 'url': _url
1646 }
1646 }
1647 return tmpl % data
1647 return tmpl % data
1648
1648
1649
1649
1650 def get_active_pattern_entries(repo_name):
1650 def get_active_pattern_entries(repo_name):
1651 repo = None
1651 repo = None
1652 if repo_name:
1652 if repo_name:
1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1653 # Retrieving repo_name to avoid invalid repo_name to explode on
1654 # IssueTrackerSettingsModel but still passing invalid name further down
1654 # IssueTrackerSettingsModel but still passing invalid name further down
1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1655 repo = Repository.get_by_repo_name(repo_name, cache=True)
1656
1656
1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1657 settings_model = IssueTrackerSettingsModel(repo=repo)
1658 active_entries = settings_model.get_settings(cache=True)
1658 active_entries = settings_model.get_settings(cache=True)
1659 return active_entries
1659 return active_entries
1660
1660
1661
1661
1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1662 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1663
1663
1664 allowed_link_formats = [
1664 allowed_link_formats = [
1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1665 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1666
1666
1667
1667
1668 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1668 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1669
1669
1670 if link_format not in allowed_link_formats:
1670 if link_format not in allowed_link_formats:
1671 raise ValueError('Link format can be only one of:{} got {}'.format(
1671 raise ValueError('Link format can be only one of:{} got {}'.format(
1672 allowed_link_formats, link_format))
1672 allowed_link_formats, link_format))
1673
1673
1674 if active_entries is None:
1674 if active_entries is None:
1675 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1675 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1676 active_entries = get_active_pattern_entries(repo_name)
1676 active_entries = get_active_pattern_entries(repo_name)
1677
1677
1678 issues_data = []
1678 issues_data = []
1679 errors = []
1679 errors = []
1680 new_text = text_string
1680 new_text = text_string
1681
1681
1682 log.debug('Got %s entries to process', len(active_entries))
1682 log.debug('Got %s pattern entries to process', len(active_entries))
1683 for uid, entry in active_entries.items():
1683 for uid, entry in active_entries.items():
1684 log.debug('found issue tracker entry with uid %s', uid)
1685
1684
1686 if not (entry['pat'] and entry['url']):
1685 if not (entry['pat'] and entry['url']):
1687 log.debug('skipping due to missing data')
1686 log.debug('skipping due to missing data')
1688 continue
1687 continue
1689
1688
1690 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1689 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1691 uid, entry['pat'], entry['url'], entry['pref'])
1690 uid, entry['pat'], entry['url'], entry['pref'])
1692
1691
1693 if entry.get('pat_compiled'):
1692 if entry.get('pat_compiled'):
1694 pattern = entry['pat_compiled']
1693 pattern = entry['pat_compiled']
1695 else:
1694 else:
1696 try:
1695 try:
1697 pattern = regex.compile(r'%s' % entry['pat'])
1696 pattern = regex.compile(r'%s' % entry['pat'])
1698 except regex.error as e:
1697 except regex.error as e:
1699 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1700 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1701 errors.append(regex_err)
1700 errors.append(regex_err)
1702 continue
1701 continue
1703
1702
1704 data_func = partial(
1703 data_func = partial(
1705 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1704 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1706 return_raw_data=True)
1705 return_raw_data=True)
1707
1706
1708 for match_obj in pattern.finditer(text_string):
1707 for match_obj in pattern.finditer(text_string):
1709 issues_data.append(data_func(match_obj))
1708 issues_data.append(data_func(match_obj))
1710
1709
1711 url_func = partial(
1710 url_func = partial(
1712 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1711 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1713 link_format=link_format)
1712 link_format=link_format)
1714
1713
1715 new_text = pattern.sub(url_func, new_text)
1714 new_text = pattern.sub(url_func, new_text)
1716 log.debug('processed prefix:uid `%s`', uid)
1715 log.debug('processed prefix:uid `%s`', uid)
1717
1716
1718 # finally use global replace, eg !123 -> pr-link, those will not catch
1717 # finally use global replace, eg !123 -> pr-link, those will not catch
1719 # if already similar pattern exists
1718 # if already similar pattern exists
1720 server_url = '${scheme}://${netloc}'
1719 server_url = '${scheme}://${netloc}'
1721 pr_entry = {
1720 pr_entry = {
1722 'pref': '!',
1721 'pref': '!',
1723 'url': server_url + '/_admin/pull-requests/${id}',
1722 'url': server_url + '/_admin/pull-requests/${id}',
1724 'desc': 'Pull Request !${id}',
1723 'desc': 'Pull Request !${id}',
1725 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1724 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1726 }
1725 }
1727 pr_url_func = partial(
1726 pr_url_func = partial(
1728 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1727 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1729 link_format=link_format+'+hovercard')
1728 link_format=link_format+'+hovercard')
1730 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1731 log.debug('processed !pr pattern')
1730 log.debug('processed !pr pattern')
1732
1731
1733 return new_text, issues_data, errors
1732 return new_text, issues_data, errors
1734
1733
1735
1734
1736 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1737 issues_container_callback=None, error_container=None):
1736 issues_container_callback=None, error_container=None):
1738 """
1737 """
1739 Parses given text message and makes proper links.
1738 Parses given text message and makes proper links.
1740 issues are linked to given issue-server, and rest is a commit link
1739 issues are linked to given issue-server, and rest is a commit link
1741 """
1740 """
1742
1741
1743 def escaper(_text):
1742 def escaper(_text):
1744 return _text.replace('<', '&lt;').replace('>', '&gt;')
1743 return _text.replace('<', '&lt;').replace('>', '&gt;')
1745
1744
1746 new_text = escaper(commit_text)
1745 new_text = escaper(commit_text)
1747
1746
1748 # extract http/https links and make them real urls
1747 # extract http/https links and make them real urls
1749 new_text = urlify_text(new_text, safe=False)
1748 new_text = urlify_text(new_text, safe=False)
1750
1749
1751 # urlify commits - extract commit ids and make link out of them, if we have
1750 # urlify commits - extract commit ids and make link out of them, if we have
1752 # the scope of repository present.
1751 # the scope of repository present.
1753 if repository:
1752 if repository:
1754 new_text = urlify_commits(new_text, repository)
1753 new_text = urlify_commits(new_text, repository)
1755
1754
1756 # process issue tracker patterns
1755 # process issue tracker patterns
1757 new_text, issues, errors = process_patterns(
1756 new_text, issues, errors = process_patterns(
1758 new_text, repository or '', active_entries=active_pattern_entries)
1757 new_text, repository or '', active_entries=active_pattern_entries)
1759
1758
1760 if issues_container_callback is not None:
1759 if issues_container_callback is not None:
1761 for issue in issues:
1760 for issue in issues:
1762 issues_container_callback(issue)
1761 issues_container_callback(issue)
1763
1762
1764 if error_container is not None:
1763 if error_container is not None:
1765 error_container.extend(errors)
1764 error_container.extend(errors)
1766
1765
1767 return literal(new_text)
1766 return literal(new_text)
1768
1767
1769
1768
1770 def render_binary(repo_name, file_obj):
1769 def render_binary(repo_name, file_obj):
1771 """
1770 """
1772 Choose how to render a binary file
1771 Choose how to render a binary file
1773 """
1772 """
1774
1773
1775 # unicode
1774 # unicode
1776 filename = file_obj.name
1775 filename = file_obj.name
1777
1776
1778 # images
1777 # images
1779 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1778 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1780 if fnmatch.fnmatch(filename, pat=ext):
1779 if fnmatch.fnmatch(filename, pat=ext):
1781 src = route_path(
1780 src = route_path(
1782 'repo_file_raw', repo_name=repo_name,
1781 'repo_file_raw', repo_name=repo_name,
1783 commit_id=file_obj.commit.raw_id,
1782 commit_id=file_obj.commit.raw_id,
1784 f_path=file_obj.path)
1783 f_path=file_obj.path)
1785
1784
1786 return literal(
1785 return literal(
1787 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1786 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1788
1787
1789
1788
1790 def renderer_from_filename(filename, exclude=None):
1789 def renderer_from_filename(filename, exclude=None):
1791 """
1790 """
1792 choose a renderer based on filename, this works only for text based files
1791 choose a renderer based on filename, this works only for text based files
1793 """
1792 """
1794
1793
1795 # ipython
1794 # ipython
1796 for ext in ['*.ipynb']:
1795 for ext in ['*.ipynb']:
1797 if fnmatch.fnmatch(filename, pat=ext):
1796 if fnmatch.fnmatch(filename, pat=ext):
1798 return 'jupyter'
1797 return 'jupyter'
1799
1798
1800 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1799 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1801 if is_markup:
1800 if is_markup:
1802 return is_markup
1801 return is_markup
1803 return None
1802 return None
1804
1803
1805
1804
1806 def render(source, renderer='rst', mentions=False, relative_urls=None,
1805 def render(source, renderer='rst', mentions=False, relative_urls=None,
1807 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1806 repo_name=None, active_pattern_entries=None, issues_container_callback=None):
1808
1807
1809 def maybe_convert_relative_links(html_source):
1808 def maybe_convert_relative_links(html_source):
1810 if relative_urls:
1809 if relative_urls:
1811 return relative_links(html_source, relative_urls)
1810 return relative_links(html_source, relative_urls)
1812 return html_source
1811 return html_source
1813
1812
1814 if renderer == 'plain':
1813 if renderer == 'plain':
1815 return literal(
1814 return literal(
1816 MarkupRenderer.plain(source, leading_newline=False))
1815 MarkupRenderer.plain(source, leading_newline=False))
1817
1816
1818 elif renderer == 'rst':
1817 elif renderer == 'rst':
1819 if repo_name:
1818 if repo_name:
1820 # process patterns on comments if we pass in repo name
1819 # process patterns on comments if we pass in repo name
1821 source, issues, errors = process_patterns(
1820 source, issues, errors = process_patterns(
1822 source, repo_name, link_format='rst',
1821 source, repo_name, link_format='rst',
1823 active_entries=active_pattern_entries)
1822 active_entries=active_pattern_entries)
1824 if issues_container_callback is not None:
1823 if issues_container_callback is not None:
1825 for issue in issues:
1824 for issue in issues:
1826 issues_container_callback(issue)
1825 issues_container_callback(issue)
1827
1826
1828 return literal(
1827 return literal(
1829 '<div class="rst-block">%s</div>' %
1828 '<div class="rst-block">%s</div>' %
1830 maybe_convert_relative_links(
1829 maybe_convert_relative_links(
1831 MarkupRenderer.rst(source, mentions=mentions)))
1830 MarkupRenderer.rst(source, mentions=mentions)))
1832
1831
1833 elif renderer == 'markdown':
1832 elif renderer == 'markdown':
1834 if repo_name:
1833 if repo_name:
1835 # process patterns on comments if we pass in repo name
1834 # process patterns on comments if we pass in repo name
1836 source, issues, errors = process_patterns(
1835 source, issues, errors = process_patterns(
1837 source, repo_name, link_format='markdown',
1836 source, repo_name, link_format='markdown',
1838 active_entries=active_pattern_entries)
1837 active_entries=active_pattern_entries)
1839 if issues_container_callback is not None:
1838 if issues_container_callback is not None:
1840 for issue in issues:
1839 for issue in issues:
1841 issues_container_callback(issue)
1840 issues_container_callback(issue)
1842
1841
1843
1842
1844 return literal(
1843 return literal(
1845 '<div class="markdown-block">%s</div>' %
1844 '<div class="markdown-block">%s</div>' %
1846 maybe_convert_relative_links(
1845 maybe_convert_relative_links(
1847 MarkupRenderer.markdown(source, flavored=True,
1846 MarkupRenderer.markdown(source, flavored=True,
1848 mentions=mentions)))
1847 mentions=mentions)))
1849
1848
1850 elif renderer == 'jupyter':
1849 elif renderer == 'jupyter':
1851 return literal(
1850 return literal(
1852 '<div class="ipynb">%s</div>' %
1851 '<div class="ipynb">%s</div>' %
1853 maybe_convert_relative_links(
1852 maybe_convert_relative_links(
1854 MarkupRenderer.jupyter(source)))
1853 MarkupRenderer.jupyter(source)))
1855
1854
1856 # None means just show the file-source
1855 # None means just show the file-source
1857 return None
1856 return None
1858
1857
1859
1858
1860 def commit_status(repo, commit_id):
1859 def commit_status(repo, commit_id):
1861 return ChangesetStatusModel().get_status(repo, commit_id)
1860 return ChangesetStatusModel().get_status(repo, commit_id)
1862
1861
1863
1862
1864 def commit_status_lbl(commit_status):
1863 def commit_status_lbl(commit_status):
1865 return dict(ChangesetStatus.STATUSES).get(commit_status)
1864 return dict(ChangesetStatus.STATUSES).get(commit_status)
1866
1865
1867
1866
1868 def commit_time(repo_name, commit_id):
1867 def commit_time(repo_name, commit_id):
1869 repo = Repository.get_by_repo_name(repo_name)
1868 repo = Repository.get_by_repo_name(repo_name)
1870 commit = repo.get_commit(commit_id=commit_id)
1869 commit = repo.get_commit(commit_id=commit_id)
1871 return commit.date
1870 return commit.date
1872
1871
1873
1872
1874 def get_permission_name(key):
1873 def get_permission_name(key):
1875 return dict(Permission.PERMS).get(key)
1874 return dict(Permission.PERMS).get(key)
1876
1875
1877
1876
1878 def journal_filter_help(request):
1877 def journal_filter_help(request):
1879 _ = request.translate
1878 _ = request.translate
1880 from rhodecode.lib.audit_logger import ACTIONS
1879 from rhodecode.lib.audit_logger import ACTIONS
1881 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1880 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1882
1881
1883 return _(
1882 return _(
1884 'Example filter terms:\n' +
1883 'Example filter terms:\n' +
1885 ' repository:vcs\n' +
1884 ' repository:vcs\n' +
1886 ' username:marcin\n' +
1885 ' username:marcin\n' +
1887 ' username:(NOT marcin)\n' +
1886 ' username:(NOT marcin)\n' +
1888 ' action:*push*\n' +
1887 ' action:*push*\n' +
1889 ' ip:127.0.0.1\n' +
1888 ' ip:127.0.0.1\n' +
1890 ' date:20120101\n' +
1889 ' date:20120101\n' +
1891 ' date:[20120101100000 TO 20120102]\n' +
1890 ' date:[20120101100000 TO 20120102]\n' +
1892 '\n' +
1891 '\n' +
1893 'Actions: {actions}\n' +
1892 'Actions: {actions}\n' +
1894 '\n' +
1893 '\n' +
1895 'Generate wildcards using \'*\' character:\n' +
1894 'Generate wildcards using \'*\' character:\n' +
1896 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1895 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1897 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1896 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1898 '\n' +
1897 '\n' +
1899 'Optional AND / OR operators in queries\n' +
1898 'Optional AND / OR operators in queries\n' +
1900 ' "repository:vcs OR repository:test"\n' +
1899 ' "repository:vcs OR repository:test"\n' +
1901 ' "username:test AND repository:test*"\n'
1900 ' "username:test AND repository:test*"\n'
1902 ).format(actions=actions)
1901 ).format(actions=actions)
1903
1902
1904
1903
1905 def not_mapped_error(repo_name):
1904 def not_mapped_error(repo_name):
1906 from rhodecode.translation import _
1905 from rhodecode.translation import _
1907 flash(_('%s repository is not mapped to db perhaps'
1906 flash(_('%s repository is not mapped to db perhaps'
1908 ' it was created or renamed from the filesystem'
1907 ' it was created or renamed from the filesystem'
1909 ' please run the application again'
1908 ' please run the application again'
1910 ' in order to rescan repositories') % repo_name, category='error')
1909 ' in order to rescan repositories') % repo_name, category='error')
1911
1910
1912
1911
1913 def ip_range(ip_addr):
1912 def ip_range(ip_addr):
1914 from rhodecode.model.db import UserIpMap
1913 from rhodecode.model.db import UserIpMap
1915 s, e = UserIpMap._get_ip_range(ip_addr)
1914 s, e = UserIpMap._get_ip_range(ip_addr)
1916 return '%s - %s' % (s, e)
1915 return '%s - %s' % (s, e)
1917
1916
1918
1917
1919 def form(url, method='post', needs_csrf_token=True, **attrs):
1918 def form(url, method='post', needs_csrf_token=True, **attrs):
1920 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1919 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1921 if method.lower() != 'get' and needs_csrf_token:
1920 if method.lower() != 'get' and needs_csrf_token:
1922 raise Exception(
1921 raise Exception(
1923 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1922 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1924 'CSRF token. If the endpoint does not require such token you can ' +
1923 'CSRF token. If the endpoint does not require such token you can ' +
1925 'explicitly set the parameter needs_csrf_token to false.')
1924 'explicitly set the parameter needs_csrf_token to false.')
1926
1925
1927 return insecure_form(url, method=method, **attrs)
1926 return insecure_form(url, method=method, **attrs)
1928
1927
1929
1928
1930 def secure_form(form_url, method="POST", multipart=False, **attrs):
1929 def secure_form(form_url, method="POST", multipart=False, **attrs):
1931 """Start a form tag that points the action to an url. This
1930 """Start a form tag that points the action to an url. This
1932 form tag will also include the hidden field containing
1931 form tag will also include the hidden field containing
1933 the auth token.
1932 the auth token.
1934
1933
1935 The url options should be given either as a string, or as a
1934 The url options should be given either as a string, or as a
1936 ``url()`` function. The method for the form defaults to POST.
1935 ``url()`` function. The method for the form defaults to POST.
1937
1936
1938 Options:
1937 Options:
1939
1938
1940 ``multipart``
1939 ``multipart``
1941 If set to True, the enctype is set to "multipart/form-data".
1940 If set to True, the enctype is set to "multipart/form-data".
1942 ``method``
1941 ``method``
1943 The method to use when submitting the form, usually either
1942 The method to use when submitting the form, usually either
1944 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1943 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1945 hidden input with name _method is added to simulate the verb
1944 hidden input with name _method is added to simulate the verb
1946 over POST.
1945 over POST.
1947
1946
1948 """
1947 """
1949
1948
1950 if 'request' in attrs:
1949 if 'request' in attrs:
1951 session = attrs['request'].session
1950 session = attrs['request'].session
1952 del attrs['request']
1951 del attrs['request']
1953 else:
1952 else:
1954 raise ValueError(
1953 raise ValueError(
1955 'Calling this form requires request= to be passed as argument')
1954 'Calling this form requires request= to be passed as argument')
1956
1955
1957 _form = insecure_form(form_url, method, multipart, **attrs)
1956 _form = insecure_form(form_url, method, multipart, **attrs)
1958 token = literal(
1957 token = literal(
1959 '<input type="hidden" name="{}" value="{}">'.format(
1958 '<input type="hidden" name="{}" value="{}">'.format(
1960 csrf_token_key, get_csrf_token(session)))
1959 csrf_token_key, get_csrf_token(session)))
1961
1960
1962 return literal("%s\n%s" % (_form, token))
1961 return literal("%s\n%s" % (_form, token))
1963
1962
1964
1963
1965 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1964 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1966 select_html = select(name, selected, options, **attrs)
1965 select_html = select(name, selected, options, **attrs)
1967
1966
1968 select2 = """
1967 select2 = """
1969 <script>
1968 <script>
1970 $(document).ready(function() {
1969 $(document).ready(function() {
1971 $('#%s').select2({
1970 $('#%s').select2({
1972 containerCssClass: 'drop-menu %s',
1971 containerCssClass: 'drop-menu %s',
1973 dropdownCssClass: 'drop-menu-dropdown',
1972 dropdownCssClass: 'drop-menu-dropdown',
1974 dropdownAutoWidth: true%s
1973 dropdownAutoWidth: true%s
1975 });
1974 });
1976 });
1975 });
1977 </script>
1976 </script>
1978 """
1977 """
1979
1978
1980 filter_option = """,
1979 filter_option = """,
1981 minimumResultsForSearch: -1
1980 minimumResultsForSearch: -1
1982 """
1981 """
1983 input_id = attrs.get('id') or name
1982 input_id = attrs.get('id') or name
1984 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1983 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1985 filter_enabled = "" if enable_filter else filter_option
1984 filter_enabled = "" if enable_filter else filter_option
1986 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1985 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1987
1986
1988 return literal(select_html+select_script)
1987 return literal(select_html+select_script)
1989
1988
1990
1989
1991 def get_visual_attr(tmpl_context_var, attr_name):
1990 def get_visual_attr(tmpl_context_var, attr_name):
1992 """
1991 """
1993 A safe way to get a variable from visual variable of template context
1992 A safe way to get a variable from visual variable of template context
1994
1993
1995 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1994 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1996 :param attr_name: name of the attribute we fetch from the c.visual
1995 :param attr_name: name of the attribute we fetch from the c.visual
1997 """
1996 """
1998 visual = getattr(tmpl_context_var, 'visual', None)
1997 visual = getattr(tmpl_context_var, 'visual', None)
1999 if not visual:
1998 if not visual:
2000 return
1999 return
2001 else:
2000 else:
2002 return getattr(visual, attr_name, None)
2001 return getattr(visual, attr_name, None)
2003
2002
2004
2003
2005 def get_last_path_part(file_node):
2004 def get_last_path_part(file_node):
2006 if not file_node.path:
2005 if not file_node.path:
2007 return u'/'
2006 return u'/'
2008
2007
2009 path = safe_unicode(file_node.path.split('/')[-1])
2008 path = safe_unicode(file_node.path.split('/')[-1])
2010 return u'../' + path
2009 return u'../' + path
2011
2010
2012
2011
2013 def route_url(*args, **kwargs):
2012 def route_url(*args, **kwargs):
2014 """
2013 """
2015 Wrapper around pyramids `route_url` (fully qualified url) function.
2014 Wrapper around pyramids `route_url` (fully qualified url) function.
2016 """
2015 """
2017 req = get_current_request()
2016 req = get_current_request()
2018 return req.route_url(*args, **kwargs)
2017 return req.route_url(*args, **kwargs)
2019
2018
2020
2019
2021 def route_path(*args, **kwargs):
2020 def route_path(*args, **kwargs):
2022 """
2021 """
2023 Wrapper around pyramids `route_path` function.
2022 Wrapper around pyramids `route_path` function.
2024 """
2023 """
2025 req = get_current_request()
2024 req = get_current_request()
2026 return req.route_path(*args, **kwargs)
2025 return req.route_path(*args, **kwargs)
2027
2026
2028
2027
2029 def route_path_or_none(*args, **kwargs):
2028 def route_path_or_none(*args, **kwargs):
2030 try:
2029 try:
2031 return route_path(*args, **kwargs)
2030 return route_path(*args, **kwargs)
2032 except KeyError:
2031 except KeyError:
2033 return None
2032 return None
2034
2033
2035
2034
2036 def current_route_path(request, **kw):
2035 def current_route_path(request, **kw):
2037 new_args = request.GET.mixed()
2036 new_args = request.GET.mixed()
2038 new_args.update(kw)
2037 new_args.update(kw)
2039 return request.current_route_path(_query=new_args)
2038 return request.current_route_path(_query=new_args)
2040
2039
2041
2040
2042 def curl_api_example(method, args):
2041 def curl_api_example(method, args):
2043 args_json = json.dumps(OrderedDict([
2042 args_json = json.dumps(OrderedDict([
2044 ('id', 1),
2043 ('id', 1),
2045 ('auth_token', 'SECRET'),
2044 ('auth_token', 'SECRET'),
2046 ('method', method),
2045 ('method', method),
2047 ('args', args)
2046 ('args', args)
2048 ]))
2047 ]))
2049
2048
2050 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2049 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2051 api_url=route_url('apiv2'),
2050 api_url=route_url('apiv2'),
2052 args_json=args_json
2051 args_json=args_json
2053 )
2052 )
2054
2053
2055
2054
2056 def api_call_example(method, args):
2055 def api_call_example(method, args):
2057 """
2056 """
2058 Generates an API call example via CURL
2057 Generates an API call example via CURL
2059 """
2058 """
2060 curl_call = curl_api_example(method, args)
2059 curl_call = curl_api_example(method, args)
2061
2060
2062 return literal(
2061 return literal(
2063 curl_call +
2062 curl_call +
2064 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2063 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2065 "and needs to be of `api calls` role."
2064 "and needs to be of `api calls` role."
2066 .format(token_url=route_url('my_account_auth_tokens')))
2065 .format(token_url=route_url('my_account_auth_tokens')))
2067
2066
2068
2067
2069 def notification_description(notification, request):
2068 def notification_description(notification, request):
2070 """
2069 """
2071 Generate notification human readable description based on notification type
2070 Generate notification human readable description based on notification type
2072 """
2071 """
2073 from rhodecode.model.notification import NotificationModel
2072 from rhodecode.model.notification import NotificationModel
2074 return NotificationModel().make_description(
2073 return NotificationModel().make_description(
2075 notification, translate=request.translate)
2074 notification, translate=request.translate)
2076
2075
2077
2076
2078 def go_import_header(request, db_repo=None):
2077 def go_import_header(request, db_repo=None):
2079 """
2078 """
2080 Creates a header for go-import functionality in Go Lang
2079 Creates a header for go-import functionality in Go Lang
2081 """
2080 """
2082
2081
2083 if not db_repo:
2082 if not db_repo:
2084 return
2083 return
2085 if 'go-get' not in request.GET:
2084 if 'go-get' not in request.GET:
2086 return
2085 return
2087
2086
2088 clone_url = db_repo.clone_url()
2087 clone_url = db_repo.clone_url()
2089 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2088 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2090 # we have a repo and go-get flag,
2089 # we have a repo and go-get flag,
2091 return literal('<meta name="go-import" content="{} {} {}">'.format(
2090 return literal('<meta name="go-import" content="{} {} {}">'.format(
2092 prefix, db_repo.repo_type, clone_url))
2091 prefix, db_repo.repo_type, clone_url))
2093
2092
2094
2093
2095 def reviewer_as_json(*args, **kwargs):
2094 def reviewer_as_json(*args, **kwargs):
2096 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2095 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2097 return _reviewer_as_json(*args, **kwargs)
2096 return _reviewer_as_json(*args, **kwargs)
2098
2097
2099
2098
2100 def get_repo_view_type(request):
2099 def get_repo_view_type(request):
2101 route_name = request.matched_route.name
2100 route_name = request.matched_route.name
2102 route_to_view_type = {
2101 route_to_view_type = {
2103 'repo_changelog': 'commits',
2102 'repo_changelog': 'commits',
2104 'repo_commits': 'commits',
2103 'repo_commits': 'commits',
2105 'repo_files': 'files',
2104 'repo_files': 'files',
2106 'repo_summary': 'summary',
2105 'repo_summary': 'summary',
2107 'repo_commit': 'commit'
2106 'repo_commit': 'commit'
2108 }
2107 }
2109
2108
2110 return route_to_view_type.get(route_name)
2109 return route_to_view_type.get(route_name)
2111
2110
2112
2111
2113 def is_active(menu_entry, selected):
2112 def is_active(menu_entry, selected):
2114 """
2113 """
2115 Returns active class for selecting menus in templates
2114 Returns active class for selecting menus in templates
2116 <li class=${h.is_active('settings', current_active)}></li>
2115 <li class=${h.is_active('settings', current_active)}></li>
2117 """
2116 """
2118 if not isinstance(menu_entry, list):
2117 if not isinstance(menu_entry, list):
2119 menu_entry = [menu_entry]
2118 menu_entry = [menu_entry]
2120
2119
2121 if selected in menu_entry:
2120 if selected in menu_entry:
2122 return "active"
2121 return "active"
2123
2122
2124
2123
2125 class IssuesRegistry(object):
2124 class IssuesRegistry(object):
2126 """
2125 """
2127 issue_registry = IssuesRegistry()
2126 issue_registry = IssuesRegistry()
2128 some_func(issues_callback=issues_registry(...))
2127 some_func(issues_callback=issues_registry(...))
2129 """
2128 """
2130
2129
2131 def __init__(self):
2130 def __init__(self):
2132 self.issues = []
2131 self.issues = []
2133 self.unique_issues = collections.defaultdict(lambda: [])
2132 self.unique_issues = collections.defaultdict(lambda: [])
2134
2133
2135 def __call__(self, commit_dict=None):
2134 def __call__(self, commit_dict=None):
2136 def callback(issue):
2135 def callback(issue):
2137 if commit_dict and issue:
2136 if commit_dict and issue:
2138 issue['commit'] = commit_dict
2137 issue['commit'] = commit_dict
2139 self.issues.append(issue)
2138 self.issues.append(issue)
2140 self.unique_issues[issue['id']].append(issue)
2139 self.unique_issues[issue['id']].append(issue)
2141 return callback
2140 return callback
2142
2141
2143 def get_issues(self):
2142 def get_issues(self):
2144 return self.issues
2143 return self.issues
2145
2144
2146 @property
2145 @property
2147 def issues_unique_count(self):
2146 def issues_unique_count(self):
2148 return len(set(i['id'] for i in self.issues))
2147 return len(set(i['id'] for i in self.issues))
@@ -1,170 +1,179 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default user-profile">
3 <div class="panel panel-default user-profile">
4 <div class="panel-heading">
4 <div class="panel-heading">
5 <h3 class="panel-title">
5 <h3 class="panel-title">
6 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
6 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')}
7 &nbsp;- ${_('User Profile')}
7 &nbsp;- ${_('User Profile')}
8 </h3>
8 </h3>
9 </div>
9 </div>
10 <div class="panel-body">
10 <div class="panel-body">
11 <div class="user-profile-content">
11 <div class="user-profile-content">
12 ${h.secure_form(h.route_path('user_update', user_id=c.user.user_id), class_='form', request=request)}
12 ${h.secure_form(h.route_path('user_update', user_id=c.user.user_id), class_='form', request=request)}
13 <% readonly = None %>
13 <% readonly = None %>
14 <% disabled = "" %>
14 <% disabled = "" %>
15 %if c.extern_type != 'rhodecode':
15 % if c.edit_mode:
16 ${h.hidden('edit', '1')}
17 % endif
18 %if c.extern_type != 'rhodecode' and not c.edit_mode:
16 <% readonly = "readonly" %>
19 <% readonly = "readonly" %>
17 <% disabled = " disabled" %>
20 <% disabled = " disabled" %>
18 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
21 <div class="alert-warning" style="margin:0px 0px 20px 0px; padding: 10px">
19 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
22 <strong>${_('This user was created from external source (%s). Editing some of the settings is limited.' % c.extern_type)}</strong>
20 </div>
23 </div>
21 %endif
24 %endif
22 <div class="form">
25 <div class="form">
23 <div class="fields">
26 <div class="fields">
24 <div class="field">
27 <div class="field">
25 <div class="label photo">
28 <div class="label photo">
26 ${_('Photo')}:
29 ${_('Photo')}:
27 </div>
30 </div>
28 <div class="input profile">
31 <div class="input profile">
29 %if c.visual.use_gravatar:
32 %if c.visual.use_gravatar:
30 ${base.gravatar(c.user.email, 100)}
33 ${base.gravatar(c.user.email, 100)}
31 <p class="help-block">${_('Change the avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
34 <p class="help-block">${_('Change the avatar at')} <a href="http://gravatar.com">gravatar.com</a>.</p>
32 %else:
35 %else:
33 ${base.gravatar(c.user.email, 100)}
36 ${base.gravatar(c.user.email, 100)}
34 %endif
37 %endif
35 </div>
38 </div>
36 </div>
39 </div>
37 <div class="field">
40 <div class="field">
38 <div class="label">
41 <div class="label">
39 ${_('Username')}:
42 ${_('Username')}:
40 </div>
43 </div>
41 <div class="input">
44 <div class="input">
42 ${h.text('username', class_='%s medium' % disabled, readonly=readonly)}
45 ${h.text('username', class_='%s medium' % disabled, readonly=readonly)}
46 <br/>
47 % if c.extern_type != 'rhodecode' and c.is_super_admin:
48 <p class="help-block">
49 ${_('Super-admin can edit this field by entering ')} <a href="${h.current_route_path(request, edit=1)}">edit mode</a>
50 </p>
51 % endif
43 </div>
52 </div>
44 </div>
53 </div>
45 <div class="field">
54 <div class="field">
46 <div class="label">
55 <div class="label">
47 <label for="name">${_('First Name')}:</label>
56 <label for="name">${_('First Name')}:</label>
48 </div>
57 </div>
49 <div class="input">
58 <div class="input">
50 ${h.text('firstname', class_="medium")}
59 ${h.text('firstname', class_="medium")}
51 </div>
60 </div>
52 </div>
61 </div>
53
62
54 <div class="field">
63 <div class="field">
55 <div class="label">
64 <div class="label">
56 <label for="lastname">${_('Last Name')}:</label>
65 <label for="lastname">${_('Last Name')}:</label>
57 </div>
66 </div>
58 <div class="input">
67 <div class="input">
59 ${h.text('lastname', class_="medium")}
68 ${h.text('lastname', class_="medium")}
60 </div>
69 </div>
61 </div>
70 </div>
62
71
63 <div class="field">
72 <div class="field">
64 <div class="label">
73 <div class="label">
65 <label for="email">${_('Email')}:</label>
74 <label for="email">${_('Email')}:</label>
66 </div>
75 </div>
67 <div class="input">
76 <div class="input">
68 ## we should be able to edit email !
77 ## we should be able to edit email !
69 ${h.text('email', class_="medium")}
78 ${h.text('email', class_="medium")}
70 </div>
79 </div>
71 </div>
80 </div>
72 <div class="field">
81 <div class="field">
73 <div class="label">
82 <div class="label">
74 <label for="description">${_('Description')}:</label>
83 <label for="description">${_('Description')}:</label>
75 </div>
84 </div>
76 <div class="input textarea editor">
85 <div class="input textarea editor">
77 ${h.textarea('description', rows=10, class_="medium")}
86 ${h.textarea('description', rows=10, class_="medium")}
78 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
87 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
79 <span class="help-block">
88 <span class="help-block">
80 % if c.visual.stylify_metatags:
89 % if c.visual.stylify_metatags:
81 ${_('Plain text format with {metatags} support.').format(metatags=metatags_url)|n}
90 ${_('Plain text format with {metatags} support.').format(metatags=metatags_url)|n}
82 % else:
91 % else:
83 ${_('Plain text format.')}
92 ${_('Plain text format.')}
84 % endif
93 % endif
85 </span>
94 </span>
86 <span id="meta-tags-desc" style="display: none">
95 <span id="meta-tags-desc" style="display: none">
87 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
96 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
88 ${dt.metatags_help()}
97 ${dt.metatags_help()}
89 </span>
98 </span>
90 </div>
99 </div>
91 </div>
100 </div>
92 <div class="field">
101 <div class="field">
93 <div class="label">
102 <div class="label">
94 ${_('New Password')}:
103 ${_('New Password')}:
95 </div>
104 </div>
96 <div class="input">
105 <div class="input">
97 ${h.password('new_password',class_='%s medium' % disabled,autocomplete="off",readonly=readonly)}
106 ${h.password('new_password',class_='%s medium' % disabled,autocomplete="off",readonly=readonly)}
98 </div>
107 </div>
99 </div>
108 </div>
100 <div class="field">
109 <div class="field">
101 <div class="label">
110 <div class="label">
102 ${_('New Password Confirmation')}:
111 ${_('New Password Confirmation')}:
103 </div>
112 </div>
104 <div class="input">
113 <div class="input">
105 ${h.password('password_confirmation',class_="%s medium" % disabled,autocomplete="off",readonly=readonly)}
114 ${h.password('password_confirmation',class_="%s medium" % disabled,autocomplete="off",readonly=readonly)}
106 </div>
115 </div>
107 </div>
116 </div>
108 <div class="field">
117 <div class="field">
109 <div class="label-text">
118 <div class="label-text">
110 ${_('Active')}:
119 ${_('Active')}:
111 </div>
120 </div>
112 <div class="input user-checkbox">
121 <div class="input user-checkbox">
113 ${h.checkbox('active',value=True)}
122 ${h.checkbox('active',value=True)}
114 </div>
123 </div>
115 </div>
124 </div>
116 <div class="field">
125 <div class="field">
117 <div class="label-text">
126 <div class="label-text">
118 ${_('Super-admin')}:
127 ${_('Super-admin')}:
119 </div>
128 </div>
120 <div class="input user-checkbox">
129 <div class="input user-checkbox">
121 ${h.checkbox('admin',value=True)}
130 ${h.checkbox('admin',value=True)}
122 </div>
131 </div>
123 </div>
132 </div>
124 <div class="field">
133 <div class="field">
125 <div class="label-text">
134 <div class="label-text">
126 ${_('Authentication type')}:
135 ${_('Authentication type')}:
127 </div>
136 </div>
128 <div class="input">
137 <div class="input">
129 ${h.select('extern_type', c.extern_type, c.allowed_extern_types)}
138 ${h.select('extern_type', c.extern_type, c.allowed_extern_types)}
130 <p class="help-block">${_('When user was created using an external source. He is bound to authentication using this method.')}</p>
139 <p class="help-block">${_('When user was created using an external source. He is bound to authentication using this method.')}</p>
131 </div>
140 </div>
132 </div>
141 </div>
133 <div class="field">
142 <div class="field">
134 <div class="label-text">
143 <div class="label-text">
135 ${_('Name in Source of Record')}:
144 ${_('Name in Source of Record')}:
136 </div>
145 </div>
137 <div class="input">
146 <div class="input">
138 <p>${c.extern_name}</p>
147 <p>${c.extern_name}</p>
139 ${h.hidden('extern_name', readonly="readonly")}
148 ${h.hidden('extern_name', readonly="readonly")}
140 </div>
149 </div>
141 </div>
150 </div>
142 <div class="field">
151 <div class="field">
143 <div class="label">
152 <div class="label">
144 ${_('Language')}:
153 ${_('Language')}:
145 </div>
154 </div>
146 <div class="input">
155 <div class="input">
147 ## allowed_languages is defined in the users.py
156 ## allowed_languages is defined in the users.py
148 ## c.language comes from base.py as a default language
157 ## c.language comes from base.py as a default language
149 ${h.select('language', c.language, c.allowed_languages)}
158 ${h.select('language', c.language, c.allowed_languages)}
150 <p class="help-block">${h.literal(_('User interface language. Help translate %(rc_link)s into your language.') % {'rc_link': h.link_to('RhodeCode Enterprise', h.route_url('rhodecode_translations'))})}</p>
159 <p class="help-block">${h.literal(_('User interface language. Help translate %(rc_link)s into your language.') % {'rc_link': h.link_to('RhodeCode Enterprise', h.route_url('rhodecode_translations'))})}</p>
151 </div>
160 </div>
152 </div>
161 </div>
153 <div class="buttons">
162 <div class="buttons">
154 ${h.submit('save', _('Save'), class_="btn")}
163 ${h.submit('save', _('Save'), class_="btn")}
155 ${h.reset('reset', _('Reset'), class_="btn")}
164 ${h.reset('reset', _('Reset'), class_="btn")}
156 </div>
165 </div>
157 </div>
166 </div>
158 </div>
167 </div>
159 ${h.end_form()}
168 ${h.end_form()}
160 </div>
169 </div>
161 </div>
170 </div>
162 </div>
171 </div>
163
172
164 <script>
173 <script>
165 $('#language').select2({
174 $('#language').select2({
166 'containerCssClass': "drop-menu",
175 'containerCssClass': "drop-menu",
167 'dropdownCssClass': "drop-menu-dropdown",
176 'dropdownCssClass': "drop-menu-dropdown",
168 'dropdownAutoWidth': true
177 'dropdownAutoWidth': true
169 });
178 });
170 </script>
179 </script>
General Comments 0
You need to be logged in to leave comments. Login now