##// END OF EJS Templates
partial-renderer: use package resource format for templates....
marcink -
r2313:d272914f default
parent child Browse files
Show More
@@ -1,248 +1,248 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.response import Response
29 29 from pyramid.renderers import render
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
34 34 from rhodecode.lib import helpers as h, audit_logger
35 35 from rhodecode.lib.utils2 import safe_unicode
36 36
37 37 from rhodecode.model.forms import UserGroupForm
38 38 from rhodecode.model.permission import PermissionModel
39 39 from rhodecode.model.scm import UserGroupList
40 40 from rhodecode.model.db import (
41 41 or_, count, User, UserGroup, UserGroupMember)
42 42 from rhodecode.model.meta import Session
43 43 from rhodecode.model.user_group import UserGroupModel
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class AdminUserGroupsView(BaseAppView, DataGridAppView):
49 49
50 50 def load_default_context(self):
51 51 c = self._get_local_tmpl_context()
52 52
53 53 PermissionModel().set_global_permission_choices(
54 54 c, gettext_translator=self.request.translate)
55 55
56 56 self._register_global_c(c)
57 57 return c
58 58
59 59 # permission check in data loading of
60 60 # `user_groups_list_data` via UserGroupList
61 61 @LoginRequired()
62 62 @NotAnonymous()
63 63 @view_config(
64 64 route_name='user_groups', request_method='GET',
65 65 renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
66 66 def user_groups_list(self):
67 67 c = self.load_default_context()
68 68 return self._get_template_context(c)
69 69
70 70 # permission check inside
71 71 @LoginRequired()
72 72 @NotAnonymous()
73 73 @view_config(
74 74 route_name='user_groups_data', request_method='GET',
75 75 renderer='json_ext', xhr=True)
76 76 def user_groups_list_data(self):
77 77 self.load_default_context()
78 78 column_map = {
79 79 'active': 'users_group_active',
80 80 'description': 'user_group_description',
81 81 'members': 'members_total',
82 82 'owner': 'user_username',
83 83 'sync': 'group_data'
84 84 }
85 85 draw, start, limit = self._extract_chunk(self.request)
86 86 search_q, order_by, order_dir = self._extract_ordering(
87 87 self.request, column_map=column_map)
88 88
89 89 _render = self.request.get_partial_renderer(
90 'data_table/_dt_elements.mako')
90 'rhodecode:templates/data_table/_dt_elements.mako')
91 91
92 92 def user_group_name(user_group_id, user_group_name):
93 93 return _render("user_group_name", user_group_id, user_group_name)
94 94
95 95 def user_group_actions(user_group_id, user_group_name):
96 96 return _render("user_group_actions", user_group_id, user_group_name)
97 97
98 98 def user_profile(username):
99 99 return _render('user_profile', username)
100 100
101 101 auth_user_group_list = UserGroupList(
102 102 UserGroup.query().all(), perm_set=['usergroup.admin'])
103 103
104 104 allowed_ids = [-1]
105 105 for user_group in auth_user_group_list:
106 106 allowed_ids.append(user_group.users_group_id)
107 107
108 108 user_groups_data_total_count = UserGroup.query()\
109 109 .filter(UserGroup.users_group_id.in_(allowed_ids))\
110 110 .count()
111 111
112 112 member_count = count(UserGroupMember.user_id)
113 113 base_q = Session.query(
114 114 UserGroup.users_group_name,
115 115 UserGroup.user_group_description,
116 116 UserGroup.users_group_active,
117 117 UserGroup.users_group_id,
118 118 UserGroup.group_data,
119 119 User,
120 120 member_count.label('member_count')
121 121 ) \
122 122 .filter(UserGroup.users_group_id.in_(allowed_ids)) \
123 123 .outerjoin(UserGroupMember) \
124 124 .join(User, User.user_id == UserGroup.user_id) \
125 125 .group_by(UserGroup, User)
126 126
127 127 if search_q:
128 128 like_expression = u'%{}%'.format(safe_unicode(search_q))
129 129 base_q = base_q.filter(or_(
130 130 UserGroup.users_group_name.ilike(like_expression),
131 131 ))
132 132
133 133 user_groups_data_total_filtered_count = base_q.count()
134 134
135 135 if order_by == 'members_total':
136 136 sort_col = member_count
137 137 elif order_by == 'user_username':
138 138 sort_col = User.username
139 139 else:
140 140 sort_col = getattr(UserGroup, order_by, None)
141 141
142 142 if isinstance(sort_col, count) or sort_col:
143 143 if order_dir == 'asc':
144 144 sort_col = sort_col.asc()
145 145 else:
146 146 sort_col = sort_col.desc()
147 147
148 148 base_q = base_q.order_by(sort_col)
149 149 base_q = base_q.offset(start).limit(limit)
150 150
151 151 # authenticated access to user groups
152 152 auth_user_group_list = base_q.all()
153 153
154 154 user_groups_data = []
155 155 for user_gr in auth_user_group_list:
156 156 user_groups_data.append({
157 157 "users_group_name": user_group_name(
158 158 user_gr.users_group_id, h.escape(user_gr.users_group_name)),
159 159 "name_raw": h.escape(user_gr.users_group_name),
160 160 "description": h.escape(user_gr.user_group_description),
161 161 "members": user_gr.member_count,
162 162 # NOTE(marcink): because of advanced query we
163 163 # need to load it like that
164 164 "sync": UserGroup._load_group_data(
165 165 user_gr.group_data).get('extern_type'),
166 166 "active": h.bool2icon(user_gr.users_group_active),
167 167 "owner": user_profile(user_gr.User.username),
168 168 "action": user_group_actions(
169 169 user_gr.users_group_id, user_gr.users_group_name)
170 170 })
171 171
172 172 data = ({
173 173 'draw': draw,
174 174 'data': user_groups_data,
175 175 'recordsTotal': user_groups_data_total_count,
176 176 'recordsFiltered': user_groups_data_total_filtered_count,
177 177 })
178 178
179 179 return data
180 180
181 181 @LoginRequired()
182 182 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
183 183 @view_config(
184 184 route_name='user_groups_new', request_method='GET',
185 185 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
186 186 def user_groups_new(self):
187 187 c = self.load_default_context()
188 188 return self._get_template_context(c)
189 189
190 190 @LoginRequired()
191 191 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
192 192 @CSRFRequired()
193 193 @view_config(
194 194 route_name='user_groups_create', request_method='POST',
195 195 renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
196 196 def user_groups_create(self):
197 197 _ = self.request.translate
198 198 c = self.load_default_context()
199 199 users_group_form = UserGroupForm()()
200 200
201 201 user_group_name = self.request.POST.get('users_group_name')
202 202 try:
203 203 form_result = users_group_form.to_python(dict(self.request.POST))
204 204 user_group = UserGroupModel().create(
205 205 name=form_result['users_group_name'],
206 206 description=form_result['user_group_description'],
207 207 owner=self._rhodecode_user.user_id,
208 208 active=form_result['users_group_active'])
209 209 Session().flush()
210 210 creation_data = user_group.get_api_data()
211 211 user_group_name = form_result['users_group_name']
212 212
213 213 audit_logger.store_web(
214 214 'user_group.create', action_data={'data': creation_data},
215 215 user=self._rhodecode_user)
216 216
217 217 user_group_link = h.link_to(
218 218 h.escape(user_group_name),
219 219 h.route_path(
220 220 'edit_user_group', user_group_id=user_group.users_group_id))
221 221 h.flash(h.literal(_('Created user group %(user_group_link)s')
222 222 % {'user_group_link': user_group_link}),
223 223 category='success')
224 224 Session().commit()
225 225 user_group_id = user_group.users_group_id
226 226 except formencode.Invalid as errors:
227 227
228 228 data = render(
229 229 'rhodecode:templates/admin/user_groups/user_group_add.mako',
230 230 self._get_template_context(c), self.request)
231 231 html = formencode.htmlfill.render(
232 232 data,
233 233 defaults=errors.value,
234 234 errors=errors.error_dict or {},
235 235 prefix_error=False,
236 236 encoding="UTF-8",
237 237 force_defaults=False
238 238 )
239 239 return Response(html)
240 240
241 241 except Exception:
242 242 log.exception("Exception creating user group")
243 243 h.flash(_('Error occurred during creation of user group %s') \
244 244 % user_group_name, category='error')
245 245 raise HTTPFound(h.route_path('user_groups_new'))
246 246
247 247 raise HTTPFound(
248 248 h.route_path('edit_user_group', user_group_id=user_group_id))
@@ -1,1179 +1,1179 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
32 32 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34 from rhodecode.events import trigger
35 35
36 36 from rhodecode.lib import audit_logger
37 37 from rhodecode.lib.exceptions import (
38 38 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
39 39 UserOwnsUserGroupsException, DefaultUserException)
40 40 from rhodecode.lib.ext_json import json
41 41 from rhodecode.lib.auth import (
42 42 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
43 43 from rhodecode.lib import helpers as h
44 44 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
45 45 from rhodecode.model.auth_token import AuthTokenModel
46 46 from rhodecode.model.forms import (
47 47 UserForm, UserIndividualPermissionsForm, UserPermissionsForm)
48 48 from rhodecode.model.permission import PermissionModel
49 49 from rhodecode.model.repo_group import RepoGroupModel
50 50 from rhodecode.model.ssh_key import SshKeyModel
51 51 from rhodecode.model.user import UserModel
52 52 from rhodecode.model.user_group import UserGroupModel
53 53 from rhodecode.model.db import (
54 54 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
55 55 UserApiKeys, UserSshKeys, RepoGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 class AdminUsersView(BaseAppView, DataGridAppView):
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 self._register_global_c(c)
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @HasPermissionAllDecorator('hg.admin')
70 70 @view_config(
71 71 route_name='users', request_method='GET',
72 72 renderer='rhodecode:templates/admin/users/users.mako')
73 73 def users_list(self):
74 74 c = self.load_default_context()
75 75 return self._get_template_context(c)
76 76
77 77 @LoginRequired()
78 78 @HasPermissionAllDecorator('hg.admin')
79 79 @view_config(
80 80 # renderer defined below
81 81 route_name='users_data', request_method='GET',
82 82 renderer='json_ext', xhr=True)
83 83 def users_list_data(self):
84 84 self.load_default_context()
85 85 column_map = {
86 86 'first_name': 'name',
87 87 'last_name': 'lastname',
88 88 }
89 89 draw, start, limit = self._extract_chunk(self.request)
90 90 search_q, order_by, order_dir = self._extract_ordering(
91 91 self.request, column_map=column_map)
92 92
93 93 _render = self.request.get_partial_renderer(
94 'data_table/_dt_elements.mako')
94 'rhodecode:templates/data_table/_dt_elements.mako')
95 95
96 96 def user_actions(user_id, username):
97 97 return _render("user_actions", user_id, username)
98 98
99 99 users_data_total_count = User.query()\
100 100 .filter(User.username != User.DEFAULT_USER) \
101 101 .count()
102 102
103 103 # json generate
104 104 base_q = User.query().filter(User.username != User.DEFAULT_USER)
105 105
106 106 if search_q:
107 107 like_expression = u'%{}%'.format(safe_unicode(search_q))
108 108 base_q = base_q.filter(or_(
109 109 User.username.ilike(like_expression),
110 110 User._email.ilike(like_expression),
111 111 User.name.ilike(like_expression),
112 112 User.lastname.ilike(like_expression),
113 113 ))
114 114
115 115 users_data_total_filtered_count = base_q.count()
116 116
117 117 sort_col = getattr(User, order_by, None)
118 118 if sort_col:
119 119 if order_dir == 'asc':
120 120 # handle null values properly to order by NULL last
121 121 if order_by in ['last_activity']:
122 122 sort_col = coalesce(sort_col, datetime.date.max)
123 123 sort_col = sort_col.asc()
124 124 else:
125 125 # handle null values properly to order by NULL last
126 126 if order_by in ['last_activity']:
127 127 sort_col = coalesce(sort_col, datetime.date.min)
128 128 sort_col = sort_col.desc()
129 129
130 130 base_q = base_q.order_by(sort_col)
131 131 base_q = base_q.offset(start).limit(limit)
132 132
133 133 users_list = base_q.all()
134 134
135 135 users_data = []
136 136 for user in users_list:
137 137 users_data.append({
138 138 "username": h.gravatar_with_user(self.request, user.username),
139 139 "email": user.email,
140 140 "first_name": user.first_name,
141 141 "last_name": user.last_name,
142 142 "last_login": h.format_date(user.last_login),
143 143 "last_activity": h.format_date(user.last_activity),
144 144 "active": h.bool2icon(user.active),
145 145 "active_raw": user.active,
146 146 "admin": h.bool2icon(user.admin),
147 147 "extern_type": user.extern_type,
148 148 "extern_name": user.extern_name,
149 149 "action": user_actions(user.user_id, user.username),
150 150 })
151 151
152 152 data = ({
153 153 'draw': draw,
154 154 'data': users_data,
155 155 'recordsTotal': users_data_total_count,
156 156 'recordsFiltered': users_data_total_filtered_count,
157 157 })
158 158
159 159 return data
160 160
161 161 def _set_personal_repo_group_template_vars(self, c_obj):
162 162 DummyUser = AttributeDict({
163 163 'username': '${username}',
164 164 'user_id': '${user_id}',
165 165 })
166 166 c_obj.default_create_repo_group = RepoGroupModel() \
167 167 .get_default_create_personal_repo_group()
168 168 c_obj.personal_repo_group_name = RepoGroupModel() \
169 169 .get_personal_group_name(DummyUser)
170 170
171 171 @LoginRequired()
172 172 @HasPermissionAllDecorator('hg.admin')
173 173 @view_config(
174 174 route_name='users_new', request_method='GET',
175 175 renderer='rhodecode:templates/admin/users/user_add.mako')
176 176 def users_new(self):
177 177 _ = self.request.translate
178 178 c = self.load_default_context()
179 179 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
180 180 self._set_personal_repo_group_template_vars(c)
181 181 return self._get_template_context(c)
182 182
183 183 @LoginRequired()
184 184 @HasPermissionAllDecorator('hg.admin')
185 185 @CSRFRequired()
186 186 @view_config(
187 187 route_name='users_create', request_method='POST',
188 188 renderer='rhodecode:templates/admin/users/user_add.mako')
189 189 def users_create(self):
190 190 _ = self.request.translate
191 191 c = self.load_default_context()
192 192 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
193 193 user_model = UserModel()
194 194 user_form = UserForm()()
195 195 try:
196 196 form_result = user_form.to_python(dict(self.request.POST))
197 197 user = user_model.create(form_result)
198 198 Session().flush()
199 199 creation_data = user.get_api_data()
200 200 username = form_result['username']
201 201
202 202 audit_logger.store_web(
203 203 'user.create', action_data={'data': creation_data},
204 204 user=c.rhodecode_user)
205 205
206 206 user_link = h.link_to(
207 207 h.escape(username),
208 208 h.route_path('user_edit', user_id=user.user_id))
209 209 h.flash(h.literal(_('Created user %(user_link)s')
210 210 % {'user_link': user_link}), category='success')
211 211 Session().commit()
212 212 except formencode.Invalid as errors:
213 213 self._set_personal_repo_group_template_vars(c)
214 214 data = render(
215 215 'rhodecode:templates/admin/users/user_add.mako',
216 216 self._get_template_context(c), self.request)
217 217 html = formencode.htmlfill.render(
218 218 data,
219 219 defaults=errors.value,
220 220 errors=errors.error_dict or {},
221 221 prefix_error=False,
222 222 encoding="UTF-8",
223 223 force_defaults=False
224 224 )
225 225 return Response(html)
226 226 except UserCreationError as e:
227 227 h.flash(e, 'error')
228 228 except Exception:
229 229 log.exception("Exception creation of user")
230 230 h.flash(_('Error occurred during creation of user %s')
231 231 % self.request.POST.get('username'), category='error')
232 232 raise HTTPFound(h.route_path('users'))
233 233
234 234
235 235 class UsersView(UserAppView):
236 236 ALLOW_SCOPED_TOKENS = False
237 237 """
238 238 This view has alternative version inside EE, if modified please take a look
239 239 in there as well.
240 240 """
241 241
242 242 def load_default_context(self):
243 243 c = self._get_local_tmpl_context()
244 244 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
245 245 c.allowed_languages = [
246 246 ('en', 'English (en)'),
247 247 ('de', 'German (de)'),
248 248 ('fr', 'French (fr)'),
249 249 ('it', 'Italian (it)'),
250 250 ('ja', 'Japanese (ja)'),
251 251 ('pl', 'Polish (pl)'),
252 252 ('pt', 'Portuguese (pt)'),
253 253 ('ru', 'Russian (ru)'),
254 254 ('zh', 'Chinese (zh)'),
255 255 ]
256 256 req = self.request
257 257
258 258 c.available_permissions = req.registry.settings['available_permissions']
259 259 PermissionModel().set_global_permission_choices(
260 260 c, gettext_translator=req.translate)
261 261
262 262 self._register_global_c(c)
263 263 return c
264 264
265 265 @LoginRequired()
266 266 @HasPermissionAllDecorator('hg.admin')
267 267 @CSRFRequired()
268 268 @view_config(
269 269 route_name='user_update', request_method='POST',
270 270 renderer='rhodecode:templates/admin/users/user_edit.mako')
271 271 def user_update(self):
272 272 _ = self.request.translate
273 273 c = self.load_default_context()
274 274
275 275 user_id = self.db_user_id
276 276 c.user = self.db_user
277 277
278 278 c.active = 'profile'
279 279 c.extern_type = c.user.extern_type
280 280 c.extern_name = c.user.extern_name
281 281 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
282 282 available_languages = [x[0] for x in c.allowed_languages]
283 283 _form = UserForm(edit=True, available_languages=available_languages,
284 284 old_data={'user_id': user_id,
285 285 'email': c.user.email})()
286 286 form_result = {}
287 287 old_values = c.user.get_api_data()
288 288 try:
289 289 form_result = _form.to_python(dict(self.request.POST))
290 290 skip_attrs = ['extern_type', 'extern_name']
291 291 # TODO: plugin should define if username can be updated
292 292 if c.extern_type != "rhodecode":
293 293 # forbid updating username for external accounts
294 294 skip_attrs.append('username')
295 295
296 296 UserModel().update_user(
297 297 user_id, skip_attrs=skip_attrs, **form_result)
298 298
299 299 audit_logger.store_web(
300 300 'user.edit', action_data={'old_data': old_values},
301 301 user=c.rhodecode_user)
302 302
303 303 Session().commit()
304 304 h.flash(_('User updated successfully'), category='success')
305 305 except formencode.Invalid as errors:
306 306 data = render(
307 307 'rhodecode:templates/admin/users/user_edit.mako',
308 308 self._get_template_context(c), self.request)
309 309 html = formencode.htmlfill.render(
310 310 data,
311 311 defaults=errors.value,
312 312 errors=errors.error_dict or {},
313 313 prefix_error=False,
314 314 encoding="UTF-8",
315 315 force_defaults=False
316 316 )
317 317 return Response(html)
318 318 except UserCreationError as e:
319 319 h.flash(e, 'error')
320 320 except Exception:
321 321 log.exception("Exception updating user")
322 322 h.flash(_('Error occurred during update of user %s')
323 323 % form_result.get('username'), category='error')
324 324 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
325 325
326 326 @LoginRequired()
327 327 @HasPermissionAllDecorator('hg.admin')
328 328 @CSRFRequired()
329 329 @view_config(
330 330 route_name='user_delete', request_method='POST',
331 331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 332 def user_delete(self):
333 333 _ = self.request.translate
334 334 c = self.load_default_context()
335 335 c.user = self.db_user
336 336
337 337 _repos = c.user.repositories
338 338 _repo_groups = c.user.repository_groups
339 339 _user_groups = c.user.user_groups
340 340
341 341 handle_repos = None
342 342 handle_repo_groups = None
343 343 handle_user_groups = None
344 344 # dummy call for flash of handle
345 345 set_handle_flash_repos = lambda: None
346 346 set_handle_flash_repo_groups = lambda: None
347 347 set_handle_flash_user_groups = lambda: None
348 348
349 349 if _repos and self.request.POST.get('user_repos'):
350 350 do = self.request.POST['user_repos']
351 351 if do == 'detach':
352 352 handle_repos = 'detach'
353 353 set_handle_flash_repos = lambda: h.flash(
354 354 _('Detached %s repositories') % len(_repos),
355 355 category='success')
356 356 elif do == 'delete':
357 357 handle_repos = 'delete'
358 358 set_handle_flash_repos = lambda: h.flash(
359 359 _('Deleted %s repositories') % len(_repos),
360 360 category='success')
361 361
362 362 if _repo_groups and self.request.POST.get('user_repo_groups'):
363 363 do = self.request.POST['user_repo_groups']
364 364 if do == 'detach':
365 365 handle_repo_groups = 'detach'
366 366 set_handle_flash_repo_groups = lambda: h.flash(
367 367 _('Detached %s repository groups') % len(_repo_groups),
368 368 category='success')
369 369 elif do == 'delete':
370 370 handle_repo_groups = 'delete'
371 371 set_handle_flash_repo_groups = lambda: h.flash(
372 372 _('Deleted %s repository groups') % len(_repo_groups),
373 373 category='success')
374 374
375 375 if _user_groups and self.request.POST.get('user_user_groups'):
376 376 do = self.request.POST['user_user_groups']
377 377 if do == 'detach':
378 378 handle_user_groups = 'detach'
379 379 set_handle_flash_user_groups = lambda: h.flash(
380 380 _('Detached %s user groups') % len(_user_groups),
381 381 category='success')
382 382 elif do == 'delete':
383 383 handle_user_groups = 'delete'
384 384 set_handle_flash_user_groups = lambda: h.flash(
385 385 _('Deleted %s user groups') % len(_user_groups),
386 386 category='success')
387 387
388 388 old_values = c.user.get_api_data()
389 389 try:
390 390 UserModel().delete(c.user, handle_repos=handle_repos,
391 391 handle_repo_groups=handle_repo_groups,
392 392 handle_user_groups=handle_user_groups)
393 393
394 394 audit_logger.store_web(
395 395 'user.delete', action_data={'old_data': old_values},
396 396 user=c.rhodecode_user)
397 397
398 398 Session().commit()
399 399 set_handle_flash_repos()
400 400 set_handle_flash_repo_groups()
401 401 set_handle_flash_user_groups()
402 402 h.flash(_('Successfully deleted user'), category='success')
403 403 except (UserOwnsReposException, UserOwnsRepoGroupsException,
404 404 UserOwnsUserGroupsException, DefaultUserException) as e:
405 405 h.flash(e, category='warning')
406 406 except Exception:
407 407 log.exception("Exception during deletion of user")
408 408 h.flash(_('An error occurred during deletion of user'),
409 409 category='error')
410 410 raise HTTPFound(h.route_path('users'))
411 411
412 412 @LoginRequired()
413 413 @HasPermissionAllDecorator('hg.admin')
414 414 @view_config(
415 415 route_name='user_edit', request_method='GET',
416 416 renderer='rhodecode:templates/admin/users/user_edit.mako')
417 417 def user_edit(self):
418 418 _ = self.request.translate
419 419 c = self.load_default_context()
420 420 c.user = self.db_user
421 421
422 422 c.active = 'profile'
423 423 c.extern_type = c.user.extern_type
424 424 c.extern_name = c.user.extern_name
425 425 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
426 426
427 427 defaults = c.user.get_dict()
428 428 defaults.update({'language': c.user.user_data.get('language')})
429 429
430 430 data = render(
431 431 'rhodecode:templates/admin/users/user_edit.mako',
432 432 self._get_template_context(c), self.request)
433 433 html = formencode.htmlfill.render(
434 434 data,
435 435 defaults=defaults,
436 436 encoding="UTF-8",
437 437 force_defaults=False
438 438 )
439 439 return Response(html)
440 440
441 441 @LoginRequired()
442 442 @HasPermissionAllDecorator('hg.admin')
443 443 @view_config(
444 444 route_name='user_edit_advanced', request_method='GET',
445 445 renderer='rhodecode:templates/admin/users/user_edit.mako')
446 446 def user_edit_advanced(self):
447 447 _ = self.request.translate
448 448 c = self.load_default_context()
449 449
450 450 user_id = self.db_user_id
451 451 c.user = self.db_user
452 452
453 453 c.active = 'advanced'
454 454 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
455 455 c.personal_repo_group_name = RepoGroupModel()\
456 456 .get_personal_group_name(c.user)
457 457
458 458 c.user_to_review_rules = sorted(
459 459 (x.user for x in c.user.user_review_rules),
460 460 key=lambda u: u.username.lower())
461 461
462 462 c.first_admin = User.get_first_super_admin()
463 463 defaults = c.user.get_dict()
464 464
465 465 # Interim workaround if the user participated on any pull requests as a
466 466 # reviewer.
467 467 has_review = len(c.user.reviewer_pull_requests)
468 468 c.can_delete_user = not has_review
469 469 c.can_delete_user_message = ''
470 470 inactive_link = h.link_to(
471 471 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
472 472 if has_review == 1:
473 473 c.can_delete_user_message = h.literal(_(
474 474 'The user participates as reviewer in {} pull request and '
475 475 'cannot be deleted. \nYou can set the user to '
476 476 '"{}" instead of deleting it.').format(
477 477 has_review, inactive_link))
478 478 elif has_review:
479 479 c.can_delete_user_message = h.literal(_(
480 480 'The user participates as reviewer in {} pull requests and '
481 481 'cannot be deleted. \nYou can set the user to '
482 482 '"{}" instead of deleting it.').format(
483 483 has_review, inactive_link))
484 484
485 485 data = render(
486 486 'rhodecode:templates/admin/users/user_edit.mako',
487 487 self._get_template_context(c), self.request)
488 488 html = formencode.htmlfill.render(
489 489 data,
490 490 defaults=defaults,
491 491 encoding="UTF-8",
492 492 force_defaults=False
493 493 )
494 494 return Response(html)
495 495
496 496 @LoginRequired()
497 497 @HasPermissionAllDecorator('hg.admin')
498 498 @view_config(
499 499 route_name='user_edit_global_perms', request_method='GET',
500 500 renderer='rhodecode:templates/admin/users/user_edit.mako')
501 501 def user_edit_global_perms(self):
502 502 _ = self.request.translate
503 503 c = self.load_default_context()
504 504 c.user = self.db_user
505 505
506 506 c.active = 'global_perms'
507 507
508 508 c.default_user = User.get_default_user()
509 509 defaults = c.user.get_dict()
510 510 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
511 511 defaults.update(c.default_user.get_default_perms())
512 512 defaults.update(c.user.get_default_perms())
513 513
514 514 data = render(
515 515 'rhodecode:templates/admin/users/user_edit.mako',
516 516 self._get_template_context(c), self.request)
517 517 html = formencode.htmlfill.render(
518 518 data,
519 519 defaults=defaults,
520 520 encoding="UTF-8",
521 521 force_defaults=False
522 522 )
523 523 return Response(html)
524 524
525 525 @LoginRequired()
526 526 @HasPermissionAllDecorator('hg.admin')
527 527 @CSRFRequired()
528 528 @view_config(
529 529 route_name='user_edit_global_perms_update', request_method='POST',
530 530 renderer='rhodecode:templates/admin/users/user_edit.mako')
531 531 def user_edit_global_perms_update(self):
532 532 _ = self.request.translate
533 533 c = self.load_default_context()
534 534
535 535 user_id = self.db_user_id
536 536 c.user = self.db_user
537 537
538 538 c.active = 'global_perms'
539 539 try:
540 540 # first stage that verifies the checkbox
541 541 _form = UserIndividualPermissionsForm()
542 542 form_result = _form.to_python(dict(self.request.POST))
543 543 inherit_perms = form_result['inherit_default_permissions']
544 544 c.user.inherit_default_permissions = inherit_perms
545 545 Session().add(c.user)
546 546
547 547 if not inherit_perms:
548 548 # only update the individual ones if we un check the flag
549 549 _form = UserPermissionsForm(
550 550 [x[0] for x in c.repo_create_choices],
551 551 [x[0] for x in c.repo_create_on_write_choices],
552 552 [x[0] for x in c.repo_group_create_choices],
553 553 [x[0] for x in c.user_group_create_choices],
554 554 [x[0] for x in c.fork_choices],
555 555 [x[0] for x in c.inherit_default_permission_choices])()
556 556
557 557 form_result = _form.to_python(dict(self.request.POST))
558 558 form_result.update({'perm_user_id': c.user.user_id})
559 559
560 560 PermissionModel().update_user_permissions(form_result)
561 561
562 562 # TODO(marcink): implement global permissions
563 563 # audit_log.store_web('user.edit.permissions')
564 564
565 565 Session().commit()
566 566 h.flash(_('User global permissions updated successfully'),
567 567 category='success')
568 568
569 569 except formencode.Invalid as errors:
570 570 data = render(
571 571 'rhodecode:templates/admin/users/user_edit.mako',
572 572 self._get_template_context(c), self.request)
573 573 html = formencode.htmlfill.render(
574 574 data,
575 575 defaults=errors.value,
576 576 errors=errors.error_dict or {},
577 577 prefix_error=False,
578 578 encoding="UTF-8",
579 579 force_defaults=False
580 580 )
581 581 return Response(html)
582 582 except Exception:
583 583 log.exception("Exception during permissions saving")
584 584 h.flash(_('An error occurred during permissions saving'),
585 585 category='error')
586 586 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
587 587
588 588 @LoginRequired()
589 589 @HasPermissionAllDecorator('hg.admin')
590 590 @CSRFRequired()
591 591 @view_config(
592 592 route_name='user_force_password_reset', request_method='POST',
593 593 renderer='rhodecode:templates/admin/users/user_edit.mako')
594 594 def user_force_password_reset(self):
595 595 """
596 596 toggle reset password flag for this user
597 597 """
598 598 _ = self.request.translate
599 599 c = self.load_default_context()
600 600
601 601 user_id = self.db_user_id
602 602 c.user = self.db_user
603 603
604 604 try:
605 605 old_value = c.user.user_data.get('force_password_change')
606 606 c.user.update_userdata(force_password_change=not old_value)
607 607
608 608 if old_value:
609 609 msg = _('Force password change disabled for user')
610 610 audit_logger.store_web(
611 611 'user.edit.password_reset.disabled',
612 612 user=c.rhodecode_user)
613 613 else:
614 614 msg = _('Force password change enabled for user')
615 615 audit_logger.store_web(
616 616 'user.edit.password_reset.enabled',
617 617 user=c.rhodecode_user)
618 618
619 619 Session().commit()
620 620 h.flash(msg, category='success')
621 621 except Exception:
622 622 log.exception("Exception during password reset for user")
623 623 h.flash(_('An error occurred during password reset for user'),
624 624 category='error')
625 625
626 626 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
627 627
628 628 @LoginRequired()
629 629 @HasPermissionAllDecorator('hg.admin')
630 630 @CSRFRequired()
631 631 @view_config(
632 632 route_name='user_create_personal_repo_group', request_method='POST',
633 633 renderer='rhodecode:templates/admin/users/user_edit.mako')
634 634 def user_create_personal_repo_group(self):
635 635 """
636 636 Create personal repository group for this user
637 637 """
638 638 from rhodecode.model.repo_group import RepoGroupModel
639 639
640 640 _ = self.request.translate
641 641 c = self.load_default_context()
642 642
643 643 user_id = self.db_user_id
644 644 c.user = self.db_user
645 645
646 646 personal_repo_group = RepoGroup.get_user_personal_repo_group(
647 647 c.user.user_id)
648 648 if personal_repo_group:
649 649 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
650 650
651 651 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
652 652 c.user)
653 653 named_personal_group = RepoGroup.get_by_group_name(
654 654 personal_repo_group_name)
655 655 try:
656 656
657 657 if named_personal_group and named_personal_group.user_id == c.user.user_id:
658 658 # migrate the same named group, and mark it as personal
659 659 named_personal_group.personal = True
660 660 Session().add(named_personal_group)
661 661 Session().commit()
662 662 msg = _('Linked repository group `%s` as personal' % (
663 663 personal_repo_group_name,))
664 664 h.flash(msg, category='success')
665 665 elif not named_personal_group:
666 666 RepoGroupModel().create_personal_repo_group(c.user)
667 667
668 668 msg = _('Created repository group `%s`' % (
669 669 personal_repo_group_name,))
670 670 h.flash(msg, category='success')
671 671 else:
672 672 msg = _('Repository group `%s` is already taken' % (
673 673 personal_repo_group_name,))
674 674 h.flash(msg, category='warning')
675 675 except Exception:
676 676 log.exception("Exception during repository group creation")
677 677 msg = _(
678 678 'An error occurred during repository group creation for user')
679 679 h.flash(msg, category='error')
680 680 Session().rollback()
681 681
682 682 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
683 683
684 684 @LoginRequired()
685 685 @HasPermissionAllDecorator('hg.admin')
686 686 @view_config(
687 687 route_name='edit_user_auth_tokens', request_method='GET',
688 688 renderer='rhodecode:templates/admin/users/user_edit.mako')
689 689 def auth_tokens(self):
690 690 _ = self.request.translate
691 691 c = self.load_default_context()
692 692 c.user = self.db_user
693 693
694 694 c.active = 'auth_tokens'
695 695
696 696 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
697 697 c.role_values = [
698 698 (x, AuthTokenModel.cls._get_role_name(x))
699 699 for x in AuthTokenModel.cls.ROLES]
700 700 c.role_options = [(c.role_values, _("Role"))]
701 701 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
702 702 c.user.user_id, show_expired=True)
703 703 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
704 704 return self._get_template_context(c)
705 705
706 706 def maybe_attach_token_scope(self, token):
707 707 # implemented in EE edition
708 708 pass
709 709
710 710 @LoginRequired()
711 711 @HasPermissionAllDecorator('hg.admin')
712 712 @CSRFRequired()
713 713 @view_config(
714 714 route_name='edit_user_auth_tokens_add', request_method='POST')
715 715 def auth_tokens_add(self):
716 716 _ = self.request.translate
717 717 c = self.load_default_context()
718 718
719 719 user_id = self.db_user_id
720 720 c.user = self.db_user
721 721
722 722 user_data = c.user.get_api_data()
723 723 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
724 724 description = self.request.POST.get('description')
725 725 role = self.request.POST.get('role')
726 726
727 727 token = AuthTokenModel().create(
728 728 c.user.user_id, description, lifetime, role)
729 729 token_data = token.get_api_data()
730 730
731 731 self.maybe_attach_token_scope(token)
732 732 audit_logger.store_web(
733 733 'user.edit.token.add', action_data={
734 734 'data': {'token': token_data, 'user': user_data}},
735 735 user=self._rhodecode_user, )
736 736 Session().commit()
737 737
738 738 h.flash(_("Auth token successfully created"), category='success')
739 739 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
740 740
741 741 @LoginRequired()
742 742 @HasPermissionAllDecorator('hg.admin')
743 743 @CSRFRequired()
744 744 @view_config(
745 745 route_name='edit_user_auth_tokens_delete', request_method='POST')
746 746 def auth_tokens_delete(self):
747 747 _ = self.request.translate
748 748 c = self.load_default_context()
749 749
750 750 user_id = self.db_user_id
751 751 c.user = self.db_user
752 752
753 753 user_data = c.user.get_api_data()
754 754
755 755 del_auth_token = self.request.POST.get('del_auth_token')
756 756
757 757 if del_auth_token:
758 758 token = UserApiKeys.get_or_404(del_auth_token)
759 759 token_data = token.get_api_data()
760 760
761 761 AuthTokenModel().delete(del_auth_token, c.user.user_id)
762 762 audit_logger.store_web(
763 763 'user.edit.token.delete', action_data={
764 764 'data': {'token': token_data, 'user': user_data}},
765 765 user=self._rhodecode_user,)
766 766 Session().commit()
767 767 h.flash(_("Auth token successfully deleted"), category='success')
768 768
769 769 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
770 770
771 771 @LoginRequired()
772 772 @HasPermissionAllDecorator('hg.admin')
773 773 @view_config(
774 774 route_name='edit_user_ssh_keys', request_method='GET',
775 775 renderer='rhodecode:templates/admin/users/user_edit.mako')
776 776 def ssh_keys(self):
777 777 _ = self.request.translate
778 778 c = self.load_default_context()
779 779 c.user = self.db_user
780 780
781 781 c.active = 'ssh_keys'
782 782 c.default_key = self.request.GET.get('default_key')
783 783 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
784 784 return self._get_template_context(c)
785 785
786 786 @LoginRequired()
787 787 @HasPermissionAllDecorator('hg.admin')
788 788 @view_config(
789 789 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
790 790 renderer='rhodecode:templates/admin/users/user_edit.mako')
791 791 def ssh_keys_generate_keypair(self):
792 792 _ = self.request.translate
793 793 c = self.load_default_context()
794 794
795 795 c.user = self.db_user
796 796
797 797 c.active = 'ssh_keys_generate'
798 798 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
799 799 c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
800 800
801 801 return self._get_template_context(c)
802 802
803 803 @LoginRequired()
804 804 @HasPermissionAllDecorator('hg.admin')
805 805 @CSRFRequired()
806 806 @view_config(
807 807 route_name='edit_user_ssh_keys_add', request_method='POST')
808 808 def ssh_keys_add(self):
809 809 _ = self.request.translate
810 810 c = self.load_default_context()
811 811
812 812 user_id = self.db_user_id
813 813 c.user = self.db_user
814 814
815 815 user_data = c.user.get_api_data()
816 816 key_data = self.request.POST.get('key_data')
817 817 description = self.request.POST.get('description')
818 818
819 819 try:
820 820 if not key_data:
821 821 raise ValueError('Please add a valid public key')
822 822
823 823 key = SshKeyModel().parse_key(key_data.strip())
824 824 fingerprint = key.hash_md5()
825 825
826 826 ssh_key = SshKeyModel().create(
827 827 c.user.user_id, fingerprint, key_data, description)
828 828 ssh_key_data = ssh_key.get_api_data()
829 829
830 830 audit_logger.store_web(
831 831 'user.edit.ssh_key.add', action_data={
832 832 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
833 833 user=self._rhodecode_user, )
834 834 Session().commit()
835 835
836 836 # Trigger an event on change of keys.
837 837 trigger(SshKeyFileChangeEvent(), self.request.registry)
838 838
839 839 h.flash(_("Ssh Key successfully created"), category='success')
840 840
841 841 except IntegrityError:
842 842 log.exception("Exception during ssh key saving")
843 843 h.flash(_('An error occurred during ssh key saving: {}').format(
844 844 'Such key already exists, please use a different one'),
845 845 category='error')
846 846 except Exception as e:
847 847 log.exception("Exception during ssh key saving")
848 848 h.flash(_('An error occurred during ssh key saving: {}').format(e),
849 849 category='error')
850 850
851 851 return HTTPFound(
852 852 h.route_path('edit_user_ssh_keys', user_id=user_id))
853 853
854 854 @LoginRequired()
855 855 @HasPermissionAllDecorator('hg.admin')
856 856 @CSRFRequired()
857 857 @view_config(
858 858 route_name='edit_user_ssh_keys_delete', request_method='POST')
859 859 def ssh_keys_delete(self):
860 860 _ = self.request.translate
861 861 c = self.load_default_context()
862 862
863 863 user_id = self.db_user_id
864 864 c.user = self.db_user
865 865
866 866 user_data = c.user.get_api_data()
867 867
868 868 del_ssh_key = self.request.POST.get('del_ssh_key')
869 869
870 870 if del_ssh_key:
871 871 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
872 872 ssh_key_data = ssh_key.get_api_data()
873 873
874 874 SshKeyModel().delete(del_ssh_key, c.user.user_id)
875 875 audit_logger.store_web(
876 876 'user.edit.ssh_key.delete', action_data={
877 877 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
878 878 user=self._rhodecode_user,)
879 879 Session().commit()
880 880 # Trigger an event on change of keys.
881 881 trigger(SshKeyFileChangeEvent(), self.request.registry)
882 882 h.flash(_("Ssh key successfully deleted"), category='success')
883 883
884 884 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
885 885
886 886 @LoginRequired()
887 887 @HasPermissionAllDecorator('hg.admin')
888 888 @view_config(
889 889 route_name='edit_user_emails', request_method='GET',
890 890 renderer='rhodecode:templates/admin/users/user_edit.mako')
891 891 def emails(self):
892 892 _ = self.request.translate
893 893 c = self.load_default_context()
894 894 c.user = self.db_user
895 895
896 896 c.active = 'emails'
897 897 c.user_email_map = UserEmailMap.query() \
898 898 .filter(UserEmailMap.user == c.user).all()
899 899
900 900 return self._get_template_context(c)
901 901
902 902 @LoginRequired()
903 903 @HasPermissionAllDecorator('hg.admin')
904 904 @CSRFRequired()
905 905 @view_config(
906 906 route_name='edit_user_emails_add', request_method='POST')
907 907 def emails_add(self):
908 908 _ = self.request.translate
909 909 c = self.load_default_context()
910 910
911 911 user_id = self.db_user_id
912 912 c.user = self.db_user
913 913
914 914 email = self.request.POST.get('new_email')
915 915 user_data = c.user.get_api_data()
916 916 try:
917 917 UserModel().add_extra_email(c.user.user_id, email)
918 918 audit_logger.store_web(
919 919 'user.edit.email.add',
920 920 action_data={'email': email, 'user': user_data},
921 921 user=self._rhodecode_user)
922 922 Session().commit()
923 923 h.flash(_("Added new email address `%s` for user account") % email,
924 924 category='success')
925 925 except formencode.Invalid as error:
926 926 h.flash(h.escape(error.error_dict['email']), category='error')
927 927 except IntegrityError:
928 928 log.warning("Email %s already exists", email)
929 929 h.flash(_('Email `{}` is already registered for another user.').format(email),
930 930 category='error')
931 931 except Exception:
932 932 log.exception("Exception during email saving")
933 933 h.flash(_('An error occurred during email saving'),
934 934 category='error')
935 935 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
936 936
937 937 @LoginRequired()
938 938 @HasPermissionAllDecorator('hg.admin')
939 939 @CSRFRequired()
940 940 @view_config(
941 941 route_name='edit_user_emails_delete', request_method='POST')
942 942 def emails_delete(self):
943 943 _ = self.request.translate
944 944 c = self.load_default_context()
945 945
946 946 user_id = self.db_user_id
947 947 c.user = self.db_user
948 948
949 949 email_id = self.request.POST.get('del_email_id')
950 950 user_model = UserModel()
951 951
952 952 email = UserEmailMap.query().get(email_id).email
953 953 user_data = c.user.get_api_data()
954 954 user_model.delete_extra_email(c.user.user_id, email_id)
955 955 audit_logger.store_web(
956 956 'user.edit.email.delete',
957 957 action_data={'email': email, 'user': user_data},
958 958 user=self._rhodecode_user)
959 959 Session().commit()
960 960 h.flash(_("Removed email address from user account"),
961 961 category='success')
962 962 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
963 963
964 964 @LoginRequired()
965 965 @HasPermissionAllDecorator('hg.admin')
966 966 @view_config(
967 967 route_name='edit_user_ips', request_method='GET',
968 968 renderer='rhodecode:templates/admin/users/user_edit.mako')
969 969 def ips(self):
970 970 _ = self.request.translate
971 971 c = self.load_default_context()
972 972 c.user = self.db_user
973 973
974 974 c.active = 'ips'
975 975 c.user_ip_map = UserIpMap.query() \
976 976 .filter(UserIpMap.user == c.user).all()
977 977
978 978 c.inherit_default_ips = c.user.inherit_default_permissions
979 979 c.default_user_ip_map = UserIpMap.query() \
980 980 .filter(UserIpMap.user == User.get_default_user()).all()
981 981
982 982 return self._get_template_context(c)
983 983
984 984 @LoginRequired()
985 985 @HasPermissionAllDecorator('hg.admin')
986 986 @CSRFRequired()
987 987 @view_config(
988 988 route_name='edit_user_ips_add', request_method='POST')
989 989 # NOTE(marcink): this view is allowed for default users, as we can
990 990 # edit their IP white list
991 991 def ips_add(self):
992 992 _ = self.request.translate
993 993 c = self.load_default_context()
994 994
995 995 user_id = self.db_user_id
996 996 c.user = self.db_user
997 997
998 998 user_model = UserModel()
999 999 desc = self.request.POST.get('description')
1000 1000 try:
1001 1001 ip_list = user_model.parse_ip_range(
1002 1002 self.request.POST.get('new_ip'))
1003 1003 except Exception as e:
1004 1004 ip_list = []
1005 1005 log.exception("Exception during ip saving")
1006 1006 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1007 1007 category='error')
1008 1008 added = []
1009 1009 user_data = c.user.get_api_data()
1010 1010 for ip in ip_list:
1011 1011 try:
1012 1012 user_model.add_extra_ip(c.user.user_id, ip, desc)
1013 1013 audit_logger.store_web(
1014 1014 'user.edit.ip.add',
1015 1015 action_data={'ip': ip, 'user': user_data},
1016 1016 user=self._rhodecode_user)
1017 1017 Session().commit()
1018 1018 added.append(ip)
1019 1019 except formencode.Invalid as error:
1020 1020 msg = error.error_dict['ip']
1021 1021 h.flash(msg, category='error')
1022 1022 except Exception:
1023 1023 log.exception("Exception during ip saving")
1024 1024 h.flash(_('An error occurred during ip saving'),
1025 1025 category='error')
1026 1026 if added:
1027 1027 h.flash(
1028 1028 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1029 1029 category='success')
1030 1030 if 'default_user' in self.request.POST:
1031 1031 # case for editing global IP list we do it for 'DEFAULT' user
1032 1032 raise HTTPFound(h.route_path('admin_permissions_ips'))
1033 1033 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1034 1034
1035 1035 @LoginRequired()
1036 1036 @HasPermissionAllDecorator('hg.admin')
1037 1037 @CSRFRequired()
1038 1038 @view_config(
1039 1039 route_name='edit_user_ips_delete', request_method='POST')
1040 1040 # NOTE(marcink): this view is allowed for default users, as we can
1041 1041 # edit their IP white list
1042 1042 def ips_delete(self):
1043 1043 _ = self.request.translate
1044 1044 c = self.load_default_context()
1045 1045
1046 1046 user_id = self.db_user_id
1047 1047 c.user = self.db_user
1048 1048
1049 1049 ip_id = self.request.POST.get('del_ip_id')
1050 1050 user_model = UserModel()
1051 1051 user_data = c.user.get_api_data()
1052 1052 ip = UserIpMap.query().get(ip_id).ip_addr
1053 1053 user_model.delete_extra_ip(c.user.user_id, ip_id)
1054 1054 audit_logger.store_web(
1055 1055 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1056 1056 user=self._rhodecode_user)
1057 1057 Session().commit()
1058 1058 h.flash(_("Removed ip address from user whitelist"), category='success')
1059 1059
1060 1060 if 'default_user' in self.request.POST:
1061 1061 # case for editing global IP list we do it for 'DEFAULT' user
1062 1062 raise HTTPFound(h.route_path('admin_permissions_ips'))
1063 1063 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1064 1064
1065 1065 @LoginRequired()
1066 1066 @HasPermissionAllDecorator('hg.admin')
1067 1067 @view_config(
1068 1068 route_name='edit_user_groups_management', request_method='GET',
1069 1069 renderer='rhodecode:templates/admin/users/user_edit.mako')
1070 1070 def groups_management(self):
1071 1071 c = self.load_default_context()
1072 1072 c.user = self.db_user
1073 1073 c.data = c.user.group_member
1074 1074
1075 1075 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1076 1076 for group in c.user.group_member]
1077 1077 c.groups = json.dumps(groups)
1078 1078 c.active = 'groups'
1079 1079
1080 1080 return self._get_template_context(c)
1081 1081
1082 1082 @LoginRequired()
1083 1083 @HasPermissionAllDecorator('hg.admin')
1084 1084 @CSRFRequired()
1085 1085 @view_config(
1086 1086 route_name='edit_user_groups_management_updates', request_method='POST')
1087 1087 def groups_management_updates(self):
1088 1088 _ = self.request.translate
1089 1089 c = self.load_default_context()
1090 1090
1091 1091 user_id = self.db_user_id
1092 1092 c.user = self.db_user
1093 1093
1094 1094 user_groups = set(self.request.POST.getall('users_group_id'))
1095 1095 user_groups_objects = []
1096 1096
1097 1097 for ugid in user_groups:
1098 1098 user_groups_objects.append(
1099 1099 UserGroupModel().get_group(safe_int(ugid)))
1100 1100 user_group_model = UserGroupModel()
1101 1101 added_to_groups, removed_from_groups = \
1102 1102 user_group_model.change_groups(c.user, user_groups_objects)
1103 1103
1104 1104 user_data = c.user.get_api_data()
1105 1105 for user_group_id in added_to_groups:
1106 1106 user_group = UserGroup.get(user_group_id)
1107 1107 old_values = user_group.get_api_data()
1108 1108 audit_logger.store_web(
1109 1109 'user_group.edit.member.add',
1110 1110 action_data={'user': user_data, 'old_data': old_values},
1111 1111 user=self._rhodecode_user)
1112 1112
1113 1113 for user_group_id in removed_from_groups:
1114 1114 user_group = UserGroup.get(user_group_id)
1115 1115 old_values = user_group.get_api_data()
1116 1116 audit_logger.store_web(
1117 1117 'user_group.edit.member.delete',
1118 1118 action_data={'user': user_data, 'old_data': old_values},
1119 1119 user=self._rhodecode_user)
1120 1120
1121 1121 Session().commit()
1122 1122 c.active = 'user_groups_management'
1123 1123 h.flash(_("Groups successfully changed"), category='success')
1124 1124
1125 1125 return HTTPFound(h.route_path(
1126 1126 'edit_user_groups_management', user_id=user_id))
1127 1127
1128 1128 @LoginRequired()
1129 1129 @HasPermissionAllDecorator('hg.admin')
1130 1130 @view_config(
1131 1131 route_name='edit_user_audit_logs', request_method='GET',
1132 1132 renderer='rhodecode:templates/admin/users/user_edit.mako')
1133 1133 def user_audit_logs(self):
1134 1134 _ = self.request.translate
1135 1135 c = self.load_default_context()
1136 1136 c.user = self.db_user
1137 1137
1138 1138 c.active = 'audit'
1139 1139
1140 1140 p = safe_int(self.request.GET.get('page', 1), 1)
1141 1141
1142 1142 filter_term = self.request.GET.get('filter')
1143 1143 user_log = UserModel().get_user_log(c.user, filter_term)
1144 1144
1145 1145 def url_generator(**kw):
1146 1146 if filter_term:
1147 1147 kw['filter'] = filter_term
1148 1148 return self.request.current_route_path(_query=kw)
1149 1149
1150 1150 c.audit_logs = h.Page(
1151 1151 user_log, page=p, items_per_page=10, url=url_generator)
1152 1152 c.filter_term = filter_term
1153 1153 return self._get_template_context(c)
1154 1154
1155 1155 @LoginRequired()
1156 1156 @HasPermissionAllDecorator('hg.admin')
1157 1157 @view_config(
1158 1158 route_name='edit_user_perms_summary', request_method='GET',
1159 1159 renderer='rhodecode:templates/admin/users/user_edit.mako')
1160 1160 def user_perms_summary(self):
1161 1161 _ = self.request.translate
1162 1162 c = self.load_default_context()
1163 1163 c.user = self.db_user
1164 1164
1165 1165 c.active = 'perms_summary'
1166 1166 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1167 1167
1168 1168 return self._get_template_context(c)
1169 1169
1170 1170 @LoginRequired()
1171 1171 @HasPermissionAllDecorator('hg.admin')
1172 1172 @view_config(
1173 1173 route_name='edit_user_perms_summary_json', request_method='GET',
1174 1174 renderer='json_ext')
1175 1175 def user_perms_summary_json(self):
1176 1176 self.load_default_context()
1177 1177 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1178 1178
1179 1179 return perm_user.permissions
@@ -1,413 +1,413 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import time
22 22 import logging
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27
28 28 from pyramid.httpexceptions import HTTPNotFound, HTTPFound
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31 from pyramid.response import Response
32 32
33 33 from rhodecode.apps._base import BaseAppView
34 34 from rhodecode.lib import helpers as h
35 35 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
36 36 from rhodecode.lib.utils2 import time_to_datetime
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
39 39 from rhodecode.model.gist import GistModel
40 40 from rhodecode.model.meta import Session
41 41 from rhodecode.model.db import Gist, User, or_
42 42 from rhodecode.model import validation_schema
43 43 from rhodecode.model.validation_schema.schemas import gist_schema
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class GistView(BaseAppView):
50 50
51 51 def load_default_context(self):
52 52 _ = self.request.translate
53 53 c = self._get_local_tmpl_context()
54 54 c.user = c.auth_user.get_instance()
55 55
56 56 c.lifetime_values = [
57 57 (-1, _('forever')),
58 58 (5, _('5 minutes')),
59 59 (60, _('1 hour')),
60 60 (60 * 24, _('1 day')),
61 61 (60 * 24 * 30, _('1 month')),
62 62 ]
63 63
64 64 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
65 65 c.acl_options = [
66 66 (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
67 67 (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
68 68 ]
69 69
70 70 self._register_global_c(c)
71 71 return c
72 72
73 73 @LoginRequired()
74 74 @view_config(
75 75 route_name='gists_show', request_method='GET',
76 76 renderer='rhodecode:templates/admin/gists/index.mako')
77 77 def gist_show_all(self):
78 78 c = self.load_default_context()
79 79
80 80 not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
81 81 c.show_private = self.request.GET.get('private') and not_default_user
82 82 c.show_public = self.request.GET.get('public') and not_default_user
83 83 c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
84 84
85 85 gists = _gists = Gist().query()\
86 86 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
87 87 .order_by(Gist.created_on.desc())
88 88
89 89 c.active = 'public'
90 90 # MY private
91 91 if c.show_private and not c.show_public:
92 92 gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
93 93 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
94 94 c.active = 'my_private'
95 95 # MY public
96 96 elif c.show_public and not c.show_private:
97 97 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
98 98 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
99 99 c.active = 'my_public'
100 100 # MY public+private
101 101 elif c.show_private and c.show_public:
102 102 gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
103 103 Gist.gist_type == Gist.GIST_PRIVATE))\
104 104 .filter(Gist.gist_owner == self._rhodecode_user.user_id)
105 105 c.active = 'my_all'
106 106 # Show all by super-admin
107 107 elif c.show_all:
108 108 c.active = 'all'
109 109 gists = _gists
110 110
111 111 # default show ALL public gists
112 112 if not c.show_public and not c.show_private and not c.show_all:
113 113 gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
114 114 c.active = 'public'
115 115
116 116 _render = self.request.get_partial_renderer(
117 'data_table/_dt_elements.mako')
117 'rhodecode:templates/data_table/_dt_elements.mako')
118 118
119 119 data = []
120 120
121 121 for gist in gists:
122 122 data.append({
123 123 'created_on': _render('gist_created', gist.created_on),
124 124 'created_on_raw': gist.created_on,
125 125 'type': _render('gist_type', gist.gist_type),
126 126 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
127 127 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
128 128 'author_raw': h.escape(gist.owner.full_contact),
129 129 'expires': _render('gist_expires', gist.gist_expires),
130 130 'description': _render('gist_description', gist.gist_description)
131 131 })
132 132 c.data = json.dumps(data)
133 133
134 134 return self._get_template_context(c)
135 135
136 136 @LoginRequired()
137 137 @NotAnonymous()
138 138 @view_config(
139 139 route_name='gists_new', request_method='GET',
140 140 renderer='rhodecode:templates/admin/gists/new.mako')
141 141 def gist_new(self):
142 142 c = self.load_default_context()
143 143 return self._get_template_context(c)
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @CSRFRequired()
148 148 @view_config(
149 149 route_name='gists_create', request_method='POST',
150 150 renderer='rhodecode:templates/admin/gists/new.mako')
151 151 def gist_create(self):
152 152 _ = self.request.translate
153 153 c = self.load_default_context()
154 154
155 155 data = dict(self.request.POST)
156 156 data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
157 157 data['nodes'] = [{
158 158 'filename': data['filename'],
159 159 'content': data.get('content'),
160 160 'mimetype': data.get('mimetype') # None is autodetect
161 161 }]
162 162
163 163 data['gist_type'] = (
164 164 Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
165 165 data['gist_acl_level'] = (
166 166 data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
167 167
168 168 schema = gist_schema.GistSchema().bind(
169 169 lifetime_options=[x[0] for x in c.lifetime_values])
170 170
171 171 try:
172 172
173 173 schema_data = schema.deserialize(data)
174 174 # convert to safer format with just KEYs so we sure no duplicates
175 175 schema_data['nodes'] = gist_schema.sequence_to_nodes(
176 176 schema_data['nodes'])
177 177
178 178 gist = GistModel().create(
179 179 gist_id=schema_data['gistid'], # custom access id not real ID
180 180 description=schema_data['description'],
181 181 owner=self._rhodecode_user.user_id,
182 182 gist_mapping=schema_data['nodes'],
183 183 gist_type=schema_data['gist_type'],
184 184 lifetime=schema_data['lifetime'],
185 185 gist_acl_level=schema_data['gist_acl_level']
186 186 )
187 187 Session().commit()
188 188 new_gist_id = gist.gist_access_id
189 189 except validation_schema.Invalid as errors:
190 190 defaults = data
191 191 errors = errors.asdict()
192 192
193 193 if 'nodes.0.content' in errors:
194 194 errors['content'] = errors['nodes.0.content']
195 195 del errors['nodes.0.content']
196 196 if 'nodes.0.filename' in errors:
197 197 errors['filename'] = errors['nodes.0.filename']
198 198 del errors['nodes.0.filename']
199 199
200 200 data = render('rhodecode:templates/admin/gists/new.mako',
201 201 self._get_template_context(c), self.request)
202 202 html = formencode.htmlfill.render(
203 203 data,
204 204 defaults=defaults,
205 205 errors=errors,
206 206 prefix_error=False,
207 207 encoding="UTF-8",
208 208 force_defaults=False
209 209 )
210 210 return Response(html)
211 211
212 212 except Exception:
213 213 log.exception("Exception while trying to create a gist")
214 214 h.flash(_('Error occurred during gist creation'), category='error')
215 215 raise HTTPFound(h.route_url('gists_new'))
216 216 raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
217 217
218 218 @LoginRequired()
219 219 @NotAnonymous()
220 220 @CSRFRequired()
221 221 @view_config(
222 222 route_name='gist_delete', request_method='POST')
223 223 def gist_delete(self):
224 224 _ = self.request.translate
225 225 gist_id = self.request.matchdict['gist_id']
226 226
227 227 c = self.load_default_context()
228 228 c.gist = Gist.get_or_404(gist_id)
229 229
230 230 owner = c.gist.gist_owner == self._rhodecode_user.user_id
231 231 if not (h.HasPermissionAny('hg.admin')() or owner):
232 232 log.warning('Deletion of Gist was forbidden '
233 233 'by unauthorized user: `%s`', self._rhodecode_user)
234 234 raise HTTPNotFound()
235 235
236 236 GistModel().delete(c.gist)
237 237 Session().commit()
238 238 h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
239 239
240 240 raise HTTPFound(h.route_url('gists_show'))
241 241
242 242 def _get_gist(self, gist_id):
243 243
244 244 gist = Gist.get_or_404(gist_id)
245 245
246 246 # Check if this gist is expired
247 247 if gist.gist_expires != -1:
248 248 if time.time() > gist.gist_expires:
249 249 log.error(
250 250 'Gist expired at %s', time_to_datetime(gist.gist_expires))
251 251 raise HTTPNotFound()
252 252
253 253 # check if this gist requires a login
254 254 is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
255 255 if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
256 256 log.error("Anonymous user %s tried to access protected gist `%s`",
257 257 self._rhodecode_user, gist_id)
258 258 raise HTTPNotFound()
259 259 return gist
260 260
261 261 @LoginRequired()
262 262 @view_config(
263 263 route_name='gist_show', request_method='GET',
264 264 renderer='rhodecode:templates/admin/gists/show.mako')
265 265 @view_config(
266 266 route_name='gist_show_rev', request_method='GET',
267 267 renderer='rhodecode:templates/admin/gists/show.mako')
268 268 @view_config(
269 269 route_name='gist_show_formatted', request_method='GET',
270 270 renderer=None)
271 271 @view_config(
272 272 route_name='gist_show_formatted_path', request_method='GET',
273 273 renderer=None)
274 274 def gist_show(self):
275 275 gist_id = self.request.matchdict['gist_id']
276 276
277 277 # TODO(marcink): expose those via matching dict
278 278 revision = self.request.matchdict.get('revision', 'tip')
279 279 f_path = self.request.matchdict.get('f_path', None)
280 280 return_format = self.request.matchdict.get('format')
281 281
282 282 c = self.load_default_context()
283 283 c.gist = self._get_gist(gist_id)
284 284 c.render = not self.request.GET.get('no-render', False)
285 285
286 286 try:
287 287 c.file_last_commit, c.files = GistModel().get_gist_files(
288 288 gist_id, revision=revision)
289 289 except VCSError:
290 290 log.exception("Exception in gist show")
291 291 raise HTTPNotFound()
292 292
293 293 if return_format == 'raw':
294 294 content = '\n\n'.join([f.content for f in c.files
295 295 if (f_path is None or f.path == f_path)])
296 296 response = Response(content)
297 297 response.content_type = 'text/plain'
298 298 return response
299 299
300 300 return self._get_template_context(c)
301 301
302 302 @LoginRequired()
303 303 @NotAnonymous()
304 304 @view_config(
305 305 route_name='gist_edit', request_method='GET',
306 306 renderer='rhodecode:templates/admin/gists/edit.mako')
307 307 def gist_edit(self):
308 308 _ = self.request.translate
309 309 gist_id = self.request.matchdict['gist_id']
310 310 c = self.load_default_context()
311 311 c.gist = self._get_gist(gist_id)
312 312
313 313 owner = c.gist.gist_owner == self._rhodecode_user.user_id
314 314 if not (h.HasPermissionAny('hg.admin')() or owner):
315 315 raise HTTPNotFound()
316 316
317 317 try:
318 318 c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
319 319 except VCSError:
320 320 log.exception("Exception in gist edit")
321 321 raise HTTPNotFound()
322 322
323 323 if c.gist.gist_expires == -1:
324 324 expiry = _('never')
325 325 else:
326 326 # this cannot use timeago, since it's used in select2 as a value
327 327 expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
328 328
329 329 c.lifetime_values.append(
330 330 (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
331 331 )
332 332
333 333 return self._get_template_context(c)
334 334
335 335 @LoginRequired()
336 336 @NotAnonymous()
337 337 @CSRFRequired()
338 338 @view_config(
339 339 route_name='gist_update', request_method='POST',
340 340 renderer='rhodecode:templates/admin/gists/edit.mako')
341 341 def gist_update(self):
342 342 _ = self.request.translate
343 343 gist_id = self.request.matchdict['gist_id']
344 344 c = self.load_default_context()
345 345 c.gist = self._get_gist(gist_id)
346 346
347 347 owner = c.gist.gist_owner == self._rhodecode_user.user_id
348 348 if not (h.HasPermissionAny('hg.admin')() or owner):
349 349 raise HTTPNotFound()
350 350
351 351 data = peppercorn.parse(self.request.POST.items())
352 352
353 353 schema = gist_schema.GistSchema()
354 354 schema = schema.bind(
355 355 # '0' is special value to leave lifetime untouched
356 356 lifetime_options=[x[0] for x in c.lifetime_values] + [0],
357 357 )
358 358
359 359 try:
360 360 schema_data = schema.deserialize(data)
361 361 # convert to safer format with just KEYs so we sure no duplicates
362 362 schema_data['nodes'] = gist_schema.sequence_to_nodes(
363 363 schema_data['nodes'])
364 364
365 365 GistModel().update(
366 366 gist=c.gist,
367 367 description=schema_data['description'],
368 368 owner=c.gist.owner,
369 369 gist_mapping=schema_data['nodes'],
370 370 lifetime=schema_data['lifetime'],
371 371 gist_acl_level=schema_data['gist_acl_level']
372 372 )
373 373
374 374 Session().commit()
375 375 h.flash(_('Successfully updated gist content'), category='success')
376 376 except NodeNotChangedError:
377 377 # raised if nothing was changed in repo itself. We anyway then
378 378 # store only DB stuff for gist
379 379 Session().commit()
380 380 h.flash(_('Successfully updated gist data'), category='success')
381 381 except validation_schema.Invalid as errors:
382 382 errors = h.escape(errors.asdict())
383 383 h.flash(_('Error occurred during update of gist {}: {}').format(
384 384 gist_id, errors), category='error')
385 385 except Exception:
386 386 log.exception("Exception in gist edit")
387 387 h.flash(_('Error occurred during update of gist %s') % gist_id,
388 388 category='error')
389 389
390 390 raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
391 391
392 392 @LoginRequired()
393 393 @NotAnonymous()
394 394 @view_config(
395 395 route_name='gist_edit_check_revision', request_method='GET',
396 396 renderer='json_ext')
397 397 def gist_edit_check_revision(self):
398 398 _ = self.request.translate
399 399 gist_id = self.request.matchdict['gist_id']
400 400 c = self.load_default_context()
401 401 c.gist = self._get_gist(gist_id)
402 402
403 403 last_rev = c.gist.scm_instance().get_commit()
404 404 success = True
405 405 revision = self.request.GET.get('revision')
406 406
407 407 if revision != last_rev.raw_id:
408 408 log.error('Last revision %s is different then submitted %s'
409 409 % (revision, last_rev))
410 410 # our gist has newer version than we
411 411 success = False
412 412
413 413 return {'success': success}
@@ -1,580 +1,580 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode.apps._base import BaseAppView, DataGridAppView
32 32 from rhodecode import forms
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib import audit_logger
35 35 from rhodecode.lib.ext_json import json
36 36 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
37 37 from rhodecode.lib.channelstream import (
38 38 channelstream_request, ChannelstreamException)
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
44 44 PullRequest)
45 45 from rhodecode.model.forms import UserForm
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.scm import RepoList
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.repo import RepoModel
51 51 from rhodecode.model.validation_schema.schemas import user_schema
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class MyAccountView(BaseAppView, DataGridAppView):
57 57 ALLOW_SCOPED_TOKENS = False
58 58 """
59 59 This view has alternative version inside EE, if modified please take a look
60 60 in there as well.
61 61 """
62 62
63 63 def load_default_context(self):
64 64 c = self._get_local_tmpl_context()
65 65 c.user = c.auth_user.get_instance()
66 66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 67 self._register_global_c(c)
68 68 return c
69 69
70 70 @LoginRequired()
71 71 @NotAnonymous()
72 72 @view_config(
73 73 route_name='my_account_profile', request_method='GET',
74 74 renderer='rhodecode:templates/admin/my_account/my_account.mako')
75 75 def my_account_profile(self):
76 76 c = self.load_default_context()
77 77 c.active = 'profile'
78 78 return self._get_template_context(c)
79 79
80 80 @LoginRequired()
81 81 @NotAnonymous()
82 82 @view_config(
83 83 route_name='my_account_password', request_method='GET',
84 84 renderer='rhodecode:templates/admin/my_account/my_account.mako')
85 85 def my_account_password(self):
86 86 c = self.load_default_context()
87 87 c.active = 'password'
88 88 c.extern_type = c.user.extern_type
89 89
90 90 schema = user_schema.ChangePasswordSchema().bind(
91 91 username=c.user.username)
92 92
93 93 form = forms.Form(
94 94 schema,
95 95 action=h.route_path('my_account_password_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 c.form = form
99 99 return self._get_template_context(c)
100 100
101 101 @LoginRequired()
102 102 @NotAnonymous()
103 103 @CSRFRequired()
104 104 @view_config(
105 105 route_name='my_account_password_update', request_method='POST',
106 106 renderer='rhodecode:templates/admin/my_account/my_account.mako')
107 107 def my_account_password_update(self):
108 108 _ = self.request.translate
109 109 c = self.load_default_context()
110 110 c.active = 'password'
111 111 c.extern_type = c.user.extern_type
112 112
113 113 schema = user_schema.ChangePasswordSchema().bind(
114 114 username=c.user.username)
115 115
116 116 form = forms.Form(
117 117 schema, buttons=(forms.buttons.save, forms.buttons.reset))
118 118
119 119 if c.extern_type != 'rhodecode':
120 120 raise HTTPFound(self.request.route_path('my_account_password'))
121 121
122 122 controls = self.request.POST.items()
123 123 try:
124 124 valid_data = form.validate(controls)
125 125 UserModel().update_user(c.user.user_id, **valid_data)
126 126 c.user.update_userdata(force_password_change=False)
127 127 Session().commit()
128 128 except forms.ValidationFailure as e:
129 129 c.form = e
130 130 return self._get_template_context(c)
131 131
132 132 except Exception:
133 133 log.exception("Exception updating password")
134 134 h.flash(_('Error occurred during update of user password'),
135 135 category='error')
136 136 else:
137 137 instance = c.auth_user.get_instance()
138 138 self.session.setdefault('rhodecode_user', {}).update(
139 139 {'password': md5(instance.password)})
140 140 self.session.save()
141 141 h.flash(_("Successfully updated password"), category='success')
142 142
143 143 raise HTTPFound(self.request.route_path('my_account_password'))
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 @view_config(
148 148 route_name='my_account_auth_tokens', request_method='GET',
149 149 renderer='rhodecode:templates/admin/my_account/my_account.mako')
150 150 def my_account_auth_tokens(self):
151 151 _ = self.request.translate
152 152
153 153 c = self.load_default_context()
154 154 c.active = 'auth_tokens'
155 155 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
156 156 c.role_values = [
157 157 (x, AuthTokenModel.cls._get_role_name(x))
158 158 for x in AuthTokenModel.cls.ROLES]
159 159 c.role_options = [(c.role_values, _("Role"))]
160 160 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
161 161 c.user.user_id, show_expired=True)
162 162 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
163 163 return self._get_template_context(c)
164 164
165 165 def maybe_attach_token_scope(self, token):
166 166 # implemented in EE edition
167 167 pass
168 168
169 169 @LoginRequired()
170 170 @NotAnonymous()
171 171 @CSRFRequired()
172 172 @view_config(
173 173 route_name='my_account_auth_tokens_add', request_method='POST',)
174 174 def my_account_auth_tokens_add(self):
175 175 _ = self.request.translate
176 176 c = self.load_default_context()
177 177
178 178 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
179 179 description = self.request.POST.get('description')
180 180 role = self.request.POST.get('role')
181 181
182 182 token = AuthTokenModel().create(
183 183 c.user.user_id, description, lifetime, role)
184 184 token_data = token.get_api_data()
185 185
186 186 self.maybe_attach_token_scope(token)
187 187 audit_logger.store_web(
188 188 'user.edit.token.add', action_data={
189 189 'data': {'token': token_data, 'user': 'self'}},
190 190 user=self._rhodecode_user, )
191 191 Session().commit()
192 192
193 193 h.flash(_("Auth token successfully created"), category='success')
194 194 return HTTPFound(h.route_path('my_account_auth_tokens'))
195 195
196 196 @LoginRequired()
197 197 @NotAnonymous()
198 198 @CSRFRequired()
199 199 @view_config(
200 200 route_name='my_account_auth_tokens_delete', request_method='POST')
201 201 def my_account_auth_tokens_delete(self):
202 202 _ = self.request.translate
203 203 c = self.load_default_context()
204 204
205 205 del_auth_token = self.request.POST.get('del_auth_token')
206 206
207 207 if del_auth_token:
208 208 token = UserApiKeys.get_or_404(del_auth_token)
209 209 token_data = token.get_api_data()
210 210
211 211 AuthTokenModel().delete(del_auth_token, c.user.user_id)
212 212 audit_logger.store_web(
213 213 'user.edit.token.delete', action_data={
214 214 'data': {'token': token_data, 'user': 'self'}},
215 215 user=self._rhodecode_user,)
216 216 Session().commit()
217 217 h.flash(_("Auth token successfully deleted"), category='success')
218 218
219 219 return HTTPFound(h.route_path('my_account_auth_tokens'))
220 220
221 221 @LoginRequired()
222 222 @NotAnonymous()
223 223 @view_config(
224 224 route_name='my_account_emails', request_method='GET',
225 225 renderer='rhodecode:templates/admin/my_account/my_account.mako')
226 226 def my_account_emails(self):
227 227 _ = self.request.translate
228 228
229 229 c = self.load_default_context()
230 230 c.active = 'emails'
231 231
232 232 c.user_email_map = UserEmailMap.query()\
233 233 .filter(UserEmailMap.user == c.user).all()
234 234 return self._get_template_context(c)
235 235
236 236 @LoginRequired()
237 237 @NotAnonymous()
238 238 @CSRFRequired()
239 239 @view_config(
240 240 route_name='my_account_emails_add', request_method='POST')
241 241 def my_account_emails_add(self):
242 242 _ = self.request.translate
243 243 c = self.load_default_context()
244 244
245 245 email = self.request.POST.get('new_email')
246 246
247 247 try:
248 248 UserModel().add_extra_email(c.user.user_id, email)
249 249 audit_logger.store_web(
250 250 'user.edit.email.add', action_data={
251 251 'data': {'email': email, 'user': 'self'}},
252 252 user=self._rhodecode_user,)
253 253
254 254 Session().commit()
255 255 h.flash(_("Added new email address `%s` for user account") % email,
256 256 category='success')
257 257 except formencode.Invalid as error:
258 258 h.flash(h.escape(error.error_dict['email']), category='error')
259 259 except Exception:
260 260 log.exception("Exception in my_account_emails")
261 261 h.flash(_('An error occurred during email saving'),
262 262 category='error')
263 263 return HTTPFound(h.route_path('my_account_emails'))
264 264
265 265 @LoginRequired()
266 266 @NotAnonymous()
267 267 @CSRFRequired()
268 268 @view_config(
269 269 route_name='my_account_emails_delete', request_method='POST')
270 270 def my_account_emails_delete(self):
271 271 _ = self.request.translate
272 272 c = self.load_default_context()
273 273
274 274 del_email_id = self.request.POST.get('del_email_id')
275 275 if del_email_id:
276 276 email = UserEmailMap.get_or_404(del_email_id).email
277 277 UserModel().delete_extra_email(c.user.user_id, del_email_id)
278 278 audit_logger.store_web(
279 279 'user.edit.email.delete', action_data={
280 280 'data': {'email': email, 'user': 'self'}},
281 281 user=self._rhodecode_user,)
282 282 Session().commit()
283 283 h.flash(_("Email successfully deleted"),
284 284 category='success')
285 285 return HTTPFound(h.route_path('my_account_emails'))
286 286
287 287 @LoginRequired()
288 288 @NotAnonymous()
289 289 @CSRFRequired()
290 290 @view_config(
291 291 route_name='my_account_notifications_test_channelstream',
292 292 request_method='POST', renderer='json_ext')
293 293 def my_account_notifications_test_channelstream(self):
294 294 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
295 295 self._rhodecode_user.username, datetime.datetime.now())
296 296 payload = {
297 297 # 'channel': 'broadcast',
298 298 'type': 'message',
299 299 'timestamp': datetime.datetime.utcnow(),
300 300 'user': 'system',
301 301 'pm_users': [self._rhodecode_user.username],
302 302 'message': {
303 303 'message': message,
304 304 'level': 'info',
305 305 'topic': '/notifications'
306 306 }
307 307 }
308 308
309 309 registry = self.request.registry
310 310 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
311 311 channelstream_config = rhodecode_plugins.get('channelstream', {})
312 312
313 313 try:
314 314 channelstream_request(channelstream_config, [payload], '/message')
315 315 except ChannelstreamException as e:
316 316 log.exception('Failed to send channelstream data')
317 317 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
318 318 return {"response": 'Channelstream data sent. '
319 319 'You should see a new live message now.'}
320 320
321 321 def _load_my_repos_data(self, watched=False):
322 322 if watched:
323 323 admin = False
324 324 follows_repos = Session().query(UserFollowing)\
325 325 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
326 326 .options(joinedload(UserFollowing.follows_repository))\
327 327 .all()
328 328 repo_list = [x.follows_repository for x in follows_repos]
329 329 else:
330 330 admin = True
331 331 repo_list = Repository.get_all_repos(
332 332 user_id=self._rhodecode_user.user_id)
333 333 repo_list = RepoList(repo_list, perm_set=[
334 334 'repository.read', 'repository.write', 'repository.admin'])
335 335
336 336 repos_data = RepoModel().get_repos_as_dict(
337 337 repo_list=repo_list, admin=admin)
338 338 # json used to render the grid
339 339 return json.dumps(repos_data)
340 340
341 341 @LoginRequired()
342 342 @NotAnonymous()
343 343 @view_config(
344 344 route_name='my_account_repos', request_method='GET',
345 345 renderer='rhodecode:templates/admin/my_account/my_account.mako')
346 346 def my_account_repos(self):
347 347 c = self.load_default_context()
348 348 c.active = 'repos'
349 349
350 350 # json used to render the grid
351 351 c.data = self._load_my_repos_data()
352 352 return self._get_template_context(c)
353 353
354 354 @LoginRequired()
355 355 @NotAnonymous()
356 356 @view_config(
357 357 route_name='my_account_watched', request_method='GET',
358 358 renderer='rhodecode:templates/admin/my_account/my_account.mako')
359 359 def my_account_watched(self):
360 360 c = self.load_default_context()
361 361 c.active = 'watched'
362 362
363 363 # json used to render the grid
364 364 c.data = self._load_my_repos_data(watched=True)
365 365 return self._get_template_context(c)
366 366
367 367 @LoginRequired()
368 368 @NotAnonymous()
369 369 @view_config(
370 370 route_name='my_account_perms', request_method='GET',
371 371 renderer='rhodecode:templates/admin/my_account/my_account.mako')
372 372 def my_account_perms(self):
373 373 c = self.load_default_context()
374 374 c.active = 'perms'
375 375
376 376 c.perm_user = c.auth_user
377 377 return self._get_template_context(c)
378 378
379 379 @LoginRequired()
380 380 @NotAnonymous()
381 381 @view_config(
382 382 route_name='my_account_notifications', request_method='GET',
383 383 renderer='rhodecode:templates/admin/my_account/my_account.mako')
384 384 def my_notifications(self):
385 385 c = self.load_default_context()
386 386 c.active = 'notifications'
387 387
388 388 return self._get_template_context(c)
389 389
390 390 @LoginRequired()
391 391 @NotAnonymous()
392 392 @CSRFRequired()
393 393 @view_config(
394 394 route_name='my_account_notifications_toggle_visibility',
395 395 request_method='POST', renderer='json_ext')
396 396 def my_notifications_toggle_visibility(self):
397 397 user = self._rhodecode_db_user
398 398 new_status = not user.user_data.get('notification_status', True)
399 399 user.update_userdata(notification_status=new_status)
400 400 Session().commit()
401 401 return user.user_data['notification_status']
402 402
403 403 @LoginRequired()
404 404 @NotAnonymous()
405 405 @view_config(
406 406 route_name='my_account_edit',
407 407 request_method='GET',
408 408 renderer='rhodecode:templates/admin/my_account/my_account.mako')
409 409 def my_account_edit(self):
410 410 c = self.load_default_context()
411 411 c.active = 'profile_edit'
412 412
413 413 c.perm_user = c.auth_user
414 414 c.extern_type = c.user.extern_type
415 415 c.extern_name = c.user.extern_name
416 416
417 417 defaults = c.user.get_dict()
418 418
419 419 data = render('rhodecode:templates/admin/my_account/my_account.mako',
420 420 self._get_template_context(c), self.request)
421 421 html = formencode.htmlfill.render(
422 422 data,
423 423 defaults=defaults,
424 424 encoding="UTF-8",
425 425 force_defaults=False
426 426 )
427 427 return Response(html)
428 428
429 429 @LoginRequired()
430 430 @NotAnonymous()
431 431 @CSRFRequired()
432 432 @view_config(
433 433 route_name='my_account_update',
434 434 request_method='POST',
435 435 renderer='rhodecode:templates/admin/my_account/my_account.mako')
436 436 def my_account_update(self):
437 437 _ = self.request.translate
438 438 c = self.load_default_context()
439 439 c.active = 'profile_edit'
440 440
441 441 c.perm_user = c.auth_user
442 442 c.extern_type = c.user.extern_type
443 443 c.extern_name = c.user.extern_name
444 444
445 445 _form = UserForm(edit=True,
446 446 old_data={'user_id': self._rhodecode_user.user_id,
447 447 'email': self._rhodecode_user.email})()
448 448 form_result = {}
449 449 try:
450 450 post_data = dict(self.request.POST)
451 451 post_data['new_password'] = ''
452 452 post_data['password_confirmation'] = ''
453 453 form_result = _form.to_python(post_data)
454 454 # skip updating those attrs for my account
455 455 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
456 456 'new_password', 'password_confirmation']
457 457 # TODO: plugin should define if username can be updated
458 458 if c.extern_type != "rhodecode":
459 459 # forbid updating username for external accounts
460 460 skip_attrs.append('username')
461 461
462 462 UserModel().update_user(
463 463 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
464 464 **form_result)
465 465 h.flash(_('Your account was updated successfully'),
466 466 category='success')
467 467 Session().commit()
468 468
469 469 except formencode.Invalid as errors:
470 470 data = render(
471 471 'rhodecode:templates/admin/my_account/my_account.mako',
472 472 self._get_template_context(c), self.request)
473 473
474 474 html = formencode.htmlfill.render(
475 475 data,
476 476 defaults=errors.value,
477 477 errors=errors.error_dict or {},
478 478 prefix_error=False,
479 479 encoding="UTF-8",
480 480 force_defaults=False)
481 481 return Response(html)
482 482
483 483 except Exception:
484 484 log.exception("Exception updating user")
485 485 h.flash(_('Error occurred during update of user %s')
486 486 % form_result.get('username'), category='error')
487 487 raise HTTPFound(h.route_path('my_account_profile'))
488 488
489 489 raise HTTPFound(h.route_path('my_account_profile'))
490 490
491 491 def _get_pull_requests_list(self, statuses):
492 492 draw, start, limit = self._extract_chunk(self.request)
493 493 search_q, order_by, order_dir = self._extract_ordering(self.request)
494 494 _render = self.request.get_partial_renderer(
495 'data_table/_dt_elements.mako')
495 'rhodecode:templates/data_table/_dt_elements.mako')
496 496
497 497 pull_requests = PullRequestModel().get_im_participating_in(
498 498 user_id=self._rhodecode_user.user_id,
499 499 statuses=statuses,
500 500 offset=start, length=limit, order_by=order_by,
501 501 order_dir=order_dir)
502 502
503 503 pull_requests_total_count = PullRequestModel().count_im_participating_in(
504 504 user_id=self._rhodecode_user.user_id, statuses=statuses)
505 505
506 506 data = []
507 507 comments_model = CommentsModel()
508 508 for pr in pull_requests:
509 509 repo_id = pr.target_repo_id
510 510 comments = comments_model.get_all_comments(
511 511 repo_id, pull_request=pr)
512 512 owned = pr.user_id == self._rhodecode_user.user_id
513 513
514 514 data.append({
515 515 'target_repo': _render('pullrequest_target_repo',
516 516 pr.target_repo.repo_name),
517 517 'name': _render('pullrequest_name',
518 518 pr.pull_request_id, pr.target_repo.repo_name,
519 519 short=True),
520 520 'name_raw': pr.pull_request_id,
521 521 'status': _render('pullrequest_status',
522 522 pr.calculated_review_status()),
523 523 'title': _render(
524 524 'pullrequest_title', pr.title, pr.description),
525 525 'description': h.escape(pr.description),
526 526 'updated_on': _render('pullrequest_updated_on',
527 527 h.datetime_to_time(pr.updated_on)),
528 528 'updated_on_raw': h.datetime_to_time(pr.updated_on),
529 529 'created_on': _render('pullrequest_updated_on',
530 530 h.datetime_to_time(pr.created_on)),
531 531 'created_on_raw': h.datetime_to_time(pr.created_on),
532 532 'author': _render('pullrequest_author',
533 533 pr.author.full_contact, ),
534 534 'author_raw': pr.author.full_name,
535 535 'comments': _render('pullrequest_comments', len(comments)),
536 536 'comments_raw': len(comments),
537 537 'closed': pr.is_closed(),
538 538 'owned': owned
539 539 })
540 540
541 541 # json used to render the grid
542 542 data = ({
543 543 'draw': draw,
544 544 'data': data,
545 545 'recordsTotal': pull_requests_total_count,
546 546 'recordsFiltered': pull_requests_total_count,
547 547 })
548 548 return data
549 549
550 550 @LoginRequired()
551 551 @NotAnonymous()
552 552 @view_config(
553 553 route_name='my_account_pullrequests',
554 554 request_method='GET',
555 555 renderer='rhodecode:templates/admin/my_account/my_account.mako')
556 556 def my_account_pullrequests(self):
557 557 c = self.load_default_context()
558 558 c.active = 'pullrequests'
559 559 req_get = self.request.GET
560 560
561 561 c.closed = str2bool(req_get.get('pr_show_closed'))
562 562
563 563 return self._get_template_context(c)
564 564
565 565 @LoginRequired()
566 566 @NotAnonymous()
567 567 @view_config(
568 568 route_name='my_account_pullrequests_data',
569 569 request_method='GET', renderer='json_ext')
570 570 def my_account_pullrequests_data(self):
571 571 req_get = self.request.GET
572 572 closed = str2bool(req_get.get('closed'))
573 573
574 574 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
575 575 if closed:
576 576 statuses += [PullRequest.STATUS_CLOSED]
577 577
578 578 data = self._get_pull_requests_list(statuses=statuses)
579 579 return data
580 580
@@ -1,204 +1,204 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import pytz
22 22 import logging
23 23
24 24 from beaker.cache import cache_region
25 25 from pyramid.view import view_config
26 26 from pyramid.response import Response
27 27 from webhelpers.feedgenerator import Rss201rev2Feed, Atom1Feed
28 28
29 29 from rhodecode.apps._base import RepoAppView
30 30 from rhodecode.lib import audit_logger
31 31 from rhodecode.lib import helpers as h
32 32 from rhodecode.lib.auth import (
33 33 LoginRequired, HasRepoPermissionAnyDecorator)
34 34 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
35 35 from rhodecode.lib.utils2 import str2bool, safe_int, md5_safe
36 36 from rhodecode.model.db import UserApiKeys, CacheKey
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class RepoFeedView(RepoAppView):
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44
45 45 self._register_global_c(c)
46 46 self._load_defaults()
47 47 return c
48 48
49 49 def _get_config(self):
50 50 import rhodecode
51 51 config = rhodecode.CONFIG
52 52
53 53 return {
54 54 'language': 'en-us',
55 55 'feed_ttl': '5', # TTL of feed,
56 56 'feed_include_diff':
57 57 str2bool(config.get('rss_include_diff', False)),
58 58 'feed_items_per_page':
59 59 safe_int(config.get('rss_items_per_page', 20)),
60 60 'feed_diff_limit':
61 61 # we need to protect from parsing huge diffs here other way
62 62 # we can kill the server
63 63 safe_int(config.get('rss_cut_off_limit', 32 * 1024)),
64 64 }
65 65
66 66 def _load_defaults(self):
67 67 _ = self.request.translate
68 68 config = self._get_config()
69 69 # common values for feeds
70 70 self.description = _('Changes on %s repository')
71 71 self.title = self.title = _('%s %s feed') % (self.db_repo_name, '%s')
72 72 self.language = config["language"]
73 73 self.ttl = config["feed_ttl"]
74 74 self.feed_include_diff = config['feed_include_diff']
75 75 self.feed_diff_limit = config['feed_diff_limit']
76 76 self.feed_items_per_page = config['feed_items_per_page']
77 77
78 78 def _changes(self, commit):
79 79 diff_processor = DiffProcessor(
80 80 commit.diff(), diff_limit=self.feed_diff_limit)
81 81 _parsed = diff_processor.prepare(inline_diff=False)
82 82 limited_diff = isinstance(_parsed, LimitedDiffContainer)
83 83
84 84 return _parsed, limited_diff
85 85
86 86 def _get_title(self, commit):
87 87 return h.shorter(commit.message, 160)
88 88
89 89 def _get_description(self, commit):
90 90 _renderer = self.request.get_partial_renderer(
91 'feed/atom_feed_entry.mako')
91 'rhodecode:templates/feed/atom_feed_entry.mako')
92 92 parsed_diff, limited_diff = self._changes(commit)
93 93 return _renderer(
94 94 'body',
95 95 commit=commit,
96 96 parsed_diff=parsed_diff,
97 97 limited_diff=limited_diff,
98 98 feed_include_diff=self.feed_include_diff,
99 99 )
100 100
101 101 def _set_timezone(self, date, tzinfo=pytz.utc):
102 102 if not getattr(date, "tzinfo", None):
103 103 date.replace(tzinfo=tzinfo)
104 104 return date
105 105
106 106 def _get_commits(self):
107 107 return list(self.rhodecode_vcs_repo[-self.feed_items_per_page:])
108 108
109 109 def uid(self, repo_id, commit_id):
110 110 return '{}:{}'.format(md5_safe(repo_id), md5_safe(commit_id))
111 111
112 112 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
113 113 @HasRepoPermissionAnyDecorator(
114 114 'repository.read', 'repository.write', 'repository.admin')
115 115 @view_config(
116 116 route_name='atom_feed_home', request_method='GET',
117 117 renderer=None)
118 118 def atom(self):
119 119 """
120 120 Produce an atom-1.0 feed via feedgenerator module
121 121 """
122 122 self.load_default_context()
123 123
124 124 @cache_region('long_term')
125 125 def _generate_feed(cache_key):
126 126 feed = Atom1Feed(
127 127 title=self.title % self.db_repo_name,
128 128 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
129 129 description=self.description % self.db_repo_name,
130 130 language=self.language,
131 131 ttl=self.ttl
132 132 )
133 133
134 134 for commit in reversed(self._get_commits()):
135 135 date = self._set_timezone(commit.date)
136 136 feed.add_item(
137 137 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
138 138 title=self._get_title(commit),
139 139 author_name=commit.author,
140 140 description=self._get_description(commit),
141 141 link=h.route_url(
142 142 'repo_commit', repo_name=self.db_repo_name,
143 143 commit_id=commit.raw_id),
144 144 pubdate=date,)
145 145
146 146 return feed.mime_type, feed.writeString('utf-8')
147 147
148 148 invalidator_context = CacheKey.repo_context_cache(
149 149 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_ATOM)
150 150
151 151 with invalidator_context as context:
152 152 context.invalidate()
153 153 mime_type, feed = context.compute()
154 154
155 155 response = Response(feed)
156 156 response.content_type = mime_type
157 157 return response
158 158
159 159 @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
160 160 @HasRepoPermissionAnyDecorator(
161 161 'repository.read', 'repository.write', 'repository.admin')
162 162 @view_config(
163 163 route_name='rss_feed_home', request_method='GET',
164 164 renderer=None)
165 165 def rss(self):
166 166 """
167 167 Produce an rss2 feed via feedgenerator module
168 168 """
169 169 self.load_default_context()
170 170
171 171 @cache_region('long_term')
172 172 def _generate_feed(cache_key):
173 173 feed = Rss201rev2Feed(
174 174 title=self.title % self.db_repo_name,
175 175 link=h.route_url('repo_summary', repo_name=self.db_repo_name),
176 176 description=self.description % self.db_repo_name,
177 177 language=self.language,
178 178 ttl=self.ttl
179 179 )
180 180
181 181 for commit in reversed(self._get_commits()):
182 182 date = self._set_timezone(commit.date)
183 183 feed.add_item(
184 184 unique_id=self.uid(self.db_repo.repo_id, commit.raw_id),
185 185 title=self._get_title(commit),
186 186 author_name=commit.author,
187 187 description=self._get_description(commit),
188 188 link=h.route_url(
189 189 'repo_commit', repo_name=self.db_repo_name,
190 190 commit_id=commit.raw_id),
191 191 pubdate=date,)
192 192
193 193 return feed.mime_type, feed.writeString('utf-8')
194 194
195 195 invalidator_context = CacheKey.repo_context_cache(
196 196 _generate_feed, self.db_repo_name, CacheKey.CACHE_TYPE_RSS)
197 197
198 198 with invalidator_context as context:
199 199 context.invalidate()
200 200 mime_type, feed = context.compute()
201 201
202 202 response = Response(feed)
203 203 response.content_type = mime_type
204 204 return response
@@ -1,1235 +1,1235 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode import events
33 33 from rhodecode.apps._base import RepoAppView, DataGridAppView
34 34
35 35 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
36 36 from rhodecode.lib.base import vcs_operation_context
37 37 from rhodecode.lib.ext_json import json
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 40 NotAnonymous, CSRFRequired)
41 41 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
42 42 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 43 from rhodecode.lib.vcs.exceptions import (CommitDoesNotExistError,
44 44 RepositoryRequirementError, NodeDoesNotExistError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (func, or_, PullRequest, PullRequestVersion,
48 48 ChangesetComment, ChangesetStatus, Repository)
49 49 from rhodecode.model.forms import PullRequestForm
50 50 from rhodecode.model.meta import Session
51 51 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 52 from rhodecode.model.scm import ScmModel
53 53
54 54 log = logging.getLogger(__name__)
55 55
56 56
57 57 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 58
59 59 def load_default_context(self):
60 60 c = self._get_local_tmpl_context(include_app_defaults=True)
61 61 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 62 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 63 self._register_global_c(c)
64 64 return c
65 65
66 66 def _get_pull_requests_list(
67 67 self, repo_name, source, filter_type, opened_by, statuses):
68 68
69 69 draw, start, limit = self._extract_chunk(self.request)
70 70 search_q, order_by, order_dir = self._extract_ordering(self.request)
71 71 _render = self.request.get_partial_renderer(
72 'data_table/_dt_elements.mako')
72 'rhodecode:templates/data_table/_dt_elements.mako')
73 73
74 74 # pagination
75 75
76 76 if filter_type == 'awaiting_review':
77 77 pull_requests = PullRequestModel().get_awaiting_review(
78 78 repo_name, source=source, opened_by=opened_by,
79 79 statuses=statuses, offset=start, length=limit,
80 80 order_by=order_by, order_dir=order_dir)
81 81 pull_requests_total_count = PullRequestModel().count_awaiting_review(
82 82 repo_name, source=source, statuses=statuses,
83 83 opened_by=opened_by)
84 84 elif filter_type == 'awaiting_my_review':
85 85 pull_requests = PullRequestModel().get_awaiting_my_review(
86 86 repo_name, source=source, opened_by=opened_by,
87 87 user_id=self._rhodecode_user.user_id, statuses=statuses,
88 88 offset=start, length=limit, order_by=order_by,
89 89 order_dir=order_dir)
90 90 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
91 91 repo_name, source=source, user_id=self._rhodecode_user.user_id,
92 92 statuses=statuses, opened_by=opened_by)
93 93 else:
94 94 pull_requests = PullRequestModel().get_all(
95 95 repo_name, source=source, opened_by=opened_by,
96 96 statuses=statuses, offset=start, length=limit,
97 97 order_by=order_by, order_dir=order_dir)
98 98 pull_requests_total_count = PullRequestModel().count_all(
99 99 repo_name, source=source, statuses=statuses,
100 100 opened_by=opened_by)
101 101
102 102 data = []
103 103 comments_model = CommentsModel()
104 104 for pr in pull_requests:
105 105 comments = comments_model.get_all_comments(
106 106 self.db_repo.repo_id, pull_request=pr)
107 107
108 108 data.append({
109 109 'name': _render('pullrequest_name',
110 110 pr.pull_request_id, pr.target_repo.repo_name),
111 111 'name_raw': pr.pull_request_id,
112 112 'status': _render('pullrequest_status',
113 113 pr.calculated_review_status()),
114 114 'title': _render(
115 115 'pullrequest_title', pr.title, pr.description),
116 116 'description': h.escape(pr.description),
117 117 'updated_on': _render('pullrequest_updated_on',
118 118 h.datetime_to_time(pr.updated_on)),
119 119 'updated_on_raw': h.datetime_to_time(pr.updated_on),
120 120 'created_on': _render('pullrequest_updated_on',
121 121 h.datetime_to_time(pr.created_on)),
122 122 'created_on_raw': h.datetime_to_time(pr.created_on),
123 123 'author': _render('pullrequest_author',
124 124 pr.author.full_contact, ),
125 125 'author_raw': pr.author.full_name,
126 126 'comments': _render('pullrequest_comments', len(comments)),
127 127 'comments_raw': len(comments),
128 128 'closed': pr.is_closed(),
129 129 })
130 130
131 131 data = ({
132 132 'draw': draw,
133 133 'data': data,
134 134 'recordsTotal': pull_requests_total_count,
135 135 'recordsFiltered': pull_requests_total_count,
136 136 })
137 137 return data
138 138
139 139 @LoginRequired()
140 140 @HasRepoPermissionAnyDecorator(
141 141 'repository.read', 'repository.write', 'repository.admin')
142 142 @view_config(
143 143 route_name='pullrequest_show_all', request_method='GET',
144 144 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
145 145 def pull_request_list(self):
146 146 c = self.load_default_context()
147 147
148 148 req_get = self.request.GET
149 149 c.source = str2bool(req_get.get('source'))
150 150 c.closed = str2bool(req_get.get('closed'))
151 151 c.my = str2bool(req_get.get('my'))
152 152 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
153 153 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
154 154
155 155 c.active = 'open'
156 156 if c.my:
157 157 c.active = 'my'
158 158 if c.closed:
159 159 c.active = 'closed'
160 160 if c.awaiting_review and not c.source:
161 161 c.active = 'awaiting'
162 162 if c.source and not c.awaiting_review:
163 163 c.active = 'source'
164 164 if c.awaiting_my_review:
165 165 c.active = 'awaiting_my'
166 166
167 167 return self._get_template_context(c)
168 168
169 169 @LoginRequired()
170 170 @HasRepoPermissionAnyDecorator(
171 171 'repository.read', 'repository.write', 'repository.admin')
172 172 @view_config(
173 173 route_name='pullrequest_show_all_data', request_method='GET',
174 174 renderer='json_ext', xhr=True)
175 175 def pull_request_list_data(self):
176 176
177 177 # additional filters
178 178 req_get = self.request.GET
179 179 source = str2bool(req_get.get('source'))
180 180 closed = str2bool(req_get.get('closed'))
181 181 my = str2bool(req_get.get('my'))
182 182 awaiting_review = str2bool(req_get.get('awaiting_review'))
183 183 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
184 184
185 185 filter_type = 'awaiting_review' if awaiting_review \
186 186 else 'awaiting_my_review' if awaiting_my_review \
187 187 else None
188 188
189 189 opened_by = None
190 190 if my:
191 191 opened_by = [self._rhodecode_user.user_id]
192 192
193 193 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
194 194 if closed:
195 195 statuses = [PullRequest.STATUS_CLOSED]
196 196
197 197 data = self._get_pull_requests_list(
198 198 repo_name=self.db_repo_name, source=source,
199 199 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
200 200
201 201 return data
202 202
203 203 def _get_pr_version(self, pull_request_id, version=None):
204 204 at_version = None
205 205
206 206 if version and version == 'latest':
207 207 pull_request_ver = PullRequest.get(pull_request_id)
208 208 pull_request_obj = pull_request_ver
209 209 _org_pull_request_obj = pull_request_obj
210 210 at_version = 'latest'
211 211 elif version:
212 212 pull_request_ver = PullRequestVersion.get_or_404(version)
213 213 pull_request_obj = pull_request_ver
214 214 _org_pull_request_obj = pull_request_ver.pull_request
215 215 at_version = pull_request_ver.pull_request_version_id
216 216 else:
217 217 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
218 218 pull_request_id)
219 219
220 220 pull_request_display_obj = PullRequest.get_pr_display_object(
221 221 pull_request_obj, _org_pull_request_obj)
222 222
223 223 return _org_pull_request_obj, pull_request_obj, \
224 224 pull_request_display_obj, at_version
225 225
226 226 def _get_diffset(self, source_repo_name, source_repo,
227 227 source_ref_id, target_ref_id,
228 228 target_commit, source_commit, diff_limit, fulldiff,
229 229 file_limit, display_inline_comments):
230 230
231 231 vcs_diff = PullRequestModel().get_diff(
232 232 source_repo, source_ref_id, target_ref_id)
233 233
234 234 diff_processor = diffs.DiffProcessor(
235 235 vcs_diff, format='newdiff', diff_limit=diff_limit,
236 236 file_limit=file_limit, show_full_diff=fulldiff)
237 237
238 238 _parsed = diff_processor.prepare()
239 239
240 240 def _node_getter(commit):
241 241 def get_node(fname):
242 242 try:
243 243 return commit.get_node(fname)
244 244 except NodeDoesNotExistError:
245 245 return None
246 246
247 247 return get_node
248 248
249 249 diffset = codeblocks.DiffSet(
250 250 repo_name=self.db_repo_name,
251 251 source_repo_name=source_repo_name,
252 252 source_node_getter=_node_getter(target_commit),
253 253 target_node_getter=_node_getter(source_commit),
254 254 comments=display_inline_comments
255 255 )
256 256 diffset = diffset.render_patchset(
257 257 _parsed, target_commit.raw_id, source_commit.raw_id)
258 258
259 259 return diffset
260 260
261 261 @LoginRequired()
262 262 @HasRepoPermissionAnyDecorator(
263 263 'repository.read', 'repository.write', 'repository.admin')
264 264 @view_config(
265 265 route_name='pullrequest_show', request_method='GET',
266 266 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
267 267 def pull_request_show(self):
268 268 pull_request_id = self.request.matchdict['pull_request_id']
269 269
270 270 c = self.load_default_context()
271 271
272 272 version = self.request.GET.get('version')
273 273 from_version = self.request.GET.get('from_version') or version
274 274 merge_checks = self.request.GET.get('merge_checks')
275 275 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
276 276
277 277 (pull_request_latest,
278 278 pull_request_at_ver,
279 279 pull_request_display_obj,
280 280 at_version) = self._get_pr_version(
281 281 pull_request_id, version=version)
282 282 pr_closed = pull_request_latest.is_closed()
283 283
284 284 if pr_closed and (version or from_version):
285 285 # not allow to browse versions
286 286 raise HTTPFound(h.route_path(
287 287 'pullrequest_show', repo_name=self.db_repo_name,
288 288 pull_request_id=pull_request_id))
289 289
290 290 versions = pull_request_display_obj.versions()
291 291
292 292 c.at_version = at_version
293 293 c.at_version_num = (at_version
294 294 if at_version and at_version != 'latest'
295 295 else None)
296 296 c.at_version_pos = ChangesetComment.get_index_from_version(
297 297 c.at_version_num, versions)
298 298
299 299 (prev_pull_request_latest,
300 300 prev_pull_request_at_ver,
301 301 prev_pull_request_display_obj,
302 302 prev_at_version) = self._get_pr_version(
303 303 pull_request_id, version=from_version)
304 304
305 305 c.from_version = prev_at_version
306 306 c.from_version_num = (prev_at_version
307 307 if prev_at_version and prev_at_version != 'latest'
308 308 else None)
309 309 c.from_version_pos = ChangesetComment.get_index_from_version(
310 310 c.from_version_num, versions)
311 311
312 312 # define if we're in COMPARE mode or VIEW at version mode
313 313 compare = at_version != prev_at_version
314 314
315 315 # pull_requests repo_name we opened it against
316 316 # ie. target_repo must match
317 317 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
318 318 raise HTTPNotFound()
319 319
320 320 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
321 321 pull_request_at_ver)
322 322
323 323 c.pull_request = pull_request_display_obj
324 324 c.pull_request_latest = pull_request_latest
325 325
326 326 if compare or (at_version and not at_version == 'latest'):
327 327 c.allowed_to_change_status = False
328 328 c.allowed_to_update = False
329 329 c.allowed_to_merge = False
330 330 c.allowed_to_delete = False
331 331 c.allowed_to_comment = False
332 332 c.allowed_to_close = False
333 333 else:
334 334 can_change_status = PullRequestModel().check_user_change_status(
335 335 pull_request_at_ver, self._rhodecode_user)
336 336 c.allowed_to_change_status = can_change_status and not pr_closed
337 337
338 338 c.allowed_to_update = PullRequestModel().check_user_update(
339 339 pull_request_latest, self._rhodecode_user) and not pr_closed
340 340 c.allowed_to_merge = PullRequestModel().check_user_merge(
341 341 pull_request_latest, self._rhodecode_user) and not pr_closed
342 342 c.allowed_to_delete = PullRequestModel().check_user_delete(
343 343 pull_request_latest, self._rhodecode_user) and not pr_closed
344 344 c.allowed_to_comment = not pr_closed
345 345 c.allowed_to_close = c.allowed_to_merge and not pr_closed
346 346
347 347 c.forbid_adding_reviewers = False
348 348 c.forbid_author_to_review = False
349 349 c.forbid_commit_author_to_review = False
350 350
351 351 if pull_request_latest.reviewer_data and \
352 352 'rules' in pull_request_latest.reviewer_data:
353 353 rules = pull_request_latest.reviewer_data['rules'] or {}
354 354 try:
355 355 c.forbid_adding_reviewers = rules.get(
356 356 'forbid_adding_reviewers')
357 357 c.forbid_author_to_review = rules.get(
358 358 'forbid_author_to_review')
359 359 c.forbid_commit_author_to_review = rules.get(
360 360 'forbid_commit_author_to_review')
361 361 except Exception:
362 362 pass
363 363
364 364 # check merge capabilities
365 365 _merge_check = MergeCheck.validate(
366 366 pull_request_latest, user=self._rhodecode_user,
367 367 translator=self.request.translate)
368 368 c.pr_merge_errors = _merge_check.error_details
369 369 c.pr_merge_possible = not _merge_check.failed
370 370 c.pr_merge_message = _merge_check.merge_msg
371 371
372 372 c.pr_merge_info = MergeCheck.get_merge_conditions(
373 373 pull_request_latest, translator=self.request.translate)
374 374
375 375 c.pull_request_review_status = _merge_check.review_status
376 376 if merge_checks:
377 377 self.request.override_renderer = \
378 378 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
379 379 return self._get_template_context(c)
380 380
381 381 comments_model = CommentsModel()
382 382
383 383 # reviewers and statuses
384 384 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
385 385 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
386 386
387 387 # GENERAL COMMENTS with versions #
388 388 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
389 389 q = q.order_by(ChangesetComment.comment_id.asc())
390 390 general_comments = q
391 391
392 392 # pick comments we want to render at current version
393 393 c.comment_versions = comments_model.aggregate_comments(
394 394 general_comments, versions, c.at_version_num)
395 395 c.comments = c.comment_versions[c.at_version_num]['until']
396 396
397 397 # INLINE COMMENTS with versions #
398 398 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
399 399 q = q.order_by(ChangesetComment.comment_id.asc())
400 400 inline_comments = q
401 401
402 402 c.inline_versions = comments_model.aggregate_comments(
403 403 inline_comments, versions, c.at_version_num, inline=True)
404 404
405 405 # inject latest version
406 406 latest_ver = PullRequest.get_pr_display_object(
407 407 pull_request_latest, pull_request_latest)
408 408
409 409 c.versions = versions + [latest_ver]
410 410
411 411 # if we use version, then do not show later comments
412 412 # than current version
413 413 display_inline_comments = collections.defaultdict(
414 414 lambda: collections.defaultdict(list))
415 415 for co in inline_comments:
416 416 if c.at_version_num:
417 417 # pick comments that are at least UPTO given version, so we
418 418 # don't render comments for higher version
419 419 should_render = co.pull_request_version_id and \
420 420 co.pull_request_version_id <= c.at_version_num
421 421 else:
422 422 # showing all, for 'latest'
423 423 should_render = True
424 424
425 425 if should_render:
426 426 display_inline_comments[co.f_path][co.line_no].append(co)
427 427
428 428 # load diff data into template context, if we use compare mode then
429 429 # diff is calculated based on changes between versions of PR
430 430
431 431 source_repo = pull_request_at_ver.source_repo
432 432 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
433 433
434 434 target_repo = pull_request_at_ver.target_repo
435 435 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
436 436
437 437 if compare:
438 438 # in compare switch the diff base to latest commit from prev version
439 439 target_ref_id = prev_pull_request_display_obj.revisions[0]
440 440
441 441 # despite opening commits for bookmarks/branches/tags, we always
442 442 # convert this to rev to prevent changes after bookmark or branch change
443 443 c.source_ref_type = 'rev'
444 444 c.source_ref = source_ref_id
445 445
446 446 c.target_ref_type = 'rev'
447 447 c.target_ref = target_ref_id
448 448
449 449 c.source_repo = source_repo
450 450 c.target_repo = target_repo
451 451
452 452 c.commit_ranges = []
453 453 source_commit = EmptyCommit()
454 454 target_commit = EmptyCommit()
455 455 c.missing_requirements = False
456 456
457 457 source_scm = source_repo.scm_instance()
458 458 target_scm = target_repo.scm_instance()
459 459
460 460 # try first shadow repo, fallback to regular repo
461 461 try:
462 462 commits_source_repo = pull_request_latest.get_shadow_repo()
463 463 except Exception:
464 464 log.debug('Failed to get shadow repo', exc_info=True)
465 465 commits_source_repo = source_scm
466 466
467 467 c.commits_source_repo = commits_source_repo
468 468 commit_cache = {}
469 469 try:
470 470 pre_load = ["author", "branch", "date", "message"]
471 471 show_revs = pull_request_at_ver.revisions
472 472 for rev in show_revs:
473 473 comm = commits_source_repo.get_commit(
474 474 commit_id=rev, pre_load=pre_load)
475 475 c.commit_ranges.append(comm)
476 476 commit_cache[comm.raw_id] = comm
477 477
478 478 # Order here matters, we first need to get target, and then
479 479 # the source
480 480 target_commit = commits_source_repo.get_commit(
481 481 commit_id=safe_str(target_ref_id))
482 482
483 483 source_commit = commits_source_repo.get_commit(
484 484 commit_id=safe_str(source_ref_id))
485 485
486 486 except CommitDoesNotExistError:
487 487 log.warning(
488 488 'Failed to get commit from `{}` repo'.format(
489 489 commits_source_repo), exc_info=True)
490 490 except RepositoryRequirementError:
491 491 log.warning(
492 492 'Failed to get all required data from repo', exc_info=True)
493 493 c.missing_requirements = True
494 494
495 495 c.ancestor = None # set it to None, to hide it from PR view
496 496
497 497 try:
498 498 ancestor_id = source_scm.get_common_ancestor(
499 499 source_commit.raw_id, target_commit.raw_id, target_scm)
500 500 c.ancestor_commit = source_scm.get_commit(ancestor_id)
501 501 except Exception:
502 502 c.ancestor_commit = None
503 503
504 504 c.statuses = source_repo.statuses(
505 505 [x.raw_id for x in c.commit_ranges])
506 506
507 507 # auto collapse if we have more than limit
508 508 collapse_limit = diffs.DiffProcessor._collapse_commits_over
509 509 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
510 510 c.compare_mode = compare
511 511
512 512 # diff_limit is the old behavior, will cut off the whole diff
513 513 # if the limit is applied otherwise will just hide the
514 514 # big files from the front-end
515 515 diff_limit = c.visual.cut_off_limit_diff
516 516 file_limit = c.visual.cut_off_limit_file
517 517
518 518 c.missing_commits = False
519 519 if (c.missing_requirements
520 520 or isinstance(source_commit, EmptyCommit)
521 521 or source_commit == target_commit):
522 522
523 523 c.missing_commits = True
524 524 else:
525 525
526 526 c.diffset = self._get_diffset(
527 527 c.source_repo.repo_name, commits_source_repo,
528 528 source_ref_id, target_ref_id,
529 529 target_commit, source_commit,
530 530 diff_limit, c.fulldiff, file_limit, display_inline_comments)
531 531
532 532 c.limited_diff = c.diffset.limited_diff
533 533
534 534 # calculate removed files that are bound to comments
535 535 comment_deleted_files = [
536 536 fname for fname in display_inline_comments
537 537 if fname not in c.diffset.file_stats]
538 538
539 539 c.deleted_files_comments = collections.defaultdict(dict)
540 540 for fname, per_line_comments in display_inline_comments.items():
541 541 if fname in comment_deleted_files:
542 542 c.deleted_files_comments[fname]['stats'] = 0
543 543 c.deleted_files_comments[fname]['comments'] = list()
544 544 for lno, comments in per_line_comments.items():
545 545 c.deleted_files_comments[fname]['comments'].extend(
546 546 comments)
547 547
548 548 # this is a hack to properly display links, when creating PR, the
549 549 # compare view and others uses different notation, and
550 550 # compare_commits.mako renders links based on the target_repo.
551 551 # We need to swap that here to generate it properly on the html side
552 552 c.target_repo = c.source_repo
553 553
554 554 c.commit_statuses = ChangesetStatus.STATUSES
555 555
556 556 c.show_version_changes = not pr_closed
557 557 if c.show_version_changes:
558 558 cur_obj = pull_request_at_ver
559 559 prev_obj = prev_pull_request_at_ver
560 560
561 561 old_commit_ids = prev_obj.revisions
562 562 new_commit_ids = cur_obj.revisions
563 563 commit_changes = PullRequestModel()._calculate_commit_id_changes(
564 564 old_commit_ids, new_commit_ids)
565 565 c.commit_changes_summary = commit_changes
566 566
567 567 # calculate the diff for commits between versions
568 568 c.commit_changes = []
569 569 mark = lambda cs, fw: list(
570 570 h.itertools.izip_longest([], cs, fillvalue=fw))
571 571 for c_type, raw_id in mark(commit_changes.added, 'a') \
572 572 + mark(commit_changes.removed, 'r') \
573 573 + mark(commit_changes.common, 'c'):
574 574
575 575 if raw_id in commit_cache:
576 576 commit = commit_cache[raw_id]
577 577 else:
578 578 try:
579 579 commit = commits_source_repo.get_commit(raw_id)
580 580 except CommitDoesNotExistError:
581 581 # in case we fail extracting still use "dummy" commit
582 582 # for display in commit diff
583 583 commit = h.AttributeDict(
584 584 {'raw_id': raw_id,
585 585 'message': 'EMPTY or MISSING COMMIT'})
586 586 c.commit_changes.append([c_type, commit])
587 587
588 588 # current user review statuses for each version
589 589 c.review_versions = {}
590 590 if self._rhodecode_user.user_id in allowed_reviewers:
591 591 for co in general_comments:
592 592 if co.author.user_id == self._rhodecode_user.user_id:
593 593 # each comment has a status change
594 594 status = co.status_change
595 595 if status:
596 596 _ver_pr = status[0].comment.pull_request_version_id
597 597 c.review_versions[_ver_pr] = status[0]
598 598
599 599 return self._get_template_context(c)
600 600
601 601 def assure_not_empty_repo(self):
602 602 _ = self.request.translate
603 603
604 604 try:
605 605 self.db_repo.scm_instance().get_commit()
606 606 except EmptyRepositoryError:
607 607 h.flash(h.literal(_('There are no commits yet')),
608 608 category='warning')
609 609 raise HTTPFound(
610 610 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
611 611
612 612 @LoginRequired()
613 613 @NotAnonymous()
614 614 @HasRepoPermissionAnyDecorator(
615 615 'repository.read', 'repository.write', 'repository.admin')
616 616 @view_config(
617 617 route_name='pullrequest_new', request_method='GET',
618 618 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
619 619 def pull_request_new(self):
620 620 _ = self.request.translate
621 621 c = self.load_default_context()
622 622
623 623 self.assure_not_empty_repo()
624 624 source_repo = self.db_repo
625 625
626 626 commit_id = self.request.GET.get('commit')
627 627 branch_ref = self.request.GET.get('branch')
628 628 bookmark_ref = self.request.GET.get('bookmark')
629 629
630 630 try:
631 631 source_repo_data = PullRequestModel().generate_repo_data(
632 632 source_repo, commit_id=commit_id,
633 633 branch=branch_ref, bookmark=bookmark_ref, translator=self.request.translate)
634 634 except CommitDoesNotExistError as e:
635 635 log.exception(e)
636 636 h.flash(_('Commit does not exist'), 'error')
637 637 raise HTTPFound(
638 638 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
639 639
640 640 default_target_repo = source_repo
641 641
642 642 if source_repo.parent:
643 643 parent_vcs_obj = source_repo.parent.scm_instance()
644 644 if parent_vcs_obj and not parent_vcs_obj.is_empty():
645 645 # change default if we have a parent repo
646 646 default_target_repo = source_repo.parent
647 647
648 648 target_repo_data = PullRequestModel().generate_repo_data(
649 649 default_target_repo, translator=self.request.translate)
650 650
651 651 selected_source_ref = source_repo_data['refs']['selected_ref']
652 652
653 653 title_source_ref = selected_source_ref.split(':', 2)[1]
654 654 c.default_title = PullRequestModel().generate_pullrequest_title(
655 655 source=source_repo.repo_name,
656 656 source_ref=title_source_ref,
657 657 target=default_target_repo.repo_name
658 658 )
659 659
660 660 c.default_repo_data = {
661 661 'source_repo_name': source_repo.repo_name,
662 662 'source_refs_json': json.dumps(source_repo_data),
663 663 'target_repo_name': default_target_repo.repo_name,
664 664 'target_refs_json': json.dumps(target_repo_data),
665 665 }
666 666 c.default_source_ref = selected_source_ref
667 667
668 668 return self._get_template_context(c)
669 669
670 670 @LoginRequired()
671 671 @NotAnonymous()
672 672 @HasRepoPermissionAnyDecorator(
673 673 'repository.read', 'repository.write', 'repository.admin')
674 674 @view_config(
675 675 route_name='pullrequest_repo_refs', request_method='GET',
676 676 renderer='json_ext', xhr=True)
677 677 def pull_request_repo_refs(self):
678 678 target_repo_name = self.request.matchdict['target_repo_name']
679 679 repo = Repository.get_by_repo_name(target_repo_name)
680 680 if not repo:
681 681 raise HTTPNotFound()
682 682 return PullRequestModel().generate_repo_data(
683 683 repo, translator=self.request.translate)
684 684
685 685 @LoginRequired()
686 686 @NotAnonymous()
687 687 @HasRepoPermissionAnyDecorator(
688 688 'repository.read', 'repository.write', 'repository.admin')
689 689 @view_config(
690 690 route_name='pullrequest_repo_destinations', request_method='GET',
691 691 renderer='json_ext', xhr=True)
692 692 def pull_request_repo_destinations(self):
693 693 _ = self.request.translate
694 694 filter_query = self.request.GET.get('query')
695 695
696 696 query = Repository.query() \
697 697 .order_by(func.length(Repository.repo_name)) \
698 698 .filter(
699 699 or_(Repository.repo_name == self.db_repo.repo_name,
700 700 Repository.fork_id == self.db_repo.repo_id))
701 701
702 702 if filter_query:
703 703 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
704 704 query = query.filter(
705 705 Repository.repo_name.ilike(ilike_expression))
706 706
707 707 add_parent = False
708 708 if self.db_repo.parent:
709 709 if filter_query in self.db_repo.parent.repo_name:
710 710 parent_vcs_obj = self.db_repo.parent.scm_instance()
711 711 if parent_vcs_obj and not parent_vcs_obj.is_empty():
712 712 add_parent = True
713 713
714 714 limit = 20 - 1 if add_parent else 20
715 715 all_repos = query.limit(limit).all()
716 716 if add_parent:
717 717 all_repos += [self.db_repo.parent]
718 718
719 719 repos = []
720 720 for obj in ScmModel().get_repos(all_repos):
721 721 repos.append({
722 722 'id': obj['name'],
723 723 'text': obj['name'],
724 724 'type': 'repo',
725 725 'obj': obj['dbrepo']
726 726 })
727 727
728 728 data = {
729 729 'more': False,
730 730 'results': [{
731 731 'text': _('Repositories'),
732 732 'children': repos
733 733 }] if repos else []
734 734 }
735 735 return data
736 736
737 737 @LoginRequired()
738 738 @NotAnonymous()
739 739 @HasRepoPermissionAnyDecorator(
740 740 'repository.read', 'repository.write', 'repository.admin')
741 741 @CSRFRequired()
742 742 @view_config(
743 743 route_name='pullrequest_create', request_method='POST',
744 744 renderer=None)
745 745 def pull_request_create(self):
746 746 _ = self.request.translate
747 747 self.assure_not_empty_repo()
748 748
749 749 controls = peppercorn.parse(self.request.POST.items())
750 750
751 751 try:
752 752 _form = PullRequestForm(self.db_repo.repo_id)().to_python(controls)
753 753 except formencode.Invalid as errors:
754 754 if errors.error_dict.get('revisions'):
755 755 msg = 'Revisions: %s' % errors.error_dict['revisions']
756 756 elif errors.error_dict.get('pullrequest_title'):
757 757 msg = _('Pull request requires a title with min. 3 chars')
758 758 else:
759 759 msg = _('Error creating pull request: {}').format(errors)
760 760 log.exception(msg)
761 761 h.flash(msg, 'error')
762 762
763 763 # would rather just go back to form ...
764 764 raise HTTPFound(
765 765 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
766 766
767 767 source_repo = _form['source_repo']
768 768 source_ref = _form['source_ref']
769 769 target_repo = _form['target_repo']
770 770 target_ref = _form['target_ref']
771 771 commit_ids = _form['revisions'][::-1]
772 772
773 773 # find the ancestor for this pr
774 774 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
775 775 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
776 776
777 777 # re-check permissions again here
778 778 # source_repo we must have read permissions
779 779
780 780 source_perm = HasRepoPermissionAny(
781 781 'repository.read',
782 782 'repository.write', 'repository.admin')(source_db_repo.repo_name)
783 783 if not source_perm:
784 784 msg = _('Not Enough permissions to source repo `{}`.'.format(
785 785 source_db_repo.repo_name))
786 786 h.flash(msg, category='error')
787 787 # copy the args back to redirect
788 788 org_query = self.request.GET.mixed()
789 789 raise HTTPFound(
790 790 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
791 791 _query=org_query))
792 792
793 793 # target repo we must have write permissions, and also later on
794 794 # we want to check branch permissions here
795 795 target_perm = HasRepoPermissionAny(
796 796 'repository.write', 'repository.admin')(target_db_repo.repo_name)
797 797 if not target_perm:
798 798 msg = _('Not Enough permissions to target repo `{}`.'.format(
799 799 target_db_repo.repo_name))
800 800 h.flash(msg, category='error')
801 801 # copy the args back to redirect
802 802 org_query = self.request.GET.mixed()
803 803 raise HTTPFound(
804 804 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
805 805 _query=org_query))
806 806
807 807 source_scm = source_db_repo.scm_instance()
808 808 target_scm = target_db_repo.scm_instance()
809 809
810 810 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
811 811 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
812 812
813 813 ancestor = source_scm.get_common_ancestor(
814 814 source_commit.raw_id, target_commit.raw_id, target_scm)
815 815
816 816 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
817 817 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
818 818
819 819 pullrequest_title = _form['pullrequest_title']
820 820 title_source_ref = source_ref.split(':', 2)[1]
821 821 if not pullrequest_title:
822 822 pullrequest_title = PullRequestModel().generate_pullrequest_title(
823 823 source=source_repo,
824 824 source_ref=title_source_ref,
825 825 target=target_repo
826 826 )
827 827
828 828 description = _form['pullrequest_desc']
829 829
830 830 get_default_reviewers_data, validate_default_reviewers = \
831 831 PullRequestModel().get_reviewer_functions()
832 832
833 833 # recalculate reviewers logic, to make sure we can validate this
834 834 reviewer_rules = get_default_reviewers_data(
835 835 self._rhodecode_db_user, source_db_repo,
836 836 source_commit, target_db_repo, target_commit)
837 837
838 838 given_reviewers = _form['review_members']
839 839 reviewers = validate_default_reviewers(given_reviewers, reviewer_rules)
840 840
841 841 try:
842 842 pull_request = PullRequestModel().create(
843 843 self._rhodecode_user.user_id, source_repo, source_ref,
844 844 target_repo, target_ref, commit_ids, reviewers,
845 845 pullrequest_title, description, reviewer_rules
846 846 )
847 847 Session().commit()
848 848
849 849 h.flash(_('Successfully opened new pull request'),
850 850 category='success')
851 851 except Exception:
852 852 msg = _('Error occurred during creation of this pull request.')
853 853 log.exception(msg)
854 854 h.flash(msg, category='error')
855 855
856 856 # copy the args back to redirect
857 857 org_query = self.request.GET.mixed()
858 858 raise HTTPFound(
859 859 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
860 860 _query=org_query))
861 861
862 862 raise HTTPFound(
863 863 h.route_path('pullrequest_show', repo_name=target_repo,
864 864 pull_request_id=pull_request.pull_request_id))
865 865
866 866 @LoginRequired()
867 867 @NotAnonymous()
868 868 @HasRepoPermissionAnyDecorator(
869 869 'repository.read', 'repository.write', 'repository.admin')
870 870 @CSRFRequired()
871 871 @view_config(
872 872 route_name='pullrequest_update', request_method='POST',
873 873 renderer='json_ext')
874 874 def pull_request_update(self):
875 875 pull_request = PullRequest.get_or_404(
876 876 self.request.matchdict['pull_request_id'])
877 877
878 878 # only owner or admin can update it
879 879 allowed_to_update = PullRequestModel().check_user_update(
880 880 pull_request, self._rhodecode_user)
881 881 if allowed_to_update:
882 882 controls = peppercorn.parse(self.request.POST.items())
883 883
884 884 if 'review_members' in controls:
885 885 self._update_reviewers(
886 886 pull_request, controls['review_members'],
887 887 pull_request.reviewer_data)
888 888 elif str2bool(self.request.POST.get('update_commits', 'false')):
889 889 self._update_commits(pull_request)
890 890 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
891 891 self._edit_pull_request(pull_request)
892 892 else:
893 893 raise HTTPBadRequest()
894 894 return True
895 895 raise HTTPForbidden()
896 896
897 897 def _edit_pull_request(self, pull_request):
898 898 _ = self.request.translate
899 899 try:
900 900 PullRequestModel().edit(
901 901 pull_request, self.request.POST.get('title'),
902 902 self.request.POST.get('description'), self._rhodecode_user)
903 903 except ValueError:
904 904 msg = _(u'Cannot update closed pull requests.')
905 905 h.flash(msg, category='error')
906 906 return
907 907 else:
908 908 Session().commit()
909 909
910 910 msg = _(u'Pull request title & description updated.')
911 911 h.flash(msg, category='success')
912 912 return
913 913
914 914 def _update_commits(self, pull_request):
915 915 _ = self.request.translate
916 916 resp = PullRequestModel().update_commits(pull_request)
917 917
918 918 if resp.executed:
919 919
920 920 if resp.target_changed and resp.source_changed:
921 921 changed = 'target and source repositories'
922 922 elif resp.target_changed and not resp.source_changed:
923 923 changed = 'target repository'
924 924 elif not resp.target_changed and resp.source_changed:
925 925 changed = 'source repository'
926 926 else:
927 927 changed = 'nothing'
928 928
929 929 msg = _(
930 930 u'Pull request updated to "{source_commit_id}" with '
931 931 u'{count_added} added, {count_removed} removed commits. '
932 932 u'Source of changes: {change_source}')
933 933 msg = msg.format(
934 934 source_commit_id=pull_request.source_ref_parts.commit_id,
935 935 count_added=len(resp.changes.added),
936 936 count_removed=len(resp.changes.removed),
937 937 change_source=changed)
938 938 h.flash(msg, category='success')
939 939
940 940 channel = '/repo${}$/pr/{}'.format(
941 941 pull_request.target_repo.repo_name,
942 942 pull_request.pull_request_id)
943 943 message = msg + (
944 944 ' - <a onclick="window.location.reload()">'
945 945 '<strong>{}</strong></a>'.format(_('Reload page')))
946 946 channelstream.post_message(
947 947 channel, message, self._rhodecode_user.username,
948 948 registry=self.request.registry)
949 949 else:
950 950 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
951 951 warning_reasons = [
952 952 UpdateFailureReason.NO_CHANGE,
953 953 UpdateFailureReason.WRONG_REF_TYPE,
954 954 ]
955 955 category = 'warning' if resp.reason in warning_reasons else 'error'
956 956 h.flash(msg, category=category)
957 957
958 958 @LoginRequired()
959 959 @NotAnonymous()
960 960 @HasRepoPermissionAnyDecorator(
961 961 'repository.read', 'repository.write', 'repository.admin')
962 962 @CSRFRequired()
963 963 @view_config(
964 964 route_name='pullrequest_merge', request_method='POST',
965 965 renderer='json_ext')
966 966 def pull_request_merge(self):
967 967 """
968 968 Merge will perform a server-side merge of the specified
969 969 pull request, if the pull request is approved and mergeable.
970 970 After successful merging, the pull request is automatically
971 971 closed, with a relevant comment.
972 972 """
973 973 pull_request = PullRequest.get_or_404(
974 974 self.request.matchdict['pull_request_id'])
975 975
976 976 check = MergeCheck.validate(pull_request, self._rhodecode_db_user,
977 977 translator=self.request.translate)
978 978 merge_possible = not check.failed
979 979
980 980 for err_type, error_msg in check.errors:
981 981 h.flash(error_msg, category=err_type)
982 982
983 983 if merge_possible:
984 984 log.debug("Pre-conditions checked, trying to merge.")
985 985 extras = vcs_operation_context(
986 986 self.request.environ, repo_name=pull_request.target_repo.repo_name,
987 987 username=self._rhodecode_db_user.username, action='push',
988 988 scm=pull_request.target_repo.repo_type)
989 989 self._merge_pull_request(
990 990 pull_request, self._rhodecode_db_user, extras)
991 991 else:
992 992 log.debug("Pre-conditions failed, NOT merging.")
993 993
994 994 raise HTTPFound(
995 995 h.route_path('pullrequest_show',
996 996 repo_name=pull_request.target_repo.repo_name,
997 997 pull_request_id=pull_request.pull_request_id))
998 998
999 999 def _merge_pull_request(self, pull_request, user, extras):
1000 1000 _ = self.request.translate
1001 1001 merge_resp = PullRequestModel().merge(pull_request, user, extras=extras)
1002 1002
1003 1003 if merge_resp.executed:
1004 1004 log.debug("The merge was successful, closing the pull request.")
1005 1005 PullRequestModel().close_pull_request(
1006 1006 pull_request.pull_request_id, user)
1007 1007 Session().commit()
1008 1008 msg = _('Pull request was successfully merged and closed.')
1009 1009 h.flash(msg, category='success')
1010 1010 else:
1011 1011 log.debug(
1012 1012 "The merge was not successful. Merge response: %s",
1013 1013 merge_resp)
1014 1014 msg = PullRequestModel().merge_status_message(
1015 1015 merge_resp.failure_reason)
1016 1016 h.flash(msg, category='error')
1017 1017
1018 1018 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1019 1019 _ = self.request.translate
1020 1020 get_default_reviewers_data, validate_default_reviewers = \
1021 1021 PullRequestModel().get_reviewer_functions()
1022 1022
1023 1023 try:
1024 1024 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1025 1025 except ValueError as e:
1026 1026 log.error('Reviewers Validation: {}'.format(e))
1027 1027 h.flash(e, category='error')
1028 1028 return
1029 1029
1030 1030 PullRequestModel().update_reviewers(
1031 1031 pull_request, reviewers, self._rhodecode_user)
1032 1032 h.flash(_('Pull request reviewers updated.'), category='success')
1033 1033 Session().commit()
1034 1034
1035 1035 @LoginRequired()
1036 1036 @NotAnonymous()
1037 1037 @HasRepoPermissionAnyDecorator(
1038 1038 'repository.read', 'repository.write', 'repository.admin')
1039 1039 @CSRFRequired()
1040 1040 @view_config(
1041 1041 route_name='pullrequest_delete', request_method='POST',
1042 1042 renderer='json_ext')
1043 1043 def pull_request_delete(self):
1044 1044 _ = self.request.translate
1045 1045
1046 1046 pull_request = PullRequest.get_or_404(
1047 1047 self.request.matchdict['pull_request_id'])
1048 1048
1049 1049 pr_closed = pull_request.is_closed()
1050 1050 allowed_to_delete = PullRequestModel().check_user_delete(
1051 1051 pull_request, self._rhodecode_user) and not pr_closed
1052 1052
1053 1053 # only owner can delete it !
1054 1054 if allowed_to_delete:
1055 1055 PullRequestModel().delete(pull_request, self._rhodecode_user)
1056 1056 Session().commit()
1057 1057 h.flash(_('Successfully deleted pull request'),
1058 1058 category='success')
1059 1059 raise HTTPFound(h.route_path('pullrequest_show_all',
1060 1060 repo_name=self.db_repo_name))
1061 1061
1062 1062 log.warning('user %s tried to delete pull request without access',
1063 1063 self._rhodecode_user)
1064 1064 raise HTTPNotFound()
1065 1065
1066 1066 @LoginRequired()
1067 1067 @NotAnonymous()
1068 1068 @HasRepoPermissionAnyDecorator(
1069 1069 'repository.read', 'repository.write', 'repository.admin')
1070 1070 @CSRFRequired()
1071 1071 @view_config(
1072 1072 route_name='pullrequest_comment_create', request_method='POST',
1073 1073 renderer='json_ext')
1074 1074 def pull_request_comment_create(self):
1075 1075 _ = self.request.translate
1076 1076
1077 1077 pull_request = PullRequest.get_or_404(
1078 1078 self.request.matchdict['pull_request_id'])
1079 1079 pull_request_id = pull_request.pull_request_id
1080 1080
1081 1081 if pull_request.is_closed():
1082 1082 log.debug('comment: forbidden because pull request is closed')
1083 1083 raise HTTPForbidden()
1084 1084
1085 1085 allowed_to_comment = PullRequestModel().check_user_comment(
1086 1086 pull_request, self._rhodecode_user)
1087 1087 if not allowed_to_comment:
1088 1088 log.debug(
1089 1089 'comment: forbidden because pull request is from forbidden repo')
1090 1090 raise HTTPForbidden()
1091 1091
1092 1092 c = self.load_default_context()
1093 1093
1094 1094 status = self.request.POST.get('changeset_status', None)
1095 1095 text = self.request.POST.get('text')
1096 1096 comment_type = self.request.POST.get('comment_type')
1097 1097 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1098 1098 close_pull_request = self.request.POST.get('close_pull_request')
1099 1099
1100 1100 # the logic here should work like following, if we submit close
1101 1101 # pr comment, use `close_pull_request_with_comment` function
1102 1102 # else handle regular comment logic
1103 1103
1104 1104 if close_pull_request:
1105 1105 # only owner or admin or person with write permissions
1106 1106 allowed_to_close = PullRequestModel().check_user_update(
1107 1107 pull_request, self._rhodecode_user)
1108 1108 if not allowed_to_close:
1109 1109 log.debug('comment: forbidden because not allowed to close '
1110 1110 'pull request %s', pull_request_id)
1111 1111 raise HTTPForbidden()
1112 1112 comment, status = PullRequestModel().close_pull_request_with_comment(
1113 1113 pull_request, self._rhodecode_user, self.db_repo, message=text)
1114 1114 Session().flush()
1115 1115 events.trigger(
1116 1116 events.PullRequestCommentEvent(pull_request, comment))
1117 1117
1118 1118 else:
1119 1119 # regular comment case, could be inline, or one with status.
1120 1120 # for that one we check also permissions
1121 1121
1122 1122 allowed_to_change_status = PullRequestModel().check_user_change_status(
1123 1123 pull_request, self._rhodecode_user)
1124 1124
1125 1125 if status and allowed_to_change_status:
1126 1126 message = (_('Status change %(transition_icon)s %(status)s')
1127 1127 % {'transition_icon': '>',
1128 1128 'status': ChangesetStatus.get_status_lbl(status)})
1129 1129 text = text or message
1130 1130
1131 1131 comment = CommentsModel().create(
1132 1132 text=text,
1133 1133 repo=self.db_repo.repo_id,
1134 1134 user=self._rhodecode_user.user_id,
1135 1135 pull_request=pull_request,
1136 1136 f_path=self.request.POST.get('f_path'),
1137 1137 line_no=self.request.POST.get('line'),
1138 1138 status_change=(ChangesetStatus.get_status_lbl(status)
1139 1139 if status and allowed_to_change_status else None),
1140 1140 status_change_type=(status
1141 1141 if status and allowed_to_change_status else None),
1142 1142 comment_type=comment_type,
1143 1143 resolves_comment_id=resolves_comment_id
1144 1144 )
1145 1145
1146 1146 if allowed_to_change_status:
1147 1147 # calculate old status before we change it
1148 1148 old_calculated_status = pull_request.calculated_review_status()
1149 1149
1150 1150 # get status if set !
1151 1151 if status:
1152 1152 ChangesetStatusModel().set_status(
1153 1153 self.db_repo.repo_id,
1154 1154 status,
1155 1155 self._rhodecode_user.user_id,
1156 1156 comment,
1157 1157 pull_request=pull_request
1158 1158 )
1159 1159
1160 1160 Session().flush()
1161 1161 events.trigger(
1162 1162 events.PullRequestCommentEvent(pull_request, comment))
1163 1163
1164 1164 # we now calculate the status of pull request, and based on that
1165 1165 # calculation we set the commits status
1166 1166 calculated_status = pull_request.calculated_review_status()
1167 1167 if old_calculated_status != calculated_status:
1168 1168 PullRequestModel()._trigger_pull_request_hook(
1169 1169 pull_request, self._rhodecode_user, 'review_status_change')
1170 1170
1171 1171 Session().commit()
1172 1172
1173 1173 data = {
1174 1174 'target_id': h.safeid(h.safe_unicode(
1175 1175 self.request.POST.get('f_path'))),
1176 1176 }
1177 1177 if comment:
1178 1178 c.co = comment
1179 1179 rendered_comment = render(
1180 1180 'rhodecode:templates/changeset/changeset_comment_block.mako',
1181 1181 self._get_template_context(c), self.request)
1182 1182
1183 1183 data.update(comment.get_dict())
1184 1184 data.update({'rendered_text': rendered_comment})
1185 1185
1186 1186 return data
1187 1187
1188 1188 @LoginRequired()
1189 1189 @NotAnonymous()
1190 1190 @HasRepoPermissionAnyDecorator(
1191 1191 'repository.read', 'repository.write', 'repository.admin')
1192 1192 @CSRFRequired()
1193 1193 @view_config(
1194 1194 route_name='pullrequest_comment_delete', request_method='POST',
1195 1195 renderer='json_ext')
1196 1196 def pull_request_comment_delete(self):
1197 1197 pull_request = PullRequest.get_or_404(
1198 1198 self.request.matchdict['pull_request_id'])
1199 1199
1200 1200 comment = ChangesetComment.get_or_404(
1201 1201 self.request.matchdict['comment_id'])
1202 1202 comment_id = comment.comment_id
1203 1203
1204 1204 if pull_request.is_closed():
1205 1205 log.debug('comment: forbidden because pull request is closed')
1206 1206 raise HTTPForbidden()
1207 1207
1208 1208 if not comment:
1209 1209 log.debug('Comment with id:%s not found, skipping', comment_id)
1210 1210 # comment already deleted in another call probably
1211 1211 return True
1212 1212
1213 1213 if comment.pull_request.is_closed():
1214 1214 # don't allow deleting comments on closed pull request
1215 1215 raise HTTPForbidden()
1216 1216
1217 1217 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1218 1218 super_admin = h.HasPermissionAny('hg.admin')()
1219 1219 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1220 1220 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1221 1221 comment_repo_admin = is_repo_admin and is_repo_comment
1222 1222
1223 1223 if super_admin or comment_owner or comment_repo_admin:
1224 1224 old_calculated_status = comment.pull_request.calculated_review_status()
1225 1225 CommentsModel().delete(comment=comment, user=self._rhodecode_user)
1226 1226 Session().commit()
1227 1227 calculated_status = comment.pull_request.calculated_review_status()
1228 1228 if old_calculated_status != calculated_status:
1229 1229 PullRequestModel()._trigger_pull_request_hook(
1230 1230 comment.pull_request, self._rhodecode_user, 'review_status_change')
1231 1231 return True
1232 1232 else:
1233 1233 log.warning('No permissions for user %s to delete comment_id: %s',
1234 1234 self._rhodecode_db_user, comment_id)
1235 1235 raise HTTPNotFound()
@@ -1,2101 +1,2102 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Helper functions
23 23
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 27
28 28 import random
29 29 import hashlib
30 30 import StringIO
31 31 import urllib
32 32 import math
33 33 import logging
34 34 import re
35 35 import urlparse
36 36 import time
37 37 import string
38 38 import hashlib
39 39 from collections import OrderedDict
40 40
41 41 import pygments
42 42 import itertools
43 43 import fnmatch
44 44
45 45 from datetime import datetime
46 46 from functools import partial
47 47 from pygments.formatters.html import HtmlFormatter
48 48 from pygments import highlight as code_highlight
49 49 from pygments.lexers import (
50 50 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
51 51
52 52 from pyramid.threadlocal import get_current_request
53 53
54 54 from webhelpers.html import literal, HTML, escape
55 55 from webhelpers.html.tools import *
56 56 from webhelpers.html.builder import make_tag
57 57 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
58 58 end_form, file, form as wh_form, hidden, image, javascript_link, link_to, \
59 59 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
60 60 submit, text, password, textarea, title, ul, xml_declaration, radio
61 61 from webhelpers.html.tools import auto_link, button_to, highlight, \
62 62 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
63 63 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
64 64 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
65 65 replace_whitespace, urlify, truncate, wrap_paragraphs
66 66 from webhelpers.date import time_ago_in_words
67 67 from webhelpers.paginate import Page as _Page
68 68 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
69 69 convert_boolean_attrs, NotGiven, _make_safe_id_component
70 70 from webhelpers2.number import format_byte_size
71 71
72 72 from rhodecode.lib.action_parser import action_parser
73 73 from rhodecode.lib.ext_json import json
74 74 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
75 75 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
76 76 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime, \
77 77 AttributeDict, safe_int, md5, md5_safe
78 78 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
79 79 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
80 80 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
81 81 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
82 82 from rhodecode.model.changeset_status import ChangesetStatusModel
83 83 from rhodecode.model.db import Permission, User, Repository
84 84 from rhodecode.model.repo_group import RepoGroupModel
85 85 from rhodecode.model.settings import IssueTrackerSettingsModel
86 86
87 87 log = logging.getLogger(__name__)
88 88
89 89
90 90 DEFAULT_USER = User.DEFAULT_USER
91 91 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
92 92
93 93
94 94 def url(*args, **kw):
95 95 from pylons import url as pylons_url
96 96 return pylons_url(*args, **kw)
97 97
98 98
99 99 def asset(path, ver=None, **kwargs):
100 100 """
101 101 Helper to generate a static asset file path for rhodecode assets
102 102
103 103 eg. h.asset('images/image.png', ver='3923')
104 104
105 105 :param path: path of asset
106 106 :param ver: optional version query param to append as ?ver=
107 107 """
108 108 request = get_current_request()
109 109 query = {}
110 110 query.update(kwargs)
111 111 if ver:
112 112 query = {'ver': ver}
113 113 return request.static_path(
114 114 'rhodecode:public/{}'.format(path), _query=query)
115 115
116 116
117 117 default_html_escape_table = {
118 118 ord('&'): u'&amp;',
119 119 ord('<'): u'&lt;',
120 120 ord('>'): u'&gt;',
121 121 ord('"'): u'&quot;',
122 122 ord("'"): u'&#39;',
123 123 }
124 124
125 125
126 126 def html_escape(text, html_escape_table=default_html_escape_table):
127 127 """Produce entities within text."""
128 128 return text.translate(html_escape_table)
129 129
130 130
131 131 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
132 132 """
133 133 Truncate string ``s`` at the first occurrence of ``sub``.
134 134
135 135 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
136 136 """
137 137 suffix_if_chopped = suffix_if_chopped or ''
138 138 pos = s.find(sub)
139 139 if pos == -1:
140 140 return s
141 141
142 142 if inclusive:
143 143 pos += len(sub)
144 144
145 145 chopped = s[:pos]
146 146 left = s[pos:].strip()
147 147
148 148 if left and suffix_if_chopped:
149 149 chopped += suffix_if_chopped
150 150
151 151 return chopped
152 152
153 153
154 154 def shorter(text, size=20):
155 155 postfix = '...'
156 156 if len(text) > size:
157 157 return text[:size - len(postfix)] + postfix
158 158 return text
159 159
160 160
161 161 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
162 162 """
163 163 Reset button
164 164 """
165 165 _set_input_attrs(attrs, type, name, value)
166 166 _set_id_attr(attrs, id, name)
167 167 convert_boolean_attrs(attrs, ["disabled"])
168 168 return HTML.input(**attrs)
169 169
170 170 reset = _reset
171 171 safeid = _make_safe_id_component
172 172
173 173
174 174 def branding(name, length=40):
175 175 return truncate(name, length, indicator="")
176 176
177 177
178 178 def FID(raw_id, path):
179 179 """
180 180 Creates a unique ID for filenode based on it's hash of path and commit
181 181 it's safe to use in urls
182 182
183 183 :param raw_id:
184 184 :param path:
185 185 """
186 186
187 187 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
188 188
189 189
190 190 class _GetError(object):
191 191 """Get error from form_errors, and represent it as span wrapped error
192 192 message
193 193
194 194 :param field_name: field to fetch errors for
195 195 :param form_errors: form errors dict
196 196 """
197 197
198 198 def __call__(self, field_name, form_errors):
199 199 tmpl = """<span class="error_msg">%s</span>"""
200 200 if form_errors and field_name in form_errors:
201 201 return literal(tmpl % form_errors.get(field_name))
202 202
203 203 get_error = _GetError()
204 204
205 205
206 206 class _ToolTip(object):
207 207
208 208 def __call__(self, tooltip_title, trim_at=50):
209 209 """
210 210 Special function just to wrap our text into nice formatted
211 211 autowrapped text
212 212
213 213 :param tooltip_title:
214 214 """
215 215 tooltip_title = escape(tooltip_title)
216 216 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
217 217 return tooltip_title
218 218 tooltip = _ToolTip()
219 219
220 220
221 221 def files_breadcrumbs(repo_name, commit_id, file_path):
222 222 if isinstance(file_path, str):
223 223 file_path = safe_unicode(file_path)
224 224
225 225 # TODO: johbo: Is this always a url like path, or is this operating
226 226 # system dependent?
227 227 path_segments = file_path.split('/')
228 228
229 229 repo_name_html = escape(repo_name)
230 230 if len(path_segments) == 1 and path_segments[0] == '':
231 231 url_segments = [repo_name_html]
232 232 else:
233 233 url_segments = [
234 234 link_to(
235 235 repo_name_html,
236 236 route_path(
237 237 'repo_files',
238 238 repo_name=repo_name,
239 239 commit_id=commit_id,
240 240 f_path=''),
241 241 class_='pjax-link')]
242 242
243 243 last_cnt = len(path_segments) - 1
244 244 for cnt, segment in enumerate(path_segments):
245 245 if not segment:
246 246 continue
247 247 segment_html = escape(segment)
248 248
249 249 if cnt != last_cnt:
250 250 url_segments.append(
251 251 link_to(
252 252 segment_html,
253 253 route_path(
254 254 'repo_files',
255 255 repo_name=repo_name,
256 256 commit_id=commit_id,
257 257 f_path='/'.join(path_segments[:cnt + 1])),
258 258 class_='pjax-link'))
259 259 else:
260 260 url_segments.append(segment_html)
261 261
262 262 return literal('/'.join(url_segments))
263 263
264 264
265 265 class CodeHtmlFormatter(HtmlFormatter):
266 266 """
267 267 My code Html Formatter for source codes
268 268 """
269 269
270 270 def wrap(self, source, outfile):
271 271 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
272 272
273 273 def _wrap_code(self, source):
274 274 for cnt, it in enumerate(source):
275 275 i, t = it
276 276 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
277 277 yield i, t
278 278
279 279 def _wrap_tablelinenos(self, inner):
280 280 dummyoutfile = StringIO.StringIO()
281 281 lncount = 0
282 282 for t, line in inner:
283 283 if t:
284 284 lncount += 1
285 285 dummyoutfile.write(line)
286 286
287 287 fl = self.linenostart
288 288 mw = len(str(lncount + fl - 1))
289 289 sp = self.linenospecial
290 290 st = self.linenostep
291 291 la = self.lineanchors
292 292 aln = self.anchorlinenos
293 293 nocls = self.noclasses
294 294 if sp:
295 295 lines = []
296 296
297 297 for i in range(fl, fl + lncount):
298 298 if i % st == 0:
299 299 if i % sp == 0:
300 300 if aln:
301 301 lines.append('<a href="#%s%d" class="special">%*d</a>' %
302 302 (la, i, mw, i))
303 303 else:
304 304 lines.append('<span class="special">%*d</span>' % (mw, i))
305 305 else:
306 306 if aln:
307 307 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
308 308 else:
309 309 lines.append('%*d' % (mw, i))
310 310 else:
311 311 lines.append('')
312 312 ls = '\n'.join(lines)
313 313 else:
314 314 lines = []
315 315 for i in range(fl, fl + lncount):
316 316 if i % st == 0:
317 317 if aln:
318 318 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
319 319 else:
320 320 lines.append('%*d' % (mw, i))
321 321 else:
322 322 lines.append('')
323 323 ls = '\n'.join(lines)
324 324
325 325 # in case you wonder about the seemingly redundant <div> here: since the
326 326 # content in the other cell also is wrapped in a div, some browsers in
327 327 # some configurations seem to mess up the formatting...
328 328 if nocls:
329 329 yield 0, ('<table class="%stable">' % self.cssclass +
330 330 '<tr><td><div class="linenodiv" '
331 331 'style="background-color: #f0f0f0; padding-right: 10px">'
332 332 '<pre style="line-height: 125%">' +
333 333 ls + '</pre></div></td><td id="hlcode" class="code">')
334 334 else:
335 335 yield 0, ('<table class="%stable">' % self.cssclass +
336 336 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
337 337 ls + '</pre></div></td><td id="hlcode" class="code">')
338 338 yield 0, dummyoutfile.getvalue()
339 339 yield 0, '</td></tr></table>'
340 340
341 341
342 342 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
343 343 def __init__(self, **kw):
344 344 # only show these line numbers if set
345 345 self.only_lines = kw.pop('only_line_numbers', [])
346 346 self.query_terms = kw.pop('query_terms', [])
347 347 self.max_lines = kw.pop('max_lines', 5)
348 348 self.line_context = kw.pop('line_context', 3)
349 349 self.url = kw.pop('url', None)
350 350
351 351 super(CodeHtmlFormatter, self).__init__(**kw)
352 352
353 353 def _wrap_code(self, source):
354 354 for cnt, it in enumerate(source):
355 355 i, t = it
356 356 t = '<pre>%s</pre>' % t
357 357 yield i, t
358 358
359 359 def _wrap_tablelinenos(self, inner):
360 360 yield 0, '<table class="code-highlight %stable">' % self.cssclass
361 361
362 362 last_shown_line_number = 0
363 363 current_line_number = 1
364 364
365 365 for t, line in inner:
366 366 if not t:
367 367 yield t, line
368 368 continue
369 369
370 370 if current_line_number in self.only_lines:
371 371 if last_shown_line_number + 1 != current_line_number:
372 372 yield 0, '<tr>'
373 373 yield 0, '<td class="line">...</td>'
374 374 yield 0, '<td id="hlcode" class="code"></td>'
375 375 yield 0, '</tr>'
376 376
377 377 yield 0, '<tr>'
378 378 if self.url:
379 379 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
380 380 self.url, current_line_number, current_line_number)
381 381 else:
382 382 yield 0, '<td class="line"><a href="">%i</a></td>' % (
383 383 current_line_number)
384 384 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
385 385 yield 0, '</tr>'
386 386
387 387 last_shown_line_number = current_line_number
388 388
389 389 current_line_number += 1
390 390
391 391
392 392 yield 0, '</table>'
393 393
394 394
395 395 def extract_phrases(text_query):
396 396 """
397 397 Extracts phrases from search term string making sure phrases
398 398 contained in double quotes are kept together - and discarding empty values
399 399 or fully whitespace values eg.
400 400
401 401 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
402 402
403 403 """
404 404
405 405 in_phrase = False
406 406 buf = ''
407 407 phrases = []
408 408 for char in text_query:
409 409 if in_phrase:
410 410 if char == '"': # end phrase
411 411 phrases.append(buf)
412 412 buf = ''
413 413 in_phrase = False
414 414 continue
415 415 else:
416 416 buf += char
417 417 continue
418 418 else:
419 419 if char == '"': # start phrase
420 420 in_phrase = True
421 421 phrases.append(buf)
422 422 buf = ''
423 423 continue
424 424 elif char == ' ':
425 425 phrases.append(buf)
426 426 buf = ''
427 427 continue
428 428 else:
429 429 buf += char
430 430
431 431 phrases.append(buf)
432 432 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
433 433 return phrases
434 434
435 435
436 436 def get_matching_offsets(text, phrases):
437 437 """
438 438 Returns a list of string offsets in `text` that the list of `terms` match
439 439
440 440 >>> get_matching_offsets('some text here', ['some', 'here'])
441 441 [(0, 4), (10, 14)]
442 442
443 443 """
444 444 offsets = []
445 445 for phrase in phrases:
446 446 for match in re.finditer(phrase, text):
447 447 offsets.append((match.start(), match.end()))
448 448
449 449 return offsets
450 450
451 451
452 452 def normalize_text_for_matching(x):
453 453 """
454 454 Replaces all non alnum characters to spaces and lower cases the string,
455 455 useful for comparing two text strings without punctuation
456 456 """
457 457 return re.sub(r'[^\w]', ' ', x.lower())
458 458
459 459
460 460 def get_matching_line_offsets(lines, terms):
461 461 """ Return a set of `lines` indices (starting from 1) matching a
462 462 text search query, along with `context` lines above/below matching lines
463 463
464 464 :param lines: list of strings representing lines
465 465 :param terms: search term string to match in lines eg. 'some text'
466 466 :param context: number of lines above/below a matching line to add to result
467 467 :param max_lines: cut off for lines of interest
468 468 eg.
469 469
470 470 text = '''
471 471 words words words
472 472 words words words
473 473 some text some
474 474 words words words
475 475 words words words
476 476 text here what
477 477 '''
478 478 get_matching_line_offsets(text, 'text', context=1)
479 479 {3: [(5, 9)], 6: [(0, 4)]]
480 480
481 481 """
482 482 matching_lines = {}
483 483 phrases = [normalize_text_for_matching(phrase)
484 484 for phrase in extract_phrases(terms)]
485 485
486 486 for line_index, line in enumerate(lines, start=1):
487 487 match_offsets = get_matching_offsets(
488 488 normalize_text_for_matching(line), phrases)
489 489 if match_offsets:
490 490 matching_lines[line_index] = match_offsets
491 491
492 492 return matching_lines
493 493
494 494
495 495 def hsv_to_rgb(h, s, v):
496 496 """ Convert hsv color values to rgb """
497 497
498 498 if s == 0.0:
499 499 return v, v, v
500 500 i = int(h * 6.0) # XXX assume int() truncates!
501 501 f = (h * 6.0) - i
502 502 p = v * (1.0 - s)
503 503 q = v * (1.0 - s * f)
504 504 t = v * (1.0 - s * (1.0 - f))
505 505 i = i % 6
506 506 if i == 0:
507 507 return v, t, p
508 508 if i == 1:
509 509 return q, v, p
510 510 if i == 2:
511 511 return p, v, t
512 512 if i == 3:
513 513 return p, q, v
514 514 if i == 4:
515 515 return t, p, v
516 516 if i == 5:
517 517 return v, p, q
518 518
519 519
520 520 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
521 521 """
522 522 Generator for getting n of evenly distributed colors using
523 523 hsv color and golden ratio. It always return same order of colors
524 524
525 525 :param n: number of colors to generate
526 526 :param saturation: saturation of returned colors
527 527 :param lightness: lightness of returned colors
528 528 :returns: RGB tuple
529 529 """
530 530
531 531 golden_ratio = 0.618033988749895
532 532 h = 0.22717784590367374
533 533
534 534 for _ in xrange(n):
535 535 h += golden_ratio
536 536 h %= 1
537 537 HSV_tuple = [h, saturation, lightness]
538 538 RGB_tuple = hsv_to_rgb(*HSV_tuple)
539 539 yield map(lambda x: str(int(x * 256)), RGB_tuple)
540 540
541 541
542 542 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
543 543 """
544 544 Returns a function which when called with an argument returns a unique
545 545 color for that argument, eg.
546 546
547 547 :param n: number of colors to generate
548 548 :param saturation: saturation of returned colors
549 549 :param lightness: lightness of returned colors
550 550 :returns: css RGB string
551 551
552 552 >>> color_hash = color_hasher()
553 553 >>> color_hash('hello')
554 554 'rgb(34, 12, 59)'
555 555 >>> color_hash('hello')
556 556 'rgb(34, 12, 59)'
557 557 >>> color_hash('other')
558 558 'rgb(90, 224, 159)'
559 559 """
560 560
561 561 color_dict = {}
562 562 cgenerator = unique_color_generator(
563 563 saturation=saturation, lightness=lightness)
564 564
565 565 def get_color_string(thing):
566 566 if thing in color_dict:
567 567 col = color_dict[thing]
568 568 else:
569 569 col = color_dict[thing] = cgenerator.next()
570 570 return "rgb(%s)" % (', '.join(col))
571 571
572 572 return get_color_string
573 573
574 574
575 575 def get_lexer_safe(mimetype=None, filepath=None):
576 576 """
577 577 Tries to return a relevant pygments lexer using mimetype/filepath name,
578 578 defaulting to plain text if none could be found
579 579 """
580 580 lexer = None
581 581 try:
582 582 if mimetype:
583 583 lexer = get_lexer_for_mimetype(mimetype)
584 584 if not lexer:
585 585 lexer = get_lexer_for_filename(filepath)
586 586 except pygments.util.ClassNotFound:
587 587 pass
588 588
589 589 if not lexer:
590 590 lexer = get_lexer_by_name('text')
591 591
592 592 return lexer
593 593
594 594
595 595 def get_lexer_for_filenode(filenode):
596 596 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
597 597 return lexer
598 598
599 599
600 600 def pygmentize(filenode, **kwargs):
601 601 """
602 602 pygmentize function using pygments
603 603
604 604 :param filenode:
605 605 """
606 606 lexer = get_lexer_for_filenode(filenode)
607 607 return literal(code_highlight(filenode.content, lexer,
608 608 CodeHtmlFormatter(**kwargs)))
609 609
610 610
611 611 def is_following_repo(repo_name, user_id):
612 612 from rhodecode.model.scm import ScmModel
613 613 return ScmModel().is_following_repo(repo_name, user_id)
614 614
615 615
616 616 class _Message(object):
617 617 """A message returned by ``Flash.pop_messages()``.
618 618
619 619 Converting the message to a string returns the message text. Instances
620 620 also have the following attributes:
621 621
622 622 * ``message``: the message text.
623 623 * ``category``: the category specified when the message was created.
624 624 """
625 625
626 626 def __init__(self, category, message):
627 627 self.category = category
628 628 self.message = message
629 629
630 630 def __str__(self):
631 631 return self.message
632 632
633 633 __unicode__ = __str__
634 634
635 635 def __html__(self):
636 636 return escape(safe_unicode(self.message))
637 637
638 638
639 639 class Flash(object):
640 640 # List of allowed categories. If None, allow any category.
641 641 categories = ["warning", "notice", "error", "success"]
642 642
643 643 # Default category if none is specified.
644 644 default_category = "notice"
645 645
646 646 def __init__(self, session_key="flash", categories=None,
647 647 default_category=None):
648 648 """
649 649 Instantiate a ``Flash`` object.
650 650
651 651 ``session_key`` is the key to save the messages under in the user's
652 652 session.
653 653
654 654 ``categories`` is an optional list which overrides the default list
655 655 of categories.
656 656
657 657 ``default_category`` overrides the default category used for messages
658 658 when none is specified.
659 659 """
660 660 self.session_key = session_key
661 661 if categories is not None:
662 662 self.categories = categories
663 663 if default_category is not None:
664 664 self.default_category = default_category
665 665 if self.categories and self.default_category not in self.categories:
666 666 raise ValueError(
667 667 "unrecognized default category %r" % (self.default_category,))
668 668
669 669 def pop_messages(self, session=None, request=None):
670 670 """
671 671 Return all accumulated messages and delete them from the session.
672 672
673 673 The return value is a list of ``Message`` objects.
674 674 """
675 675 messages = []
676 676
677 677 if not session:
678 678 if not request:
679 679 request = get_current_request()
680 680 session = request.session
681 681
682 682 # Pop the 'old' pylons flash messages. They are tuples of the form
683 683 # (category, message)
684 684 for cat, msg in session.pop(self.session_key, []):
685 685 messages.append(_Message(cat, msg))
686 686
687 687 # Pop the 'new' pyramid flash messages for each category as list
688 688 # of strings.
689 689 for cat in self.categories:
690 690 for msg in session.pop_flash(queue=cat):
691 691 messages.append(_Message(cat, msg))
692 692 # Map messages from the default queue to the 'notice' category.
693 693 for msg in session.pop_flash():
694 694 messages.append(_Message('notice', msg))
695 695
696 696 session.save()
697 697 return messages
698 698
699 699 def json_alerts(self, session=None, request=None):
700 700 payloads = []
701 701 messages = flash.pop_messages(session=session, request=request)
702 702 if messages:
703 703 for message in messages:
704 704 subdata = {}
705 705 if hasattr(message.message, 'rsplit'):
706 706 flash_data = message.message.rsplit('|DELIM|', 1)
707 707 org_message = flash_data[0]
708 708 if len(flash_data) > 1:
709 709 subdata = json.loads(flash_data[1])
710 710 else:
711 711 org_message = message.message
712 712 payloads.append({
713 713 'message': {
714 714 'message': u'{}'.format(org_message),
715 715 'level': message.category,
716 716 'force': True,
717 717 'subdata': subdata
718 718 }
719 719 })
720 720 return json.dumps(payloads)
721 721
722 722 def __call__(self, message, category=None, ignore_duplicate=False,
723 723 session=None, request=None):
724 724
725 725 if not session:
726 726 if not request:
727 727 request = get_current_request()
728 728 session = request.session
729 729
730 730 session.flash(
731 731 message, queue=category, allow_duplicate=not ignore_duplicate)
732 732
733 733
734 734 flash = Flash()
735 735
736 736 #==============================================================================
737 737 # SCM FILTERS available via h.
738 738 #==============================================================================
739 739 from rhodecode.lib.vcs.utils import author_name, author_email
740 740 from rhodecode.lib.utils2 import credentials_filter, age as _age
741 741 from rhodecode.model.db import User, ChangesetStatus
742 742
743 743 age = _age
744 744 capitalize = lambda x: x.capitalize()
745 745 email = author_email
746 746 short_id = lambda x: x[:12]
747 747 hide_credentials = lambda x: ''.join(credentials_filter(x))
748 748
749 749
750 750 def age_component(datetime_iso, value=None, time_is_local=False):
751 751 title = value or format_date(datetime_iso)
752 752 tzinfo = '+00:00'
753 753
754 754 # detect if we have a timezone info, otherwise, add it
755 755 if isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
756 756 if time_is_local:
757 757 tzinfo = time.strftime("+%H:%M",
758 758 time.gmtime(
759 759 (datetime.now() - datetime.utcnow()).seconds + 1
760 760 )
761 761 )
762 762
763 763 return literal(
764 764 '<time class="timeago tooltip" '
765 765 'title="{1}{2}" datetime="{0}{2}">{1}</time>'.format(
766 766 datetime_iso, title, tzinfo))
767 767
768 768
769 769 def _shorten_commit_id(commit_id):
770 770 from rhodecode import CONFIG
771 771 def_len = safe_int(CONFIG.get('rhodecode_show_sha_length', 12))
772 772 return commit_id[:def_len]
773 773
774 774
775 775 def show_id(commit):
776 776 """
777 777 Configurable function that shows ID
778 778 by default it's r123:fffeeefffeee
779 779
780 780 :param commit: commit instance
781 781 """
782 782 from rhodecode import CONFIG
783 783 show_idx = str2bool(CONFIG.get('rhodecode_show_revision_number', True))
784 784
785 785 raw_id = _shorten_commit_id(commit.raw_id)
786 786 if show_idx:
787 787 return 'r%s:%s' % (commit.idx, raw_id)
788 788 else:
789 789 return '%s' % (raw_id, )
790 790
791 791
792 792 def format_date(date):
793 793 """
794 794 use a standardized formatting for dates used in RhodeCode
795 795
796 796 :param date: date/datetime object
797 797 :return: formatted date
798 798 """
799 799
800 800 if date:
801 801 _fmt = "%a, %d %b %Y %H:%M:%S"
802 802 return safe_unicode(date.strftime(_fmt))
803 803
804 804 return u""
805 805
806 806
807 807 class _RepoChecker(object):
808 808
809 809 def __init__(self, backend_alias):
810 810 self._backend_alias = backend_alias
811 811
812 812 def __call__(self, repository):
813 813 if hasattr(repository, 'alias'):
814 814 _type = repository.alias
815 815 elif hasattr(repository, 'repo_type'):
816 816 _type = repository.repo_type
817 817 else:
818 818 _type = repository
819 819 return _type == self._backend_alias
820 820
821 821 is_git = _RepoChecker('git')
822 822 is_hg = _RepoChecker('hg')
823 823 is_svn = _RepoChecker('svn')
824 824
825 825
826 826 def get_repo_type_by_name(repo_name):
827 827 repo = Repository.get_by_repo_name(repo_name)
828 828 return repo.repo_type
829 829
830 830
831 831 def is_svn_without_proxy(repository):
832 832 if is_svn(repository):
833 833 from rhodecode.model.settings import VcsSettingsModel
834 834 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
835 835 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
836 836 return False
837 837
838 838
839 839 def discover_user(author):
840 840 """
841 841 Tries to discover RhodeCode User based on the autho string. Author string
842 842 is typically `FirstName LastName <email@address.com>`
843 843 """
844 844
845 845 # if author is already an instance use it for extraction
846 846 if isinstance(author, User):
847 847 return author
848 848
849 849 # Valid email in the attribute passed, see if they're in the system
850 850 _email = author_email(author)
851 851 if _email != '':
852 852 user = User.get_by_email(_email, case_insensitive=True, cache=True)
853 853 if user is not None:
854 854 return user
855 855
856 856 # Maybe it's a username, we try to extract it and fetch by username ?
857 857 _author = author_name(author)
858 858 user = User.get_by_username(_author, case_insensitive=True, cache=True)
859 859 if user is not None:
860 860 return user
861 861
862 862 return None
863 863
864 864
865 865 def email_or_none(author):
866 866 # extract email from the commit string
867 867 _email = author_email(author)
868 868
869 869 # If we have an email, use it, otherwise
870 870 # see if it contains a username we can get an email from
871 871 if _email != '':
872 872 return _email
873 873 else:
874 874 user = User.get_by_username(
875 875 author_name(author), case_insensitive=True, cache=True)
876 876
877 877 if user is not None:
878 878 return user.email
879 879
880 880 # No valid email, not a valid user in the system, none!
881 881 return None
882 882
883 883
884 884 def link_to_user(author, length=0, **kwargs):
885 885 user = discover_user(author)
886 886 # user can be None, but if we have it already it means we can re-use it
887 887 # in the person() function, so we save 1 intensive-query
888 888 if user:
889 889 author = user
890 890
891 891 display_person = person(author, 'username_or_name_or_email')
892 892 if length:
893 893 display_person = shorter(display_person, length)
894 894
895 895 if user:
896 896 return link_to(
897 897 escape(display_person),
898 898 route_path('user_profile', username=user.username),
899 899 **kwargs)
900 900 else:
901 901 return escape(display_person)
902 902
903 903
904 904 def person(author, show_attr="username_and_name"):
905 905 user = discover_user(author)
906 906 if user:
907 907 return getattr(user, show_attr)
908 908 else:
909 909 _author = author_name(author)
910 910 _email = email(author)
911 911 return _author or _email
912 912
913 913
914 914 def author_string(email):
915 915 if email:
916 916 user = User.get_by_email(email, case_insensitive=True, cache=True)
917 917 if user:
918 918 if user.first_name or user.last_name:
919 919 return '%s %s &lt;%s&gt;' % (
920 920 user.first_name, user.last_name, email)
921 921 else:
922 922 return email
923 923 else:
924 924 return email
925 925 else:
926 926 return None
927 927
928 928
929 929 def person_by_id(id_, show_attr="username_and_name"):
930 930 # attr to return from fetched user
931 931 person_getter = lambda usr: getattr(usr, show_attr)
932 932
933 933 #maybe it's an ID ?
934 934 if str(id_).isdigit() or isinstance(id_, int):
935 935 id_ = int(id_)
936 936 user = User.get(id_)
937 937 if user is not None:
938 938 return person_getter(user)
939 939 return id_
940 940
941 941
942 942 def gravatar_with_user(request, author, show_disabled=False):
943 _render = request.get_partial_renderer('base/base.mako')
943 _render = request.get_partial_renderer(
944 'rhodecode:templates/base/base.mako')
944 945 return _render('gravatar_with_user', author, show_disabled=show_disabled)
945 946
946 947
947 948 tags_paterns = OrderedDict((
948 949 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
949 950 '<div class="metatag" tag="lang">\\2</div>')),
950 951
951 952 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
952 953 '<div class="metatag" tag="see">see: \\1 </div>')),
953 954
954 955 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((.*?)\)\]'),
955 956 '<div class="metatag" tag="url"> <a href="\\2">\\1</a> </div>')),
956 957
957 958 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
958 959 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
959 960
960 961 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
961 962 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
962 963
963 964 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
964 965 '<div class="metatag" tag="state \\1">\\1</div>')),
965 966
966 967 # label in grey
967 968 ('label', (re.compile(r'\[([a-z]+)\]'),
968 969 '<div class="metatag" tag="label">\\1</div>')),
969 970
970 971 # generic catch all in grey
971 972 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
972 973 '<div class="metatag" tag="generic">\\1</div>')),
973 974 ))
974 975
975 976
976 977 def extract_metatags(value):
977 978 """
978 979 Extract supported meta-tags from given text value
979 980 """
980 981 if not value:
981 982 return ''
982 983
983 984 tags = []
984 985 for key, val in tags_paterns.items():
985 986 pat, replace_html = val
986 987 tags.extend([(key, x.group()) for x in pat.finditer(value)])
987 988 value = pat.sub('', value)
988 989
989 990 return tags, value
990 991
991 992
992 993 def style_metatag(tag_type, value):
993 994 """
994 995 converts tags from value into html equivalent
995 996 """
996 997 if not value:
997 998 return ''
998 999
999 1000 html_value = value
1000 1001 tag_data = tags_paterns.get(tag_type)
1001 1002 if tag_data:
1002 1003 pat, replace_html = tag_data
1003 1004 # convert to plain `unicode` instead of a markup tag to be used in
1004 1005 # regex expressions. safe_unicode doesn't work here
1005 1006 html_value = pat.sub(replace_html, unicode(value))
1006 1007
1007 1008 return html_value
1008 1009
1009 1010
1010 1011 def bool2icon(value):
1011 1012 """
1012 1013 Returns boolean value of a given value, represented as html element with
1013 1014 classes that will represent icons
1014 1015
1015 1016 :param value: given value to convert to html node
1016 1017 """
1017 1018
1018 1019 if value: # does bool conversion
1019 1020 return HTML.tag('i', class_="icon-true")
1020 1021 else: # not true as bool
1021 1022 return HTML.tag('i', class_="icon-false")
1022 1023
1023 1024
1024 1025 #==============================================================================
1025 1026 # PERMS
1026 1027 #==============================================================================
1027 1028 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
1028 1029 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll, \
1029 1030 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token, \
1030 1031 csrf_token_key
1031 1032
1032 1033
1033 1034 #==============================================================================
1034 1035 # GRAVATAR URL
1035 1036 #==============================================================================
1036 1037 class InitialsGravatar(object):
1037 1038 def __init__(self, email_address, first_name, last_name, size=30,
1038 1039 background=None, text_color='#fff'):
1039 1040 self.size = size
1040 1041 self.first_name = first_name
1041 1042 self.last_name = last_name
1042 1043 self.email_address = email_address
1043 1044 self.background = background or self.str2color(email_address)
1044 1045 self.text_color = text_color
1045 1046
1046 1047 def get_color_bank(self):
1047 1048 """
1048 1049 returns a predefined list of colors that gravatars can use.
1049 1050 Those are randomized distinct colors that guarantee readability and
1050 1051 uniqueness.
1051 1052
1052 1053 generated with: http://phrogz.net/css/distinct-colors.html
1053 1054 """
1054 1055 return [
1055 1056 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1056 1057 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1057 1058 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1058 1059 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1059 1060 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1060 1061 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1061 1062 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1062 1063 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1063 1064 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1064 1065 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1065 1066 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1066 1067 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1067 1068 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1068 1069 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1069 1070 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1070 1071 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1071 1072 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1072 1073 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1073 1074 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1074 1075 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1075 1076 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1076 1077 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1077 1078 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1078 1079 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1079 1080 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1080 1081 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1081 1082 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1082 1083 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1083 1084 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1084 1085 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1085 1086 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1086 1087 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1087 1088 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1088 1089 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1089 1090 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1090 1091 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1091 1092 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1092 1093 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1093 1094 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1094 1095 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1095 1096 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1096 1097 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1097 1098 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1098 1099 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1099 1100 '#4f8c46', '#368dd9', '#5c0073'
1100 1101 ]
1101 1102
1102 1103 def rgb_to_hex_color(self, rgb_tuple):
1103 1104 """
1104 1105 Converts an rgb_tuple passed to an hex color.
1105 1106
1106 1107 :param rgb_tuple: tuple with 3 ints represents rgb color space
1107 1108 """
1108 1109 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1109 1110
1110 1111 def email_to_int_list(self, email_str):
1111 1112 """
1112 1113 Get every byte of the hex digest value of email and turn it to integer.
1113 1114 It's going to be always between 0-255
1114 1115 """
1115 1116 digest = md5_safe(email_str.lower())
1116 1117 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1117 1118
1118 1119 def pick_color_bank_index(self, email_str, color_bank):
1119 1120 return self.email_to_int_list(email_str)[0] % len(color_bank)
1120 1121
1121 1122 def str2color(self, email_str):
1122 1123 """
1123 1124 Tries to map in a stable algorithm an email to color
1124 1125
1125 1126 :param email_str:
1126 1127 """
1127 1128 color_bank = self.get_color_bank()
1128 1129 # pick position (module it's length so we always find it in the
1129 1130 # bank even if it's smaller than 256 values
1130 1131 pos = self.pick_color_bank_index(email_str, color_bank)
1131 1132 return color_bank[pos]
1132 1133
1133 1134 def normalize_email(self, email_address):
1134 1135 import unicodedata
1135 1136 # default host used to fill in the fake/missing email
1136 1137 default_host = u'localhost'
1137 1138
1138 1139 if not email_address:
1139 1140 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1140 1141
1141 1142 email_address = safe_unicode(email_address)
1142 1143
1143 1144 if u'@' not in email_address:
1144 1145 email_address = u'%s@%s' % (email_address, default_host)
1145 1146
1146 1147 if email_address.endswith(u'@'):
1147 1148 email_address = u'%s%s' % (email_address, default_host)
1148 1149
1149 1150 email_address = unicodedata.normalize('NFKD', email_address)\
1150 1151 .encode('ascii', 'ignore')
1151 1152 return email_address
1152 1153
1153 1154 def get_initials(self):
1154 1155 """
1155 1156 Returns 2 letter initials calculated based on the input.
1156 1157 The algorithm picks first given email address, and takes first letter
1157 1158 of part before @, and then the first letter of server name. In case
1158 1159 the part before @ is in a format of `somestring.somestring2` it replaces
1159 1160 the server letter with first letter of somestring2
1160 1161
1161 1162 In case function was initialized with both first and lastname, this
1162 1163 overrides the extraction from email by first letter of the first and
1163 1164 last name. We add special logic to that functionality, In case Full name
1164 1165 is compound, like Guido Von Rossum, we use last part of the last name
1165 1166 (Von Rossum) picking `R`.
1166 1167
1167 1168 Function also normalizes the non-ascii characters to they ascii
1168 1169 representation, eg Δ„ => A
1169 1170 """
1170 1171 import unicodedata
1171 1172 # replace non-ascii to ascii
1172 1173 first_name = unicodedata.normalize(
1173 1174 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1174 1175 last_name = unicodedata.normalize(
1175 1176 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1176 1177
1177 1178 # do NFKD encoding, and also make sure email has proper format
1178 1179 email_address = self.normalize_email(self.email_address)
1179 1180
1180 1181 # first push the email initials
1181 1182 prefix, server = email_address.split('@', 1)
1182 1183
1183 1184 # check if prefix is maybe a 'first_name.last_name' syntax
1184 1185 _dot_split = prefix.rsplit('.', 1)
1185 1186 if len(_dot_split) == 2 and _dot_split[1]:
1186 1187 initials = [_dot_split[0][0], _dot_split[1][0]]
1187 1188 else:
1188 1189 initials = [prefix[0], server[0]]
1189 1190
1190 1191 # then try to replace either first_name or last_name
1191 1192 fn_letter = (first_name or " ")[0].strip()
1192 1193 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1193 1194
1194 1195 if fn_letter:
1195 1196 initials[0] = fn_letter
1196 1197
1197 1198 if ln_letter:
1198 1199 initials[1] = ln_letter
1199 1200
1200 1201 return ''.join(initials).upper()
1201 1202
1202 1203 def get_img_data_by_type(self, font_family, img_type):
1203 1204 default_user = """
1204 1205 <svg xmlns="http://www.w3.org/2000/svg"
1205 1206 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1206 1207 viewBox="-15 -10 439.165 429.164"
1207 1208
1208 1209 xml:space="preserve"
1209 1210 style="background:{background};" >
1210 1211
1211 1212 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1212 1213 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1213 1214 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1214 1215 168.596,153.916,216.671,
1215 1216 204.583,216.671z" fill="{text_color}"/>
1216 1217 <path d="M407.164,374.717L360.88,
1217 1218 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1218 1219 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1219 1220 15.366-44.203,23.488-69.076,23.488c-24.877,
1220 1221 0-48.762-8.122-69.078-23.488
1221 1222 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1222 1223 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1223 1224 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1224 1225 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1225 1226 19.402-10.527 C409.699,390.129,
1226 1227 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1227 1228 </svg>""".format(
1228 1229 size=self.size,
1229 1230 background='#979797', # @grey4
1230 1231 text_color=self.text_color,
1231 1232 font_family=font_family)
1232 1233
1233 1234 return {
1234 1235 "default_user": default_user
1235 1236 }[img_type]
1236 1237
1237 1238 def get_img_data(self, svg_type=None):
1238 1239 """
1239 1240 generates the svg metadata for image
1240 1241 """
1241 1242
1242 1243 font_family = ','.join([
1243 1244 'proximanovaregular',
1244 1245 'Proxima Nova Regular',
1245 1246 'Proxima Nova',
1246 1247 'Arial',
1247 1248 'Lucida Grande',
1248 1249 'sans-serif'
1249 1250 ])
1250 1251 if svg_type:
1251 1252 return self.get_img_data_by_type(font_family, svg_type)
1252 1253
1253 1254 initials = self.get_initials()
1254 1255 img_data = """
1255 1256 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1256 1257 width="{size}" height="{size}"
1257 1258 style="width: 100%; height: 100%; background-color: {background}"
1258 1259 viewBox="0 0 {size} {size}">
1259 1260 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1260 1261 pointer-events="auto" fill="{text_color}"
1261 1262 font-family="{font_family}"
1262 1263 style="font-weight: 400; font-size: {f_size}px;">{text}
1263 1264 </text>
1264 1265 </svg>""".format(
1265 1266 size=self.size,
1266 1267 f_size=self.size/1.85, # scale the text inside the box nicely
1267 1268 background=self.background,
1268 1269 text_color=self.text_color,
1269 1270 text=initials.upper(),
1270 1271 font_family=font_family)
1271 1272
1272 1273 return img_data
1273 1274
1274 1275 def generate_svg(self, svg_type=None):
1275 1276 img_data = self.get_img_data(svg_type)
1276 1277 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1277 1278
1278 1279
1279 1280 def initials_gravatar(email_address, first_name, last_name, size=30):
1280 1281 svg_type = None
1281 1282 if email_address == User.DEFAULT_USER_EMAIL:
1282 1283 svg_type = 'default_user'
1283 1284 klass = InitialsGravatar(email_address, first_name, last_name, size)
1284 1285 return klass.generate_svg(svg_type=svg_type)
1285 1286
1286 1287
1287 1288 def gravatar_url(email_address, size=30, request=None):
1288 1289 request = get_current_request()
1289 1290 if request and hasattr(request, 'call_context'):
1290 1291 _use_gravatar = request.call_context.visual.use_gravatar
1291 1292 _gravatar_url = request.call_context.visual.gravatar_url
1292 1293 else:
1293 1294 # doh, we need to re-import those to mock it later
1294 1295 from pylons import tmpl_context as c
1295 1296
1296 1297 _use_gravatar = c.visual.use_gravatar
1297 1298 _gravatar_url = c.visual.gravatar_url
1298 1299
1299 1300 _gravatar_url = _gravatar_url or User.DEFAULT_GRAVATAR_URL
1300 1301
1301 1302 email_address = email_address or User.DEFAULT_USER_EMAIL
1302 1303 if isinstance(email_address, unicode):
1303 1304 # hashlib crashes on unicode items
1304 1305 email_address = safe_str(email_address)
1305 1306
1306 1307 # empty email or default user
1307 1308 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1308 1309 return initials_gravatar(User.DEFAULT_USER_EMAIL, '', '', size=size)
1309 1310
1310 1311 if _use_gravatar:
1311 1312 # TODO: Disuse pyramid thread locals. Think about another solution to
1312 1313 # get the host and schema here.
1313 1314 request = get_current_request()
1314 1315 tmpl = safe_str(_gravatar_url)
1315 1316 tmpl = tmpl.replace('{email}', email_address)\
1316 1317 .replace('{md5email}', md5_safe(email_address.lower())) \
1317 1318 .replace('{netloc}', request.host)\
1318 1319 .replace('{scheme}', request.scheme)\
1319 1320 .replace('{size}', safe_str(size))
1320 1321 return tmpl
1321 1322 else:
1322 1323 return initials_gravatar(email_address, '', '', size=size)
1323 1324
1324 1325
1325 1326 class Page(_Page):
1326 1327 """
1327 1328 Custom pager to match rendering style with paginator
1328 1329 """
1329 1330
1330 1331 def _get_pos(self, cur_page, max_page, items):
1331 1332 edge = (items / 2) + 1
1332 1333 if (cur_page <= edge):
1333 1334 radius = max(items / 2, items - cur_page)
1334 1335 elif (max_page - cur_page) < edge:
1335 1336 radius = (items - 1) - (max_page - cur_page)
1336 1337 else:
1337 1338 radius = items / 2
1338 1339
1339 1340 left = max(1, (cur_page - (radius)))
1340 1341 right = min(max_page, cur_page + (radius))
1341 1342 return left, cur_page, right
1342 1343
1343 1344 def _range(self, regexp_match):
1344 1345 """
1345 1346 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
1346 1347
1347 1348 Arguments:
1348 1349
1349 1350 regexp_match
1350 1351 A "re" (regular expressions) match object containing the
1351 1352 radius of linked pages around the current page in
1352 1353 regexp_match.group(1) as a string
1353 1354
1354 1355 This function is supposed to be called as a callable in
1355 1356 re.sub.
1356 1357
1357 1358 """
1358 1359 radius = int(regexp_match.group(1))
1359 1360
1360 1361 # Compute the first and last page number within the radius
1361 1362 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
1362 1363 # -> leftmost_page = 5
1363 1364 # -> rightmost_page = 9
1364 1365 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
1365 1366 self.last_page,
1366 1367 (radius * 2) + 1)
1367 1368 nav_items = []
1368 1369
1369 1370 # Create a link to the first page (unless we are on the first page
1370 1371 # or there would be no need to insert '..' spacers)
1371 1372 if self.page != self.first_page and self.first_page < leftmost_page:
1372 1373 nav_items.append(self._pagerlink(self.first_page, self.first_page))
1373 1374
1374 1375 # Insert dots if there are pages between the first page
1375 1376 # and the currently displayed page range
1376 1377 if leftmost_page - self.first_page > 1:
1377 1378 # Wrap in a SPAN tag if nolink_attr is set
1378 1379 text = '..'
1379 1380 if self.dotdot_attr:
1380 1381 text = HTML.span(c=text, **self.dotdot_attr)
1381 1382 nav_items.append(text)
1382 1383
1383 1384 for thispage in xrange(leftmost_page, rightmost_page + 1):
1384 1385 # Hilight the current page number and do not use a link
1385 1386 if thispage == self.page:
1386 1387 text = '%s' % (thispage,)
1387 1388 # Wrap in a SPAN tag if nolink_attr is set
1388 1389 if self.curpage_attr:
1389 1390 text = HTML.span(c=text, **self.curpage_attr)
1390 1391 nav_items.append(text)
1391 1392 # Otherwise create just a link to that page
1392 1393 else:
1393 1394 text = '%s' % (thispage,)
1394 1395 nav_items.append(self._pagerlink(thispage, text))
1395 1396
1396 1397 # Insert dots if there are pages between the displayed
1397 1398 # page numbers and the end of the page range
1398 1399 if self.last_page - rightmost_page > 1:
1399 1400 text = '..'
1400 1401 # Wrap in a SPAN tag if nolink_attr is set
1401 1402 if self.dotdot_attr:
1402 1403 text = HTML.span(c=text, **self.dotdot_attr)
1403 1404 nav_items.append(text)
1404 1405
1405 1406 # Create a link to the very last page (unless we are on the last
1406 1407 # page or there would be no need to insert '..' spacers)
1407 1408 if self.page != self.last_page and rightmost_page < self.last_page:
1408 1409 nav_items.append(self._pagerlink(self.last_page, self.last_page))
1409 1410
1410 1411 ## prerender links
1411 1412 #_page_link = url.current()
1412 1413 #nav_items.append(literal('<link rel="prerender" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1413 1414 #nav_items.append(literal('<link rel="prefetch" href="%s?page=%s">' % (_page_link, str(int(self.page)+1))))
1414 1415 return self.separator.join(nav_items)
1415 1416
1416 1417 def pager(self, format='~2~', page_param='page', partial_param='partial',
1417 1418 show_if_single_page=False, separator=' ', onclick=None,
1418 1419 symbol_first='<<', symbol_last='>>',
1419 1420 symbol_previous='<', symbol_next='>',
1420 1421 link_attr={'class': 'pager_link', 'rel': 'prerender'},
1421 1422 curpage_attr={'class': 'pager_curpage'},
1422 1423 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
1423 1424
1424 1425 self.curpage_attr = curpage_attr
1425 1426 self.separator = separator
1426 1427 self.pager_kwargs = kwargs
1427 1428 self.page_param = page_param
1428 1429 self.partial_param = partial_param
1429 1430 self.onclick = onclick
1430 1431 self.link_attr = link_attr
1431 1432 self.dotdot_attr = dotdot_attr
1432 1433
1433 1434 # Don't show navigator if there is no more than one page
1434 1435 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
1435 1436 return ''
1436 1437
1437 1438 from string import Template
1438 1439 # Replace ~...~ in token format by range of pages
1439 1440 result = re.sub(r'~(\d+)~', self._range, format)
1440 1441
1441 1442 # Interpolate '%' variables
1442 1443 result = Template(result).safe_substitute({
1443 1444 'first_page': self.first_page,
1444 1445 'last_page': self.last_page,
1445 1446 'page': self.page,
1446 1447 'page_count': self.page_count,
1447 1448 'items_per_page': self.items_per_page,
1448 1449 'first_item': self.first_item,
1449 1450 'last_item': self.last_item,
1450 1451 'item_count': self.item_count,
1451 1452 'link_first': self.page > self.first_page and \
1452 1453 self._pagerlink(self.first_page, symbol_first) or '',
1453 1454 'link_last': self.page < self.last_page and \
1454 1455 self._pagerlink(self.last_page, symbol_last) or '',
1455 1456 'link_previous': self.previous_page and \
1456 1457 self._pagerlink(self.previous_page, symbol_previous) \
1457 1458 or HTML.span(symbol_previous, class_="pg-previous disabled"),
1458 1459 'link_next': self.next_page and \
1459 1460 self._pagerlink(self.next_page, symbol_next) \
1460 1461 or HTML.span(symbol_next, class_="pg-next disabled")
1461 1462 })
1462 1463
1463 1464 return literal(result)
1464 1465
1465 1466
1466 1467 #==============================================================================
1467 1468 # REPO PAGER, PAGER FOR REPOSITORY
1468 1469 #==============================================================================
1469 1470 class RepoPage(Page):
1470 1471
1471 1472 def __init__(self, collection, page=1, items_per_page=20,
1472 1473 item_count=None, url=None, **kwargs):
1473 1474
1474 1475 """Create a "RepoPage" instance. special pager for paging
1475 1476 repository
1476 1477 """
1477 1478 self._url_generator = url
1478 1479
1479 1480 # Safe the kwargs class-wide so they can be used in the pager() method
1480 1481 self.kwargs = kwargs
1481 1482
1482 1483 # Save a reference to the collection
1483 1484 self.original_collection = collection
1484 1485
1485 1486 self.collection = collection
1486 1487
1487 1488 # The self.page is the number of the current page.
1488 1489 # The first page has the number 1!
1489 1490 try:
1490 1491 self.page = int(page) # make it int() if we get it as a string
1491 1492 except (ValueError, TypeError):
1492 1493 self.page = 1
1493 1494
1494 1495 self.items_per_page = items_per_page
1495 1496
1496 1497 # Unless the user tells us how many items the collections has
1497 1498 # we calculate that ourselves.
1498 1499 if item_count is not None:
1499 1500 self.item_count = item_count
1500 1501 else:
1501 1502 self.item_count = len(self.collection)
1502 1503
1503 1504 # Compute the number of the first and last available page
1504 1505 if self.item_count > 0:
1505 1506 self.first_page = 1
1506 1507 self.page_count = int(math.ceil(float(self.item_count) /
1507 1508 self.items_per_page))
1508 1509 self.last_page = self.first_page + self.page_count - 1
1509 1510
1510 1511 # Make sure that the requested page number is the range of
1511 1512 # valid pages
1512 1513 if self.page > self.last_page:
1513 1514 self.page = self.last_page
1514 1515 elif self.page < self.first_page:
1515 1516 self.page = self.first_page
1516 1517
1517 1518 # Note: the number of items on this page can be less than
1518 1519 # items_per_page if the last page is not full
1519 1520 self.first_item = max(0, (self.item_count) - (self.page *
1520 1521 items_per_page))
1521 1522 self.last_item = ((self.item_count - 1) - items_per_page *
1522 1523 (self.page - 1))
1523 1524
1524 1525 self.items = list(self.collection[self.first_item:self.last_item + 1])
1525 1526
1526 1527 # Links to previous and next page
1527 1528 if self.page > self.first_page:
1528 1529 self.previous_page = self.page - 1
1529 1530 else:
1530 1531 self.previous_page = None
1531 1532
1532 1533 if self.page < self.last_page:
1533 1534 self.next_page = self.page + 1
1534 1535 else:
1535 1536 self.next_page = None
1536 1537
1537 1538 # No items available
1538 1539 else:
1539 1540 self.first_page = None
1540 1541 self.page_count = 0
1541 1542 self.last_page = None
1542 1543 self.first_item = None
1543 1544 self.last_item = None
1544 1545 self.previous_page = None
1545 1546 self.next_page = None
1546 1547 self.items = []
1547 1548
1548 1549 # This is a subclass of the 'list' type. Initialise the list now.
1549 1550 list.__init__(self, reversed(self.items))
1550 1551
1551 1552
1552 1553 def breadcrumb_repo_link(repo):
1553 1554 """
1554 1555 Makes a breadcrumbs path link to repo
1555 1556
1556 1557 ex::
1557 1558 group >> subgroup >> repo
1558 1559
1559 1560 :param repo: a Repository instance
1560 1561 """
1561 1562
1562 1563 path = [
1563 1564 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name))
1564 1565 for group in repo.groups_with_parents
1565 1566 ] + [
1566 1567 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name))
1567 1568 ]
1568 1569
1569 1570 return literal(' &raquo; '.join(path))
1570 1571
1571 1572
1572 1573 def format_byte_size_binary(file_size):
1573 1574 """
1574 1575 Formats file/folder sizes to standard.
1575 1576 """
1576 1577 if file_size is None:
1577 1578 file_size = 0
1578 1579
1579 1580 formatted_size = format_byte_size(file_size, binary=True)
1580 1581 return formatted_size
1581 1582
1582 1583
1583 1584 def urlify_text(text_, safe=True):
1584 1585 """
1585 1586 Extrac urls from text and make html links out of them
1586 1587
1587 1588 :param text_:
1588 1589 """
1589 1590
1590 1591 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1591 1592 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1592 1593
1593 1594 def url_func(match_obj):
1594 1595 url_full = match_obj.groups()[0]
1595 1596 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1596 1597 _newtext = url_pat.sub(url_func, text_)
1597 1598 if safe:
1598 1599 return literal(_newtext)
1599 1600 return _newtext
1600 1601
1601 1602
1602 1603 def urlify_commits(text_, repository):
1603 1604 """
1604 1605 Extract commit ids from text and make link from them
1605 1606
1606 1607 :param text_:
1607 1608 :param repository: repo name to build the URL with
1608 1609 """
1609 1610
1610 1611 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1611 1612
1612 1613 def url_func(match_obj):
1613 1614 commit_id = match_obj.groups()[1]
1614 1615 pref = match_obj.groups()[0]
1615 1616 suf = match_obj.groups()[2]
1616 1617
1617 1618 tmpl = (
1618 1619 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1619 1620 '%(commit_id)s</a>%(suf)s'
1620 1621 )
1621 1622 return tmpl % {
1622 1623 'pref': pref,
1623 1624 'cls': 'revision-link',
1624 1625 'url': route_url('repo_commit', repo_name=repository,
1625 1626 commit_id=commit_id),
1626 1627 'commit_id': commit_id,
1627 1628 'suf': suf
1628 1629 }
1629 1630
1630 1631 newtext = URL_PAT.sub(url_func, text_)
1631 1632
1632 1633 return newtext
1633 1634
1634 1635
1635 1636 def _process_url_func(match_obj, repo_name, uid, entry,
1636 1637 return_raw_data=False, link_format='html'):
1637 1638 pref = ''
1638 1639 if match_obj.group().startswith(' '):
1639 1640 pref = ' '
1640 1641
1641 1642 issue_id = ''.join(match_obj.groups())
1642 1643
1643 1644 if link_format == 'html':
1644 1645 tmpl = (
1645 1646 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1646 1647 '%(issue-prefix)s%(id-repr)s'
1647 1648 '</a>')
1648 1649 elif link_format == 'rst':
1649 1650 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1650 1651 elif link_format == 'markdown':
1651 1652 tmpl = '[%(issue-prefix)s%(id-repr)s](%(url)s)'
1652 1653 else:
1653 1654 raise ValueError('Bad link_format:{}'.format(link_format))
1654 1655
1655 1656 (repo_name_cleaned,
1656 1657 parent_group_name) = RepoGroupModel().\
1657 1658 _get_group_name_and_parent(repo_name)
1658 1659
1659 1660 # variables replacement
1660 1661 named_vars = {
1661 1662 'id': issue_id,
1662 1663 'repo': repo_name,
1663 1664 'repo_name': repo_name_cleaned,
1664 1665 'group_name': parent_group_name
1665 1666 }
1666 1667 # named regex variables
1667 1668 named_vars.update(match_obj.groupdict())
1668 1669 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1669 1670
1670 1671 data = {
1671 1672 'pref': pref,
1672 1673 'cls': 'issue-tracker-link',
1673 1674 'url': _url,
1674 1675 'id-repr': issue_id,
1675 1676 'issue-prefix': entry['pref'],
1676 1677 'serv': entry['url'],
1677 1678 }
1678 1679 if return_raw_data:
1679 1680 return {
1680 1681 'id': issue_id,
1681 1682 'url': _url
1682 1683 }
1683 1684 return tmpl % data
1684 1685
1685 1686
1686 1687 def process_patterns(text_string, repo_name, link_format='html'):
1687 1688 allowed_formats = ['html', 'rst', 'markdown']
1688 1689 if link_format not in allowed_formats:
1689 1690 raise ValueError('Link format can be only one of:{} got {}'.format(
1690 1691 allowed_formats, link_format))
1691 1692
1692 1693 repo = None
1693 1694 if repo_name:
1694 1695 # Retrieving repo_name to avoid invalid repo_name to explode on
1695 1696 # IssueTrackerSettingsModel but still passing invalid name further down
1696 1697 repo = Repository.get_by_repo_name(repo_name, cache=True)
1697 1698
1698 1699 settings_model = IssueTrackerSettingsModel(repo=repo)
1699 1700 active_entries = settings_model.get_settings(cache=True)
1700 1701
1701 1702 issues_data = []
1702 1703 newtext = text_string
1703 1704
1704 1705 for uid, entry in active_entries.items():
1705 1706 log.debug('found issue tracker entry with uid %s' % (uid,))
1706 1707
1707 1708 if not (entry['pat'] and entry['url']):
1708 1709 log.debug('skipping due to missing data')
1709 1710 continue
1710 1711
1711 1712 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s'
1712 1713 % (uid, entry['pat'], entry['url'], entry['pref']))
1713 1714
1714 1715 try:
1715 1716 pattern = re.compile(r'%s' % entry['pat'])
1716 1717 except re.error:
1717 1718 log.exception(
1718 1719 'issue tracker pattern: `%s` failed to compile',
1719 1720 entry['pat'])
1720 1721 continue
1721 1722
1722 1723 data_func = partial(
1723 1724 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1724 1725 return_raw_data=True)
1725 1726
1726 1727 for match_obj in pattern.finditer(text_string):
1727 1728 issues_data.append(data_func(match_obj))
1728 1729
1729 1730 url_func = partial(
1730 1731 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1731 1732 link_format=link_format)
1732 1733
1733 1734 newtext = pattern.sub(url_func, newtext)
1734 1735 log.debug('processed prefix:uid `%s`' % (uid,))
1735 1736
1736 1737 return newtext, issues_data
1737 1738
1738 1739
1739 1740 def urlify_commit_message(commit_text, repository=None):
1740 1741 """
1741 1742 Parses given text message and makes proper links.
1742 1743 issues are linked to given issue-server, and rest is a commit link
1743 1744
1744 1745 :param commit_text:
1745 1746 :param repository:
1746 1747 """
1747 1748 from pylons import url # doh, we need to re-import url to mock it later
1748 1749
1749 1750 def escaper(string):
1750 1751 return string.replace('<', '&lt;').replace('>', '&gt;')
1751 1752
1752 1753 newtext = escaper(commit_text)
1753 1754
1754 1755 # extract http/https links and make them real urls
1755 1756 newtext = urlify_text(newtext, safe=False)
1756 1757
1757 1758 # urlify commits - extract commit ids and make link out of them, if we have
1758 1759 # the scope of repository present.
1759 1760 if repository:
1760 1761 newtext = urlify_commits(newtext, repository)
1761 1762
1762 1763 # process issue tracker patterns
1763 1764 newtext, issues = process_patterns(newtext, repository or '')
1764 1765
1765 1766 return literal(newtext)
1766 1767
1767 1768
1768 1769 def render_binary(repo_name, file_obj):
1769 1770 """
1770 1771 Choose how to render a binary file
1771 1772 """
1772 1773 filename = file_obj.name
1773 1774
1774 1775 # images
1775 1776 for ext in ['*.png', '*.jpg', '*.ico', '*.gif']:
1776 1777 if fnmatch.fnmatch(filename, pat=ext):
1777 1778 alt = filename
1778 1779 src = route_path(
1779 1780 'repo_file_raw', repo_name=repo_name,
1780 1781 commit_id=file_obj.commit.raw_id, f_path=file_obj.path)
1781 1782 return literal('<img class="rendered-binary" alt="{}" src="{}">'.format(alt, src))
1782 1783
1783 1784
1784 1785 def renderer_from_filename(filename, exclude=None):
1785 1786 """
1786 1787 choose a renderer based on filename, this works only for text based files
1787 1788 """
1788 1789
1789 1790 # ipython
1790 1791 for ext in ['*.ipynb']:
1791 1792 if fnmatch.fnmatch(filename, pat=ext):
1792 1793 return 'jupyter'
1793 1794
1794 1795 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1795 1796 if is_markup:
1796 1797 return is_markup
1797 1798 return None
1798 1799
1799 1800
1800 1801 def render(source, renderer='rst', mentions=False, relative_urls=None,
1801 1802 repo_name=None):
1802 1803
1803 1804 def maybe_convert_relative_links(html_source):
1804 1805 if relative_urls:
1805 1806 return relative_links(html_source, relative_urls)
1806 1807 return html_source
1807 1808
1808 1809 if renderer == 'rst':
1809 1810 if repo_name:
1810 1811 # process patterns on comments if we pass in repo name
1811 1812 source, issues = process_patterns(
1812 1813 source, repo_name, link_format='rst')
1813 1814
1814 1815 return literal(
1815 1816 '<div class="rst-block">%s</div>' %
1816 1817 maybe_convert_relative_links(
1817 1818 MarkupRenderer.rst(source, mentions=mentions)))
1818 1819 elif renderer == 'markdown':
1819 1820 if repo_name:
1820 1821 # process patterns on comments if we pass in repo name
1821 1822 source, issues = process_patterns(
1822 1823 source, repo_name, link_format='markdown')
1823 1824
1824 1825 return literal(
1825 1826 '<div class="markdown-block">%s</div>' %
1826 1827 maybe_convert_relative_links(
1827 1828 MarkupRenderer.markdown(source, flavored=True,
1828 1829 mentions=mentions)))
1829 1830 elif renderer == 'jupyter':
1830 1831 return literal(
1831 1832 '<div class="ipynb">%s</div>' %
1832 1833 maybe_convert_relative_links(
1833 1834 MarkupRenderer.jupyter(source)))
1834 1835
1835 1836 # None means just show the file-source
1836 1837 return None
1837 1838
1838 1839
1839 1840 def commit_status(repo, commit_id):
1840 1841 return ChangesetStatusModel().get_status(repo, commit_id)
1841 1842
1842 1843
1843 1844 def commit_status_lbl(commit_status):
1844 1845 return dict(ChangesetStatus.STATUSES).get(commit_status)
1845 1846
1846 1847
1847 1848 def commit_time(repo_name, commit_id):
1848 1849 repo = Repository.get_by_repo_name(repo_name)
1849 1850 commit = repo.get_commit(commit_id=commit_id)
1850 1851 return commit.date
1851 1852
1852 1853
1853 1854 def get_permission_name(key):
1854 1855 return dict(Permission.PERMS).get(key)
1855 1856
1856 1857
1857 1858 def journal_filter_help(request):
1858 1859 _ = request.translate
1859 1860
1860 1861 return _(
1861 1862 'Example filter terms:\n' +
1862 1863 ' repository:vcs\n' +
1863 1864 ' username:marcin\n' +
1864 1865 ' username:(NOT marcin)\n' +
1865 1866 ' action:*push*\n' +
1866 1867 ' ip:127.0.0.1\n' +
1867 1868 ' date:20120101\n' +
1868 1869 ' date:[20120101100000 TO 20120102]\n' +
1869 1870 '\n' +
1870 1871 'Generate wildcards using \'*\' character:\n' +
1871 1872 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1872 1873 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1873 1874 '\n' +
1874 1875 'Optional AND / OR operators in queries\n' +
1875 1876 ' "repository:vcs OR repository:test"\n' +
1876 1877 ' "username:test AND repository:test*"\n'
1877 1878 )
1878 1879
1879 1880
1880 1881 def search_filter_help(searcher, request):
1881 1882 _ = request.translate
1882 1883
1883 1884 terms = ''
1884 1885 return _(
1885 1886 'Example filter terms for `{searcher}` search:\n' +
1886 1887 '{terms}\n' +
1887 1888 'Generate wildcards using \'*\' character:\n' +
1888 1889 ' "repo_name:vcs*" - search everything starting with \'vcs\'\n' +
1889 1890 ' "repo_name:*vcs*" - search for repository containing \'vcs\'\n' +
1890 1891 '\n' +
1891 1892 'Optional AND / OR operators in queries\n' +
1892 1893 ' "repo_name:vcs OR repo_name:test"\n' +
1893 1894 ' "owner:test AND repo_name:test*"\n' +
1894 1895 'More: {search_doc}'
1895 1896 ).format(searcher=searcher.name,
1896 1897 terms=terms, search_doc=searcher.query_lang_doc)
1897 1898
1898 1899
1899 1900 def not_mapped_error(repo_name):
1900 1901 from rhodecode.translation import _
1901 1902 flash(_('%s repository is not mapped to db perhaps'
1902 1903 ' it was created or renamed from the filesystem'
1903 1904 ' please run the application again'
1904 1905 ' in order to rescan repositories') % repo_name, category='error')
1905 1906
1906 1907
1907 1908 def ip_range(ip_addr):
1908 1909 from rhodecode.model.db import UserIpMap
1909 1910 s, e = UserIpMap._get_ip_range(ip_addr)
1910 1911 return '%s - %s' % (s, e)
1911 1912
1912 1913
1913 1914 def form(url, method='post', needs_csrf_token=True, **attrs):
1914 1915 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1915 1916 if method.lower() != 'get' and needs_csrf_token:
1916 1917 raise Exception(
1917 1918 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1918 1919 'CSRF token. If the endpoint does not require such token you can ' +
1919 1920 'explicitly set the parameter needs_csrf_token to false.')
1920 1921
1921 1922 return wh_form(url, method=method, **attrs)
1922 1923
1923 1924
1924 1925 def secure_form(form_url, method="POST", multipart=False, **attrs):
1925 1926 """Start a form tag that points the action to an url. This
1926 1927 form tag will also include the hidden field containing
1927 1928 the auth token.
1928 1929
1929 1930 The url options should be given either as a string, or as a
1930 1931 ``url()`` function. The method for the form defaults to POST.
1931 1932
1932 1933 Options:
1933 1934
1934 1935 ``multipart``
1935 1936 If set to True, the enctype is set to "multipart/form-data".
1936 1937 ``method``
1937 1938 The method to use when submitting the form, usually either
1938 1939 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1939 1940 hidden input with name _method is added to simulate the verb
1940 1941 over POST.
1941 1942
1942 1943 """
1943 1944 from webhelpers.pylonslib.secure_form import insecure_form
1944 1945
1945 1946 session = None
1946 1947
1947 1948 # TODO(marcink): after pyramid migration require request variable ALWAYS
1948 1949 if 'request' in attrs:
1949 1950 session = attrs['request'].session
1950 1951 del attrs['request']
1951 1952
1952 1953 form = insecure_form(form_url, method, multipart, **attrs)
1953 1954 token = literal(
1954 1955 '<input type="hidden" id="{}" name="{}" value="{}">'.format(
1955 1956 csrf_token_key, csrf_token_key, get_csrf_token(session)))
1956 1957
1957 1958 return literal("%s\n%s" % (form, token))
1958 1959
1959 1960
1960 1961 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1961 1962 select_html = select(name, selected, options, **attrs)
1962 1963 select2 = """
1963 1964 <script>
1964 1965 $(document).ready(function() {
1965 1966 $('#%s').select2({
1966 1967 containerCssClass: 'drop-menu',
1967 1968 dropdownCssClass: 'drop-menu-dropdown',
1968 1969 dropdownAutoWidth: true%s
1969 1970 });
1970 1971 });
1971 1972 </script>
1972 1973 """
1973 1974 filter_option = """,
1974 1975 minimumResultsForSearch: -1
1975 1976 """
1976 1977 input_id = attrs.get('id') or name
1977 1978 filter_enabled = "" if enable_filter else filter_option
1978 1979 select_script = literal(select2 % (input_id, filter_enabled))
1979 1980
1980 1981 return literal(select_html+select_script)
1981 1982
1982 1983
1983 1984 def get_visual_attr(tmpl_context_var, attr_name):
1984 1985 """
1985 1986 A safe way to get a variable from visual variable of template context
1986 1987
1987 1988 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1988 1989 :param attr_name: name of the attribute we fetch from the c.visual
1989 1990 """
1990 1991 visual = getattr(tmpl_context_var, 'visual', None)
1991 1992 if not visual:
1992 1993 return
1993 1994 else:
1994 1995 return getattr(visual, attr_name, None)
1995 1996
1996 1997
1997 1998 def get_last_path_part(file_node):
1998 1999 if not file_node.path:
1999 2000 return u''
2000 2001
2001 2002 path = safe_unicode(file_node.path.split('/')[-1])
2002 2003 return u'../' + path
2003 2004
2004 2005
2005 2006 def route_url(*args, **kwargs):
2006 2007 """
2007 2008 Wrapper around pyramids `route_url` (fully qualified url) function.
2008 2009 It is used to generate URLs from within pylons views or templates.
2009 2010 This will be removed when pyramid migration if finished.
2010 2011 """
2011 2012 req = get_current_request()
2012 2013 return req.route_url(*args, **kwargs)
2013 2014
2014 2015
2015 2016 def route_path(*args, **kwargs):
2016 2017 """
2017 2018 Wrapper around pyramids `route_path` function. It is used to generate
2018 2019 URLs from within pylons views or templates. This will be removed when
2019 2020 pyramid migration if finished.
2020 2021 """
2021 2022 req = get_current_request()
2022 2023 return req.route_path(*args, **kwargs)
2023 2024
2024 2025
2025 2026 def route_path_or_none(*args, **kwargs):
2026 2027 try:
2027 2028 return route_path(*args, **kwargs)
2028 2029 except KeyError:
2029 2030 return None
2030 2031
2031 2032
2032 2033 def current_route_path(request, **kw):
2033 2034 new_args = request.GET.mixed()
2034 2035 new_args.update(kw)
2035 2036 return request.current_route_path(_query=new_args)
2036 2037
2037 2038
2038 2039 def static_url(*args, **kwds):
2039 2040 """
2040 2041 Wrapper around pyramids `route_path` function. It is used to generate
2041 2042 URLs from within pylons views or templates. This will be removed when
2042 2043 pyramid migration if finished.
2043 2044 """
2044 2045 req = get_current_request()
2045 2046 return req.static_url(*args, **kwds)
2046 2047
2047 2048
2048 2049 def resource_path(*args, **kwds):
2049 2050 """
2050 2051 Wrapper around pyramids `route_path` function. It is used to generate
2051 2052 URLs from within pylons views or templates. This will be removed when
2052 2053 pyramid migration if finished.
2053 2054 """
2054 2055 req = get_current_request()
2055 2056 return req.resource_path(*args, **kwds)
2056 2057
2057 2058
2058 2059 def api_call_example(method, args):
2059 2060 """
2060 2061 Generates an API call example via CURL
2061 2062 """
2062 2063 args_json = json.dumps(OrderedDict([
2063 2064 ('id', 1),
2064 2065 ('auth_token', 'SECRET'),
2065 2066 ('method', method),
2066 2067 ('args', args)
2067 2068 ]))
2068 2069 return literal(
2069 2070 "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{data}'"
2070 2071 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2071 2072 "and needs to be of `api calls` role."
2072 2073 .format(
2073 2074 api_url=route_url('apiv2'),
2074 2075 token_url=route_url('my_account_auth_tokens'),
2075 2076 data=args_json))
2076 2077
2077 2078
2078 2079 def notification_description(notification, request):
2079 2080 """
2080 2081 Generate notification human readable description based on notification type
2081 2082 """
2082 2083 from rhodecode.model.notification import NotificationModel
2083 2084 return NotificationModel().make_description(
2084 2085 notification, translate=request.translate)
2085 2086
2086 2087
2087 2088 def go_import_header(request, db_repo=None):
2088 2089 """
2089 2090 Creates a header for go-import functionality in Go Lang
2090 2091 """
2091 2092
2092 2093 if not db_repo:
2093 2094 return
2094 2095 if 'go-get' not in request.GET:
2095 2096 return
2096 2097
2097 2098 clone_url = db_repo.clone_url()
2098 2099 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2099 2100 # we have a repo and go-get flag,
2100 2101 return literal('<meta name="go-import" content="{} {} {}">'.format(
2101 2102 prefix, db_repo.repo_type, clone_url))
@@ -1,97 +1,98 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 from mako import exceptions
24 24 from pyramid.renderers import get_renderer
25 25
26 26 log = logging.getLogger(__name__)
27 27
28 28
29 29 def get_partial_renderer(request, tmpl_name):
30 30 return PyramidPartialRenderer(request, tmpl_name=tmpl_name)
31 31
32 32
33 33 class PyramidPartialRenderer(object):
34 34
35 35 """
36 36 Partial renderer used to render chunks of html used in datagrids
37 37 use like::
38 38
39 _renderer = request.get_partial_renderer('_dt/template_base.mako')
39 _renderer = request.get_partial_renderer(
40 'rhodecode:templates/_dt/template_base.mako')
40 41 _render('quick_menu', args, kwargs)
41 42
42 43 :param tmpl_name: template path relate to /templates/ dir
43 44 """
44 45
45 46 def __init__(self, request, tmpl_name):
46 47 self.tmpl_name = tmpl_name
47 48 self.request = request
48 49
49 50 def _mako_lookup(self):
50 51 _tmpl_lookup = get_renderer('root.mako').lookup
51 52 return _tmpl_lookup.get_template(self.tmpl_name)
52 53
53 54 def get_call_context(self):
54 55 return self.request.call_context
55 56
56 57 def get_helpers(self):
57 58 from rhodecode.lib import helpers
58 59 return helpers
59 60
60 61 def _update_kwargs_for_render(self, kwargs):
61 62 """
62 63 Inject params required for Mako rendering
63 64 """
64 65
65 66 _kwargs = {
66 67 '_': self.request.translate,
67 68 '_ungettext': self.request.plularize,
68 69 'h': self.get_helpers(),
69 70 'c': self.get_call_context(),
70 71
71 72 'request': self.request,
72 73 }
73 74 _kwargs.update(kwargs)
74 75 return _kwargs
75 76
76 77 def _render_with_exc(self, render_func, args, kwargs):
77 78 try:
78 79 return render_func.render(*args, **kwargs)
79 80 except:
80 81 log.error(exceptions.text_error_template().render())
81 82 raise
82 83
83 84 def _get_template(self, template_obj, def_name):
84 85 if def_name:
85 86 tmpl = template_obj.get_def(def_name)
86 87 else:
87 88 tmpl = template_obj
88 89 return tmpl
89 90
90 91 def render(self, def_name, *args, **kwargs):
91 92 lookup_obj = self._mako_lookup()
92 93 tmpl = self._get_template(lookup_obj, def_name=def_name)
93 94 kwargs = self._update_kwargs_for_render(kwargs)
94 95 return self._render_with_exc(tmpl, args, kwargs)
95 96
96 97 def __call__(self, tmpl, *args, **kwargs):
97 98 return self.render(tmpl, *args, **kwargs)
@@ -1,377 +1,386 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Model for notifications
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from sqlalchemy.sql.expression import false, true
31 31
32 32 import rhodecode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import Notification, User, UserNotification
36 36 from rhodecode.model.meta import Session
37 37 from rhodecode.translation import TranslationString
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class NotificationModel(BaseModel):
43 43
44 44 cls = Notification
45 45
46 46 def __get_notification(self, notification):
47 47 if isinstance(notification, Notification):
48 48 return notification
49 49 elif isinstance(notification, (int, long)):
50 50 return Notification.get(notification)
51 51 else:
52 52 if notification:
53 53 raise Exception('notification must be int, long or Instance'
54 54 ' of Notification got %s' % type(notification))
55 55
56 56 def create(
57 57 self, created_by, notification_subject, notification_body,
58 58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 59 mention_recipients=None, with_email=True, email_kwargs=None):
60 60 """
61 61
62 62 Creates notification of given type
63 63
64 64 :param created_by: int, str or User instance. User who created this
65 65 notification
66 66 :param notification_subject: subject of notification itself
67 67 :param notification_body: body of notification text
68 68 :param notification_type: type of notification, based on that we
69 69 pick templates
70 70
71 71 :param recipients: list of int, str or User objects, when None
72 72 is given send to all admins
73 73 :param mention_recipients: list of int, str or User objects,
74 74 that were mentioned
75 75 :param with_email: send email with this notification
76 76 :param email_kwargs: dict with arguments to generate email
77 77 """
78 78
79 79 from rhodecode.lib.celerylib import tasks, run_task
80 80
81 81 if recipients and not getattr(recipients, '__iter__', False):
82 82 raise Exception('recipients must be an iterable object')
83 83
84 84 created_by_obj = self._get_user(created_by)
85 85 # default MAIN body if not given
86 86 email_kwargs = email_kwargs or {'body': notification_body}
87 87 mention_recipients = mention_recipients or set()
88 88
89 89 if not created_by_obj:
90 90 raise Exception('unknown user %s' % created_by)
91 91
92 92 if recipients is None:
93 93 # recipients is None means to all admins
94 94 recipients_objs = User.query().filter(User.admin == true()).all()
95 95 log.debug('sending notifications %s to admins: %s',
96 96 notification_type, recipients_objs)
97 97 else:
98 98 recipients_objs = []
99 99 for u in recipients:
100 100 obj = self._get_user(u)
101 101 if obj:
102 102 recipients_objs.append(obj)
103 103 else: # we didn't find this user, log the error and carry on
104 104 log.error('cannot notify unknown user %r', u)
105 105
106 106 recipients_objs = set(recipients_objs)
107 107 if not recipients_objs:
108 108 raise Exception('no valid recipients specified')
109 109
110 110 log.debug('sending notifications %s to %s',
111 111 notification_type, recipients_objs)
112 112
113 113 # add mentioned users into recipients
114 114 final_recipients = set(recipients_objs).union(mention_recipients)
115 115 notification = Notification.create(
116 116 created_by=created_by_obj, subject=notification_subject,
117 117 body=notification_body, recipients=final_recipients,
118 118 type_=notification_type
119 119 )
120 120
121 121 if not with_email: # skip sending email, and just create notification
122 122 return notification
123 123
124 124 # don't send email to person who created this comment
125 125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
126 126
127 127 # now notify all recipients in question
128 128
129 129 for recipient in rec_objs.union(mention_recipients):
130 130 # inject current recipient
131 131 email_kwargs['recipient'] = recipient
132 132 email_kwargs['mention'] = recipient in mention_recipients
133 133 (subject, headers, email_body,
134 134 email_body_plaintext) = EmailNotificationModel().render_email(
135 135 notification_type, **email_kwargs)
136 136
137 137 log.debug(
138 138 'Creating notification email task for user:`%s`', recipient)
139 139 task = run_task(
140 140 tasks.send_email, recipient.email, subject,
141 141 email_body_plaintext, email_body)
142 142 log.debug('Created email task: %s', task)
143 143
144 144 return notification
145 145
146 146 def delete(self, user, notification):
147 147 # we don't want to remove actual notification just the assignment
148 148 try:
149 149 notification = self.__get_notification(notification)
150 150 user = self._get_user(user)
151 151 if notification and user:
152 152 obj = UserNotification.query()\
153 153 .filter(UserNotification.user == user)\
154 154 .filter(UserNotification.notification == notification)\
155 155 .one()
156 156 Session().delete(obj)
157 157 return True
158 158 except Exception:
159 159 log.error(traceback.format_exc())
160 160 raise
161 161
162 162 def get_for_user(self, user, filter_=None):
163 163 """
164 164 Get mentions for given user, filter them if filter dict is given
165 165 """
166 166 user = self._get_user(user)
167 167
168 168 q = UserNotification.query()\
169 169 .filter(UserNotification.user == user)\
170 170 .join((
171 171 Notification, UserNotification.notification_id ==
172 172 Notification.notification_id))
173 173 if filter_ == ['all']:
174 174 q = q # no filter
175 175 elif filter_ == ['unread']:
176 176 q = q.filter(UserNotification.read == false())
177 177 elif filter_:
178 178 q = q.filter(Notification.type_.in_(filter_))
179 179
180 180 return q
181 181
182 182 def mark_read(self, user, notification):
183 183 try:
184 184 notification = self.__get_notification(notification)
185 185 user = self._get_user(user)
186 186 if notification and user:
187 187 obj = UserNotification.query()\
188 188 .filter(UserNotification.user == user)\
189 189 .filter(UserNotification.notification == notification)\
190 190 .one()
191 191 obj.read = True
192 192 Session().add(obj)
193 193 return True
194 194 except Exception:
195 195 log.error(traceback.format_exc())
196 196 raise
197 197
198 198 def mark_all_read_for_user(self, user, filter_=None):
199 199 user = self._get_user(user)
200 200 q = UserNotification.query()\
201 201 .filter(UserNotification.user == user)\
202 202 .filter(UserNotification.read == false())\
203 203 .join((
204 204 Notification, UserNotification.notification_id ==
205 205 Notification.notification_id))
206 206 if filter_ == ['unread']:
207 207 q = q.filter(UserNotification.read == false())
208 208 elif filter_:
209 209 q = q.filter(Notification.type_.in_(filter_))
210 210
211 211 # this is a little inefficient but sqlalchemy doesn't support
212 212 # update on joined tables :(
213 213 for obj in q.all():
214 214 obj.read = True
215 215 Session().add(obj)
216 216
217 217 def get_unread_cnt_for_user(self, user):
218 218 user = self._get_user(user)
219 219 return UserNotification.query()\
220 220 .filter(UserNotification.read == false())\
221 221 .filter(UserNotification.user == user).count()
222 222
223 223 def get_unread_for_user(self, user):
224 224 user = self._get_user(user)
225 225 return [x.notification for x in UserNotification.query()
226 226 .filter(UserNotification.read == false())
227 227 .filter(UserNotification.user == user).all()]
228 228
229 229 def get_user_notification(self, user, notification):
230 230 user = self._get_user(user)
231 231 notification = self.__get_notification(notification)
232 232
233 233 return UserNotification.query()\
234 234 .filter(UserNotification.notification == notification)\
235 235 .filter(UserNotification.user == user).scalar()
236 236
237 237 def make_description(self, notification, translate, show_age=True):
238 238 """
239 239 Creates a human readable description based on properties
240 240 of notification object
241 241 """
242 242 _ = translate
243 243 _map = {
244 244 notification.TYPE_CHANGESET_COMMENT: [
245 245 _('%(user)s commented on commit %(date_or_age)s'),
246 246 _('%(user)s commented on commit at %(date_or_age)s'),
247 247 ],
248 248 notification.TYPE_MESSAGE: [
249 249 _('%(user)s sent message %(date_or_age)s'),
250 250 _('%(user)s sent message at %(date_or_age)s'),
251 251 ],
252 252 notification.TYPE_MENTION: [
253 253 _('%(user)s mentioned you %(date_or_age)s'),
254 254 _('%(user)s mentioned you at %(date_or_age)s'),
255 255 ],
256 256 notification.TYPE_REGISTRATION: [
257 257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 259 ],
260 260 notification.TYPE_PULL_REQUEST: [
261 261 _('%(user)s opened new pull request %(date_or_age)s'),
262 262 _('%(user)s opened new pull request at %(date_or_age)s'),
263 263 ],
264 264 notification.TYPE_PULL_REQUEST_COMMENT: [
265 265 _('%(user)s commented on pull request %(date_or_age)s'),
266 266 _('%(user)s commented on pull request at %(date_or_age)s'),
267 267 ],
268 268 }
269 269
270 270 templates = _map[notification.type_]
271 271
272 272 if show_age:
273 273 template = templates[0]
274 274 date_or_age = h.age(notification.created_on)
275 275 if translate:
276 276 date_or_age = translate(date_or_age)
277 277
278 278 if isinstance(date_or_age, TranslationString):
279 279 date_or_age = date_or_age.interpolate()
280 280
281 281 else:
282 282 template = templates[1]
283 283 date_or_age = h.format_date(notification.created_on)
284 284
285 285 return template % {
286 286 'user': notification.created_by_user.username,
287 287 'date_or_age': date_or_age,
288 288 }
289 289
290 290
291 291 class EmailNotificationModel(BaseModel):
292 292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
293 293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
294 294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
295 295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
296 296 TYPE_MAIN = Notification.TYPE_MESSAGE
297 297
298 298 TYPE_PASSWORD_RESET = 'password_reset'
299 299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 300 TYPE_EMAIL_TEST = 'email_test'
301 301 TYPE_TEST = 'test'
302 302
303 303 email_types = {
304 TYPE_MAIN: 'email_templates/main.mako',
305 TYPE_TEST: 'email_templates/test.mako',
306 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
307 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
308 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
309 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
310 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
311 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
312 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
304 TYPE_MAIN:
305 'rhodecode:templates/email_templates/main.mako',
306 TYPE_TEST:
307 'rhodecode:templates/email_templates/test.mako',
308 TYPE_EMAIL_TEST:
309 'rhodecode:templates/email_templates/email_test.mako',
310 TYPE_REGISTRATION:
311 'rhodecode:templates/email_templates/user_registration.mako',
312 TYPE_PASSWORD_RESET:
313 'rhodecode:templates/email_templates/password_reset.mako',
314 TYPE_PASSWORD_RESET_CONFIRMATION:
315 'rhodecode:templates/email_templates/password_reset_confirmation.mako',
316 TYPE_COMMIT_COMMENT:
317 'rhodecode:templates/email_templates/commit_comment.mako',
318 TYPE_PULL_REQUEST:
319 'rhodecode:templates/email_templates/pull_request_review.mako',
320 TYPE_PULL_REQUEST_COMMENT:
321 'rhodecode:templates/email_templates/pull_request_comment.mako',
313 322 }
314 323
315 324 def __init__(self):
316 325 """
317 326 Example usage::
318 327
319 328 (subject, headers, email_body,
320 329 email_body_plaintext) = EmailNotificationModel().render_email(
321 330 EmailNotificationModel.TYPE_TEST, **email_kwargs)
322 331
323 332 """
324 333 super(EmailNotificationModel, self).__init__()
325 334 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
326 335
327 336 def _update_kwargs_for_render(self, kwargs):
328 337 """
329 338 Inject params required for Mako rendering
330 339
331 340 :param kwargs:
332 341 """
333 342
334 343 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
335 344 instance_url = h.route_url('home')
336 345 _kwargs = {
337 346 'instance_url': instance_url,
338 347 'whitespace_filter': self.whitespace_filter
339 348 }
340 349 _kwargs.update(kwargs)
341 350 return _kwargs
342 351
343 352 def whitespace_filter(self, text):
344 353 return text.replace('\n', '').replace('\t', '')
345 354
346 355 def get_renderer(self, type_, request):
347 356 template_name = self.email_types[type_]
348 357 return request.get_partial_renderer(template_name)
349 358
350 359 def render_email(self, type_, **kwargs):
351 360 """
352 361 renders template for email, and returns a tuple of
353 362 (subject, email_headers, email_html_body, email_plaintext_body)
354 363 """
355 364 # translator and helpers inject
356 365 _kwargs = self._update_kwargs_for_render(kwargs)
357 366 request = get_current_request()
358 367 email_template = self.get_renderer(type_, request=request)
359 368
360 369 subject = email_template.render('subject', **_kwargs)
361 370
362 371 try:
363 372 headers = email_template.render('headers', **_kwargs)
364 373 except AttributeError:
365 374 # it's not defined in template, ok we can skip it
366 375 headers = ''
367 376
368 377 try:
369 378 body_plaintext = email_template.render('body_plaintext', **_kwargs)
370 379 except AttributeError:
371 380 # it's not defined in template, ok we can skip it
372 381 body_plaintext = ''
373 382
374 383 # render WHOLE template
375 384 body = email_template.render(None, **_kwargs)
376 385
377 386 return subject, headers, body, body_plaintext
@@ -1,1032 +1,1032 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import re
23 23 import shutil
24 24 import time
25 25 import logging
26 26 import traceback
27 27 import datetime
28 28
29 29 from pyramid.threadlocal import get_current_request
30 30 from zope.cachedescriptors.property import Lazy as LazyProperty
31 31
32 32 from rhodecode import events
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError
36 36 from rhodecode.lib.hooks_base import log_delete_repository
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
40 40 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
41 41 get_current_rhodecode_user, safe_int, datetime_to_time,
42 42 action_logger_generic)
43 43 from rhodecode.lib.vcs.backends import get_backend
44 44 from rhodecode.model import BaseModel
45 45 from rhodecode.model.db import (
46 46 _hash_key, joinedload, or_, Repository, UserRepoToPerm, UserGroupRepoToPerm,
47 47 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission,
48 48 Statistics, UserGroup, RepoGroup, RepositoryField, UserLog)
49 49
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55
56 56 class RepoModel(BaseModel):
57 57
58 58 cls = Repository
59 59
60 60 def _get_user_group(self, users_group):
61 61 return self._get_instance(UserGroup, users_group,
62 62 callback=UserGroup.get_by_group_name)
63 63
64 64 def _get_repo_group(self, repo_group):
65 65 return self._get_instance(RepoGroup, repo_group,
66 66 callback=RepoGroup.get_by_group_name)
67 67
68 68 def _create_default_perms(self, repository, private):
69 69 # create default permission
70 70 default = 'repository.read'
71 71 def_user = User.get_default_user()
72 72 for p in def_user.user_perms:
73 73 if p.permission.permission_name.startswith('repository.'):
74 74 default = p.permission.permission_name
75 75 break
76 76
77 77 default_perm = 'repository.none' if private else default
78 78
79 79 repo_to_perm = UserRepoToPerm()
80 80 repo_to_perm.permission = Permission.get_by_key(default_perm)
81 81
82 82 repo_to_perm.repository = repository
83 83 repo_to_perm.user_id = def_user.user_id
84 84
85 85 return repo_to_perm
86 86
87 87 @LazyProperty
88 88 def repos_path(self):
89 89 """
90 90 Gets the repositories root path from database
91 91 """
92 92 settings_model = VcsSettingsModel(sa=self.sa)
93 93 return settings_model.get_repos_location()
94 94
95 95 def get(self, repo_id, cache=False):
96 96 repo = self.sa.query(Repository) \
97 97 .filter(Repository.repo_id == repo_id)
98 98
99 99 if cache:
100 100 repo = repo.options(
101 101 FromCache("sql_cache_short", "get_repo_%s" % repo_id))
102 102 return repo.scalar()
103 103
104 104 def get_repo(self, repository):
105 105 return self._get_repo(repository)
106 106
107 107 def get_by_repo_name(self, repo_name, cache=False):
108 108 repo = self.sa.query(Repository) \
109 109 .filter(Repository.repo_name == repo_name)
110 110
111 111 if cache:
112 112 name_key = _hash_key(repo_name)
113 113 repo = repo.options(
114 114 FromCache("sql_cache_short", "get_repo_%s" % name_key))
115 115 return repo.scalar()
116 116
117 117 def _extract_id_from_repo_name(self, repo_name):
118 118 if repo_name.startswith('/'):
119 119 repo_name = repo_name.lstrip('/')
120 120 by_id_match = re.match(r'^_(\d{1,})', repo_name)
121 121 if by_id_match:
122 122 return by_id_match.groups()[0]
123 123
124 124 def get_repo_by_id(self, repo_name):
125 125 """
126 126 Extracts repo_name by id from special urls.
127 127 Example url is _11/repo_name
128 128
129 129 :param repo_name:
130 130 :return: repo object if matched else None
131 131 """
132 132
133 133 try:
134 134 _repo_id = self._extract_id_from_repo_name(repo_name)
135 135 if _repo_id:
136 136 return self.get(_repo_id)
137 137 except Exception:
138 138 log.exception('Failed to extract repo_name from URL')
139 139
140 140 return None
141 141
142 142 def get_repos_for_root(self, root, traverse=False):
143 143 if traverse:
144 144 like_expression = u'{}%'.format(safe_unicode(root))
145 145 repos = Repository.query().filter(
146 146 Repository.repo_name.like(like_expression)).all()
147 147 else:
148 148 if root and not isinstance(root, RepoGroup):
149 149 raise ValueError(
150 150 'Root must be an instance '
151 151 'of RepoGroup, got:{} instead'.format(type(root)))
152 152 repos = Repository.query().filter(Repository.group == root).all()
153 153 return repos
154 154
155 155 def get_url(self, repo, request=None, permalink=False):
156 156 if not request:
157 157 request = get_current_request()
158 158
159 159 if not request:
160 160 return
161 161
162 162 if permalink:
163 163 return request.route_url(
164 164 'repo_summary', repo_name=safe_str(repo.repo_id))
165 165 else:
166 166 return request.route_url(
167 167 'repo_summary', repo_name=safe_str(repo.repo_name))
168 168
169 169 def get_commit_url(self, repo, commit_id, request=None, permalink=False):
170 170 if not request:
171 171 request = get_current_request()
172 172
173 173 if not request:
174 174 return
175 175
176 176 if permalink:
177 177 return request.route_url(
178 178 'repo_commit', repo_name=safe_str(repo.repo_id),
179 179 commit_id=commit_id)
180 180
181 181 else:
182 182 return request.route_url(
183 183 'repo_commit', repo_name=safe_str(repo.repo_name),
184 184 commit_id=commit_id)
185 185
186 186 def get_repo_log(self, repo, filter_term):
187 187 repo_log = UserLog.query()\
188 188 .filter(or_(UserLog.repository_id == repo.repo_id,
189 189 UserLog.repository_name == repo.repo_name))\
190 190 .options(joinedload(UserLog.user))\
191 191 .options(joinedload(UserLog.repository))\
192 192 .order_by(UserLog.action_date.desc())
193 193
194 194 repo_log = user_log_filter(repo_log, filter_term)
195 195 return repo_log
196 196
197 197 @classmethod
198 198 def update_repoinfo(cls, repositories=None):
199 199 if not repositories:
200 200 repositories = Repository.getAll()
201 201 for repo in repositories:
202 202 repo.update_commit_cache()
203 203
204 204 def get_repos_as_dict(self, repo_list=None, admin=False,
205 205 super_user_actions=False):
206 206 _render = get_current_request().get_partial_renderer(
207 'data_table/_dt_elements.mako')
207 'rhodecode:templates/data_table/_dt_elements.mako')
208 208 c = _render.get_call_context()
209 209
210 210 def quick_menu(repo_name):
211 211 return _render('quick_menu', repo_name)
212 212
213 213 def repo_lnk(name, rtype, rstate, private, fork_of):
214 214 return _render('repo_name', name, rtype, rstate, private, fork_of,
215 215 short_name=not admin, admin=False)
216 216
217 217 def last_change(last_change):
218 218 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
219 219 last_change = last_change + datetime.timedelta(seconds=
220 220 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
221 221 return _render("last_change", last_change)
222 222
223 223 def rss_lnk(repo_name):
224 224 return _render("rss", repo_name)
225 225
226 226 def atom_lnk(repo_name):
227 227 return _render("atom", repo_name)
228 228
229 229 def last_rev(repo_name, cs_cache):
230 230 return _render('revision', repo_name, cs_cache.get('revision'),
231 231 cs_cache.get('raw_id'), cs_cache.get('author'),
232 232 cs_cache.get('message'))
233 233
234 234 def desc(desc):
235 235 return _render('repo_desc', desc, c.visual.stylify_metatags)
236 236
237 237 def state(repo_state):
238 238 return _render("repo_state", repo_state)
239 239
240 240 def repo_actions(repo_name):
241 241 return _render('repo_actions', repo_name, super_user_actions)
242 242
243 243 def user_profile(username):
244 244 return _render('user_profile', username)
245 245
246 246 repos_data = []
247 247 for repo in repo_list:
248 248 cs_cache = repo.changeset_cache
249 249 row = {
250 250 "menu": quick_menu(repo.repo_name),
251 251
252 252 "name": repo_lnk(repo.repo_name, repo.repo_type,
253 253 repo.repo_state, repo.private, repo.fork),
254 254 "name_raw": repo.repo_name.lower(),
255 255
256 256 "last_change": last_change(repo.last_db_change),
257 257 "last_change_raw": datetime_to_time(repo.last_db_change),
258 258
259 259 "last_changeset": last_rev(repo.repo_name, cs_cache),
260 260 "last_changeset_raw": cs_cache.get('revision'),
261 261
262 262 "desc": desc(repo.description_safe),
263 263 "owner": user_profile(repo.user.username),
264 264
265 265 "state": state(repo.repo_state),
266 266 "rss": rss_lnk(repo.repo_name),
267 267
268 268 "atom": atom_lnk(repo.repo_name),
269 269 }
270 270 if admin:
271 271 row.update({
272 272 "action": repo_actions(repo.repo_name),
273 273 })
274 274 repos_data.append(row)
275 275
276 276 return repos_data
277 277
278 278 def _get_defaults(self, repo_name):
279 279 """
280 280 Gets information about repository, and returns a dict for
281 281 usage in forms
282 282
283 283 :param repo_name:
284 284 """
285 285
286 286 repo_info = Repository.get_by_repo_name(repo_name)
287 287
288 288 if repo_info is None:
289 289 return None
290 290
291 291 defaults = repo_info.get_dict()
292 292 defaults['repo_name'] = repo_info.just_name
293 293
294 294 groups = repo_info.groups_with_parents
295 295 parent_group = groups[-1] if groups else None
296 296
297 297 # we use -1 as this is how in HTML, we mark an empty group
298 298 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
299 299
300 300 keys_to_process = (
301 301 {'k': 'repo_type', 'strip': False},
302 302 {'k': 'repo_enable_downloads', 'strip': True},
303 303 {'k': 'repo_description', 'strip': True},
304 304 {'k': 'repo_enable_locking', 'strip': True},
305 305 {'k': 'repo_landing_rev', 'strip': True},
306 306 {'k': 'clone_uri', 'strip': False},
307 307 {'k': 'repo_private', 'strip': True},
308 308 {'k': 'repo_enable_statistics', 'strip': True}
309 309 )
310 310
311 311 for item in keys_to_process:
312 312 attr = item['k']
313 313 if item['strip']:
314 314 attr = remove_prefix(item['k'], 'repo_')
315 315
316 316 val = defaults[attr]
317 317 if item['k'] == 'repo_landing_rev':
318 318 val = ':'.join(defaults[attr])
319 319 defaults[item['k']] = val
320 320 if item['k'] == 'clone_uri':
321 321 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
322 322
323 323 # fill owner
324 324 if repo_info.user:
325 325 defaults.update({'user': repo_info.user.username})
326 326 else:
327 327 replacement_user = User.get_first_super_admin().username
328 328 defaults.update({'user': replacement_user})
329 329
330 330 return defaults
331 331
332 332 def update(self, repo, **kwargs):
333 333 try:
334 334 cur_repo = self._get_repo(repo)
335 335 source_repo_name = cur_repo.repo_name
336 336 if 'user' in kwargs:
337 337 cur_repo.user = User.get_by_username(kwargs['user'])
338 338
339 339 if 'repo_group' in kwargs:
340 340 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
341 341 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
342 342
343 343 update_keys = [
344 344 (1, 'repo_description'),
345 345 (1, 'repo_landing_rev'),
346 346 (1, 'repo_private'),
347 347 (1, 'repo_enable_downloads'),
348 348 (1, 'repo_enable_locking'),
349 349 (1, 'repo_enable_statistics'),
350 350 (0, 'clone_uri'),
351 351 (0, 'fork_id')
352 352 ]
353 353 for strip, k in update_keys:
354 354 if k in kwargs:
355 355 val = kwargs[k]
356 356 if strip:
357 357 k = remove_prefix(k, 'repo_')
358 358
359 359 setattr(cur_repo, k, val)
360 360
361 361 new_name = cur_repo.get_new_name(kwargs['repo_name'])
362 362 cur_repo.repo_name = new_name
363 363
364 364 # if private flag is set, reset default permission to NONE
365 365 if kwargs.get('repo_private'):
366 366 EMPTY_PERM = 'repository.none'
367 367 RepoModel().grant_user_permission(
368 368 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
369 369 )
370 370
371 371 # handle extra fields
372 372 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
373 373 kwargs):
374 374 k = RepositoryField.un_prefix_key(field)
375 375 ex_field = RepositoryField.get_by_key_name(
376 376 key=k, repo=cur_repo)
377 377 if ex_field:
378 378 ex_field.field_value = kwargs[field]
379 379 self.sa.add(ex_field)
380 380 cur_repo.updated_on = datetime.datetime.now()
381 381 self.sa.add(cur_repo)
382 382
383 383 if source_repo_name != new_name:
384 384 # rename repository
385 385 self._rename_filesystem_repo(
386 386 old=source_repo_name, new=new_name)
387 387
388 388 return cur_repo
389 389 except Exception:
390 390 log.error(traceback.format_exc())
391 391 raise
392 392
393 393 def _create_repo(self, repo_name, repo_type, description, owner,
394 394 private=False, clone_uri=None, repo_group=None,
395 395 landing_rev='rev:tip', fork_of=None,
396 396 copy_fork_permissions=False, enable_statistics=False,
397 397 enable_locking=False, enable_downloads=False,
398 398 copy_group_permissions=False,
399 399 state=Repository.STATE_PENDING):
400 400 """
401 401 Create repository inside database with PENDING state, this should be
402 402 only executed by create() repo. With exception of importing existing
403 403 repos
404 404 """
405 405 from rhodecode.model.scm import ScmModel
406 406
407 407 owner = self._get_user(owner)
408 408 fork_of = self._get_repo(fork_of)
409 409 repo_group = self._get_repo_group(safe_int(repo_group))
410 410
411 411 try:
412 412 repo_name = safe_unicode(repo_name)
413 413 description = safe_unicode(description)
414 414 # repo name is just a name of repository
415 415 # while repo_name_full is a full qualified name that is combined
416 416 # with name and path of group
417 417 repo_name_full = repo_name
418 418 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
419 419
420 420 new_repo = Repository()
421 421 new_repo.repo_state = state
422 422 new_repo.enable_statistics = False
423 423 new_repo.repo_name = repo_name_full
424 424 new_repo.repo_type = repo_type
425 425 new_repo.user = owner
426 426 new_repo.group = repo_group
427 427 new_repo.description = description or repo_name
428 428 new_repo.private = private
429 429 new_repo.clone_uri = clone_uri
430 430 new_repo.landing_rev = landing_rev
431 431
432 432 new_repo.enable_statistics = enable_statistics
433 433 new_repo.enable_locking = enable_locking
434 434 new_repo.enable_downloads = enable_downloads
435 435
436 436 if repo_group:
437 437 new_repo.enable_locking = repo_group.enable_locking
438 438
439 439 if fork_of:
440 440 parent_repo = fork_of
441 441 new_repo.fork = parent_repo
442 442
443 443 events.trigger(events.RepoPreCreateEvent(new_repo))
444 444
445 445 self.sa.add(new_repo)
446 446
447 447 EMPTY_PERM = 'repository.none'
448 448 if fork_of and copy_fork_permissions:
449 449 repo = fork_of
450 450 user_perms = UserRepoToPerm.query() \
451 451 .filter(UserRepoToPerm.repository == repo).all()
452 452 group_perms = UserGroupRepoToPerm.query() \
453 453 .filter(UserGroupRepoToPerm.repository == repo).all()
454 454
455 455 for perm in user_perms:
456 456 UserRepoToPerm.create(
457 457 perm.user, new_repo, perm.permission)
458 458
459 459 for perm in group_perms:
460 460 UserGroupRepoToPerm.create(
461 461 perm.users_group, new_repo, perm.permission)
462 462 # in case we copy permissions and also set this repo to private
463 463 # override the default user permission to make it a private
464 464 # repo
465 465 if private:
466 466 RepoModel(self.sa).grant_user_permission(
467 467 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
468 468
469 469 elif repo_group and copy_group_permissions:
470 470 user_perms = UserRepoGroupToPerm.query() \
471 471 .filter(UserRepoGroupToPerm.group == repo_group).all()
472 472
473 473 group_perms = UserGroupRepoGroupToPerm.query() \
474 474 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
475 475
476 476 for perm in user_perms:
477 477 perm_name = perm.permission.permission_name.replace(
478 478 'group.', 'repository.')
479 479 perm_obj = Permission.get_by_key(perm_name)
480 480 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
481 481
482 482 for perm in group_perms:
483 483 perm_name = perm.permission.permission_name.replace(
484 484 'group.', 'repository.')
485 485 perm_obj = Permission.get_by_key(perm_name)
486 486 UserGroupRepoToPerm.create(
487 487 perm.users_group, new_repo, perm_obj)
488 488
489 489 if private:
490 490 RepoModel(self.sa).grant_user_permission(
491 491 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
492 492
493 493 else:
494 494 perm_obj = self._create_default_perms(new_repo, private)
495 495 self.sa.add(perm_obj)
496 496
497 497 # now automatically start following this repository as owner
498 498 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
499 499 owner.user_id)
500 500
501 501 # we need to flush here, in order to check if database won't
502 502 # throw any exceptions, create filesystem dirs at the very end
503 503 self.sa.flush()
504 504 events.trigger(events.RepoCreateEvent(new_repo))
505 505 return new_repo
506 506
507 507 except Exception:
508 508 log.error(traceback.format_exc())
509 509 raise
510 510
511 511 def create(self, form_data, cur_user):
512 512 """
513 513 Create repository using celery tasks
514 514
515 515 :param form_data:
516 516 :param cur_user:
517 517 """
518 518 from rhodecode.lib.celerylib import tasks, run_task
519 519 return run_task(tasks.create_repo, form_data, cur_user)
520 520
521 521 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
522 522 perm_deletions=None, check_perms=True,
523 523 cur_user=None):
524 524 if not perm_additions:
525 525 perm_additions = []
526 526 if not perm_updates:
527 527 perm_updates = []
528 528 if not perm_deletions:
529 529 perm_deletions = []
530 530
531 531 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
532 532
533 533 changes = {
534 534 'added': [],
535 535 'updated': [],
536 536 'deleted': []
537 537 }
538 538 # update permissions
539 539 for member_id, perm, member_type in perm_updates:
540 540 member_id = int(member_id)
541 541 if member_type == 'user':
542 542 member_name = User.get(member_id).username
543 543 # this updates also current one if found
544 544 self.grant_user_permission(
545 545 repo=repo, user=member_id, perm=perm)
546 546 else: # set for user group
547 547 # check if we have permissions to alter this usergroup
548 548 member_name = UserGroup.get(member_id).users_group_name
549 549 if not check_perms or HasUserGroupPermissionAny(
550 550 *req_perms)(member_name, user=cur_user):
551 551 self.grant_user_group_permission(
552 552 repo=repo, group_name=member_id, perm=perm)
553 553
554 554 changes['updated'].append({'type': member_type, 'id': member_id,
555 555 'name': member_name, 'new_perm': perm})
556 556
557 557 # set new permissions
558 558 for member_id, perm, member_type in perm_additions:
559 559 member_id = int(member_id)
560 560 if member_type == 'user':
561 561 member_name = User.get(member_id).username
562 562 self.grant_user_permission(
563 563 repo=repo, user=member_id, perm=perm)
564 564 else: # set for user group
565 565 # check if we have permissions to alter this usergroup
566 566 member_name = UserGroup.get(member_id).users_group_name
567 567 if not check_perms or HasUserGroupPermissionAny(
568 568 *req_perms)(member_name, user=cur_user):
569 569 self.grant_user_group_permission(
570 570 repo=repo, group_name=member_id, perm=perm)
571 571 changes['added'].append({'type': member_type, 'id': member_id,
572 572 'name': member_name, 'new_perm': perm})
573 573 # delete permissions
574 574 for member_id, perm, member_type in perm_deletions:
575 575 member_id = int(member_id)
576 576 if member_type == 'user':
577 577 member_name = User.get(member_id).username
578 578 self.revoke_user_permission(repo=repo, user=member_id)
579 579 else: # set for user group
580 580 # check if we have permissions to alter this usergroup
581 581 member_name = UserGroup.get(member_id).users_group_name
582 582 if not check_perms or HasUserGroupPermissionAny(
583 583 *req_perms)(member_name, user=cur_user):
584 584 self.revoke_user_group_permission(
585 585 repo=repo, group_name=member_id)
586 586
587 587 changes['deleted'].append({'type': member_type, 'id': member_id,
588 588 'name': member_name, 'new_perm': perm})
589 589 return changes
590 590
591 591 def create_fork(self, form_data, cur_user):
592 592 """
593 593 Simple wrapper into executing celery task for fork creation
594 594
595 595 :param form_data:
596 596 :param cur_user:
597 597 """
598 598 from rhodecode.lib.celerylib import tasks, run_task
599 599 return run_task(tasks.create_repo_fork, form_data, cur_user)
600 600
601 601 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
602 602 """
603 603 Delete given repository, forks parameter defines what do do with
604 604 attached forks. Throws AttachedForksError if deleted repo has attached
605 605 forks
606 606
607 607 :param repo:
608 608 :param forks: str 'delete' or 'detach'
609 609 :param fs_remove: remove(archive) repo from filesystem
610 610 """
611 611 if not cur_user:
612 612 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
613 613 repo = self._get_repo(repo)
614 614 if repo:
615 615 if forks == 'detach':
616 616 for r in repo.forks:
617 617 r.fork = None
618 618 self.sa.add(r)
619 619 elif forks == 'delete':
620 620 for r in repo.forks:
621 621 self.delete(r, forks='delete')
622 622 elif [f for f in repo.forks]:
623 623 raise AttachedForksError()
624 624
625 625 old_repo_dict = repo.get_dict()
626 626 events.trigger(events.RepoPreDeleteEvent(repo))
627 627 try:
628 628 self.sa.delete(repo)
629 629 if fs_remove:
630 630 self._delete_filesystem_repo(repo)
631 631 else:
632 632 log.debug('skipping removal from filesystem')
633 633 old_repo_dict.update({
634 634 'deleted_by': cur_user,
635 635 'deleted_on': time.time(),
636 636 })
637 637 log_delete_repository(**old_repo_dict)
638 638 events.trigger(events.RepoDeleteEvent(repo))
639 639 except Exception:
640 640 log.error(traceback.format_exc())
641 641 raise
642 642
643 643 def grant_user_permission(self, repo, user, perm):
644 644 """
645 645 Grant permission for user on given repository, or update existing one
646 646 if found
647 647
648 648 :param repo: Instance of Repository, repository_id, or repository name
649 649 :param user: Instance of User, user_id or username
650 650 :param perm: Instance of Permission, or permission_name
651 651 """
652 652 user = self._get_user(user)
653 653 repo = self._get_repo(repo)
654 654 permission = self._get_perm(perm)
655 655
656 656 # check if we have that permission already
657 657 obj = self.sa.query(UserRepoToPerm) \
658 658 .filter(UserRepoToPerm.user == user) \
659 659 .filter(UserRepoToPerm.repository == repo) \
660 660 .scalar()
661 661 if obj is None:
662 662 # create new !
663 663 obj = UserRepoToPerm()
664 664 obj.repository = repo
665 665 obj.user = user
666 666 obj.permission = permission
667 667 self.sa.add(obj)
668 668 log.debug('Granted perm %s to %s on %s', perm, user, repo)
669 669 action_logger_generic(
670 670 'granted permission: {} to user: {} on repo: {}'.format(
671 671 perm, user, repo), namespace='security.repo')
672 672 return obj
673 673
674 674 def revoke_user_permission(self, repo, user):
675 675 """
676 676 Revoke permission for user on given repository
677 677
678 678 :param repo: Instance of Repository, repository_id, or repository name
679 679 :param user: Instance of User, user_id or username
680 680 """
681 681
682 682 user = self._get_user(user)
683 683 repo = self._get_repo(repo)
684 684
685 685 obj = self.sa.query(UserRepoToPerm) \
686 686 .filter(UserRepoToPerm.repository == repo) \
687 687 .filter(UserRepoToPerm.user == user) \
688 688 .scalar()
689 689 if obj:
690 690 self.sa.delete(obj)
691 691 log.debug('Revoked perm on %s on %s', repo, user)
692 692 action_logger_generic(
693 693 'revoked permission from user: {} on repo: {}'.format(
694 694 user, repo), namespace='security.repo')
695 695
696 696 def grant_user_group_permission(self, repo, group_name, perm):
697 697 """
698 698 Grant permission for user group on given repository, or update
699 699 existing one if found
700 700
701 701 :param repo: Instance of Repository, repository_id, or repository name
702 702 :param group_name: Instance of UserGroup, users_group_id,
703 703 or user group name
704 704 :param perm: Instance of Permission, or permission_name
705 705 """
706 706 repo = self._get_repo(repo)
707 707 group_name = self._get_user_group(group_name)
708 708 permission = self._get_perm(perm)
709 709
710 710 # check if we have that permission already
711 711 obj = self.sa.query(UserGroupRepoToPerm) \
712 712 .filter(UserGroupRepoToPerm.users_group == group_name) \
713 713 .filter(UserGroupRepoToPerm.repository == repo) \
714 714 .scalar()
715 715
716 716 if obj is None:
717 717 # create new
718 718 obj = UserGroupRepoToPerm()
719 719
720 720 obj.repository = repo
721 721 obj.users_group = group_name
722 722 obj.permission = permission
723 723 self.sa.add(obj)
724 724 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
725 725 action_logger_generic(
726 726 'granted permission: {} to usergroup: {} on repo: {}'.format(
727 727 perm, group_name, repo), namespace='security.repo')
728 728
729 729 return obj
730 730
731 731 def revoke_user_group_permission(self, repo, group_name):
732 732 """
733 733 Revoke permission for user group on given repository
734 734
735 735 :param repo: Instance of Repository, repository_id, or repository name
736 736 :param group_name: Instance of UserGroup, users_group_id,
737 737 or user group name
738 738 """
739 739 repo = self._get_repo(repo)
740 740 group_name = self._get_user_group(group_name)
741 741
742 742 obj = self.sa.query(UserGroupRepoToPerm) \
743 743 .filter(UserGroupRepoToPerm.repository == repo) \
744 744 .filter(UserGroupRepoToPerm.users_group == group_name) \
745 745 .scalar()
746 746 if obj:
747 747 self.sa.delete(obj)
748 748 log.debug('Revoked perm to %s on %s', repo, group_name)
749 749 action_logger_generic(
750 750 'revoked permission from usergroup: {} on repo: {}'.format(
751 751 group_name, repo), namespace='security.repo')
752 752
753 753 def delete_stats(self, repo_name):
754 754 """
755 755 removes stats for given repo
756 756
757 757 :param repo_name:
758 758 """
759 759 repo = self._get_repo(repo_name)
760 760 try:
761 761 obj = self.sa.query(Statistics) \
762 762 .filter(Statistics.repository == repo).scalar()
763 763 if obj:
764 764 self.sa.delete(obj)
765 765 except Exception:
766 766 log.error(traceback.format_exc())
767 767 raise
768 768
769 769 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
770 770 field_type='str', field_desc=''):
771 771
772 772 repo = self._get_repo(repo_name)
773 773
774 774 new_field = RepositoryField()
775 775 new_field.repository = repo
776 776 new_field.field_key = field_key
777 777 new_field.field_type = field_type # python type
778 778 new_field.field_value = field_value
779 779 new_field.field_desc = field_desc
780 780 new_field.field_label = field_label
781 781 self.sa.add(new_field)
782 782 return new_field
783 783
784 784 def delete_repo_field(self, repo_name, field_key):
785 785 repo = self._get_repo(repo_name)
786 786 field = RepositoryField.get_by_key_name(field_key, repo)
787 787 if field:
788 788 self.sa.delete(field)
789 789
790 790 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
791 791 clone_uri=None, repo_store_location=None,
792 792 use_global_config=False):
793 793 """
794 794 makes repository on filesystem. It's group aware means it'll create
795 795 a repository within a group, and alter the paths accordingly of
796 796 group location
797 797
798 798 :param repo_name:
799 799 :param alias:
800 800 :param parent:
801 801 :param clone_uri:
802 802 :param repo_store_location:
803 803 """
804 804 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
805 805 from rhodecode.model.scm import ScmModel
806 806
807 807 if Repository.NAME_SEP in repo_name:
808 808 raise ValueError(
809 809 'repo_name must not contain groups got `%s`' % repo_name)
810 810
811 811 if isinstance(repo_group, RepoGroup):
812 812 new_parent_path = os.sep.join(repo_group.full_path_splitted)
813 813 else:
814 814 new_parent_path = repo_group or ''
815 815
816 816 if repo_store_location:
817 817 _paths = [repo_store_location]
818 818 else:
819 819 _paths = [self.repos_path, new_parent_path, repo_name]
820 820 # we need to make it str for mercurial
821 821 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
822 822
823 823 # check if this path is not a repository
824 824 if is_valid_repo(repo_path, self.repos_path):
825 825 raise Exception('This path %s is a valid repository' % repo_path)
826 826
827 827 # check if this path is a group
828 828 if is_valid_repo_group(repo_path, self.repos_path):
829 829 raise Exception('This path %s is a valid group' % repo_path)
830 830
831 831 log.info('creating repo %s in %s from url: `%s`',
832 832 repo_name, safe_unicode(repo_path),
833 833 obfuscate_url_pw(clone_uri))
834 834
835 835 backend = get_backend(repo_type)
836 836
837 837 config_repo = None if use_global_config else repo_name
838 838 if config_repo and new_parent_path:
839 839 config_repo = Repository.NAME_SEP.join(
840 840 (new_parent_path, config_repo))
841 841 config = make_db_config(clear_session=False, repo=config_repo)
842 842 config.set('extensions', 'largefiles', '')
843 843
844 844 # patch and reset hooks section of UI config to not run any
845 845 # hooks on creating remote repo
846 846 config.clear_section('hooks')
847 847
848 848 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
849 849 if repo_type == 'git':
850 850 repo = backend(
851 851 repo_path, config=config, create=True, src_url=clone_uri,
852 852 bare=True)
853 853 else:
854 854 repo = backend(
855 855 repo_path, config=config, create=True, src_url=clone_uri)
856 856
857 857 ScmModel().install_hooks(repo, repo_type=repo_type)
858 858
859 859 log.debug('Created repo %s with %s backend',
860 860 safe_unicode(repo_name), safe_unicode(repo_type))
861 861 return repo
862 862
863 863 def _rename_filesystem_repo(self, old, new):
864 864 """
865 865 renames repository on filesystem
866 866
867 867 :param old: old name
868 868 :param new: new name
869 869 """
870 870 log.info('renaming repo from %s to %s', old, new)
871 871
872 872 old_path = os.path.join(self.repos_path, old)
873 873 new_path = os.path.join(self.repos_path, new)
874 874 if os.path.isdir(new_path):
875 875 raise Exception(
876 876 'Was trying to rename to already existing dir %s' % new_path
877 877 )
878 878 shutil.move(old_path, new_path)
879 879
880 880 def _delete_filesystem_repo(self, repo):
881 881 """
882 882 removes repo from filesystem, the removal is acctually made by
883 883 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
884 884 repository is no longer valid for rhodecode, can be undeleted later on
885 885 by reverting the renames on this repository
886 886
887 887 :param repo: repo object
888 888 """
889 889 rm_path = os.path.join(self.repos_path, repo.repo_name)
890 890 repo_group = repo.group
891 891 log.info("Removing repository %s", rm_path)
892 892 # disable hg/git internal that it doesn't get detected as repo
893 893 alias = repo.repo_type
894 894
895 895 config = make_db_config(clear_session=False)
896 896 config.set('extensions', 'largefiles', '')
897 897 bare = getattr(repo.scm_instance(config=config), 'bare', False)
898 898
899 899 # skip this for bare git repos
900 900 if not bare:
901 901 # disable VCS repo
902 902 vcs_path = os.path.join(rm_path, '.%s' % alias)
903 903 if os.path.exists(vcs_path):
904 904 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
905 905
906 906 _now = datetime.datetime.now()
907 907 _ms = str(_now.microsecond).rjust(6, '0')
908 908 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
909 909 repo.just_name)
910 910 if repo_group:
911 911 # if repository is in group, prefix the removal path with the group
912 912 args = repo_group.full_path_splitted + [_d]
913 913 _d = os.path.join(*args)
914 914
915 915 if os.path.isdir(rm_path):
916 916 shutil.move(rm_path, os.path.join(self.repos_path, _d))
917 917
918 918
919 919 class ReadmeFinder:
920 920 """
921 921 Utility which knows how to find a readme for a specific commit.
922 922
923 923 The main idea is that this is a configurable algorithm. When creating an
924 924 instance you can define parameters, currently only the `default_renderer`.
925 925 Based on this configuration the method :meth:`search` behaves slightly
926 926 different.
927 927 """
928 928
929 929 readme_re = re.compile(r'^readme(\.[^\.]+)?$', re.IGNORECASE)
930 930 path_re = re.compile(r'^docs?', re.IGNORECASE)
931 931
932 932 default_priorities = {
933 933 None: 0,
934 934 '.text': 2,
935 935 '.txt': 3,
936 936 '.rst': 1,
937 937 '.rest': 2,
938 938 '.md': 1,
939 939 '.mkdn': 2,
940 940 '.mdown': 3,
941 941 '.markdown': 4,
942 942 }
943 943
944 944 path_priority = {
945 945 'doc': 0,
946 946 'docs': 1,
947 947 }
948 948
949 949 FALLBACK_PRIORITY = 99
950 950
951 951 RENDERER_TO_EXTENSION = {
952 952 'rst': ['.rst', '.rest'],
953 953 'markdown': ['.md', 'mkdn', '.mdown', '.markdown'],
954 954 }
955 955
956 956 def __init__(self, default_renderer=None):
957 957 self._default_renderer = default_renderer
958 958 self._renderer_extensions = self.RENDERER_TO_EXTENSION.get(
959 959 default_renderer, [])
960 960
961 961 def search(self, commit, path='/'):
962 962 """
963 963 Find a readme in the given `commit`.
964 964 """
965 965 nodes = commit.get_nodes(path)
966 966 matches = self._match_readmes(nodes)
967 967 matches = self._sort_according_to_priority(matches)
968 968 if matches:
969 969 return matches[0].node
970 970
971 971 paths = self._match_paths(nodes)
972 972 paths = self._sort_paths_according_to_priority(paths)
973 973 for path in paths:
974 974 match = self.search(commit, path=path)
975 975 if match:
976 976 return match
977 977
978 978 return None
979 979
980 980 def _match_readmes(self, nodes):
981 981 for node in nodes:
982 982 if not node.is_file():
983 983 continue
984 984 path = node.path.rsplit('/', 1)[-1]
985 985 match = self.readme_re.match(path)
986 986 if match:
987 987 extension = match.group(1)
988 988 yield ReadmeMatch(node, match, self._priority(extension))
989 989
990 990 def _match_paths(self, nodes):
991 991 for node in nodes:
992 992 if not node.is_dir():
993 993 continue
994 994 match = self.path_re.match(node.path)
995 995 if match:
996 996 yield node.path
997 997
998 998 def _priority(self, extension):
999 999 renderer_priority = (
1000 1000 0 if extension in self._renderer_extensions else 1)
1001 1001 extension_priority = self.default_priorities.get(
1002 1002 extension, self.FALLBACK_PRIORITY)
1003 1003 return (renderer_priority, extension_priority)
1004 1004
1005 1005 def _sort_according_to_priority(self, matches):
1006 1006
1007 1007 def priority_and_path(match):
1008 1008 return (match.priority, match.path)
1009 1009
1010 1010 return sorted(matches, key=priority_and_path)
1011 1011
1012 1012 def _sort_paths_according_to_priority(self, paths):
1013 1013
1014 1014 def priority_and_path(path):
1015 1015 return (self.path_priority.get(path, self.FALLBACK_PRIORITY), path)
1016 1016
1017 1017 return sorted(paths, key=priority_and_path)
1018 1018
1019 1019
1020 1020 class ReadmeMatch:
1021 1021
1022 1022 def __init__(self, node, match, priority):
1023 1023 self.node = node
1024 1024 self._match = match
1025 1025 self.priority = priority
1026 1026
1027 1027 @property
1028 1028 def path(self):
1029 1029 return self.node.path
1030 1030
1031 1031 def __repr__(self):
1032 1032 return '<ReadmeMatch {} priority={}'.format(self.path, self.priority)
@@ -1,764 +1,764 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 repo group model for RhodeCode
24 24 """
25 25
26 26 import os
27 27 import datetime
28 28 import itertools
29 29 import logging
30 30 import shutil
31 31 import traceback
32 32 import string
33 33
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import (_hash_key,
39 39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 40 UserGroup, Repository)
41 41 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.lib.utils2 import action_logger_generic, datetime_to_time
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoGroupModel(BaseModel):
49 49
50 50 cls = RepoGroup
51 51 PERSONAL_GROUP_DESC = 'personal repo group of user `%(username)s`'
52 52 PERSONAL_GROUP_PATTERN = '${username}' # default
53 53
54 54 def _get_user_group(self, users_group):
55 55 return self._get_instance(UserGroup, users_group,
56 56 callback=UserGroup.get_by_group_name)
57 57
58 58 def _get_repo_group(self, repo_group):
59 59 return self._get_instance(RepoGroup, repo_group,
60 60 callback=RepoGroup.get_by_group_name)
61 61
62 62 @LazyProperty
63 63 def repos_path(self):
64 64 """
65 65 Gets the repositories root path from database
66 66 """
67 67
68 68 settings_model = VcsSettingsModel(sa=self.sa)
69 69 return settings_model.get_repos_location()
70 70
71 71 def get_by_group_name(self, repo_group_name, cache=None):
72 72 repo = self.sa.query(RepoGroup) \
73 73 .filter(RepoGroup.group_name == repo_group_name)
74 74
75 75 if cache:
76 76 name_key = _hash_key(repo_group_name)
77 77 repo = repo.options(
78 78 FromCache("sql_cache_short", "get_repo_group_%s" % name_key))
79 79 return repo.scalar()
80 80
81 81 def get_default_create_personal_repo_group(self):
82 82 value = SettingsModel().get_setting_by_name(
83 83 'create_personal_repo_group')
84 84 return value.app_settings_value if value else None or False
85 85
86 86 def get_personal_group_name_pattern(self):
87 87 value = SettingsModel().get_setting_by_name(
88 88 'personal_repo_group_pattern')
89 89 val = value.app_settings_value if value else None
90 90 group_template = val or self.PERSONAL_GROUP_PATTERN
91 91
92 92 group_template = group_template.lstrip('/')
93 93 return group_template
94 94
95 95 def get_personal_group_name(self, user):
96 96 template = self.get_personal_group_name_pattern()
97 97 return string.Template(template).safe_substitute(
98 98 username=user.username,
99 99 user_id=user.user_id,
100 100 )
101 101
102 102 def create_personal_repo_group(self, user, commit_early=True):
103 103 desc = self.PERSONAL_GROUP_DESC % {'username': user.username}
104 104 personal_repo_group_name = self.get_personal_group_name(user)
105 105
106 106 # create a new one
107 107 RepoGroupModel().create(
108 108 group_name=personal_repo_group_name,
109 109 group_description=desc,
110 110 owner=user.username,
111 111 personal=True,
112 112 commit_early=commit_early)
113 113
114 114 def _create_default_perms(self, new_group):
115 115 # create default permission
116 116 default_perm = 'group.read'
117 117 def_user = User.get_default_user()
118 118 for p in def_user.user_perms:
119 119 if p.permission.permission_name.startswith('group.'):
120 120 default_perm = p.permission.permission_name
121 121 break
122 122
123 123 repo_group_to_perm = UserRepoGroupToPerm()
124 124 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
125 125
126 126 repo_group_to_perm.group = new_group
127 127 repo_group_to_perm.user_id = def_user.user_id
128 128 return repo_group_to_perm
129 129
130 130 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False,
131 131 get_object=False):
132 132 """
133 133 Get's the group name and a parent group name from given group name.
134 134 If repo_in_path is set to truth, we asume the full path also includes
135 135 repo name, in such case we clean the last element.
136 136
137 137 :param group_name_full:
138 138 """
139 139 split_paths = 1
140 140 if repo_in_path:
141 141 split_paths = 2
142 142 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
143 143
144 144 if repo_in_path and len(_parts) > 1:
145 145 # such case last element is the repo_name
146 146 _parts.pop(-1)
147 147 group_name_cleaned = _parts[-1] # just the group name
148 148 parent_repo_group_name = None
149 149
150 150 if len(_parts) > 1:
151 151 parent_repo_group_name = _parts[0]
152 152
153 153 parent_group = None
154 154 if parent_repo_group_name:
155 155 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
156 156
157 157 if get_object:
158 158 return group_name_cleaned, parent_repo_group_name, parent_group
159 159
160 160 return group_name_cleaned, parent_repo_group_name
161 161
162 162 def check_exist_filesystem(self, group_name, exc_on_failure=True):
163 163 create_path = os.path.join(self.repos_path, group_name)
164 164 log.debug('creating new group in %s', create_path)
165 165
166 166 if os.path.isdir(create_path):
167 167 if exc_on_failure:
168 168 abs_create_path = os.path.abspath(create_path)
169 169 raise Exception('Directory `{}` already exists !'.format(abs_create_path))
170 170 return False
171 171 return True
172 172
173 173 def _create_group(self, group_name):
174 174 """
175 175 makes repository group on filesystem
176 176
177 177 :param repo_name:
178 178 :param parent_id:
179 179 """
180 180
181 181 self.check_exist_filesystem(group_name)
182 182 create_path = os.path.join(self.repos_path, group_name)
183 183 log.debug('creating new group in %s', create_path)
184 184 os.makedirs(create_path, mode=0755)
185 185 log.debug('created group in %s', create_path)
186 186
187 187 def _rename_group(self, old, new):
188 188 """
189 189 Renames a group on filesystem
190 190
191 191 :param group_name:
192 192 """
193 193
194 194 if old == new:
195 195 log.debug('skipping group rename')
196 196 return
197 197
198 198 log.debug('renaming repository group from %s to %s', old, new)
199 199
200 200 old_path = os.path.join(self.repos_path, old)
201 201 new_path = os.path.join(self.repos_path, new)
202 202
203 203 log.debug('renaming repos paths from %s to %s', old_path, new_path)
204 204
205 205 if os.path.isdir(new_path):
206 206 raise Exception('Was trying to rename to already '
207 207 'existing dir %s' % new_path)
208 208 shutil.move(old_path, new_path)
209 209
210 210 def _delete_filesystem_group(self, group, force_delete=False):
211 211 """
212 212 Deletes a group from a filesystem
213 213
214 214 :param group: instance of group from database
215 215 :param force_delete: use shutil rmtree to remove all objects
216 216 """
217 217 paths = group.full_path.split(RepoGroup.url_sep())
218 218 paths = os.sep.join(paths)
219 219
220 220 rm_path = os.path.join(self.repos_path, paths)
221 221 log.info("Removing group %s", rm_path)
222 222 # delete only if that path really exists
223 223 if os.path.isdir(rm_path):
224 224 if force_delete:
225 225 shutil.rmtree(rm_path)
226 226 else:
227 227 # archive that group`
228 228 _now = datetime.datetime.now()
229 229 _ms = str(_now.microsecond).rjust(6, '0')
230 230 _d = 'rm__%s_GROUP_%s' % (
231 231 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
232 232 shutil.move(rm_path, os.path.join(self.repos_path, _d))
233 233
234 234 def create(self, group_name, group_description, owner, just_db=False,
235 235 copy_permissions=False, personal=None, commit_early=True):
236 236
237 237 (group_name_cleaned,
238 238 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
239 239
240 240 parent_group = None
241 241 if parent_group_name:
242 242 parent_group = self._get_repo_group(parent_group_name)
243 243 if not parent_group:
244 244 # we tried to create a nested group, but the parent is not
245 245 # existing
246 246 raise ValueError(
247 247 'Parent group `%s` given in `%s` group name '
248 248 'is not yet existing.' % (parent_group_name, group_name))
249 249
250 250 # because we are doing a cleanup, we need to check if such directory
251 251 # already exists. If we don't do that we can accidentally delete
252 252 # existing directory via cleanup that can cause data issues, since
253 253 # delete does a folder rename to special syntax later cleanup
254 254 # functions can delete this
255 255 cleanup_group = self.check_exist_filesystem(group_name,
256 256 exc_on_failure=False)
257 257 user = self._get_user(owner)
258 258 if not user:
259 259 raise ValueError('Owner %s not found as rhodecode user', owner)
260 260
261 261 try:
262 262 new_repo_group = RepoGroup()
263 263 new_repo_group.user = user
264 264 new_repo_group.group_description = group_description or group_name
265 265 new_repo_group.parent_group = parent_group
266 266 new_repo_group.group_name = group_name
267 267 new_repo_group.personal = personal
268 268
269 269 self.sa.add(new_repo_group)
270 270
271 271 # create an ADMIN permission for owner except if we're super admin,
272 272 # later owner should go into the owner field of groups
273 273 if not user.is_admin:
274 274 self.grant_user_permission(repo_group=new_repo_group,
275 275 user=owner, perm='group.admin')
276 276
277 277 if parent_group and copy_permissions:
278 278 # copy permissions from parent
279 279 user_perms = UserRepoGroupToPerm.query() \
280 280 .filter(UserRepoGroupToPerm.group == parent_group).all()
281 281
282 282 group_perms = UserGroupRepoGroupToPerm.query() \
283 283 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
284 284
285 285 for perm in user_perms:
286 286 # don't copy over the permission for user who is creating
287 287 # this group, if he is not super admin he get's admin
288 288 # permission set above
289 289 if perm.user != user or user.is_admin:
290 290 UserRepoGroupToPerm.create(
291 291 perm.user, new_repo_group, perm.permission)
292 292
293 293 for perm in group_perms:
294 294 UserGroupRepoGroupToPerm.create(
295 295 perm.users_group, new_repo_group, perm.permission)
296 296 else:
297 297 perm_obj = self._create_default_perms(new_repo_group)
298 298 self.sa.add(perm_obj)
299 299
300 300 # now commit the changes, earlier so we are sure everything is in
301 301 # the database.
302 302 if commit_early:
303 303 self.sa.commit()
304 304 if not just_db:
305 305 self._create_group(new_repo_group.group_name)
306 306
307 307 # trigger the post hook
308 308 from rhodecode.lib.hooks_base import log_create_repository_group
309 309 repo_group = RepoGroup.get_by_group_name(group_name)
310 310 log_create_repository_group(
311 311 created_by=user.username, **repo_group.get_dict())
312 312
313 313 # Trigger create event.
314 314 events.trigger(events.RepoGroupCreateEvent(repo_group))
315 315
316 316 return new_repo_group
317 317 except Exception:
318 318 self.sa.rollback()
319 319 log.exception('Exception occurred when creating repository group, '
320 320 'doing cleanup...')
321 321 # rollback things manually !
322 322 repo_group = RepoGroup.get_by_group_name(group_name)
323 323 if repo_group:
324 324 RepoGroup.delete(repo_group.group_id)
325 325 self.sa.commit()
326 326 if cleanup_group:
327 327 RepoGroupModel()._delete_filesystem_group(repo_group)
328 328 raise
329 329
330 330 def update_permissions(
331 331 self, repo_group, perm_additions=None, perm_updates=None,
332 332 perm_deletions=None, recursive=None, check_perms=True,
333 333 cur_user=None):
334 334 from rhodecode.model.repo import RepoModel
335 335 from rhodecode.lib.auth import HasUserGroupPermissionAny
336 336
337 337 if not perm_additions:
338 338 perm_additions = []
339 339 if not perm_updates:
340 340 perm_updates = []
341 341 if not perm_deletions:
342 342 perm_deletions = []
343 343
344 344 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
345 345
346 346 changes = {
347 347 'added': [],
348 348 'updated': [],
349 349 'deleted': []
350 350 }
351 351
352 352 def _set_perm_user(obj, user, perm):
353 353 if isinstance(obj, RepoGroup):
354 354 self.grant_user_permission(
355 355 repo_group=obj, user=user, perm=perm)
356 356 elif isinstance(obj, Repository):
357 357 # private repos will not allow to change the default
358 358 # permissions using recursive mode
359 359 if obj.private and user == User.DEFAULT_USER:
360 360 return
361 361
362 362 # we set group permission but we have to switch to repo
363 363 # permission
364 364 perm = perm.replace('group.', 'repository.')
365 365 RepoModel().grant_user_permission(
366 366 repo=obj, user=user, perm=perm)
367 367
368 368 def _set_perm_group(obj, users_group, perm):
369 369 if isinstance(obj, RepoGroup):
370 370 self.grant_user_group_permission(
371 371 repo_group=obj, group_name=users_group, perm=perm)
372 372 elif isinstance(obj, Repository):
373 373 # we set group permission but we have to switch to repo
374 374 # permission
375 375 perm = perm.replace('group.', 'repository.')
376 376 RepoModel().grant_user_group_permission(
377 377 repo=obj, group_name=users_group, perm=perm)
378 378
379 379 def _revoke_perm_user(obj, user):
380 380 if isinstance(obj, RepoGroup):
381 381 self.revoke_user_permission(repo_group=obj, user=user)
382 382 elif isinstance(obj, Repository):
383 383 RepoModel().revoke_user_permission(repo=obj, user=user)
384 384
385 385 def _revoke_perm_group(obj, user_group):
386 386 if isinstance(obj, RepoGroup):
387 387 self.revoke_user_group_permission(
388 388 repo_group=obj, group_name=user_group)
389 389 elif isinstance(obj, Repository):
390 390 RepoModel().revoke_user_group_permission(
391 391 repo=obj, group_name=user_group)
392 392
393 393 # start updates
394 394 log.debug('Now updating permissions for %s in recursive mode:%s',
395 395 repo_group, recursive)
396 396
397 397 # initialize check function, we'll call that multiple times
398 398 has_group_perm = HasUserGroupPermissionAny(*req_perms)
399 399
400 400 for obj in repo_group.recursive_groups_and_repos():
401 401 # iterated obj is an instance of a repos group or repository in
402 402 # that group, recursive option can be: none, repos, groups, all
403 403 if recursive == 'all':
404 404 obj = obj
405 405 elif recursive == 'repos':
406 406 # skip groups, other than this one
407 407 if isinstance(obj, RepoGroup) and not obj == repo_group:
408 408 continue
409 409 elif recursive == 'groups':
410 410 # skip repos
411 411 if isinstance(obj, Repository):
412 412 continue
413 413 else: # recursive == 'none':
414 414 # DEFAULT option - don't apply to iterated objects
415 415 # also we do a break at the end of this loop. if we are not
416 416 # in recursive mode
417 417 obj = repo_group
418 418
419 419 change_obj = obj.get_api_data()
420 420
421 421 # update permissions
422 422 for member_id, perm, member_type in perm_updates:
423 423 member_id = int(member_id)
424 424 if member_type == 'user':
425 425 member_name = User.get(member_id).username
426 426 # this updates also current one if found
427 427 _set_perm_user(obj, user=member_id, perm=perm)
428 428 else: # set for user group
429 429 member_name = UserGroup.get(member_id).users_group_name
430 430 if not check_perms or has_group_perm(member_name,
431 431 user=cur_user):
432 432 _set_perm_group(obj, users_group=member_id, perm=perm)
433 433
434 434 changes['updated'].append(
435 435 {'change_obj': change_obj, 'type': member_type,
436 436 'id': member_id, 'name': member_name, 'new_perm': perm})
437 437
438 438 # set new permissions
439 439 for member_id, perm, member_type in perm_additions:
440 440 member_id = int(member_id)
441 441 if member_type == 'user':
442 442 member_name = User.get(member_id).username
443 443 _set_perm_user(obj, user=member_id, perm=perm)
444 444 else: # set for user group
445 445 # check if we have permissions to alter this usergroup
446 446 member_name = UserGroup.get(member_id).users_group_name
447 447 if not check_perms or has_group_perm(member_name,
448 448 user=cur_user):
449 449 _set_perm_group(obj, users_group=member_id, perm=perm)
450 450
451 451 changes['added'].append(
452 452 {'change_obj': change_obj, 'type': member_type,
453 453 'id': member_id, 'name': member_name, 'new_perm': perm})
454 454
455 455 # delete permissions
456 456 for member_id, perm, member_type in perm_deletions:
457 457 member_id = int(member_id)
458 458 if member_type == 'user':
459 459 member_name = User.get(member_id).username
460 460 _revoke_perm_user(obj, user=member_id)
461 461 else: # set for user group
462 462 # check if we have permissions to alter this usergroup
463 463 member_name = UserGroup.get(member_id).users_group_name
464 464 if not check_perms or has_group_perm(member_name,
465 465 user=cur_user):
466 466 _revoke_perm_group(obj, user_group=member_id)
467 467
468 468 changes['deleted'].append(
469 469 {'change_obj': change_obj, 'type': member_type,
470 470 'id': member_id, 'name': member_name, 'new_perm': perm})
471 471
472 472 # if it's not recursive call for all,repos,groups
473 473 # break the loop and don't proceed with other changes
474 474 if recursive not in ['all', 'repos', 'groups']:
475 475 break
476 476
477 477 return changes
478 478
479 479 def update(self, repo_group, form_data):
480 480 try:
481 481 repo_group = self._get_repo_group(repo_group)
482 482 old_path = repo_group.full_path
483 483
484 484 # change properties
485 485 if 'group_description' in form_data:
486 486 repo_group.group_description = form_data['group_description']
487 487
488 488 if 'enable_locking' in form_data:
489 489 repo_group.enable_locking = form_data['enable_locking']
490 490
491 491 if 'group_parent_id' in form_data:
492 492 parent_group = (
493 493 self._get_repo_group(form_data['group_parent_id']))
494 494 repo_group.group_parent_id = (
495 495 parent_group.group_id if parent_group else None)
496 496 repo_group.parent_group = parent_group
497 497
498 498 # mikhail: to update the full_path, we have to explicitly
499 499 # update group_name
500 500 group_name = form_data.get('group_name', repo_group.name)
501 501 repo_group.group_name = repo_group.get_new_name(group_name)
502 502
503 503 new_path = repo_group.full_path
504 504
505 505 if 'user' in form_data:
506 506 repo_group.user = User.get_by_username(form_data['user'])
507 507 repo_group.updated_on = datetime.datetime.now()
508 508 self.sa.add(repo_group)
509 509
510 510 # iterate over all members of this groups and do fixes
511 511 # set locking if given
512 512 # if obj is a repoGroup also fix the name of the group according
513 513 # to the parent
514 514 # if obj is a Repo fix it's name
515 515 # this can be potentially heavy operation
516 516 for obj in repo_group.recursive_groups_and_repos():
517 517 # set the value from it's parent
518 518 obj.enable_locking = repo_group.enable_locking
519 519 if isinstance(obj, RepoGroup):
520 520 new_name = obj.get_new_name(obj.name)
521 521 log.debug('Fixing group %s to new name %s',
522 522 obj.group_name, new_name)
523 523 obj.group_name = new_name
524 524 obj.updated_on = datetime.datetime.now()
525 525 elif isinstance(obj, Repository):
526 526 # we need to get all repositories from this new group and
527 527 # rename them accordingly to new group path
528 528 new_name = obj.get_new_name(obj.just_name)
529 529 log.debug('Fixing repo %s to new name %s',
530 530 obj.repo_name, new_name)
531 531 obj.repo_name = new_name
532 532 obj.updated_on = datetime.datetime.now()
533 533 self.sa.add(obj)
534 534
535 535 self._rename_group(old_path, new_path)
536 536
537 537 # Trigger update event.
538 538 events.trigger(events.RepoGroupUpdateEvent(repo_group))
539 539
540 540 return repo_group
541 541 except Exception:
542 542 log.error(traceback.format_exc())
543 543 raise
544 544
545 545 def delete(self, repo_group, force_delete=False, fs_remove=True):
546 546 repo_group = self._get_repo_group(repo_group)
547 547 if not repo_group:
548 548 return False
549 549 try:
550 550 self.sa.delete(repo_group)
551 551 if fs_remove:
552 552 self._delete_filesystem_group(repo_group, force_delete)
553 553 else:
554 554 log.debug('skipping removal from filesystem')
555 555
556 556 # Trigger delete event.
557 557 events.trigger(events.RepoGroupDeleteEvent(repo_group))
558 558 return True
559 559
560 560 except Exception:
561 561 log.error('Error removing repo_group %s', repo_group)
562 562 raise
563 563
564 564 def grant_user_permission(self, repo_group, user, perm):
565 565 """
566 566 Grant permission for user on given repository group, or update
567 567 existing one if found
568 568
569 569 :param repo_group: Instance of RepoGroup, repositories_group_id,
570 570 or repositories_group name
571 571 :param user: Instance of User, user_id or username
572 572 :param perm: Instance of Permission, or permission_name
573 573 """
574 574
575 575 repo_group = self._get_repo_group(repo_group)
576 576 user = self._get_user(user)
577 577 permission = self._get_perm(perm)
578 578
579 579 # check if we have that permission already
580 580 obj = self.sa.query(UserRepoGroupToPerm)\
581 581 .filter(UserRepoGroupToPerm.user == user)\
582 582 .filter(UserRepoGroupToPerm.group == repo_group)\
583 583 .scalar()
584 584 if obj is None:
585 585 # create new !
586 586 obj = UserRepoGroupToPerm()
587 587 obj.group = repo_group
588 588 obj.user = user
589 589 obj.permission = permission
590 590 self.sa.add(obj)
591 591 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
592 592 action_logger_generic(
593 593 'granted permission: {} to user: {} on repogroup: {}'.format(
594 594 perm, user, repo_group), namespace='security.repogroup')
595 595 return obj
596 596
597 597 def revoke_user_permission(self, repo_group, user):
598 598 """
599 599 Revoke permission for user on given repository group
600 600
601 601 :param repo_group: Instance of RepoGroup, repositories_group_id,
602 602 or repositories_group name
603 603 :param user: Instance of User, user_id or username
604 604 """
605 605
606 606 repo_group = self._get_repo_group(repo_group)
607 607 user = self._get_user(user)
608 608
609 609 obj = self.sa.query(UserRepoGroupToPerm)\
610 610 .filter(UserRepoGroupToPerm.user == user)\
611 611 .filter(UserRepoGroupToPerm.group == repo_group)\
612 612 .scalar()
613 613 if obj:
614 614 self.sa.delete(obj)
615 615 log.debug('Revoked perm on %s on %s', repo_group, user)
616 616 action_logger_generic(
617 617 'revoked permission from user: {} on repogroup: {}'.format(
618 618 user, repo_group), namespace='security.repogroup')
619 619
620 620 def grant_user_group_permission(self, repo_group, group_name, perm):
621 621 """
622 622 Grant permission for user group on given repository group, or update
623 623 existing one if found
624 624
625 625 :param repo_group: Instance of RepoGroup, repositories_group_id,
626 626 or repositories_group name
627 627 :param group_name: Instance of UserGroup, users_group_id,
628 628 or user group name
629 629 :param perm: Instance of Permission, or permission_name
630 630 """
631 631 repo_group = self._get_repo_group(repo_group)
632 632 group_name = self._get_user_group(group_name)
633 633 permission = self._get_perm(perm)
634 634
635 635 # check if we have that permission already
636 636 obj = self.sa.query(UserGroupRepoGroupToPerm)\
637 637 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
638 638 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
639 639 .scalar()
640 640
641 641 if obj is None:
642 642 # create new
643 643 obj = UserGroupRepoGroupToPerm()
644 644
645 645 obj.group = repo_group
646 646 obj.users_group = group_name
647 647 obj.permission = permission
648 648 self.sa.add(obj)
649 649 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
650 650 action_logger_generic(
651 651 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
652 652 perm, group_name, repo_group), namespace='security.repogroup')
653 653 return obj
654 654
655 655 def revoke_user_group_permission(self, repo_group, group_name):
656 656 """
657 657 Revoke permission for user group on given repository group
658 658
659 659 :param repo_group: Instance of RepoGroup, repositories_group_id,
660 660 or repositories_group name
661 661 :param group_name: Instance of UserGroup, users_group_id,
662 662 or user group name
663 663 """
664 664 repo_group = self._get_repo_group(repo_group)
665 665 group_name = self._get_user_group(group_name)
666 666
667 667 obj = self.sa.query(UserGroupRepoGroupToPerm)\
668 668 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
669 669 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
670 670 .scalar()
671 671 if obj:
672 672 self.sa.delete(obj)
673 673 log.debug('Revoked perm to %s on %s', repo_group, group_name)
674 674 action_logger_generic(
675 675 'revoked permission from usergroup: {} on repogroup: {}'.format(
676 676 group_name, repo_group), namespace='security.repogroup')
677 677
678 678 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
679 679 super_user_actions=False):
680 680
681 681 from pyramid.threadlocal import get_current_request
682 682 _render = get_current_request().get_partial_renderer(
683 'data_table/_dt_elements.mako')
683 'rhodecode:templates/data_table/_dt_elements.mako')
684 684 c = _render.get_call_context()
685 685 h = _render.get_helpers()
686 686
687 687 def quick_menu(repo_group_name):
688 688 return _render('quick_repo_group_menu', repo_group_name)
689 689
690 690 def repo_group_lnk(repo_group_name):
691 691 return _render('repo_group_name', repo_group_name)
692 692
693 693 def last_change(last_change):
694 694 if admin and isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
695 695 last_change = last_change + datetime.timedelta(seconds=
696 696 (datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
697 697 return _render("last_change", last_change)
698 698
699 699 def desc(desc, personal):
700 700 return _render(
701 701 'repo_group_desc', desc, personal, c.visual.stylify_metatags)
702 702
703 703 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
704 704 return _render(
705 705 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
706 706
707 707 def repo_group_name(repo_group_name, children_groups):
708 708 return _render("repo_group_name", repo_group_name, children_groups)
709 709
710 710 def user_profile(username):
711 711 return _render('user_profile', username)
712 712
713 713 repo_group_data = []
714 714 for group in repo_group_list:
715 715
716 716 row = {
717 717 "menu": quick_menu(group.group_name),
718 718 "name": repo_group_lnk(group.group_name),
719 719 "name_raw": group.group_name,
720 720 "last_change": last_change(group.last_db_change),
721 721 "last_change_raw": datetime_to_time(group.last_db_change),
722 722 "desc": desc(group.description_safe, group.personal),
723 723 "top_level_repos": 0,
724 724 "owner": user_profile(group.user.username)
725 725 }
726 726 if admin:
727 727 repo_count = group.repositories.count()
728 728 children_groups = map(
729 729 h.safe_unicode,
730 730 itertools.chain((g.name for g in group.parents),
731 731 (x.name for x in [group])))
732 732 row.update({
733 733 "action": repo_group_actions(
734 734 group.group_id, group.group_name, repo_count),
735 735 "top_level_repos": repo_count,
736 736 "name": repo_group_name(group.group_name, children_groups),
737 737
738 738 })
739 739 repo_group_data.append(row)
740 740
741 741 return repo_group_data
742 742
743 743 def _get_defaults(self, repo_group_name):
744 744 repo_group = RepoGroup.get_by_group_name(repo_group_name)
745 745
746 746 if repo_group is None:
747 747 return None
748 748
749 749 defaults = repo_group.get_dict()
750 750 defaults['repo_group_name'] = repo_group.name
751 751 defaults['repo_group_description'] = repo_group.group_description
752 752 defaults['repo_group_enable_locking'] = repo_group.enable_locking
753 753
754 754 # we use -1 as this is how in HTML, we mark an empty group
755 755 defaults['repo_group'] = defaults['group_parent_id'] or -1
756 756
757 757 # fill owner
758 758 if repo_group.user:
759 759 defaults.update({'user': repo_group.user.username})
760 760 else:
761 761 replacement_user = User.get_first_super_admin().username
762 762 defaults.update({'user': replacement_user})
763 763
764 764 return defaults
General Comments 0
You need to be logged in to leave comments. Login now