##// END OF EJS Templates
pull-request-reviewers: added option to add reviewers by picking an user group for pull requests....
marcink -
r1678:7e2afc04 default
parent child Browse files
Show More
@@ -1,236 +1,241 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 re
22 22 import logging
23 23
24 24 from pyramid.view import view_config
25 25
26 26 from rhodecode.apps._base import BaseAppView
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.auth import LoginRequired, NotAnonymous
29 29 from rhodecode.lib.index import searcher_from_config
30 30 from rhodecode.lib.utils2 import safe_unicode, str2bool
31 31 from rhodecode.model.db import func, Repository, RepoGroup
32 32 from rhodecode.model.repo import RepoModel
33 33 from rhodecode.model.scm import ScmModel
34 34 from rhodecode.model.user import UserModel
35 35 from rhodecode.model.user_group import UserGroupModel
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class HomeView(BaseAppView):
41 41
42 42 def load_default_context(self):
43 43 c = self._get_local_tmpl_context()
44 44 c.user = c.auth_user.get_instance()
45 45 self._register_global_c(c)
46 46 return c
47 47
48 48 @LoginRequired()
49 49 @view_config(
50 50 route_name='user_autocomplete_data', request_method='GET',
51 51 renderer='json_ext', xhr=True)
52 52 def user_autocomplete_data(self):
53 53 query = self.request.GET.get('query')
54 54 active = str2bool(self.request.GET.get('active') or True)
55 55 include_groups = str2bool(self.request.GET.get('user_groups'))
56 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
56 57
57 58 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
58 59 query, active, include_groups)
59 60
60 61 _users = UserModel().get_users(
61 62 name_contains=query, only_active=active)
62 63
63 64 if include_groups:
64 65 # extend with user groups
65 66 _user_groups = UserGroupModel().get_user_groups(
66 name_contains=query, only_active=active)
67 name_contains=query, only_active=active,
68 expand_groups=expand_groups)
67 69 _users = _users + _user_groups
68 70
69 71 return {'suggestions': _users}
70 72
71 73 @LoginRequired()
72 74 @NotAnonymous()
73 75 @view_config(
74 76 route_name='user_group_autocomplete_data', request_method='GET',
75 77 renderer='json_ext', xhr=True)
76 78 def user_group_autocomplete_data(self):
77 79 query = self.request.GET.get('query')
78 80 active = str2bool(self.request.GET.get('active') or True)
81 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
82
79 83 log.debug('generating user group list, query:%s, active:%s',
80 84 query, active)
81 85
82 86 _user_groups = UserGroupModel().get_user_groups(
83 name_contains=query, only_active=active)
87 name_contains=query, only_active=active,
88 expand_groups=expand_groups)
84 89 _user_groups = _user_groups
85 90
86 91 return {'suggestions': _user_groups}
87 92
88 93 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
89 94 query = Repository.query()\
90 95 .order_by(func.length(Repository.repo_name))\
91 96 .order_by(Repository.repo_name)
92 97
93 98 if repo_type:
94 99 query = query.filter(Repository.repo_type == repo_type)
95 100
96 101 if name_contains:
97 102 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
98 103 query = query.filter(
99 104 Repository.repo_name.ilike(ilike_expression))
100 105 query = query.limit(limit)
101 106
102 107 all_repos = query.all()
103 108 # permission checks are inside this function
104 109 repo_iter = ScmModel().get_repos(all_repos)
105 110 return [
106 111 {
107 112 'id': obj['name'],
108 113 'text': obj['name'],
109 114 'type': 'repo',
110 115 'obj': obj['dbrepo'],
111 116 'url': h.url('summary_home', repo_name=obj['name'])
112 117 }
113 118 for obj in repo_iter]
114 119
115 120 def _get_repo_group_list(self, name_contains=None, limit=20):
116 121 query = RepoGroup.query()\
117 122 .order_by(func.length(RepoGroup.group_name))\
118 123 .order_by(RepoGroup.group_name)
119 124
120 125 if name_contains:
121 126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
122 127 query = query.filter(
123 128 RepoGroup.group_name.ilike(ilike_expression))
124 129 query = query.limit(limit)
125 130
126 131 all_groups = query.all()
127 132 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
128 133 return [
129 134 {
130 135 'id': obj.group_name,
131 136 'text': obj.group_name,
132 137 'type': 'group',
133 138 'obj': {},
134 139 'url': h.url('repo_group_home', group_name=obj.group_name)
135 140 }
136 141 for obj in repo_groups_iter]
137 142
138 143 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
139 144 if not hash_starts_with or len(hash_starts_with) < 3:
140 145 return []
141 146
142 147 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
143 148
144 149 if len(commit_hashes) != 1:
145 150 return []
146 151
147 152 commit_hash_prefix = commit_hashes[0]
148 153
149 154 searcher = searcher_from_config(self.request.registry.settings)
150 155 result = searcher.search(
151 156 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
152 157 raise_on_exc=False)
153 158
154 159 return [
155 160 {
156 161 'id': entry['commit_id'],
157 162 'text': entry['commit_id'],
158 163 'type': 'commit',
159 164 'obj': {'repo': entry['repository']},
160 165 'url': h.url('changeset_home',
161 166 repo_name=entry['repository'],
162 167 revision=entry['commit_id'])
163 168 }
164 169 for entry in result['results']]
165 170
166 171 @LoginRequired()
167 172 @view_config(
168 173 route_name='repo_list_data', request_method='GET',
169 174 renderer='json_ext', xhr=True)
170 175 def repo_list_data(self):
171 176 _ = self.request.translate
172 177
173 178 query = self.request.GET.get('query')
174 179 repo_type = self.request.GET.get('repo_type')
175 180 log.debug('generating repo list, query:%s, repo_type:%s',
176 181 query, repo_type)
177 182
178 183 res = []
179 184 repos = self._get_repo_list(query, repo_type=repo_type)
180 185 if repos:
181 186 res.append({
182 187 'text': _('Repositories'),
183 188 'children': repos
184 189 })
185 190
186 191 data = {
187 192 'more': False,
188 193 'results': res
189 194 }
190 195 return data
191 196
192 197 @LoginRequired()
193 198 @view_config(
194 199 route_name='goto_switcher_data', request_method='GET',
195 200 renderer='json_ext', xhr=True)
196 201 def goto_switcher_data(self):
197 202 c = self.load_default_context()
198 203
199 204 _ = self.request.translate
200 205
201 206 query = self.request.GET.get('query')
202 207 log.debug('generating goto switcher list, query %s', query)
203 208
204 209 res = []
205 210 repo_groups = self._get_repo_group_list(query)
206 211 if repo_groups:
207 212 res.append({
208 213 'text': _('Groups'),
209 214 'children': repo_groups
210 215 })
211 216
212 217 repos = self._get_repo_list(query)
213 218 if repos:
214 219 res.append({
215 220 'text': _('Repositories'),
216 221 'children': repos
217 222 })
218 223
219 224 commits = self._get_hash_commit_list(c.auth_user, query)
220 225 if commits:
221 226 unique_repos = {}
222 227 for commit in commits:
223 228 unique_repos.setdefault(commit['obj']['repo'], []
224 229 ).append(commit)
225 230
226 231 for repo in unique_repos:
227 232 res.append({
228 233 'text': _('Commits in %(repo)s') % {'repo': repo},
229 234 'children': unique_repos[repo]
230 235 })
231 236
232 237 data = {
233 238 'more': False,
234 239 'results': res
235 240 }
236 241 return data
@@ -1,897 +1,900 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 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 35 from rhodecode import events
36 36 from rhodecode.lib.user_log_filter import user_log_filter
37 37 from rhodecode.lib.utils2 import (
38 38 safe_unicode, get_current_rhodecode_user, action_logger_generic,
39 39 AttributeDict, str2bool)
40 40 from rhodecode.lib.caching_query import FromCache
41 41 from rhodecode.model import BaseModel
42 42 from rhodecode.model.auth_token import AuthTokenModel
43 43 from rhodecode.model.db import (
44 44 or_, joinedload, User, UserToPerm, UserEmailMap, UserIpMap, UserLog)
45 45 from rhodecode.lib.exceptions import (
46 46 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
47 47 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.repo_group import RepoGroupModel
50 50
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class UserModel(BaseModel):
56 56 cls = User
57 57
58 58 def get(self, user_id, cache=False):
59 59 user = self.sa.query(User)
60 60 if cache:
61 61 user = user.options(FromCache("sql_cache_short",
62 62 "get_user_%s" % user_id))
63 63 return user.get(user_id)
64 64
65 65 def get_user(self, user):
66 66 return self._get_user(user)
67 67
68 def _serialize_user(self, user):
69 import rhodecode.lib.helpers as h
70
71 return {
72 'id': user.user_id,
73 'first_name': user.name,
74 'last_name': user.lastname,
75 'username': user.username,
76 'email': user.email,
77 'icon_link': h.gravatar_url(user.email, 30),
78 'value_display': h.person(user),
79 'value': user.username,
80 'value_type': 'user',
81 'active': user.active,
82 }
83
68 84 def get_users(self, name_contains=None, limit=20, only_active=True):
69 import rhodecode.lib.helpers as h
70 85
71 86 query = self.sa.query(User)
72 87 if only_active:
73 88 query = query.filter(User.active == true())
74 89
75 90 if name_contains:
76 91 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
77 92 query = query.filter(
78 93 or_(
79 94 User.name.ilike(ilike_expression),
80 95 User.lastname.ilike(ilike_expression),
81 96 User.username.ilike(ilike_expression)
82 97 )
83 98 )
84 99 query = query.limit(limit)
85 100 users = query.all()
86 101
87 102 _users = [
88 {
89 'id': user.user_id,
90 'first_name': user.name,
91 'last_name': user.lastname,
92 'username': user.username,
93 'email': user.email,
94 'icon_link': h.gravatar_url(user.email, 30),
95 'value_display': h.person(user),
96 'value': user.username,
97 'value_type': 'user',
98 'active': user.active,
99 }
100 for user in users
103 self._serialize_user(user) for user in users
101 104 ]
102 105 return _users
103 106
104 107 def get_by_username(self, username, cache=False, case_insensitive=False):
105 108
106 109 if case_insensitive:
107 110 user = self.sa.query(User).filter(User.username.ilike(username))
108 111 else:
109 112 user = self.sa.query(User)\
110 113 .filter(User.username == username)
111 114 if cache:
112 115 user = user.options(FromCache("sql_cache_short",
113 116 "get_user_%s" % username))
114 117 return user.scalar()
115 118
116 119 def get_by_email(self, email, cache=False, case_insensitive=False):
117 120 return User.get_by_email(email, case_insensitive, cache)
118 121
119 122 def get_by_auth_token(self, auth_token, cache=False):
120 123 return User.get_by_auth_token(auth_token, cache)
121 124
122 125 def get_active_user_count(self, cache=False):
123 126 return User.query().filter(
124 127 User.active == True).filter(
125 128 User.username != User.DEFAULT_USER).count()
126 129
127 130 def create(self, form_data, cur_user=None):
128 131 if not cur_user:
129 132 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
130 133
131 134 user_data = {
132 135 'username': form_data['username'],
133 136 'password': form_data['password'],
134 137 'email': form_data['email'],
135 138 'firstname': form_data['firstname'],
136 139 'lastname': form_data['lastname'],
137 140 'active': form_data['active'],
138 141 'extern_type': form_data['extern_type'],
139 142 'extern_name': form_data['extern_name'],
140 143 'admin': False,
141 144 'cur_user': cur_user
142 145 }
143 146
144 147 if 'create_repo_group' in form_data:
145 148 user_data['create_repo_group'] = str2bool(
146 149 form_data.get('create_repo_group'))
147 150
148 151 try:
149 152 if form_data.get('password_change'):
150 153 user_data['force_password_change'] = True
151 154 return UserModel().create_or_update(**user_data)
152 155 except Exception:
153 156 log.error(traceback.format_exc())
154 157 raise
155 158
156 159 def update_user(self, user, skip_attrs=None, **kwargs):
157 160 from rhodecode.lib.auth import get_crypt_password
158 161
159 162 user = self._get_user(user)
160 163 if user.username == User.DEFAULT_USER:
161 164 raise DefaultUserException(
162 165 _("You can't Edit this user since it's"
163 166 " crucial for entire application"))
164 167
165 168 # first store only defaults
166 169 user_attrs = {
167 170 'updating_user_id': user.user_id,
168 171 'username': user.username,
169 172 'password': user.password,
170 173 'email': user.email,
171 174 'firstname': user.name,
172 175 'lastname': user.lastname,
173 176 'active': user.active,
174 177 'admin': user.admin,
175 178 'extern_name': user.extern_name,
176 179 'extern_type': user.extern_type,
177 180 'language': user.user_data.get('language')
178 181 }
179 182
180 183 # in case there's new_password, that comes from form, use it to
181 184 # store password
182 185 if kwargs.get('new_password'):
183 186 kwargs['password'] = kwargs['new_password']
184 187
185 188 # cleanups, my_account password change form
186 189 kwargs.pop('current_password', None)
187 190 kwargs.pop('new_password', None)
188 191
189 192 # cleanups, user edit password change form
190 193 kwargs.pop('password_confirmation', None)
191 194 kwargs.pop('password_change', None)
192 195
193 196 # create repo group on user creation
194 197 kwargs.pop('create_repo_group', None)
195 198
196 199 # legacy forms send name, which is the firstname
197 200 firstname = kwargs.pop('name', None)
198 201 if firstname:
199 202 kwargs['firstname'] = firstname
200 203
201 204 for k, v in kwargs.items():
202 205 # skip if we don't want to update this
203 206 if skip_attrs and k in skip_attrs:
204 207 continue
205 208
206 209 user_attrs[k] = v
207 210
208 211 try:
209 212 return self.create_or_update(**user_attrs)
210 213 except Exception:
211 214 log.error(traceback.format_exc())
212 215 raise
213 216
214 217 def create_or_update(
215 218 self, username, password, email, firstname='', lastname='',
216 219 active=True, admin=False, extern_type=None, extern_name=None,
217 220 cur_user=None, plugin=None, force_password_change=False,
218 221 allow_to_create_user=True, create_repo_group=None,
219 222 updating_user_id=None, language=None, strict_creation_check=True):
220 223 """
221 224 Creates a new instance if not found, or updates current one
222 225
223 226 :param username:
224 227 :param password:
225 228 :param email:
226 229 :param firstname:
227 230 :param lastname:
228 231 :param active:
229 232 :param admin:
230 233 :param extern_type:
231 234 :param extern_name:
232 235 :param cur_user:
233 236 :param plugin: optional plugin this method was called from
234 237 :param force_password_change: toggles new or existing user flag
235 238 for password change
236 239 :param allow_to_create_user: Defines if the method can actually create
237 240 new users
238 241 :param create_repo_group: Defines if the method should also
239 242 create an repo group with user name, and owner
240 243 :param updating_user_id: if we set it up this is the user we want to
241 244 update this allows to editing username.
242 245 :param language: language of user from interface.
243 246
244 247 :returns: new User object with injected `is_new_user` attribute.
245 248 """
246 249 if not cur_user:
247 250 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
248 251
249 252 from rhodecode.lib.auth import (
250 253 get_crypt_password, check_password, generate_auth_token)
251 254 from rhodecode.lib.hooks_base import (
252 255 log_create_user, check_allowed_create_user)
253 256
254 257 def _password_change(new_user, password):
255 258 # empty password
256 259 if not new_user.password:
257 260 return False
258 261
259 262 # password check is only needed for RhodeCode internal auth calls
260 263 # in case it's a plugin we don't care
261 264 if not plugin:
262 265
263 266 # first check if we gave crypted password back, and if it
264 267 # matches it's not password change
265 268 if new_user.password == password:
266 269 return False
267 270
268 271 password_match = check_password(password, new_user.password)
269 272 if not password_match:
270 273 return True
271 274
272 275 return False
273 276
274 277 # read settings on default personal repo group creation
275 278 if create_repo_group is None:
276 279 default_create_repo_group = RepoGroupModel()\
277 280 .get_default_create_personal_repo_group()
278 281 create_repo_group = default_create_repo_group
279 282
280 283 user_data = {
281 284 'username': username,
282 285 'password': password,
283 286 'email': email,
284 287 'firstname': firstname,
285 288 'lastname': lastname,
286 289 'active': active,
287 290 'admin': admin
288 291 }
289 292
290 293 if updating_user_id:
291 294 log.debug('Checking for existing account in RhodeCode '
292 295 'database with user_id `%s` ' % (updating_user_id,))
293 296 user = User.get(updating_user_id)
294 297 else:
295 298 log.debug('Checking for existing account in RhodeCode '
296 299 'database with username `%s` ' % (username,))
297 300 user = User.get_by_username(username, case_insensitive=True)
298 301
299 302 if user is None:
300 303 # we check internal flag if this method is actually allowed to
301 304 # create new user
302 305 if not allow_to_create_user:
303 306 msg = ('Method wants to create new user, but it is not '
304 307 'allowed to do so')
305 308 log.warning(msg)
306 309 raise NotAllowedToCreateUserError(msg)
307 310
308 311 log.debug('Creating new user %s', username)
309 312
310 313 # only if we create user that is active
311 314 new_active_user = active
312 315 if new_active_user and strict_creation_check:
313 316 # raises UserCreationError if it's not allowed for any reason to
314 317 # create new active user, this also executes pre-create hooks
315 318 check_allowed_create_user(user_data, cur_user, strict_check=True)
316 319 events.trigger(events.UserPreCreate(user_data))
317 320 new_user = User()
318 321 edit = False
319 322 else:
320 323 log.debug('updating user %s', username)
321 324 events.trigger(events.UserPreUpdate(user, user_data))
322 325 new_user = user
323 326 edit = True
324 327
325 328 # we're not allowed to edit default user
326 329 if user.username == User.DEFAULT_USER:
327 330 raise DefaultUserException(
328 331 _("You can't edit this user (`%(username)s`) since it's "
329 332 "crucial for entire application") % {'username': user.username})
330 333
331 334 # inject special attribute that will tell us if User is new or old
332 335 new_user.is_new_user = not edit
333 336 # for users that didn's specify auth type, we use RhodeCode built in
334 337 from rhodecode.authentication.plugins import auth_rhodecode
335 338 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
336 339 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
337 340
338 341 try:
339 342 new_user.username = username
340 343 new_user.admin = admin
341 344 new_user.email = email
342 345 new_user.active = active
343 346 new_user.extern_name = safe_unicode(extern_name)
344 347 new_user.extern_type = safe_unicode(extern_type)
345 348 new_user.name = firstname
346 349 new_user.lastname = lastname
347 350
348 351 # set password only if creating an user or password is changed
349 352 if not edit or _password_change(new_user, password):
350 353 reason = 'new password' if edit else 'new user'
351 354 log.debug('Updating password reason=>%s', reason)
352 355 new_user.password = get_crypt_password(password) if password else None
353 356
354 357 if force_password_change:
355 358 new_user.update_userdata(force_password_change=True)
356 359 if language:
357 360 new_user.update_userdata(language=language)
358 361 new_user.update_userdata(notification_status=True)
359 362
360 363 self.sa.add(new_user)
361 364
362 365 if not edit and create_repo_group:
363 366 RepoGroupModel().create_personal_repo_group(
364 367 new_user, commit_early=False)
365 368
366 369 if not edit:
367 370 # add the RSS token
368 371 AuthTokenModel().create(username,
369 372 description='Generated feed token',
370 373 role=AuthTokenModel.cls.ROLE_FEED)
371 374 log_create_user(created_by=cur_user, **new_user.get_dict())
372 375 events.trigger(events.UserPostCreate(user_data))
373 376 return new_user
374 377 except (DatabaseError,):
375 378 log.error(traceback.format_exc())
376 379 raise
377 380
378 381 def create_registration(self, form_data):
379 382 from rhodecode.model.notification import NotificationModel
380 383 from rhodecode.model.notification import EmailNotificationModel
381 384
382 385 try:
383 386 form_data['admin'] = False
384 387 form_data['extern_name'] = 'rhodecode'
385 388 form_data['extern_type'] = 'rhodecode'
386 389 new_user = self.create(form_data)
387 390
388 391 self.sa.add(new_user)
389 392 self.sa.flush()
390 393
391 394 user_data = new_user.get_dict()
392 395 kwargs = {
393 396 # use SQLALCHEMY safe dump of user data
394 397 'user': AttributeDict(user_data),
395 398 'date': datetime.datetime.now()
396 399 }
397 400 notification_type = EmailNotificationModel.TYPE_REGISTRATION
398 401 # pre-generate the subject for notification itself
399 402 (subject,
400 403 _h, _e, # we don't care about those
401 404 body_plaintext) = EmailNotificationModel().render_email(
402 405 notification_type, **kwargs)
403 406
404 407 # create notification objects, and emails
405 408 NotificationModel().create(
406 409 created_by=new_user,
407 410 notification_subject=subject,
408 411 notification_body=body_plaintext,
409 412 notification_type=notification_type,
410 413 recipients=None, # all admins
411 414 email_kwargs=kwargs,
412 415 )
413 416
414 417 return new_user
415 418 except Exception:
416 419 log.error(traceback.format_exc())
417 420 raise
418 421
419 422 def _handle_user_repos(self, username, repositories, handle_mode=None):
420 423 _superadmin = self.cls.get_first_super_admin()
421 424 left_overs = True
422 425
423 426 from rhodecode.model.repo import RepoModel
424 427
425 428 if handle_mode == 'detach':
426 429 for obj in repositories:
427 430 obj.user = _superadmin
428 431 # set description we know why we super admin now owns
429 432 # additional repositories that were orphaned !
430 433 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
431 434 self.sa.add(obj)
432 435 left_overs = False
433 436 elif handle_mode == 'delete':
434 437 for obj in repositories:
435 438 RepoModel().delete(obj, forks='detach')
436 439 left_overs = False
437 440
438 441 # if nothing is done we have left overs left
439 442 return left_overs
440 443
441 444 def _handle_user_repo_groups(self, username, repository_groups,
442 445 handle_mode=None):
443 446 _superadmin = self.cls.get_first_super_admin()
444 447 left_overs = True
445 448
446 449 from rhodecode.model.repo_group import RepoGroupModel
447 450
448 451 if handle_mode == 'detach':
449 452 for r in repository_groups:
450 453 r.user = _superadmin
451 454 # set description we know why we super admin now owns
452 455 # additional repositories that were orphaned !
453 456 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
454 457 self.sa.add(r)
455 458 left_overs = False
456 459 elif handle_mode == 'delete':
457 460 for r in repository_groups:
458 461 RepoGroupModel().delete(r)
459 462 left_overs = False
460 463
461 464 # if nothing is done we have left overs left
462 465 return left_overs
463 466
464 467 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
465 468 _superadmin = self.cls.get_first_super_admin()
466 469 left_overs = True
467 470
468 471 from rhodecode.model.user_group import UserGroupModel
469 472
470 473 if handle_mode == 'detach':
471 474 for r in user_groups:
472 475 for user_user_group_to_perm in r.user_user_group_to_perm:
473 476 if user_user_group_to_perm.user.username == username:
474 477 user_user_group_to_perm.user = _superadmin
475 478 r.user = _superadmin
476 479 # set description we know why we super admin now owns
477 480 # additional repositories that were orphaned !
478 481 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
479 482 self.sa.add(r)
480 483 left_overs = False
481 484 elif handle_mode == 'delete':
482 485 for r in user_groups:
483 486 UserGroupModel().delete(r)
484 487 left_overs = False
485 488
486 489 # if nothing is done we have left overs left
487 490 return left_overs
488 491
489 492 def delete(self, user, cur_user=None, handle_repos=None,
490 493 handle_repo_groups=None, handle_user_groups=None):
491 494 if not cur_user:
492 495 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
493 496 user = self._get_user(user)
494 497
495 498 try:
496 499 if user.username == User.DEFAULT_USER:
497 500 raise DefaultUserException(
498 501 _(u"You can't remove this user since it's"
499 502 u" crucial for entire application"))
500 503
501 504 left_overs = self._handle_user_repos(
502 505 user.username, user.repositories, handle_repos)
503 506 if left_overs and user.repositories:
504 507 repos = [x.repo_name for x in user.repositories]
505 508 raise UserOwnsReposException(
506 509 _(u'user "%s" still owns %s repositories and cannot be '
507 510 u'removed. Switch owners or remove those repositories:%s')
508 511 % (user.username, len(repos), ', '.join(repos)))
509 512
510 513 left_overs = self._handle_user_repo_groups(
511 514 user.username, user.repository_groups, handle_repo_groups)
512 515 if left_overs and user.repository_groups:
513 516 repo_groups = [x.group_name for x in user.repository_groups]
514 517 raise UserOwnsRepoGroupsException(
515 518 _(u'user "%s" still owns %s repository groups and cannot be '
516 519 u'removed. Switch owners or remove those repository groups:%s')
517 520 % (user.username, len(repo_groups), ', '.join(repo_groups)))
518 521
519 522 left_overs = self._handle_user_user_groups(
520 523 user.username, user.user_groups, handle_user_groups)
521 524 if left_overs and user.user_groups:
522 525 user_groups = [x.users_group_name for x in user.user_groups]
523 526 raise UserOwnsUserGroupsException(
524 527 _(u'user "%s" still owns %s user groups and cannot be '
525 528 u'removed. Switch owners or remove those user groups:%s')
526 529 % (user.username, len(user_groups), ', '.join(user_groups)))
527 530
528 531 # we might change the user data with detach/delete, make sure
529 532 # the object is marked as expired before actually deleting !
530 533 self.sa.expire(user)
531 534 self.sa.delete(user)
532 535 from rhodecode.lib.hooks_base import log_delete_user
533 536 log_delete_user(deleted_by=cur_user, **user.get_dict())
534 537 except Exception:
535 538 log.error(traceback.format_exc())
536 539 raise
537 540
538 541 def reset_password_link(self, data, pwd_reset_url):
539 542 from rhodecode.lib.celerylib import tasks, run_task
540 543 from rhodecode.model.notification import EmailNotificationModel
541 544 user_email = data['email']
542 545 try:
543 546 user = User.get_by_email(user_email)
544 547 if user:
545 548 log.debug('password reset user found %s', user)
546 549
547 550 email_kwargs = {
548 551 'password_reset_url': pwd_reset_url,
549 552 'user': user,
550 553 'email': user_email,
551 554 'date': datetime.datetime.now()
552 555 }
553 556
554 557 (subject, headers, email_body,
555 558 email_body_plaintext) = EmailNotificationModel().render_email(
556 559 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
557 560
558 561 recipients = [user_email]
559 562
560 563 action_logger_generic(
561 564 'sending password reset email to user: {}'.format(
562 565 user), namespace='security.password_reset')
563 566
564 567 run_task(tasks.send_email, recipients, subject,
565 568 email_body_plaintext, email_body)
566 569
567 570 else:
568 571 log.debug("password reset email %s not found", user_email)
569 572 except Exception:
570 573 log.error(traceback.format_exc())
571 574 return False
572 575
573 576 return True
574 577
575 578 def reset_password(self, data):
576 579 from rhodecode.lib.celerylib import tasks, run_task
577 580 from rhodecode.model.notification import EmailNotificationModel
578 581 from rhodecode.lib import auth
579 582 user_email = data['email']
580 583 pre_db = True
581 584 try:
582 585 user = User.get_by_email(user_email)
583 586 new_passwd = auth.PasswordGenerator().gen_password(
584 587 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
585 588 if user:
586 589 user.password = auth.get_crypt_password(new_passwd)
587 590 # also force this user to reset his password !
588 591 user.update_userdata(force_password_change=True)
589 592
590 593 Session().add(user)
591 594
592 595 # now delete the token in question
593 596 UserApiKeys = AuthTokenModel.cls
594 597 UserApiKeys().query().filter(
595 598 UserApiKeys.api_key == data['token']).delete()
596 599
597 600 Session().commit()
598 601 log.info('successfully reset password for `%s`', user_email)
599 602
600 603 if new_passwd is None:
601 604 raise Exception('unable to generate new password')
602 605
603 606 pre_db = False
604 607
605 608 email_kwargs = {
606 609 'new_password': new_passwd,
607 610 'user': user,
608 611 'email': user_email,
609 612 'date': datetime.datetime.now()
610 613 }
611 614
612 615 (subject, headers, email_body,
613 616 email_body_plaintext) = EmailNotificationModel().render_email(
614 617 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
615 618 **email_kwargs)
616 619
617 620 recipients = [user_email]
618 621
619 622 action_logger_generic(
620 623 'sent new password to user: {} with email: {}'.format(
621 624 user, user_email), namespace='security.password_reset')
622 625
623 626 run_task(tasks.send_email, recipients, subject,
624 627 email_body_plaintext, email_body)
625 628
626 629 except Exception:
627 630 log.error('Failed to update user password')
628 631 log.error(traceback.format_exc())
629 632 if pre_db:
630 633 # we rollback only if local db stuff fails. If it goes into
631 634 # run_task, we're pass rollback state this wouldn't work then
632 635 Session().rollback()
633 636
634 637 return True
635 638
636 639 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
637 640 """
638 641 Fetches auth_user by user_id,or api_key if present.
639 642 Fills auth_user attributes with those taken from database.
640 643 Additionally set's is_authenitated if lookup fails
641 644 present in database
642 645
643 646 :param auth_user: instance of user to set attributes
644 647 :param user_id: user id to fetch by
645 648 :param api_key: api key to fetch by
646 649 :param username: username to fetch by
647 650 """
648 651 if user_id is None and api_key is None and username is None:
649 652 raise Exception('You need to pass user_id, api_key or username')
650 653
651 654 log.debug(
652 655 'doing fill data based on: user_id:%s api_key:%s username:%s',
653 656 user_id, api_key, username)
654 657 try:
655 658 dbuser = None
656 659 if user_id:
657 660 dbuser = self.get(user_id)
658 661 elif api_key:
659 662 dbuser = self.get_by_auth_token(api_key)
660 663 elif username:
661 664 dbuser = self.get_by_username(username)
662 665
663 666 if not dbuser:
664 667 log.warning(
665 668 'Unable to lookup user by id:%s api_key:%s username:%s',
666 669 user_id, api_key, username)
667 670 return False
668 671 if not dbuser.active:
669 672 log.debug('User `%s` is inactive, skipping fill data', username)
670 673 return False
671 674
672 675 log.debug('filling user:%s data', dbuser)
673 676
674 677 # TODO: johbo: Think about this and find a clean solution
675 678 user_data = dbuser.get_dict()
676 679 user_data.update(dbuser.get_api_data(include_secrets=True))
677 680
678 681 for k, v in user_data.iteritems():
679 682 # properties of auth user we dont update
680 683 if k not in ['auth_tokens', 'permissions']:
681 684 setattr(auth_user, k, v)
682 685
683 686 # few extras
684 687 setattr(auth_user, 'feed_token', dbuser.feed_token)
685 688 except Exception:
686 689 log.error(traceback.format_exc())
687 690 auth_user.is_authenticated = False
688 691 return False
689 692
690 693 return True
691 694
692 695 def has_perm(self, user, perm):
693 696 perm = self._get_perm(perm)
694 697 user = self._get_user(user)
695 698
696 699 return UserToPerm.query().filter(UserToPerm.user == user)\
697 700 .filter(UserToPerm.permission == perm).scalar() is not None
698 701
699 702 def grant_perm(self, user, perm):
700 703 """
701 704 Grant user global permissions
702 705
703 706 :param user:
704 707 :param perm:
705 708 """
706 709 user = self._get_user(user)
707 710 perm = self._get_perm(perm)
708 711 # if this permission is already granted skip it
709 712 _perm = UserToPerm.query()\
710 713 .filter(UserToPerm.user == user)\
711 714 .filter(UserToPerm.permission == perm)\
712 715 .scalar()
713 716 if _perm:
714 717 return
715 718 new = UserToPerm()
716 719 new.user = user
717 720 new.permission = perm
718 721 self.sa.add(new)
719 722 return new
720 723
721 724 def revoke_perm(self, user, perm):
722 725 """
723 726 Revoke users global permissions
724 727
725 728 :param user:
726 729 :param perm:
727 730 """
728 731 user = self._get_user(user)
729 732 perm = self._get_perm(perm)
730 733
731 734 obj = UserToPerm.query()\
732 735 .filter(UserToPerm.user == user)\
733 736 .filter(UserToPerm.permission == perm)\
734 737 .scalar()
735 738 if obj:
736 739 self.sa.delete(obj)
737 740
738 741 def add_extra_email(self, user, email):
739 742 """
740 743 Adds email address to UserEmailMap
741 744
742 745 :param user:
743 746 :param email:
744 747 """
745 748 from rhodecode.model import forms
746 749 form = forms.UserExtraEmailForm()()
747 750 data = form.to_python({'email': email})
748 751 user = self._get_user(user)
749 752
750 753 obj = UserEmailMap()
751 754 obj.user = user
752 755 obj.email = data['email']
753 756 self.sa.add(obj)
754 757 return obj
755 758
756 759 def delete_extra_email(self, user, email_id):
757 760 """
758 761 Removes email address from UserEmailMap
759 762
760 763 :param user:
761 764 :param email_id:
762 765 """
763 766 user = self._get_user(user)
764 767 obj = UserEmailMap.query().get(email_id)
765 768 if obj:
766 769 self.sa.delete(obj)
767 770
768 771 def parse_ip_range(self, ip_range):
769 772 ip_list = []
770 773 def make_unique(value):
771 774 seen = []
772 775 return [c for c in value if not (c in seen or seen.append(c))]
773 776
774 777 # firsts split by commas
775 778 for ip_range in ip_range.split(','):
776 779 if not ip_range:
777 780 continue
778 781 ip_range = ip_range.strip()
779 782 if '-' in ip_range:
780 783 start_ip, end_ip = ip_range.split('-', 1)
781 784 start_ip = ipaddress.ip_address(start_ip.strip())
782 785 end_ip = ipaddress.ip_address(end_ip.strip())
783 786 parsed_ip_range = []
784 787
785 788 for index in xrange(int(start_ip), int(end_ip) + 1):
786 789 new_ip = ipaddress.ip_address(index)
787 790 parsed_ip_range.append(str(new_ip))
788 791 ip_list.extend(parsed_ip_range)
789 792 else:
790 793 ip_list.append(ip_range)
791 794
792 795 return make_unique(ip_list)
793 796
794 797 def add_extra_ip(self, user, ip, description=None):
795 798 """
796 799 Adds ip address to UserIpMap
797 800
798 801 :param user:
799 802 :param ip:
800 803 """
801 804 from rhodecode.model import forms
802 805 form = forms.UserExtraIpForm()()
803 806 data = form.to_python({'ip': ip})
804 807 user = self._get_user(user)
805 808
806 809 obj = UserIpMap()
807 810 obj.user = user
808 811 obj.ip_addr = data['ip']
809 812 obj.description = description
810 813 self.sa.add(obj)
811 814 return obj
812 815
813 816 def delete_extra_ip(self, user, ip_id):
814 817 """
815 818 Removes ip address from UserIpMap
816 819
817 820 :param user:
818 821 :param ip_id:
819 822 """
820 823 user = self._get_user(user)
821 824 obj = UserIpMap.query().get(ip_id)
822 825 if obj:
823 826 self.sa.delete(obj)
824 827
825 828 def get_accounts_in_creation_order(self, current_user=None):
826 829 """
827 830 Get accounts in order of creation for deactivation for license limits
828 831
829 832 pick currently logged in user, and append to the list in position 0
830 833 pick all super-admins in order of creation date and add it to the list
831 834 pick all other accounts in order of creation and add it to the list.
832 835
833 836 Based on that list, the last accounts can be disabled as they are
834 837 created at the end and don't include any of the super admins as well
835 838 as the current user.
836 839
837 840 :param current_user: optionally current user running this operation
838 841 """
839 842
840 843 if not current_user:
841 844 current_user = get_current_rhodecode_user()
842 845 active_super_admins = [
843 846 x.user_id for x in User.query()
844 847 .filter(User.user_id != current_user.user_id)
845 848 .filter(User.active == true())
846 849 .filter(User.admin == true())
847 850 .order_by(User.created_on.asc())]
848 851
849 852 active_regular_users = [
850 853 x.user_id for x in User.query()
851 854 .filter(User.user_id != current_user.user_id)
852 855 .filter(User.active == true())
853 856 .filter(User.admin == false())
854 857 .order_by(User.created_on.asc())]
855 858
856 859 list_of_accounts = [current_user.user_id]
857 860 list_of_accounts += active_super_admins
858 861 list_of_accounts += active_regular_users
859 862
860 863 return list_of_accounts
861 864
862 865 def deactivate_last_users(self, expected_users):
863 866 """
864 867 Deactivate accounts that are over the license limits.
865 868 Algorithm of which accounts to disabled is based on the formula:
866 869
867 870 Get current user, then super admins in creation order, then regular
868 871 active users in creation order.
869 872
870 873 Using that list we mark all accounts from the end of it as inactive.
871 874 This way we block only latest created accounts.
872 875
873 876 :param expected_users: list of users in special order, we deactivate
874 877 the end N ammoun of users from that list
875 878 """
876 879
877 880 list_of_accounts = self.get_accounts_in_creation_order()
878 881
879 882 for acc_id in list_of_accounts[expected_users + 1:]:
880 883 user = User.get(acc_id)
881 884 log.info('Deactivating account %s for license unlock', user)
882 885 user.active = False
883 886 Session().add(user)
884 887 Session().commit()
885 888
886 889 return
887 890
888 891 def get_user_log(self, user, filter_term):
889 892 user_log = UserLog.query()\
890 893 .filter(or_(UserLog.user_id == user.user_id,
891 894 UserLog.username == user.username))\
892 895 .options(joinedload(UserLog.user))\
893 896 .options(joinedload(UserLog.repository))\
894 897 .order_by(UserLog.action_date.desc())
895 898
896 899 user_log = user_log_filter(user_log, filter_term)
897 900 return user_log
@@ -1,604 +1,617 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 user group model for RhodeCode
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from rhodecode.lib.utils2 import safe_str, safe_unicode
31 31 from rhodecode.model import BaseModel
32 32 from rhodecode.model.scm import UserGroupList
33 from rhodecode.model.db import true, func, UserGroupMember, UserGroup,\
34 UserGroupRepoToPerm, Permission, UserGroupToPerm, User, UserUserGroupToPerm,\
35 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm
36 from rhodecode.lib.exceptions import UserGroupAssignedException,\
37 RepoGroupAssignmentError
38 from rhodecode.lib.utils2 import get_current_rhodecode_user, action_logger_generic
33 from rhodecode.model.db import (
34 true, func, User, UserGroupMember, UserGroup,
35 UserGroupRepoToPerm, Permission, UserGroupToPerm, UserUserGroupToPerm,
36 UserGroupUserGroupToPerm, UserGroupRepoGroupToPerm)
37 from rhodecode.lib.exceptions import (
38 UserGroupAssignedException, RepoGroupAssignmentError)
39 from rhodecode.lib.utils2 import (
40 get_current_rhodecode_user, action_logger_generic)
39 41
40 42 log = logging.getLogger(__name__)
41 43
42 44
43 45 class UserGroupModel(BaseModel):
44 46
45 47 cls = UserGroup
46 48
47 49 def _get_user_group(self, user_group):
48 50 return self._get_instance(UserGroup, user_group,
49 51 callback=UserGroup.get_by_group_name)
50 52
51 53 def _create_default_perms(self, user_group):
52 54 # create default permission
53 55 default_perm = 'usergroup.read'
54 56 def_user = User.get_default_user()
55 57 for p in def_user.user_perms:
56 58 if p.permission.permission_name.startswith('usergroup.'):
57 59 default_perm = p.permission.permission_name
58 60 break
59 61
60 62 user_group_to_perm = UserUserGroupToPerm()
61 63 user_group_to_perm.permission = Permission.get_by_key(default_perm)
62 64
63 65 user_group_to_perm.user_group = user_group
64 66 user_group_to_perm.user_id = def_user.user_id
65 67 return user_group_to_perm
66 68
67 69 def update_permissions(self, user_group, perm_additions=None, perm_updates=None,
68 70 perm_deletions=None, check_perms=True, cur_user=None):
69 71 from rhodecode.lib.auth import HasUserGroupPermissionAny
70 72 if not perm_additions:
71 73 perm_additions = []
72 74 if not perm_updates:
73 75 perm_updates = []
74 76 if not perm_deletions:
75 77 perm_deletions = []
76 78
77 79 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
78 80
79 81 # update permissions
80 82 for member_id, perm, member_type in perm_updates:
81 83 member_id = int(member_id)
82 84 if member_type == 'user':
83 85 # this updates existing one
84 86 self.grant_user_permission(
85 87 user_group=user_group, user=member_id, perm=perm
86 88 )
87 89 else:
88 90 # check if we have permissions to alter this usergroup
89 91 member_name = UserGroup.get(member_id).users_group_name
90 92 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
91 93 self.grant_user_group_permission(
92 94 target_user_group=user_group, user_group=member_id, perm=perm
93 95 )
94 96
95 97 # set new permissions
96 98 for member_id, perm, member_type in perm_additions:
97 99 member_id = int(member_id)
98 100 if member_type == 'user':
99 101 self.grant_user_permission(
100 102 user_group=user_group, user=member_id, perm=perm
101 103 )
102 104 else:
103 105 # check if we have permissions to alter this usergroup
104 106 member_name = UserGroup.get(member_id).users_group_name
105 107 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
106 108 self.grant_user_group_permission(
107 109 target_user_group=user_group, user_group=member_id, perm=perm
108 110 )
109 111
110 112 # delete permissions
111 113 for member_id, perm, member_type in perm_deletions:
112 114 member_id = int(member_id)
113 115 if member_type == 'user':
114 116 self.revoke_user_permission(user_group=user_group, user=member_id)
115 117 else:
116 #check if we have permissions to alter this usergroup
118 # check if we have permissions to alter this usergroup
117 119 member_name = UserGroup.get(member_id).users_group_name
118 120 if not check_perms or HasUserGroupPermissionAny(*req_perms)(member_name, user=cur_user):
119 121 self.revoke_user_group_permission(
120 122 target_user_group=user_group, user_group=member_id
121 123 )
122 124
123 125 def get(self, user_group_id, cache=False):
124 126 return UserGroup.get(user_group_id)
125 127
126 128 def get_group(self, user_group):
127 129 return self._get_user_group(user_group)
128 130
129 131 def get_by_name(self, name, cache=False, case_insensitive=False):
130 132 return UserGroup.get_by_group_name(name, cache, case_insensitive)
131 133
132 134 def create(self, name, description, owner, active=True, group_data=None):
133 135 try:
134 136 new_user_group = UserGroup()
135 137 new_user_group.user = self._get_user(owner)
136 138 new_user_group.users_group_name = name
137 139 new_user_group.user_group_description = description
138 140 new_user_group.users_group_active = active
139 141 if group_data:
140 142 new_user_group.group_data = group_data
141 143 self.sa.add(new_user_group)
142 144 perm_obj = self._create_default_perms(new_user_group)
143 145 self.sa.add(perm_obj)
144 146
145 147 self.grant_user_permission(user_group=new_user_group,
146 148 user=owner, perm='usergroup.admin')
147 149
148 150 return new_user_group
149 151 except Exception:
150 152 log.error(traceback.format_exc())
151 153 raise
152 154
153 155 def _get_memberships_for_user_ids(self, user_group, user_id_list):
154 156 members = []
155 157 for user_id in user_id_list:
156 158 member = self._get_membership(user_group.users_group_id, user_id)
157 159 members.append(member)
158 160 return members
159 161
160 162 def _get_added_and_removed_user_ids(self, user_group, user_id_list):
161 163 current_members = user_group.members or []
162 164 current_members_ids = [m.user.user_id for m in current_members]
163 165
164 166 added_members = [
165 167 user_id for user_id in user_id_list
166 168 if user_id not in current_members_ids]
167 169 if user_id_list == []:
168 170 # all members were deleted
169 171 deleted_members = current_members_ids
170 172 else:
171 173 deleted_members = [
172 174 user_id for user_id in current_members_ids
173 175 if user_id not in user_id_list]
174 176
175 177 return (added_members, deleted_members)
176 178
177 179 def _set_users_as_members(self, user_group, user_ids):
178 180 user_group.members = []
179 181 self.sa.flush()
180 182 members = self._get_memberships_for_user_ids(
181 183 user_group, user_ids)
182 184 user_group.members = members
183 185 self.sa.add(user_group)
184 186
185 187 def _update_members_from_user_ids(self, user_group, user_ids):
186 188 added, removed = self._get_added_and_removed_user_ids(
187 189 user_group, user_ids)
188 190 self._set_users_as_members(user_group, user_ids)
189 191 self._log_user_changes('added to', user_group, added)
190 192 self._log_user_changes('removed from', user_group, removed)
191 193
192 194 def _clean_members_data(self, members_data):
193 195 if not members_data:
194 196 members_data = []
195 197
196 198 members = []
197 199 for user in members_data:
198 200 uid = int(user['member_user_id'])
199 201 if uid not in members and user['type'] in ['new', 'existing']:
200 202 members.append(uid)
201 203 return members
202 204
203 205 def update(self, user_group, form_data):
204 206 user_group = self._get_user_group(user_group)
205 207 if 'users_group_name' in form_data:
206 208 user_group.users_group_name = form_data['users_group_name']
207 209 if 'users_group_active' in form_data:
208 210 user_group.users_group_active = form_data['users_group_active']
209 211 if 'user_group_description' in form_data:
210 212 user_group.user_group_description = form_data[
211 213 'user_group_description']
212 214
213 215 # handle owner change
214 216 if 'user' in form_data:
215 217 owner = form_data['user']
216 218 if isinstance(owner, basestring):
217 219 owner = User.get_by_username(form_data['user'])
218 220
219 221 if not isinstance(owner, User):
220 222 raise ValueError(
221 223 'invalid owner for user group: %s' % form_data['user'])
222 224
223 225 user_group.user = owner
224 226
225 227 if 'users_group_members' in form_data:
226 228 members_id_list = self._clean_members_data(
227 229 form_data['users_group_members'])
228 230 self._update_members_from_user_ids(user_group, members_id_list)
229 231
230 232 self.sa.add(user_group)
231 233
232 234 def delete(self, user_group, force=False):
233 235 """
234 236 Deletes repository group, unless force flag is used
235 237 raises exception if there are members in that group, else deletes
236 238 group and users
237 239
238 240 :param user_group:
239 241 :param force:
240 242 """
241 243 user_group = self._get_user_group(user_group)
242 244 try:
243 245 # check if this group is not assigned to repo
244 246 assigned_to_repo = [x.repository for x in UserGroupRepoToPerm.query()\
245 247 .filter(UserGroupRepoToPerm.users_group == user_group).all()]
246 248 # check if this group is not assigned to repo
247 249 assigned_to_repo_group = [x.group for x in UserGroupRepoGroupToPerm.query()\
248 250 .filter(UserGroupRepoGroupToPerm.users_group == user_group).all()]
249 251
250 252 if (assigned_to_repo or assigned_to_repo_group) and not force:
251 253 assigned = ','.join(map(safe_str,
252 254 assigned_to_repo+assigned_to_repo_group))
253 255
254 256 raise UserGroupAssignedException(
255 257 'UserGroup assigned to %s' % (assigned,))
256 258 self.sa.delete(user_group)
257 259 except Exception:
258 260 log.error(traceback.format_exc())
259 261 raise
260 262
261 263 def _log_user_changes(self, action, user_group, user_or_users):
262 264 users = user_or_users
263 265 if not isinstance(users, (list, tuple)):
264 266 users = [users]
265 267 rhodecode_user = get_current_rhodecode_user()
266 268 ipaddr = getattr(rhodecode_user, 'ip_addr', '')
267 269 group_name = user_group.users_group_name
268 270
269 271 for user_or_user_id in users:
270 272 user = self._get_user(user_or_user_id)
271 273 log_text = 'User {user} {action} {group}'.format(
272 274 action=action, user=user.username, group=group_name)
273 275 log.info('Logging action: {0} by {1} ip:{2}'.format(
274 276 log_text, rhodecode_user, ipaddr))
275 277
276 278 def _find_user_in_group(self, user, user_group):
277 279 user_group_member = None
278 280 for m in user_group.members:
279 281 if m.user_id == user.user_id:
280 282 # Found this user's membership row
281 283 user_group_member = m
282 284 break
283 285
284 286 return user_group_member
285 287
286 288 def _get_membership(self, user_group_id, user_id):
287 289 user_group_member = UserGroupMember(user_group_id, user_id)
288 290 return user_group_member
289 291
290 292 def add_user_to_group(self, user_group, user):
291 293 user_group = self._get_user_group(user_group)
292 294 user = self._get_user(user)
293 295 user_member = self._find_user_in_group(user, user_group)
294 296 if user_member:
295 297 # user already in the group, skip
296 298 return True
297 299
298 300 member = self._get_membership(
299 301 user_group.users_group_id, user.user_id)
300 302 user_group.members.append(member)
301 303
302 304 try:
303 305 self.sa.add(member)
304 306 except Exception:
305 307 # what could go wrong here?
306 308 log.error(traceback.format_exc())
307 309 raise
308 310
309 311 self._log_user_changes('added to', user_group, user)
310 312 return member
311 313
312 314 def remove_user_from_group(self, user_group, user):
313 315 user_group = self._get_user_group(user_group)
314 316 user = self._get_user(user)
315 317 user_group_member = self._find_user_in_group(user, user_group)
316 318
317 319 if not user_group_member:
318 320 # User isn't in that group
319 321 return False
320 322
321 323 try:
322 324 self.sa.delete(user_group_member)
323 325 except Exception:
324 326 log.error(traceback.format_exc())
325 327 raise
326 328
327 329 self._log_user_changes('removed from', user_group, user)
328 330 return True
329 331
330 332 def has_perm(self, user_group, perm):
331 333 user_group = self._get_user_group(user_group)
332 334 perm = self._get_perm(perm)
333 335
334 336 return UserGroupToPerm.query()\
335 337 .filter(UserGroupToPerm.users_group == user_group)\
336 338 .filter(UserGroupToPerm.permission == perm).scalar() is not None
337 339
338 340 def grant_perm(self, user_group, perm):
339 341 user_group = self._get_user_group(user_group)
340 342 perm = self._get_perm(perm)
341 343
342 344 # if this permission is already granted skip it
343 345 _perm = UserGroupToPerm.query()\
344 346 .filter(UserGroupToPerm.users_group == user_group)\
345 347 .filter(UserGroupToPerm.permission == perm)\
346 348 .scalar()
347 349 if _perm:
348 350 return
349 351
350 352 new = UserGroupToPerm()
351 353 new.users_group = user_group
352 354 new.permission = perm
353 355 self.sa.add(new)
354 356 return new
355 357
356 358 def revoke_perm(self, user_group, perm):
357 359 user_group = self._get_user_group(user_group)
358 360 perm = self._get_perm(perm)
359 361
360 362 obj = UserGroupToPerm.query()\
361 363 .filter(UserGroupToPerm.users_group == user_group)\
362 364 .filter(UserGroupToPerm.permission == perm).scalar()
363 365 if obj:
364 366 self.sa.delete(obj)
365 367
366 368 def grant_user_permission(self, user_group, user, perm):
367 369 """
368 370 Grant permission for user on given user group, or update
369 371 existing one if found
370 372
371 373 :param user_group: Instance of UserGroup, users_group_id,
372 374 or users_group_name
373 375 :param user: Instance of User, user_id or username
374 376 :param perm: Instance of Permission, or permission_name
375 377 """
376 378
377 379 user_group = self._get_user_group(user_group)
378 380 user = self._get_user(user)
379 381 permission = self._get_perm(perm)
380 382
381 383 # check if we have that permission already
382 384 obj = self.sa.query(UserUserGroupToPerm)\
383 385 .filter(UserUserGroupToPerm.user == user)\
384 386 .filter(UserUserGroupToPerm.user_group == user_group)\
385 387 .scalar()
386 388 if obj is None:
387 389 # create new !
388 390 obj = UserUserGroupToPerm()
389 391 obj.user_group = user_group
390 392 obj.user = user
391 393 obj.permission = permission
392 394 self.sa.add(obj)
393 395 log.debug('Granted perm %s to %s on %s', perm, user, user_group)
394 396 action_logger_generic(
395 397 'granted permission: {} to user: {} on usergroup: {}'.format(
396 398 perm, user, user_group), namespace='security.usergroup')
397 399
398 400 return obj
399 401
400 402 def revoke_user_permission(self, user_group, user):
401 403 """
402 404 Revoke permission for user on given user group
403 405
404 406 :param user_group: Instance of UserGroup, users_group_id,
405 407 or users_group name
406 408 :param user: Instance of User, user_id or username
407 409 """
408 410
409 411 user_group = self._get_user_group(user_group)
410 412 user = self._get_user(user)
411 413
412 414 obj = self.sa.query(UserUserGroupToPerm)\
413 415 .filter(UserUserGroupToPerm.user == user)\
414 416 .filter(UserUserGroupToPerm.user_group == user_group)\
415 417 .scalar()
416 418 if obj:
417 419 self.sa.delete(obj)
418 420 log.debug('Revoked perm on %s on %s', user_group, user)
419 421 action_logger_generic(
420 422 'revoked permission from user: {} on usergroup: {}'.format(
421 423 user, user_group), namespace='security.usergroup')
422 424
423 425 def grant_user_group_permission(self, target_user_group, user_group, perm):
424 426 """
425 427 Grant user group permission for given target_user_group
426 428
427 429 :param target_user_group:
428 430 :param user_group:
429 431 :param perm:
430 432 """
431 433 target_user_group = self._get_user_group(target_user_group)
432 434 user_group = self._get_user_group(user_group)
433 435 permission = self._get_perm(perm)
434 436 # forbid assigning same user group to itself
435 437 if target_user_group == user_group:
436 438 raise RepoGroupAssignmentError('target repo:%s cannot be '
437 439 'assigned to itself' % target_user_group)
438 440
439 441 # check if we have that permission already
440 442 obj = self.sa.query(UserGroupUserGroupToPerm)\
441 443 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
442 444 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
443 445 .scalar()
444 446 if obj is None:
445 447 # create new !
446 448 obj = UserGroupUserGroupToPerm()
447 449 obj.user_group = user_group
448 450 obj.target_user_group = target_user_group
449 451 obj.permission = permission
450 452 self.sa.add(obj)
451 453 log.debug(
452 454 'Granted perm %s to %s on %s', perm, target_user_group, user_group)
453 455 action_logger_generic(
454 456 'granted permission: {} to usergroup: {} on usergroup: {}'.format(
455 457 perm, user_group, target_user_group),
456 458 namespace='security.usergroup')
457 459
458 460 return obj
459 461
460 462 def revoke_user_group_permission(self, target_user_group, user_group):
461 463 """
462 464 Revoke user group permission for given target_user_group
463 465
464 466 :param target_user_group:
465 467 :param user_group:
466 468 """
467 469 target_user_group = self._get_user_group(target_user_group)
468 470 user_group = self._get_user_group(user_group)
469 471
470 472 obj = self.sa.query(UserGroupUserGroupToPerm)\
471 473 .filter(UserGroupUserGroupToPerm.target_user_group == target_user_group)\
472 474 .filter(UserGroupUserGroupToPerm.user_group == user_group)\
473 475 .scalar()
474 476 if obj:
475 477 self.sa.delete(obj)
476 478 log.debug(
477 479 'Revoked perm on %s on %s', target_user_group, user_group)
478 480 action_logger_generic(
479 481 'revoked permission from usergroup: {} on usergroup: {}'.format(
480 482 user_group, target_user_group),
481 483 namespace='security.repogroup')
482 484
483 485 def enforce_groups(self, user, groups, extern_type=None):
484 486 user = self._get_user(user)
485 487 log.debug('Enforcing groups %s on user %s', groups, user)
486 488 current_groups = user.group_member
487 489 # find the external created groups
488 490 externals = [x.users_group for x in current_groups
489 491 if 'extern_type' in x.users_group.group_data]
490 492
491 493 # calculate from what groups user should be removed
492 494 # externals that are not in groups
493 495 for gr in externals:
494 496 if gr.users_group_name not in groups:
495 497 log.debug('Removing user %s from user group %s', user, gr)
496 498 self.remove_user_from_group(gr, user)
497 499
498 500 # now we calculate in which groups user should be == groups params
499 501 owner = User.get_first_super_admin().username
500 502 for gr in set(groups):
501 503 existing_group = UserGroup.get_by_group_name(gr)
502 504 if not existing_group:
503 505 desc = 'Automatically created from plugin:%s' % extern_type
504 506 # we use first admin account to set the owner of the group
505 507 existing_group = UserGroupModel().create(
506 508 gr, desc, owner, group_data={'extern_type': extern_type})
507 509
508 510 # we can only add users to special groups created via plugins
509 511 managed = 'extern_type' in existing_group.group_data
510 512 if managed:
511 513 log.debug('Adding user %s to user group %s', user, gr)
512 514 UserGroupModel().add_user_to_group(existing_group, user)
513 515 else:
514 516 log.debug('Skipping addition to group %s since it is '
515 517 'not set to be automatically synchronized' % gr)
516 518
517 519 def change_groups(self, user, groups):
518 520 """
519 521 This method changes user group assignment
520 522 :param user: User
521 523 :param groups: array of UserGroupModel
522 524 :return:
523 525 """
524 526 user = self._get_user(user)
525 527 log.debug('Changing user(%s) assignment to groups(%s)', user, groups)
526 528 current_groups = user.group_member
527 529 current_groups = [x.users_group for x in current_groups]
528 530
529 531 # calculate from what groups user should be removed/add
530 532 groups = set(groups)
531 533 current_groups = set(current_groups)
532 534
533 535 groups_to_remove = current_groups - groups
534 536 groups_to_add = groups - current_groups
535 537
536 538 for gr in groups_to_remove:
537 539 log.debug('Removing user %s from user group %s', user.username, gr.users_group_name)
538 540 self.remove_user_from_group(gr.users_group_name, user.username)
539 541 for gr in groups_to_add:
540 542 log.debug('Adding user %s to user group %s', user.username, gr.users_group_name)
541 543 UserGroupModel().add_user_to_group(gr.users_group_name, user.username)
542 544
545 def _serialize_user_group(self, user_group):
546 import rhodecode.lib.helpers as h
547 return {
548 'id': user_group.users_group_id,
549 # TODO: marcink figure out a way to generate the url for the
550 # icon
551 'icon_link': '',
552 'value_display': 'Group: %s (%d members)' % (
553 user_group.users_group_name, len(user_group.members),),
554 'value': user_group.users_group_name,
555 'description': user_group.user_group_description,
556 'owner': user_group.user.username,
557
558 'owner_icon': h.gravatar_url(user_group.user.email, 30),
559 'value_display_owner': h.person(user_group.user.email),
560
561 'value_type': 'user_group',
562 'active': user_group.users_group_active,
563 }
564
543 565 def get_user_groups(self, name_contains=None, limit=20, only_active=True,
544 566 expand_groups=False):
545 import rhodecode.lib.helpers as h
546
547 567 query = self.sa.query(UserGroup)
548 568 if only_active:
549 569 query = query.filter(UserGroup.users_group_active == true())
550 570
551 571 if name_contains:
552 572 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
553 573 query = query.filter(
554 574 UserGroup.users_group_name.ilike(ilike_expression))\
555 575 .order_by(func.length(UserGroup.users_group_name))\
556 576 .order_by(UserGroup.users_group_name)
557 577
558 578 query = query.limit(limit)
559 579 user_groups = query.all()
560 580 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
561 581 user_groups = UserGroupList(user_groups, perm_set=perm_set)
562 582
563 _groups = [
564 {
565 'id': group.users_group_id,
566 # TODO: marcink figure out a way to generate the url for the
567 # icon
568 'icon_link': '',
569 'value_display': 'Group: %s (%d members)' % (
570 group.users_group_name, len(group.members),),
571 'value': group.users_group_name,
572 'description': group.user_group_description,
573 'owner': group.user.username,
583 # store same serialize method to extract data from User
584 from rhodecode.model.user import UserModel
585 serialize_user = UserModel()._serialize_user
574 586
575 'owner_icon': h.gravatar_url(group.user.email, 30),
576 'value_display_owner': h.person(group.user.email),
577
578 'value_type': 'user_group',
579 'active': group.users_group_active,
580 }
581 for group in user_groups
582 ]
587 _groups = []
588 for group in user_groups:
589 entry = self._serialize_user_group(group)
590 if expand_groups:
591 expanded_members = []
592 for member in group.members:
593 expanded_members.append(serialize_user(member.user))
594 entry['members'] = expanded_members
595 _groups.append(entry)
583 596 return _groups
584 597
585 598 @staticmethod
586 599 def get_user_groups_as_dict(user_group):
587 600 import rhodecode.lib.helpers as h
588 601
589 602 data = {
590 603 'users_group_id': user_group.users_group_id,
591 604 'group_name': user_group.users_group_name,
592 605 'group_description': user_group.user_group_description,
593 606 'active': user_group.users_group_active,
594 607 "owner": user_group.user.username,
595 608 'owner_icon': h.gravatar_url(user_group.user.email, 30),
596 609 "owner_data": {
597 610 'owner': user_group.user.username,
598 611 'owner_icon': h.gravatar_url(user_group.user.email, 30)}
599 612 }
600 613 return data
601 614
602 615
603 616
604 617
@@ -1,343 +1,355 b''
1 1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Pull request reviewers
21 21 */
22 22 var removeReviewMember = function(reviewer_id, mark_delete){
23 23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 24
25 25 if(typeof(mark_delete) === undefined){
26 26 mark_delete = false;
27 27 }
28 28
29 29 if(mark_delete === true){
30 30 if (reviewer){
31 31 // now delete the input
32 32 $('#reviewer_{0} input'.format(reviewer_id)).remove();
33 33 // mark as to-delete
34 34 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
35 35 obj.addClass('to-delete');
36 36 obj.css({"text-decoration":"line-through", "opacity": 0.5});
37 37 }
38 38 }
39 39 else{
40 40 $('#reviewer_{0}'.format(reviewer_id)).remove();
41 41 }
42 42 };
43 43
44 44 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
45 45 var members = $('#review_members').get(0);
46 46 var reasons_html = '';
47 47 var reasons_inputs = '';
48 48 var reasons = reasons || [];
49 49 if (reasons) {
50 50 for (var i = 0; i < reasons.length; i++) {
51 51 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(reasons[i]);
52 52 reasons_inputs += '<input type="hidden" name="reason" value="' + escapeHtml(reasons[i]) + '">';
53 53 }
54 54 }
55 55 var tmpl = '<li id="reviewer_{2}">'+
56 56 '<input type="hidden" name="__start__" value="reviewer:mapping">'+
57 57 '<div class="reviewer_status">'+
58 58 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
59 59 '</div>'+
60 60 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
61 61 '<span class="reviewer_name user">{1}</span>'+
62 62 reasons_html +
63 63 '<input type="hidden" name="user_id" value="{2}">'+
64 64 '<input type="hidden" name="__start__" value="reasons:sequence">'+
65 65 '{3}'+
66 66 '<input type="hidden" name="__end__" value="reasons:sequence">'+
67 67 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
68 68 '<i class="icon-remove-sign"></i>'+
69 69 '</div>'+
70 70 '</div>'+
71 71 '<input type="hidden" name="__end__" value="reviewer:mapping">'+
72 72 '</li>' ;
73 73
74 74 var displayname = "{0} ({1} {2})".format(
75 75 nname, escapeHtml(fname), escapeHtml(lname));
76 76 var element = tmpl.format(gravatar_link,displayname,id,reasons_inputs);
77 77 // check if we don't have this ID already in
78 78 var ids = [];
79 79 var _els = $('#review_members li').toArray();
80 80 for (el in _els){
81 81 ids.push(_els[el].id)
82 82 }
83 83 if(ids.indexOf('reviewer_'+id) == -1){
84 84 // only add if it's not there
85 85 members.innerHTML += element;
86 86 }
87 87
88 88 };
89 89
90 90 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
91 91 var url = pyroutes.url(
92 92 'pullrequest_update',
93 93 {"repo_name": repo_name, "pull_request_id": pull_request_id});
94 94 if (typeof postData === 'string' ) {
95 95 postData += '&csrf_token=' + CSRF_TOKEN;
96 96 } else {
97 97 postData.csrf_token = CSRF_TOKEN;
98 98 }
99 99 var success = function(o) {
100 100 window.location.reload();
101 101 };
102 102 ajaxPOST(url, postData, success);
103 103 };
104 104
105 105 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
106 106 if (reviewers_ids === undefined){
107 107 var postData = '_method=put&' + $('#reviewers input').serialize();
108 108 _updatePullRequest(repo_name, pull_request_id, postData);
109 109 }
110 110 };
111 111
112 112 /**
113 113 * PULL REQUEST reject & close
114 114 */
115 115 var closePullRequest = function(repo_name, pull_request_id) {
116 116 var postData = {
117 117 '_method': 'put',
118 118 'close_pull_request': true};
119 119 _updatePullRequest(repo_name, pull_request_id, postData);
120 120 };
121 121
122 122 /**
123 123 * PULL REQUEST update commits
124 124 */
125 125 var updateCommits = function(repo_name, pull_request_id) {
126 126 var postData = {
127 127 '_method': 'put',
128 128 'update_commits': true};
129 129 _updatePullRequest(repo_name, pull_request_id, postData);
130 130 };
131 131
132 132
133 133 /**
134 134 * PULL REQUEST edit info
135 135 */
136 136 var editPullRequest = function(repo_name, pull_request_id, title, description) {
137 137 var url = pyroutes.url(
138 138 'pullrequest_update',
139 139 {"repo_name": repo_name, "pull_request_id": pull_request_id});
140 140
141 141 var postData = {
142 142 '_method': 'put',
143 143 'title': title,
144 144 'description': description,
145 145 'edit_pull_request': true,
146 146 'csrf_token': CSRF_TOKEN
147 147 };
148 148 var success = function(o) {
149 149 window.location.reload();
150 150 };
151 151 ajaxPOST(url, postData, success);
152 152 };
153 153
154 154 var initPullRequestsCodeMirror = function (textAreaId) {
155 155 var ta = $(textAreaId).get(0);
156 156 var initialHeight = '100px';
157 157
158 158 // default options
159 159 var codeMirrorOptions = {
160 160 mode: "text",
161 161 lineNumbers: false,
162 162 indentUnit: 4,
163 163 theme: 'rc-input'
164 164 };
165 165
166 166 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
167 167 // marker for manually set description
168 168 codeMirrorInstance._userDefinedDesc = false;
169 169 codeMirrorInstance.setSize(null, initialHeight);
170 170 codeMirrorInstance.on("change", function(instance, changeObj) {
171 171 var height = initialHeight;
172 172 var lines = instance.lineCount();
173 173 if (lines > 6 && lines < 20) {
174 174 height = "auto"
175 175 }
176 176 else if (lines >= 20) {
177 177 height = 20 * 15;
178 178 }
179 179 instance.setSize(null, height);
180 180
181 181 // detect if the change was trigger by auto desc, or user input
182 182 changeOrigin = changeObj.origin;
183 183
184 184 if (changeOrigin === "setValue") {
185 185 cmLog.debug('Change triggered by setValue');
186 186 }
187 187 else {
188 188 cmLog.debug('user triggered change !');
189 189 // set special marker to indicate user has created an input.
190 190 instance._userDefinedDesc = true;
191 191 }
192 192
193 193 });
194 194
195 195 return codeMirrorInstance
196 196 };
197 197
198 198 /**
199 199 * Reviewer autocomplete
200 200 */
201 var ReviewerAutoComplete = function(input_id) {
202 $('#'+input_id).autocomplete({
201 var ReviewerAutoComplete = function(inputId) {
202 $(inputId).autocomplete({
203 203 serviceUrl: pyroutes.url('user_autocomplete_data'),
204 204 minChars:2,
205 205 maxHeight:400,
206 206 deferRequestBy: 300, //miliseconds
207 207 showNoSuggestionNotice: true,
208 208 tabDisabled: true,
209 209 autoSelectFirst: true,
210 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true },
210 211 formatResult: autocompleteFormatResult,
211 212 lookupFilter: autocompleteFilterResult,
212 onSelect: function(suggestion, data){
213 var msg = _gettext('added manually by "{0}"');
214 var reasons = [msg.format(templateContext.rhodecode_user.username)];
215 addReviewMember(data.id, data.first_name, data.last_name,
216 data.username, data.icon_link, reasons);
217 $('#'+input_id).val('');
213 onSelect: function(element, data) {
214
215 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
216 if (data.value_type == 'user_group') {
217 reasons.push(_gettext('member of "{0}"').format(data.value_display));
218
219 $.each(data.members, function(index, member_data) {
220 addReviewMember(member_data.id, member_data.first_name, member_data.last_name,
221 member_data.username, member_data.icon_link, reasons);
222 })
223
224 } else {
225 addReviewMember(data.id, data.first_name, data.last_name,
226 data.username, data.icon_link, reasons);
227 }
228
229 $(inputId).val('');
218 230 }
219 231 });
220 232 };
221 233
222 234
223 235 VersionController = function () {
224 236 var self = this;
225 237 this.$verSource = $('input[name=ver_source]');
226 238 this.$verTarget = $('input[name=ver_target]');
227 239 this.$showVersionDiff = $('#show-version-diff');
228 240
229 241 this.adjustRadioSelectors = function (curNode) {
230 242 var getVal = function (item) {
231 243 if (item == 'latest') {
232 244 return Number.MAX_SAFE_INTEGER
233 245 }
234 246 else {
235 247 return parseInt(item)
236 248 }
237 249 };
238 250
239 251 var curVal = getVal($(curNode).val());
240 252 var cleared = false;
241 253
242 254 $.each(self.$verSource, function (index, value) {
243 255 var elVal = getVal($(value).val());
244 256
245 257 if (elVal > curVal) {
246 258 if ($(value).is(':checked')) {
247 259 cleared = true;
248 260 }
249 261 $(value).attr('disabled', 'disabled');
250 262 $(value).removeAttr('checked');
251 263 $(value).css({'opacity': 0.1});
252 264 }
253 265 else {
254 266 $(value).css({'opacity': 1});
255 267 $(value).removeAttr('disabled');
256 268 }
257 269 });
258 270
259 271 if (cleared) {
260 272 // if we unchecked an active, set the next one to same loc.
261 273 $(this.$verSource).filter('[value={0}]'.format(
262 274 curVal)).attr('checked', 'checked');
263 275 }
264 276
265 277 self.setLockAction(false,
266 278 $(curNode).data('verPos'),
267 279 $(this.$verSource).filter(':checked').data('verPos')
268 280 );
269 281 };
270 282
271 283
272 284 this.attachVersionListener = function () {
273 285 self.$verTarget.change(function (e) {
274 286 self.adjustRadioSelectors(this)
275 287 });
276 288 self.$verSource.change(function (e) {
277 289 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
278 290 });
279 291 };
280 292
281 293 this.init = function () {
282 294
283 295 var curNode = self.$verTarget.filter(':checked');
284 296 self.adjustRadioSelectors(curNode);
285 297 self.setLockAction(true);
286 298 self.attachVersionListener();
287 299
288 300 };
289 301
290 302 this.setLockAction = function (state, selectedVersion, otherVersion) {
291 303 var $showVersionDiff = this.$showVersionDiff;
292 304
293 305 if (state) {
294 306 $showVersionDiff.attr('disabled', 'disabled');
295 307 $showVersionDiff.addClass('disabled');
296 308 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
297 309 }
298 310 else {
299 311 $showVersionDiff.removeAttr('disabled');
300 312 $showVersionDiff.removeClass('disabled');
301 313
302 314 if (selectedVersion == otherVersion) {
303 315 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
304 316 } else {
305 317 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
306 318 }
307 319 }
308 320
309 321 };
310 322
311 323 this.showVersionDiff = function () {
312 324 var target = self.$verTarget.filter(':checked');
313 325 var source = self.$verSource.filter(':checked');
314 326
315 327 if (target.val() && source.val()) {
316 328 var params = {
317 329 'pull_request_id': templateContext.pull_request_data.pull_request_id,
318 330 'repo_name': templateContext.repo_name,
319 331 'version': target.val(),
320 332 'from_version': source.val()
321 333 };
322 334 window.location = pyroutes.url('pullrequest_show', params)
323 335 }
324 336
325 337 return false;
326 338 };
327 339
328 340 this.toggleVersionView = function (elem) {
329 341
330 342 if (this.$showVersionDiff.is(':visible')) {
331 343 $('.version-pr').hide();
332 344 this.$showVersionDiff.hide();
333 345 $(elem).html($(elem).data('toggleOn'))
334 346 } else {
335 347 $('.version-pr').show();
336 348 this.$showVersionDiff.show();
337 349 $(elem).html($(elem).data('toggleOff'))
338 350 }
339 351
340 352 return false
341 353 }
342 354
343 355 }; No newline at end of file
@@ -1,964 +1,964 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/debug_style/index.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 ${h.link_to(_('Style'), h.url('debug_style_home'))}
6 6 &raquo;
7 7 ${c.active}
8 8 </%def>
9 9
10 10
11 11 <%def name="real_main()">
12 12 <div class="box">
13 13 <div class="title">
14 14 ${self.breadcrumbs()}
15 15 </div>
16 16
17 17 <div class='sidebar-col-wrapper'>
18 18 ${self.sidebar()}
19 19
20 20 <div class="main-content">
21 21
22 22 <h2>Collapsable Content</h2>
23 23 <p>Where a section may have a very long list of information, it can be desirable to use collapsable content. There is a premade function for showing/hiding elements, though its use may or may not be practical, depending on the situation. Use it, or don't, on a case-by-case basis.</p>
24 24
25 25 <p><strong>To use the collapsable-content function:</strong> Create a toggle button using <code>&lt;div class="btn-collapse"&gt;Show More&lt;/div&gt;</code> and a data attribute using <code>data-toggle</code>. Clicking this button will toggle any sibling element(s) containing the class <code>collapsable-content</code> and an identical <code>data-toggle</code> attribute. It will also change the button to read "Show Less"; another click toggles it back to the previous state. Ideally, use pre-existing elements and add the class and attribute; creating a new div around the existing content may lead to unexpected results, as the toggle function will use <code>display:block</code> if no previous display specification was found.
26 26 </p>
27 27 <p>Notes:</p>
28 28 <ul>
29 29 <li>Changes made to the text of the button will require adjustment to the function, but for the sake of consistency and user experience, this is best avoided. </li>
30 30 <li>Collapsable content inside of a pjax loaded container will require <code>collapsableContent();</code> to be called from within the container. No variables are necessary.</li>
31 31 </ul>
32 32
33 33 </div> <!-- .main-content -->
34 34 </div> <!-- .sidebar-col-wrapper -->
35 35 </div> <!-- .box -->
36 36
37 37 <!-- CONTENT -->
38 38 <div id="content" class="wrapper">
39 39
40 40 <div class="main">
41 41
42 42 <div class="box">
43 43 <div class="title">
44 44 <h1>
45 45 Diff: enable filename with spaces on diffs
46 46 </h1>
47 47 <h1>
48 48 <i class="icon-hg" ></i>
49 49
50 50 <i class="icon-lock"></i>
51 51 <span><a href="/rhodecode-momentum">rhodecode-momentum</a></span>
52 52
53 53 </h1>
54 54 </div>
55 55
56 56 <div class="box pr-summary">
57 57 <div class="summary-details block-left">
58 58
59 59 <div class="pr-details-title">
60 60
61 61 Pull request #720 From Tue, 17 Feb 2015 16:21:38
62 62 <div class="btn-collapse" data-toggle="description">Show More</div>
63 63 </div>
64 64 <div id="summary" class="fields pr-details-content">
65 65 <div class="field">
66 66 <div class="label-summary">
67 67 <label>Origin:</label>
68 68 </div>
69 69 <div class="input">
70 70 <div>
71 71 <span class="tag">
72 72 <a href="/andersonsantos/rhodecode-momentum-fork#fix_574">book: fix_574</a>
73 73 </span>
74 74 <span class="clone-url">
75 75 <a href="/andersonsantos/rhodecode-momentum-fork">https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork</a>
76 76 </span>
77 77 </div>
78 78 <div>
79 79 <br>
80 80 <input type="text" value="hg pull -r 46b3d50315f0 https://code.rhodecode.com/andersonsantos/rhodecode-momentum-fork" readonly="readonly">
81 81 </div>
82 82 </div>
83 83 </div>
84 84 <div class="field">
85 85 <div class="label-summary">
86 86 <label>Review:</label>
87 87 </div>
88 88 <div class="input">
89 89 <div class="flag_status under_review tooltip pull-left" title="Pull request status calculated from votes"></div>
90 90 <span class="changeset-status-lbl tooltip" title="Pull request status calculated from votes">
91 91 Under Review
92 92 </span>
93 93
94 94 </div>
95 95 </div>
96 96 <div class="field collapsable-content" data-toggle="description">
97 97 <div class="label-summary">
98 98 <label>Description:</label>
99 99 </div>
100 100 <div class="input">
101 101 <div class="pr-description">Fixing issue <a class="issue- tracker-link" href="http://bugs.rhodecode.com/issues/574"># 574</a>, changing regex for capturing filenames</div>
102 102 </div>
103 103 </div>
104 104 <div class="field collapsable-content" data-toggle="description">
105 105 <div class="label-summary">
106 106 <label>Comments:</label>
107 107 </div>
108 108 <div class="input">
109 109 <div>
110 110 <div class="comments-number">
111 111 <a href="#inline-comments-container">0 Pull request comments</a>,
112 112 0 Inline Comments
113 113 </div>
114 114 </div>
115 115 </div>
116 116 </div>
117 117 </div>
118 118 </div>
119 119 <div>
120 120 <div class="reviewers-title block-right">
121 121 <div class="pr-details-title">
122 122 Author
123 123 </div>
124 124 </div>
125 125 <div class="block-right pr-details-content reviewers">
126 126 <ul class="group_members">
127 127 <li>
128 128 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
129 129 <span class="user"> <a href="/_profiles/lolek">lolek (Lolek Santos)</a></span>
130 130 </li>
131 131 </ul>
132 132 </div>
133 133 <div class="reviewers-title block-right">
134 134 <div class="pr-details-title">
135 135 Pull request reviewers
136 136 <span class="btn-collapse" data-toggle="reviewers">Show More</span>
137 137 </div>
138 138
139 139 </div>
140 140 <div id="reviewers" class="block-right pr-details-content reviewers">
141 141
142 142 <ul id="review_members" class="group_members">
143 143 <li id="reviewer_70">
144 144 <div class="reviewers_member">
145 145 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
146 146 <div class="flag_status rejected pull-left reviewer_member_status"></div>
147 147 </div>
148 148 <img class="gravatar" src="https://secure.gravatar.com/avatar/153a0fab13160b3e64a2cbc7c0373506?d=identicon&amp;s=32" height="16" width="16">
149 149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 150 </div>
151 151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
153 153 <i class="icon-remove-sign"></i>
154 154 </div>
155 155 </li>
156 156 <li id="reviewer_33" class="collapsable-content" data-toggle="reviewers">
157 157 <div class="reviewers_member">
158 158 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
159 159 <div class="flag_status approved pull-left reviewer_member_status"></div>
160 160 </div>
161 161 <img class="gravatar" src="https://secure.gravatar.com/avatar/ffd6a317ec2b66be880143cd8459d0d9?d=identicon&amp;s=32" height="16" width="16">
162 162 <span class="user"> <a href="/_profiles/jenkins-tests">garbas (Rok Garbas)</a> (reviewer)</span>
163 163 </div>
164 164 </li>
165 165 <li id="reviewer_2" class="collapsable-content" data-toggle="reviewers">
166 166 <div class="reviewers_member">
167 167 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
168 168 <div class="flag_status not_reviewed pull-left reviewer_member_status"></div>
169 169 </div>
170 170 <img class="gravatar" src="https://secure.gravatar.com/avatar/aad9d40cac1259ea39b5578554ad9d64?d=identicon&amp;s=32" height="16" width="16">
171 171 <span class="user"> <a href="/_profiles/jenkins-tests">marcink (Marcin Kuzminski)</a> (reviewer)</span>
172 172 </div>
173 173 </li>
174 174 <li id="reviewer_36" class="collapsable-content" data-toggle="reviewers">
175 175 <div class="reviewers_member">
176 176 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
177 177 <div class="flag_status approved pull-left reviewer_member_status"></div>
178 178 </div>
179 179 <img class="gravatar" src="https://secure.gravatar.com/avatar/7a4da001a0af0016ed056ab523255db9?d=identicon&amp;s=32" height="16" width="16">
180 180 <span class="user"> <a href="/_profiles/jenkins-tests">johbo (Johannes Bornhold)</a> (reviewer)</span>
181 181 </div>
182 182 </li>
183 183 <li id="reviewer_47" class="collapsable-content" data-toggle="reviewers">
184 184 <div class="reviewers_member">
185 185 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
186 186 <div class="flag_status under_review pull-left reviewer_member_status"></div>
187 187 </div>
188 188 <img class="gravatar" src="https://secure.gravatar.com/avatar/8f6dc00dce79d6bd7d415be5cea6a008?d=identicon&amp;s=32" height="16" width="16">
189 189 <span class="user"> <a href="/_profiles/jenkins-tests">lisaq (Lisa Quatmann)</a> (reviewer)</span>
190 190 </div>
191 191 </li>
192 192 <li id="reviewer_49" class="collapsable-content" data-toggle="reviewers">
193 193 <div class="reviewers_member">
194 194 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
195 195 <div class="flag_status approved pull-left reviewer_member_status"></div>
196 196 </div>
197 197 <img class="gravatar" src="https://secure.gravatar.com/avatar/89f722927932a8f737a0feafb03a606e?d=identicon&amp;s=32" height="16" width="16">
198 198 <span class="user"> <a href="/_profiles/jenkins-tests">paris (Paris Kolios)</a> (reviewer)</span>
199 199 </div>
200 200 </li>
201 201 <li id="reviewer_50" class="collapsable-content" data-toggle="reviewers">
202 202 <div class="reviewers_member">
203 203 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
204 204 <div class="flag_status approved pull-left reviewer_member_status"></div>
205 205 </div>
206 206 <img class="gravatar" src="https://secure.gravatar.com/avatar/081322c975e8545ec269372405fbd016?d=identicon&amp;s=32" height="16" width="16">
207 207 <span class="user"> <a href="/_profiles/jenkins-tests">ergo (Marcin Lulek)</a> (reviewer)</span>
208 208 </div>
209 209 </li>
210 210 <li id="reviewer_54" class="collapsable-content" data-toggle="reviewers">
211 211 <div class="reviewers_member">
212 212 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
213 213 <div class="flag_status under_review pull-left reviewer_member_status"></div>
214 214 </div>
215 215 <img class="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=32" height="16" width="16">
216 216 <span class="user"> <a href="/_profiles/jenkins-tests">anderson (Anderson Santos)</a> (reviewer)</span>
217 217 </div>
218 218 </li>
219 219 <li id="reviewer_57" class="collapsable-content" data-toggle="reviewers">
220 220 <div class="reviewers_member">
221 221 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
222 222 <div class="flag_status approved pull-left reviewer_member_status"></div>
223 223 </div>
224 224 <img class="gravatar" src="https://secure.gravatar.com/avatar/23e2ee8f5fd462cba8129a40cc1e896c?d=identicon&amp;s=32" height="16" width="16">
225 225 <span class="user"> <a href="/_profiles/jenkins-tests">gmgauthier (Greg Gauthier)</a> (reviewer)</span>
226 226 </div>
227 227 </li>
228 228 <li id="reviewer_31" class="collapsable-content" data-toggle="reviewers">
229 229 <div class="reviewers_member">
230 230 <div class="reviewer_status tooltip pull-left" title="Not Reviewed">
231 231 <div class="flag_status under_review pull-left reviewer_member_status"></div>
232 232 </div>
233 233 <img class="gravatar" src="https://secure.gravatar.com/avatar/0c9a7e6674b6f0b35d98dbe073e3f0ab?d=identicon&amp;s=32" height="16" width="16">
234 234 <span class="user"> <a href="/_profiles/jenkins-tests">ostrobel (Oliver Strobel)</a> (reviewer)</span>
235 235 </div>
236 236 </li>
237 237 </ul>
238 238 <div id="add_reviewer_input" class="ac" style="display: none;">
239 239 </div>
240 240 </div>
241 241 </div>
242 242 </div>
243 243 </div>
244 244 <div class="box">
245 245 <div class="table" >
246 246 <div id="changeset_compare_view_content">
247 247 <div class="compare_view_commits_title">
248 248 <h2>Compare View: 6 commits<span class="btn-collapse" data-toggle="commits">Show More</span></h2>
249 249
250 250 </div>
251 251 <div class="container">
252 252
253 253
254 254 <table class="rctable compare_view_commits">
255 255 <tr>
256 256 <th>Time</th>
257 257 <th>Author</th>
258 258 <th>Commit</th>
259 259 <th></th>
260 260 <th>Title</th>
261 261 </tr>
262 262 <tr id="row-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" commit_id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="compare_select">
263 263 <td class="td-time">
264 264 <span class="tooltip" title="3 hours and 23 minutes ago" tt_title="3 hours and 23 minutes ago">2015-02-18 10:13:34</span>
265 265 </td>
266 266 <td class="td-user">
267 267 <div class="gravatar_with_user">
268 268 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
269 269 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
270 270 </div>
271 271 </td>
272 272 <td class="td-hash">
273 273 <code>
274 274 <a href="/brian/documentation-rep/changeset/7e83e5cd7812dd9e055ce30e77c65cdc08154b43">r395:7e83e5cd7812</a>
275 275 </code>
276 276 </td>
277 277 <td class="expand_commit" data-commit-id="7e83e5cd7812dd9e055ce30e77c65cdc08154b43" title="Expand commit message">
278 278 <div class="show_more_col">
279 279 <i class="show_more"></i>
280 280 </div>
281 281 </td>
282 282 <td class="mid td-description">
283 283 <div class="log-container truncate-wrap">
284 284 <div id="c-7e83e5cd7812dd9e055ce30e77c65cdc08154b43" class="message truncate">rep: added how we doc to guide</div>
285 285 </div>
286 286 </td>
287 287 </tr>
288 288 <tr id="row-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" commit_id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="compare_select">
289 289 <td class="td-time">
290 290 <span class="tooltip" title="4 hours and 18 minutes ago">2015-02-18 09:18:31</span>
291 291 </td>
292 292 <td class="td-user">
293 293 <div class="gravatar_with_user">
294 294 <img class="gravatar" alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=16">
295 295 <span title="Lolek Santos <lolek@rhodecode.com>" class="user">brian (Brian Butler)</span>
296 296 </div>
297 297 </td>
298 298 <td class="td-hash">
299 299 <code>
300 300 <a href="/brian/documentation-rep/changeset/48ce1581bdb3aa7679c246cbdd3fb030623f5c87">r394:48ce1581bdb3</a>
301 301 </code>
302 302 </td>
303 303 <td class="expand_commit" data-commit-id="48ce1581bdb3aa7679c246cbdd3fb030623f5c87" title="Expand commit message">
304 304 <div class="show_more_col">
305 305 <i class="show_more"></i>
306 306 </div>
307 307 </td>
308 308 <td class="mid td-description">
309 309 <div class="log-container truncate-wrap">
310 310 <div id="c-48ce1581bdb3aa7679c246cbdd3fb030623f5c87" class="message truncate">repo 0004 - typo</div>
311 311 </div>
312 312 </td>
313 313 </tr>
314 314 <tr id="row-982d857aafb4c71e7686e419c32b71c9a837257d" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d" class="compare_select collapsable-content" data-toggle="commits">
315 315 <td class="td-time">
316 316 <span class="tooltip" title="4 hours and 22 minutes ago">2015-02-18 09:14:45</span>
317 317 </td>
318 318 <td class="td-user">
319 319 <span class="gravatar" commit_id="982d857aafb4c71e7686e419c32b71c9a837257d">
320 320 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
321 321 </span>
322 322 <span class="author">brian (Brian Butler)</span>
323 323 </td>
324 324 <td class="td-hash">
325 325 <code>
326 326 <a href="/brian/documentation-rep/changeset/982d857aafb4c71e7686e419c32b71c9a837257d">r393:982d857aafb4</a>
327 327 </code>
328 328 </td>
329 329 <td class="expand_commit" data-commit-id="982d857aafb4c71e7686e419c32b71c9a837257d" title="Expand commit message">
330 330 <div class="show_more_col">
331 331 <i class="show_more"></i>
332 332 </div>
333 333 </td>
334 334 <td class="mid td-description">
335 335 <div class="log-container truncate-wrap">
336 336 <div id="c-982d857aafb4c71e7686e419c32b71c9a837257d" class="message truncate">internals: how to doc section added</div>
337 337 </div>
338 338 </td>
339 339 </tr>
340 340 <tr id="row-4c7258ad1af6dae91bbaf87a933e3597e676fab8" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="compare_select collapsable-content" data-toggle="commits">
341 341 <td class="td-time">
342 342 <span class="tooltip" title="20 hours and 16 minutes ago">2015-02-17 17:20:44</span>
343 343 </td>
344 344 <td class="td-user">
345 345 <span class="gravatar" commit_id="4c7258ad1af6dae91bbaf87a933e3597e676fab8">
346 346 <img alt="gravatar" src="https://secure.gravatar.com/avatar/02cc31cea73b88b7209ba302c5967a9d?d=identicon&amp;s=28" height="14" width="14">
347 347 </span>
348 348 <span class="author">brian (Brian Butler)</span>
349 349 </td>
350 350 <td class="td-hash">
351 351 <code>
352 352 <a href="/brian/documentation-rep/changeset/4c7258ad1af6dae91bbaf87a933e3597e676fab8">r392:4c7258ad1af6</a>
353 353 </code>
354 354 </td>
355 355 <td class="expand_commit" data-commit-id="4c7258ad1af6dae91bbaf87a933e3597e676fab8" title="Expand commit message">
356 356 <div class="show_more_col">
357 357 <i class="show_more"></i>
358 358 </div>
359 359 </td>
360 360 <td class="mid td-description">
361 361 <div class="log-container truncate-wrap">
362 362 <div id="c-4c7258ad1af6dae91bbaf87a933e3597e676fab8" class="message truncate">REP: 0004 Documentation standards</div>
363 363 </div>
364 364 </td>
365 365 </tr>
366 366 <tr id="row-46b3d50315f0f2b1f64485ac95af4f384948f9cb" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="compare_select collapsable-content" data-toggle="commits">
367 367 <td class="td-time">
368 368 <span class="tooltip" title="18 hours and 19 minutes ago">2015-02-17 16:18:49</span>
369 369 </td>
370 370 <td class="td-user">
371 371 <span class="gravatar" commit_id="46b3d50315f0f2b1f64485ac95af4f384948f9cb">
372 372 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
373 373 </span>
374 374 <span class="author">anderson (Anderson Santos)</span>
375 375 </td>
376 376 <td class="td-hash">
377 377 <code>
378 378 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/46b3d50315f0f2b1f64485ac95af4f384948f9cb">r8743:46b3d50315f0</a>
379 379 </code>
380 380 </td>
381 381 <td class="expand_commit" data-commit-id="46b3d50315f0f2b1f64485ac95af4f384948f9cb" title="Expand commit message">
382 382 <div class="show_more_col">
383 383 <i class="show_more" ></i>
384 384 </div>
385 385 </td>
386 386 <td class="mid td-description">
387 387 <div class="log-container truncate-wrap">
388 388 <div id="c-46b3d50315f0f2b1f64485ac95af4f384948f9cb" class="message truncate">Diff: created tests for the diff with filenames with spaces</div>
389 389
390 390 </div>
391 391 </td>
392 392 </tr>
393 393 <tr id="row-1e57d2549bd6c34798075bf05ac39f708bb33b90" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90" class="compare_select collapsable-content" data-toggle="commits">
394 394 <td class="td-time">
395 395 <span class="tooltip" title="2 days ago">2015-02-16 10:06:08</span>
396 396 </td>
397 397 <td class="td-user">
398 398 <span class="gravatar" commit_id="1e57d2549bd6c34798075bf05ac39f708bb33b90">
399 399 <img alt="gravatar" src="https://secure.gravatar.com/avatar/72706ebd30734451af9ff3fb59f05ff1?d=identicon&amp;s=28" height="14" width="14">
400 400 </span>
401 401 <span class="author">anderson (Anderson Santos)</span>
402 402 </td>
403 403 <td class="td-hash">
404 404 <code>
405 405 <a href="/andersonsantos/rhodecode-momentum-fork/changeset/1e57d2549bd6c34798075bf05ac39f708bb33b90">r8742:1e57d2549bd6</a>
406 406 </code>
407 407 </td>
408 408 <td class="expand_commit" data-commit-id="1e57d2549bd6c34798075bf05ac39f708bb33b90" title="Expand commit message">
409 409 <div class="show_more_col">
410 410 <i class="show_more" ></i>
411 411 </div>
412 412 </td>
413 413 <td class="mid td-description">
414 414 <div class="log-container truncate-wrap">
415 415 <div id="c-1e57d2549bd6c34798075bf05ac39f708bb33b90" class="message truncate">Diff: fix renaming files with spaces <a class="issue-tracker-link" href="http://bugs.rhodecode.com/issues/574">#574</a></div>
416 416
417 417 </div>
418 418 </td>
419 419 </tr>
420 420 </table>
421 421 </div>
422 422
423 423 <script>
424 424 $('.expand_commit').on('click',function(e){
425 425 $(this).children('i').hide();
426 426 var cid = $(this).data('commitId');
427 427 $('#c-'+cid).css({'height': 'auto', 'margin': '.65em 1em .65em 0','white-space': 'pre-line', 'text-overflow': 'initial', 'overflow':'visible'})
428 428 $('#t-'+cid).css({'height': 'auto', 'text-overflow': 'initial', 'overflow':'visible', 'white-space':'normal'})
429 429 });
430 430 $('.compare_select').on('click',function(e){
431 431 var cid = $(this).attr('commit_id');
432 432 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
433 433 });
434 434 </script>
435 435 <div class="cs_files_title">
436 436 <span class="cs_files_expand">
437 437 <span id="expand_all_files">Expand All</span> | <span id="collapse_all_files">Collapse All</span>
438 438 </span>
439 439 <h2>
440 440 7 files changed: 55 inserted, 9 deleted
441 441 </h2>
442 442 </div>
443 443 <div class="cs_files">
444 444 <table class="compare_view_files">
445 445
446 446 <tr class="cs_A expand_file" fid="c--efbe5b7a3f13">
447 447 <td class="cs_icon_td">
448 448 <span class="expand_file_icon" fid="c--efbe5b7a3f13"></span>
449 449 </td>
450 450 <td class="cs_icon_td">
451 451 <div class="flag_status not_reviewed hidden"></div>
452 452 </td>
453 453 <td id="a_c--efbe5b7a3f13">
454 454 <a class="compare_view_filepath" href="#a_c--efbe5b7a3f13">
455 455 rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff
456 456 </a>
457 457 <span id="diff_c--efbe5b7a3f13" class="diff_links" style="display: none;">
458 458 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
459 459 Unified Diff
460 460 </a>
461 461 |
462 462 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
463 463 Side-by-side Diff
464 464 </a>
465 465 </span>
466 466 </td>
467 467 <td>
468 468 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">4</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
469 469 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff">
470 470 <i class="icon-comment"></i>
471 471 </div>
472 472 </td>
473 473 </tr>
474 474 <tr id="tr_c--efbe5b7a3f13">
475 475 <td></td>
476 476 <td></td>
477 477 <td class="injected_diff" colspan="2">
478 478
479 479 <div class="diff-container" id="diff-container-140716195039928">
480 480 <div id="c--efbe5b7a3f13_target" ></div>
481 481 <div id="c--efbe5b7a3f13" class="diffblock margined comm" >
482 482 <div class="code-body">
483 483 <div class="full_f_path" path="rhodecode/tests/fixtures/git_diff_rename_file_with_spaces.diff" style="display: none;"></div>
484 484 <table class="code-difftable">
485 485 <tr class="line context">
486 486 <td class="add-comment-line"><span class="add-comment-content"></span></td>
487 487 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
488 488 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n"></a></td>
489 489 <td class="code no-comment">
490 490 <pre>new file 100644</pre>
491 491 </td>
492 492 </tr>
493 493 <tr class="line add">
494 494 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
495 495 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
496 496 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n1">1</a></td>
497 497 <td class="code">
498 498 <pre>diff --git a/file_with_ spaces.txt b/file_with_ two spaces.txt
499 499 </pre>
500 500 </td>
501 501 </tr>
502 502 <tr class="line add">
503 503 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
504 504 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
505 505 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n2">2</a></td>
506 506 <td class="code">
507 507 <pre>similarity index 100%
508 508 </pre>
509 509 </td>
510 510 </tr>
511 511 <tr class="line add">
512 512 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
513 513 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
514 514 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n3">3</a></td>
515 515 <td class="code">
516 516 <pre>rename from file_with_ spaces.txt
517 517 </pre>
518 518 </td>
519 519 </tr>
520 520 <tr class="line add">
521 521 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
522 522 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o"></a></td>
523 523 <td id="rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4" class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n4">4</a></td>
524 524 <td class="code">
525 525 <pre>rename to file_with_ two spaces.txt
526 526 </pre>
527 527 </td>
528 528 </tr>
529 529 <tr class="line context">
530 530 <td class="add-comment-line"><span class="add-comment-content"></span></td>
531 531 <td class="lineno old"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_o...">...</a></td>
532 532 <td class="lineno new"><a href="#rhodecodetestsfixturesgit_diff_rename_file_with_spacesdiff_n...">...</a></td>
533 533 <td class="code no-comment">
534 534 <pre> No newline at end of file</pre>
535 535 </td>
536 536 </tr>
537 537 </table>
538 538 </div>
539 539 </div>
540 540 </div>
541 541
542 542 </td>
543 543 </tr>
544 544 <tr class="cs_A expand_file" fid="c--c21377f778f9">
545 545 <td class="cs_icon_td">
546 546 <span class="expand_file_icon" fid="c--c21377f778f9"></span>
547 547 </td>
548 548 <td class="cs_icon_td">
549 549 <div class="flag_status not_reviewed hidden"></div>
550 550 </td>
551 551 <td id="a_c--c21377f778f9">
552 552 <a class="compare_view_filepath" href="#a_c--c21377f778f9">
553 553 rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff
554 554 </a>
555 555 <span id="diff_c--c21377f778f9" class="diff_links" style="display: none;">
556 556 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
557 557 Unified Diff
558 558 </a>
559 559 |
560 560 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
561 561 Side-by-side Diff
562 562 </a>
563 563 </span>
564 564 </td>
565 565 <td>
566 566 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
567 567 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff">
568 568 <i class="icon-comment"></i>
569 569 </div>
570 570 </td>
571 571 </tr>
572 572 <tr id="tr_c--c21377f778f9">
573 573 <td></td>
574 574 <td></td>
575 575 <td class="injected_diff" colspan="2">
576 576
577 577 <div class="diff-container" id="diff-container-140716195038344">
578 578 <div id="c--c21377f778f9_target" ></div>
579 579 <div id="c--c21377f778f9" class="diffblock margined comm" >
580 580 <div class="code-body">
581 581 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_copy_file_with_spaces.diff" style="display: none;"></div>
582 582 <table class="code-difftable">
583 583 <tr class="line context">
584 584 <td class="add-comment-line"><span class="add-comment-content"></span></td>
585 585 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
586 586 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n"></a></td>
587 587 <td class="code no-comment">
588 588 <pre>new file 100644</pre>
589 589 </td>
590 590 </tr>
591 591 <tr class="line add">
592 592 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
593 593 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
594 594 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n1">1</a></td>
595 595 <td class="code">
596 596 <pre>diff --git a/file_changed_without_spaces.txt b/file_copied_ with spaces.txt
597 597 </pre>
598 598 </td>
599 599 </tr>
600 600 <tr class="line add">
601 601 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
602 602 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
603 603 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n2">2</a></td>
604 604 <td class="code">
605 605 <pre>copy from file_changed_without_spaces.txt
606 606 </pre>
607 607 </td>
608 608 </tr>
609 609 <tr class="line add">
610 610 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
611 611 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o"></a></td>
612 612 <td id="rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n3">3</a></td>
613 613 <td class="code">
614 614 <pre>copy to file_copied_ with spaces.txt
615 615 </pre>
616 616 </td>
617 617 </tr>
618 618 <tr class="line context">
619 619 <td class="add-comment-line"><span class="add-comment-content"></span></td>
620 620 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_o...">...</a></td>
621 621 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_copy_file_with_spacesdiff_n...">...</a></td>
622 622 <td class="code no-comment">
623 623 <pre> No newline at end of file</pre>
624 624 </td>
625 625 </tr>
626 626 </table>
627 627 </div>
628 628 </div>
629 629 </div>
630 630
631 631 </td>
632 632 </tr>
633 633 <tr class="cs_A expand_file" fid="c--ee62085ad7a8">
634 634 <td class="cs_icon_td">
635 635 <span class="expand_file_icon" fid="c--ee62085ad7a8"></span>
636 636 </td>
637 637 <td class="cs_icon_td">
638 638 <div class="flag_status not_reviewed hidden"></div>
639 639 </td>
640 640 <td id="a_c--ee62085ad7a8">
641 641 <a class="compare_view_filepath" href="#a_c--ee62085ad7a8">
642 642 rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff
643 643 </a>
644 644 <span id="diff_c--ee62085ad7a8" class="diff_links" style="display: none;">
645 645 <a href="/andersonsantos/rhodecode-momentum-fork/diff/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
646 646 Unified Diff
647 647 </a>
648 648 |
649 649 <a href="/andersonsantos/rhodecode-momentum-fork/diff-2way/rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff?diff2=46b3d50315f0f2b1f64485ac95af4f384948f9cb&amp;diff1=b78e2376b986b2cf656a2b4390b09f303291c886&amp;fulldiff=1&amp;diff=diff">
650 650 Side-by-side Diff
651 651 </a>
652 652 </span>
653 653 </td>
654 654 <td>
655 655 <div class="changes pull-right"><div style="width:100px"><div class="added top-right-rounded-corner-mid bottom-right-rounded-corner-mid top-left-rounded-corner-mid bottom-left-rounded-corner-mid" style="width:100.0%">3</div><div class="deleted top-right-rounded-corner-mid bottom-right-rounded-corner-mid" style="width:0%"></div></div></div>
656 656 <div class="comment-bubble pull-right" data-path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff">
657 657 <i class="icon-comment"></i>
658 658 </div>
659 659 </td>
660 660 </tr>
661 661 <tr id="tr_c--ee62085ad7a8">
662 662 <td></td>
663 663 <td></td>
664 664 <td class="injected_diff" colspan="2">
665 665
666 666 <div class="diff-container" id="diff-container-140716195039496">
667 667 <div id="c--ee62085ad7a8_target" ></div>
668 668 <div id="c--ee62085ad7a8" class="diffblock margined comm" >
669 669 <div class="code-body">
670 670 <div class="full_f_path" path="rhodecode/tests/fixtures/hg_diff_rename_file_with_spaces.diff" style="display: none;"></div>
671 671 <table class="code-difftable">
672 672 <tr class="line context">
673 673 <td class="add-comment-line"><span class="add-comment-content"></span></td>
674 674 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
675 675 <td class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n"></a></td>
676 676 <td class="code no-comment">
677 677 <pre>new file 100644</pre>
678 678 </td>
679 679 </tr>
680 680 <tr class="line add">
681 681 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
682 682 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
683 683 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n1">1</a></td>
684 684 <td class="code">
685 685 <pre>diff --git a/file_ with update.txt b/file_changed _.txt
686 686 </pre>
687 687 </td>
688 688 </tr>
689 689 <tr class="line add">
690 690 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
691 691 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
692 692 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n2">2</a></td>
693 693 <td class="code">
694 694 <pre>rename from file_ with update.txt
695 695 </pre>
696 696 </td>
697 697 </tr>
698 698 <tr class="line add">
699 699 <td class="add-comment-line"><span class="add-comment-content"><a href="#"><span class="icon-comment-add"></span></a></span></td>
700 700 <td class="lineno old"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_o"></a></td>
701 701 <td id="rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3" class="lineno new"><a href="#rhodecodetestsfixtureshg_diff_rename_file_with_spacesdiff_n3">3</a></td>
702 702 <td class="code">
703 703 <pre>rename to file_changed _.txt</pre>
704 704 </td>
705 705 </tr>
706 706 </table>
707 707 </div>
708 708 </div>
709 709 </div>
710 710
711 711 </td>
712 712 </tr>
713 713
714 714 </table>
715 715 </div>
716 716 </div>
717 717 </div>
718 718
719 719 </td>
720 720 </tr>
721 721 </table>
722 722 </div>
723 723 </div>
724 724 </div>
725 725
726 726
727 727
728 728
729 729 <div id="comment-inline-form-template" style="display: none;">
730 730 <div class="comment-inline-form ac">
731 731 <div class="overlay"><div class="overlay-text">Submitting...</div></div>
732 732 <form action="#" class="inline-form" method="get">
733 733 <div id="edit-container_{1}" class="clearfix">
734 734 <div class="comment-title pull-left">
735 735 Commenting on line {1}.
736 736 </div>
737 737 <div class="comment-help pull-right">
738 738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 739 </div>
740 740 <div style="clear: both"></div>
741 741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
742 742 </div>
743 743 <div id="preview-container_{1}" class="clearfix" style="display: none;">
744 744 <div class="comment-help">
745 745 Comment preview
746 746 </div>
747 747 <div id="preview-box_{1}" class="preview-box"></div>
748 748 </div>
749 749 <div class="comment-button pull-right">
750 750 <input type="hidden" name="f_path" value="{0}">
751 751 <input type="hidden" name="line" value="{1}">
752 752 <div id="preview-btn_{1}" class="btn btn-default">Preview</div>
753 753 <div id="edit-btn_{1}" class="btn" style="display: none;">Edit</div>
754 754 <input class="btn btn-success save-inline-form" id="save" name="save" type="submit" value="Comment" />
755 755 </div>
756 756 <div class="comment-button hide-inline-form-button">
757 757 <input class="btn hide-inline-form" id="hide-inline-form" name="hide-inline-form" type="reset" value="Cancel" />
758 758 </div>
759 759 </form>
760 760 </div>
761 761 </div>
762 762
763 763
764 764
765 765 <div class="comments">
766 766 <div id="inline-comments-container">
767 767
768 768 <h2>0 Pull Request Comments</h2>
769 769
770 770
771 771 </div>
772 772
773 773 </div>
774 774
775 775
776 776
777 777
778 778 <div class="pull-request-merge">
779 779 </div>
780 780 <div class="comments">
781 781 <div class="comment-form ac">
782 782 <form action="/rhodecode-momentum/pull-request-comment/720" id="comments_form" method="POST">
783 783 <div style="display: none;"><input id="csrf_token" name="csrf_token" type="hidden" value="6dbc0b19ac65237df65d57202a3e1f2df4153e38" /></div>
784 784 <div id="edit-container" class="clearfix">
785 785 <div class="comment-title pull-left">
786 786 Create a comment on this Pull Request.
787 787 </div>
788 788 <div class="comment-help pull-right">
789 789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 790 </div>
791 791 <div style="clear: both"></div>
792 792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
793 793 </div>
794 794
795 795 <div id="preview-container" class="clearfix" style="display: none;">
796 796 <div class="comment-title">
797 797 Comment preview
798 798 </div>
799 799 <div id="preview-box" class="preview-box"></div>
800 800 </div>
801 801
802 802 <div id="comment_form_extras">
803 803 </div>
804 804 <div class="action-button pull-right">
805 805 <div id="preview-btn" class="btn">
806 806 Preview
807 807 </div>
808 808 <div id="edit-btn" class="btn" style="display: none;">
809 809 Edit
810 810 </div>
811 811 <div class="comment-button">
812 812 <input class="btn btn-small btn-success comment-button-input" id="save" name="save" type="submit" value="Comment" />
813 813 </div>
814 814 </div>
815 815 </form>
816 816 </div>
817 817 </div>
818 818 <script>
819 819
820 820 $(document).ready(function() {
821 821
822 822 var cm = initCommentBoxCodeMirror('#text');
823 823
824 824 // main form preview
825 825 $('#preview-btn').on('click', function(e) {
826 826 $('#preview-btn').hide();
827 827 $('#edit-btn').show();
828 828 var _text = cm.getValue();
829 829 if (!_text) {
830 830 return;
831 831 }
832 832 var post_data = {
833 833 'text': _text,
834 834 'renderer': DEFAULT_RENDERER,
835 835 'csrf_token': CSRF_TOKEN
836 836 };
837 837 var previewbox = $('#preview-box');
838 838 previewbox.addClass('unloaded');
839 839 previewbox.html(_gettext('Loading ...'));
840 840 $('#edit-container').hide();
841 841 $('#preview-container').show();
842 842
843 843 var url = pyroutes.url('changeset_comment_preview', {'repo_name': 'rhodecode-momentum'});
844 844
845 845 ajaxPOST(url, post_data, function(o) {
846 846 previewbox.html(o);
847 847 previewbox.removeClass('unloaded');
848 848 });
849 849 });
850 850 $('#edit-btn').on('click', function(e) {
851 851 $('#preview-btn').show();
852 852 $('#edit-btn').hide();
853 853 $('#edit-container').show();
854 854 $('#preview-container').hide();
855 855 });
856 856
857 857 var formatChangeStatus = function(state, escapeMarkup) {
858 858 var originalOption = state.element;
859 859 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
860 860 '<span>' + escapeMarkup(state.text) + '</span>';
861 861 };
862 862
863 863 var formatResult = function(result, container, query, escapeMarkup) {
864 864 return formatChangeStatus(result, escapeMarkup);
865 865 };
866 866
867 867 var formatSelection = function(data, container, escapeMarkup) {
868 868 return formatChangeStatus(data, escapeMarkup);
869 869 };
870 870
871 871 $('#change_status_general').select2({
872 872 placeholder: "Status Review",
873 873 formatResult: formatResult,
874 874 formatSelection: formatSelection,
875 875 containerCssClass: "drop-menu status_box_menu",
876 876 dropdownCssClass: "drop-menu-dropdown",
877 877 dropdownAutoWidth: true,
878 878 minimumResultsForSearch: -1
879 879 });
880 880 });
881 881 </script>
882 882
883 883
884 884 <script type="text/javascript">
885 885 // TODO: switch this to pyroutes
886 886 AJAX_COMMENT_DELETE_URL = "/rhodecode-momentum/pull-request-comment/__COMMENT_ID__/delete";
887 887
888 888 $(function(){
889 ReviewerAutoComplete('user');
889 ReviewerAutoComplete('#user');
890 890
891 891 $('#open_edit_reviewers').on('click', function(e){
892 892 $('#open_edit_reviewers').hide();
893 893 $('#close_edit_reviewers').show();
894 894 $('#add_reviewer_input').show();
895 895 $('.reviewer_member_remove').css('visibility', 'visible');
896 896 });
897 897
898 898 $('#close_edit_reviewers').on('click', function(e){
899 899 $('#open_edit_reviewers').show();
900 900 $('#close_edit_reviewers').hide();
901 901 $('#add_reviewer_input').hide();
902 902 $('.reviewer_member_remove').css('visibility', 'hidden');
903 903 });
904 904
905 905 $('.show-inline-comments').on('change', function(e){
906 906 var show = 'none';
907 907 var target = e.currentTarget;
908 908 if(target.checked){
909 909 show = ''
910 910 }
911 911 var boxid = $(target).attr('id_for');
912 912 var comments = $('#{0} .inline-comments'.format(boxid));
913 913 var fn_display = function(idx){
914 914 $(this).css('display', show);
915 915 };
916 916 $(comments).each(fn_display);
917 917 var btns = $('#{0} .inline-comments-button'.format(boxid));
918 918 $(btns).each(fn_display);
919 919 });
920 920
921 921 var commentTotals = {};
922 922 $.each(file_comments, function(i, comment) {
923 923 var path = $(comment).attr('path');
924 924 var comms = $(comment).children().length;
925 925 if (path in commentTotals) {
926 926 commentTotals[path] += comms;
927 927 } else {
928 928 commentTotals[path] = comms;
929 929 }
930 930 });
931 931 $.each(commentTotals, function(path, total) {
932 932 var elem = $('.comment-bubble[data-path="'+ path +'"]')
933 933 elem.css('visibility', 'visible');
934 934 elem.html(elem.html() + ' ' + total );
935 935 });
936 936
937 937 $('#merge_pull_request_form').submit(function() {
938 938 if (!$('#merge_pull_request').attr('disabled')) {
939 939 $('#merge_pull_request').attr('disabled', 'disabled');
940 940 }
941 941 return true;
942 942 });
943 943
944 944 $('#update_pull_request').on('click', function(e){
945 945 updateReviewers(undefined, "rhodecode-momentum", "720");
946 946 });
947 947
948 948 $('#update_commits').on('click', function(e){
949 949 updateCommits("rhodecode-momentum", "720");
950 950 });
951 951
952 952 $('#close_pull_request').on('click', function(e){
953 953 closePullRequest("rhodecode-momentum", "720");
954 954 });
955 955 })
956 956 </script>
957 957
958 958 </div>
959 959 </div></div>
960 960
961 961 </div>
962 962
963 963
964 964 </%def>
@@ -1,591 +1,591 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 27 <div class="box pr-summary">
28 28
29 29 <div class="summary-details block-left">
30 30
31 31 <div class="form">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 43 </div>
44 44
45 45 <div class="field">
46 46 <div class="label label-textarea">
47 47 <label for="pullrequest_desc">${_('Description')}:</label>
48 48 </div>
49 49 <div class="textarea text-area editor">
50 50 ${h.textarea('pullrequest_desc',size=30, )}
51 51 <span class="help-block">${_('Write a short description on this pull request')}</span>
52 52 </div>
53 53 </div>
54 54
55 55 <div class="field">
56 56 <div class="label label-textarea">
57 57 <label for="pullrequest_desc">${_('Commit flow')}:</label>
58 58 </div>
59 59
60 60 ## TODO: johbo: Abusing the "content" class here to get the
61 61 ## desired effect. Should be replaced by a proper solution.
62 62
63 63 ##ORG
64 64 <div class="content">
65 65 <strong>${_('Origin repository')}:</strong>
66 66 ${c.rhodecode_db_repo.description}
67 67 </div>
68 68 <div class="content">
69 69 ${h.hidden('source_repo')}
70 70 ${h.hidden('source_ref')}
71 71 </div>
72 72
73 73 ##OTHER, most Probably the PARENT OF THIS FORK
74 74 <div class="content">
75 75 ## filled with JS
76 76 <div id="target_repo_desc"></div>
77 77 </div>
78 78
79 79 <div class="content">
80 80 ${h.hidden('target_repo')}
81 81 ${h.hidden('target_ref')}
82 82 <span id="target_ref_loading" style="display: none">
83 83 ${_('Loading refs...')}
84 84 </span>
85 85 </div>
86 86 </div>
87 87
88 88 <div class="field">
89 89 <div class="label label-textarea">
90 90 <label for="pullrequest_submit"></label>
91 91 </div>
92 92 <div class="input">
93 93 <div class="pr-submit-button">
94 94 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
95 95 </div>
96 96 <div id="pr_open_message"></div>
97 97 </div>
98 98 </div>
99 99
100 100 <div class="pr-spacing-container"></div>
101 101 </div>
102 102 </div>
103 103 </div>
104 104 <div>
105 105 <div class="reviewers-title block-right">
106 106 <div class="pr-details-title">
107 107 ${_('Pull request reviewers')}
108 108 <span class="calculate-reviewers"> - ${_('loading...')}</span>
109 109 </div>
110 110 </div>
111 111 <div id="reviewers" class="block-right pr-details-content reviewers">
112 112 ## members goes here, filled via JS based on initial selection !
113 113 <input type="hidden" name="__start__" value="review_members:sequence">
114 114 <ul id="review_members" class="group_members"></ul>
115 115 <input type="hidden" name="__end__" value="review_members:sequence">
116 116 <div id="add_reviewer_input" class='ac'>
117 117 <div class="reviewer_ac">
118 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
118 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
119 119 <div id="reviewers_container"></div>
120 120 </div>
121 121 </div>
122 122 </div>
123 123 </div>
124 124 </div>
125 125 <div class="box">
126 126 <div>
127 127 ## overview pulled by ajax
128 128 <div id="pull_request_overview"></div>
129 129 </div>
130 130 </div>
131 131 ${h.end_form()}
132 132 </div>
133 133
134 134 <script type="text/javascript">
135 135 $(function(){
136 136 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
137 137 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
138 138 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
139 139 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
140 140 var targetRepoName = '${c.repo_name}';
141 141
142 142 var $pullRequestForm = $('#pull_request_form');
143 143 var $sourceRepo = $('#source_repo', $pullRequestForm);
144 144 var $targetRepo = $('#target_repo', $pullRequestForm);
145 145 var $sourceRef = $('#source_ref', $pullRequestForm);
146 146 var $targetRef = $('#target_ref', $pullRequestForm);
147 147
148 148 var calculateContainerWidth = function() {
149 149 var maxWidth = 0;
150 150 var repoSelect2Containers = ['#source_repo', '#target_repo'];
151 151 $.each(repoSelect2Containers, function(idx, value) {
152 152 $(value).select2('container').width('auto');
153 153 var curWidth = $(value).select2('container').width();
154 154 if (maxWidth <= curWidth) {
155 155 maxWidth = curWidth;
156 156 }
157 157 $.each(repoSelect2Containers, function(idx, value) {
158 158 $(value).select2('container').width(maxWidth + 10);
159 159 });
160 160 });
161 161 };
162 162
163 163 var initRefSelection = function(selectedRef) {
164 164 return function(element, callback) {
165 165 // translate our select2 id into a text, it's a mapping to show
166 166 // simple label when selecting by internal ID.
167 167 var id, refData;
168 168 if (selectedRef === undefined) {
169 169 id = element.val();
170 170 refData = element.val().split(':');
171 171 } else {
172 172 id = selectedRef;
173 173 refData = selectedRef.split(':');
174 174 }
175 175
176 176 var text = refData[1];
177 177 if (refData[0] === 'rev') {
178 178 text = text.substring(0, 12);
179 179 }
180 180
181 181 var data = {id: id, text: text};
182 182
183 183 callback(data);
184 184 };
185 185 };
186 186
187 187 var formatRefSelection = function(item) {
188 188 var prefix = '';
189 189 var refData = item.id.split(':');
190 190 if (refData[0] === 'branch') {
191 191 prefix = '<i class="icon-branch"></i>';
192 192 }
193 193 else if (refData[0] === 'book') {
194 194 prefix = '<i class="icon-bookmark"></i>';
195 195 }
196 196 else if (refData[0] === 'tag') {
197 197 prefix = '<i class="icon-tag"></i>';
198 198 }
199 199
200 200 var originalOption = item.element;
201 201 return prefix + item.text;
202 202 };
203 203
204 204 // custom code mirror
205 205 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
206 206
207 207 var queryTargetRepo = function(self, query) {
208 208 // cache ALL results if query is empty
209 209 var cacheKey = query.term || '__';
210 210 var cachedData = self.cachedDataSource[cacheKey];
211 211
212 212 if (cachedData) {
213 213 query.callback({results: cachedData.results});
214 214 } else {
215 215 $.ajax({
216 216 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
217 217 data: {query: query.term},
218 218 dataType: 'json',
219 219 type: 'GET',
220 220 success: function(data) {
221 221 self.cachedDataSource[cacheKey] = data;
222 222 query.callback({results: data.results});
223 223 },
224 224 error: function(data, textStatus, errorThrown) {
225 225 alert(
226 226 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
227 227 }
228 228 });
229 229 }
230 230 };
231 231
232 232 var queryTargetRefs = function(initialData, query) {
233 233 var data = {results: []};
234 234 // filter initialData
235 235 $.each(initialData, function() {
236 236 var section = this.text;
237 237 var children = [];
238 238 $.each(this.children, function() {
239 239 if (query.term.length === 0 ||
240 240 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
241 241 children.push({'id': this.id, 'text': this.text})
242 242 }
243 243 });
244 244 data.results.push({'text': section, 'children': children})
245 245 });
246 246 query.callback({results: data.results});
247 247 };
248 248
249 249
250 250 var prButtonLockChecks = {
251 251 'compare': false,
252 252 'reviewers': false
253 253 };
254 254
255 255 var prButtonLock = function(lockEnabled, msg, scope) {
256 256 scope = scope || 'all';
257 257 if (scope == 'all'){
258 258 prButtonLockChecks['compare'] = !lockEnabled;
259 259 prButtonLockChecks['reviewers'] = !lockEnabled;
260 260 } else if (scope == 'compare') {
261 261 prButtonLockChecks['compare'] = !lockEnabled;
262 262 } else if (scope == 'reviewers'){
263 263 prButtonLockChecks['reviewers'] = !lockEnabled;
264 264 }
265 265 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
266 266 if (lockEnabled) {
267 267 $('#save').attr('disabled', 'disabled');
268 268 }
269 269 else if (checksMeet) {
270 270 $('#save').removeAttr('disabled');
271 271 }
272 272
273 273 if (msg) {
274 274 $('#pr_open_message').html(msg);
275 275 }
276 276 };
277 277
278 278 var loadRepoRefDiffPreview = function() {
279 279 var sourceRepo = $sourceRepo.eq(0).val();
280 280 var sourceRef = $sourceRef.eq(0).val().split(':');
281 281
282 282 var targetRepo = $targetRepo.eq(0).val();
283 283 var targetRef = $targetRef.eq(0).val().split(':');
284 284
285 285 var url_data = {
286 286 'repo_name': targetRepo,
287 287 'target_repo': sourceRepo,
288 288 'source_ref': targetRef[2],
289 289 'source_ref_type': 'rev',
290 290 'target_ref': sourceRef[2],
291 291 'target_ref_type': 'rev',
292 292 'merge': true,
293 293 '_': Date.now() // bypass browser caching
294 294 }; // gather the source/target ref and repo here
295 295
296 296 if (sourceRef.length !== 3 || targetRef.length !== 3) {
297 297 prButtonLock(true, "${_('Please select origin and destination')}");
298 298 return;
299 299 }
300 300 var url = pyroutes.url('compare_url', url_data);
301 301
302 302 // lock PR button, so we cannot send PR before it's calculated
303 303 prButtonLock(true, "${_('Loading compare ...')}", 'compare');
304 304
305 305 if (loadRepoRefDiffPreview._currentRequest) {
306 306 loadRepoRefDiffPreview._currentRequest.abort();
307 307 }
308 308
309 309 loadRepoRefDiffPreview._currentRequest = $.get(url)
310 310 .error(function(data, textStatus, errorThrown) {
311 311 alert(
312 312 "Error while processing request.\nError code {0} ({1}).".format(
313 313 data.status, data.statusText));
314 314 })
315 315 .done(function(data) {
316 316 loadRepoRefDiffPreview._currentRequest = null;
317 317 $('#pull_request_overview').html(data);
318 318 var commitElements = $(data).find('tr[commit_id]');
319 319
320 320 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
321 321 commitElements, 5);
322 322
323 323 var title = prTitleAndDesc[0];
324 324 var proposedDescription = prTitleAndDesc[1];
325 325
326 326 var useGeneratedTitle = (
327 327 $('#pullrequest_title').hasClass('autogenerated-title') ||
328 328 $('#pullrequest_title').val() === "");
329 329
330 330 if (title && useGeneratedTitle) {
331 331 // use generated title if we haven't specified our own
332 332 $('#pullrequest_title').val(title);
333 333 $('#pullrequest_title').addClass('autogenerated-title');
334 334
335 335 }
336 336
337 337 var useGeneratedDescription = (
338 338 !codeMirrorInstance._userDefinedDesc ||
339 339 codeMirrorInstance.getValue() === "");
340 340
341 341 if (proposedDescription && useGeneratedDescription) {
342 342 // set proposed content, if we haven't defined our own,
343 343 // or we don't have description written
344 344 codeMirrorInstance._userDefinedDesc = false; // reset state
345 345 codeMirrorInstance.setValue(proposedDescription);
346 346 }
347 347
348 348 var msg = '';
349 349 if (commitElements.length === 1) {
350 350 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
351 351 } else {
352 352 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
353 353 }
354 354
355 355 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
356 356
357 357 if (commitElements.length) {
358 358 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
359 359 prButtonLock(false, msg.replace('__COMMITS__', commitsLink), 'compare');
360 360 }
361 361 else {
362 362 prButtonLock(true, "${_('There are no commits to merge.')}", 'compare');
363 363 }
364 364
365 365
366 366 });
367 367 };
368 368
369 369 /**
370 370 Generate Title and Description for a PullRequest.
371 371 In case of 1 commits, the title and description is that one commit
372 372 in case of multiple commits, we iterate on them with max N number of commits,
373 373 and build description in a form
374 374 - commitN
375 375 - commitN+1
376 376 ...
377 377
378 378 Title is then constructed from branch names, or other references,
379 379 replacing '-' and '_' into spaces
380 380
381 381 * @param sourceRef
382 382 * @param elements
383 383 * @param limit
384 384 * @returns {*[]}
385 385 */
386 386 var getTitleAndDescription = function(sourceRef, elements, limit) {
387 387 var title = '';
388 388 var desc = '';
389 389
390 390 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
391 391 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
392 392 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
393 393 });
394 394 // only 1 commit, use commit message as title
395 395 if (elements.length == 1) {
396 396 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
397 397 }
398 398 else {
399 399 // use reference name
400 400 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
401 401 }
402 402
403 403 return [title, desc]
404 404 };
405 405
406 406 var Select2Box = function(element, overrides) {
407 407 var globalDefaults = {
408 408 dropdownAutoWidth: true,
409 409 containerCssClass: "drop-menu",
410 410 dropdownCssClass: "drop-menu-dropdown"
411 411 };
412 412
413 413 var initSelect2 = function(defaultOptions) {
414 414 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
415 415 element.select2(options);
416 416 };
417 417
418 418 return {
419 419 initRef: function() {
420 420 var defaultOptions = {
421 421 minimumResultsForSearch: 5,
422 422 formatSelection: formatRefSelection
423 423 };
424 424
425 425 initSelect2(defaultOptions);
426 426 },
427 427
428 428 initRepo: function(defaultValue, readOnly) {
429 429 var defaultOptions = {
430 430 initSelection : function (element, callback) {
431 431 var data = {id: defaultValue, text: defaultValue};
432 432 callback(data);
433 433 }
434 434 };
435 435
436 436 initSelect2(defaultOptions);
437 437
438 438 element.select2('val', defaultSourceRepo);
439 439 if (readOnly === true) {
440 440 element.select2('readonly', true);
441 441 }
442 442 }
443 443 };
444 444 };
445 445
446 446 var initTargetRefs = function(refsData, selectedRef){
447 447 Select2Box($targetRef, {
448 448 query: function(query) {
449 449 queryTargetRefs(refsData, query);
450 450 },
451 451 initSelection : initRefSelection(selectedRef)
452 452 }).initRef();
453 453
454 454 if (!(selectedRef === undefined)) {
455 455 $targetRef.select2('val', selectedRef);
456 456 }
457 457 };
458 458
459 459 var targetRepoChanged = function(repoData) {
460 460 // generate new DESC of target repo displayed next to select
461 461 $('#target_repo_desc').html(
462 462 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
463 463 );
464 464
465 465 // generate dynamic select2 for refs.
466 466 initTargetRefs(repoData['refs']['select2_refs'],
467 467 repoData['refs']['selected_ref']);
468 468
469 469 };
470 470
471 471 var sourceRefSelect2 = Select2Box(
472 472 $sourceRef, {
473 473 placeholder: "${_('Select commit reference')}",
474 474 query: function(query) {
475 475 var initialData = defaultSourceRepoData['refs']['select2_refs'];
476 476 queryTargetRefs(initialData, query)
477 477 },
478 478 initSelection: initRefSelection()
479 479 }
480 480 );
481 481
482 482 var sourceRepoSelect2 = Select2Box($sourceRepo, {
483 483 query: function(query) {}
484 484 });
485 485
486 486 var targetRepoSelect2 = Select2Box($targetRepo, {
487 487 cachedDataSource: {},
488 488 query: $.debounce(250, function(query) {
489 489 queryTargetRepo(this, query);
490 490 }),
491 491 formatResult: formatResult
492 492 });
493 493
494 494 sourceRefSelect2.initRef();
495 495
496 496 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
497 497
498 498 targetRepoSelect2.initRepo(defaultTargetRepo, false);
499 499
500 500 $sourceRef.on('change', function(e){
501 501 loadRepoRefDiffPreview();
502 502 loadDefaultReviewers();
503 503 });
504 504
505 505 $targetRef.on('change', function(e){
506 506 loadRepoRefDiffPreview();
507 507 loadDefaultReviewers();
508 508 });
509 509
510 510 $targetRepo.on('change', function(e){
511 511 var repoName = $(this).val();
512 512 calculateContainerWidth();
513 513 $targetRef.select2('destroy');
514 514 $('#target_ref_loading').show();
515 515
516 516 $.ajax({
517 517 url: pyroutes.url('pullrequest_repo_refs',
518 518 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
519 519 data: {},
520 520 dataType: 'json',
521 521 type: 'GET',
522 522 success: function(data) {
523 523 $('#target_ref_loading').hide();
524 524 targetRepoChanged(data);
525 525 loadRepoRefDiffPreview();
526 526 },
527 527 error: function(data, textStatus, errorThrown) {
528 528 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
529 529 }
530 530 })
531 531
532 532 });
533 533
534 534 var loadDefaultReviewers = function() {
535 535 if (loadDefaultReviewers._currentRequest) {
536 536 loadDefaultReviewers._currentRequest.abort();
537 537 }
538 538 $('.calculate-reviewers').show();
539 539 prButtonLock(true, null, 'reviewers');
540 540
541 541 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
542 542
543 543 var sourceRepo = $sourceRepo.eq(0).val();
544 544 var sourceRef = $sourceRef.eq(0).val().split(':');
545 545 var targetRepo = $targetRepo.eq(0).val();
546 546 var targetRef = $targetRef.eq(0).val().split(':');
547 547 url += '?source_repo=' + sourceRepo;
548 548 url += '&source_ref=' + sourceRef[2];
549 549 url += '&target_repo=' + targetRepo;
550 550 url += '&target_ref=' + targetRef[2];
551 551
552 552 loadDefaultReviewers._currentRequest = $.get(url)
553 553 .done(function(data) {
554 554 loadDefaultReviewers._currentRequest = null;
555 555
556 556 // reset && add the reviewer based on selected repo
557 557 $('#review_members').html('');
558 558 for (var i = 0; i < data.reviewers.length; i++) {
559 559 var reviewer = data.reviewers[i];
560 560 addReviewMember(
561 561 reviewer.user_id, reviewer.firstname,
562 562 reviewer.lastname, reviewer.username,
563 563 reviewer.gravatar_link, reviewer.reasons);
564 564 }
565 565 $('.calculate-reviewers').hide();
566 566 prButtonLock(false, null, 'reviewers');
567 567 });
568 568 };
569 569
570 570 prButtonLock(true, "${_('Please select origin and destination')}", 'all');
571 571
572 572 // auto-load on init, the target refs select2
573 573 calculateContainerWidth();
574 574 targetRepoChanged(defaultTargetRepoData);
575 575
576 576 $('#pullrequest_title').on('keyup', function(e){
577 577 $(this).removeClass('autogenerated-title');
578 578 });
579 579
580 580 % if c.default_source_ref:
581 581 // in case we have a pre-selected value, use it now
582 582 $sourceRef.select2('val', '${c.default_source_ref}');
583 583 loadRepoRefDiffPreview();
584 584 loadDefaultReviewers();
585 585 % endif
586 586
587 ReviewerAutoComplete('user');
587 ReviewerAutoComplete('#user');
588 588 });
589 589 </script>
590 590
591 591 </%def>
@@ -1,827 +1,827 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 <span id="pr-title">
13 13 ${c.pull_request.title}
14 14 %if c.pull_request.is_closed():
15 15 (${_('Closed')})
16 16 %endif
17 17 </span>
18 18 <div id="pr-title-edit" class="input" style="display: none;">
19 19 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
20 20 </div>
21 21 </%def>
22 22
23 23 <%def name="menu_bar_nav()">
24 24 ${self.menu_items(active='repositories')}
25 25 </%def>
26 26
27 27 <%def name="menu_bar_subnav()">
28 28 ${self.repo_menu(active='showpullrequest')}
29 29 </%def>
30 30
31 31 <%def name="main()">
32 32
33 33 <script type="text/javascript">
34 34 // TODO: marcink switch this to pyroutes
35 35 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
36 36 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
37 37 </script>
38 38 <div class="box">
39 39
40 40 <div class="title">
41 41 ${self.repo_page_title(c.rhodecode_db_repo)}
42 42 </div>
43 43
44 44 ${self.breadcrumbs()}
45 45
46 46 <div class="box pr-summary">
47 47
48 48 <div class="summary-details block-left">
49 49 <% summary = lambda n:{False:'summary-short'}.get(n) %>
50 50 <div class="pr-details-title">
51 51 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
52 52 %if c.allowed_to_update:
53 53 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
54 54 % if c.allowed_to_delete:
55 55 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
56 56 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
57 57 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
58 58 ${h.end_form()}
59 59 % else:
60 60 ${_('Delete')}
61 61 % endif
62 62 </div>
63 63 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
64 64 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
65 65 %endif
66 66 </div>
67 67
68 68 <div id="summary" class="fields pr-details-content">
69 69 <div class="field">
70 70 <div class="label-summary">
71 71 <label>${_('Origin')}:</label>
72 72 </div>
73 73 <div class="input">
74 74 <div class="pr-origininfo">
75 75 ## branch link is only valid if it is a branch
76 76 <span class="tag">
77 77 %if c.pull_request.source_ref_parts.type == 'branch':
78 78 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
79 79 %else:
80 80 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
81 81 %endif
82 82 </span>
83 83 <span class="clone-url">
84 84 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
85 85 </span>
86 86 <br/>
87 87 % if c.ancestor_commit:
88 88 ${_('Common ancestor')}:
89 89 <code><a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a></code>
90 90 % endif
91 91 </div>
92 92 <div class="pr-pullinfo">
93 93 %if h.is_hg(c.pull_request.source_repo):
94 94 <input type="text" class="input-monospace" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
95 95 %elif h.is_git(c.pull_request.source_repo):
96 96 <input type="text" class="input-monospace" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
97 97 %endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101 <div class="field">
102 102 <div class="label-summary">
103 103 <label>${_('Target')}:</label>
104 104 </div>
105 105 <div class="input">
106 106 <div class="pr-targetinfo">
107 107 ## branch link is only valid if it is a branch
108 108 <span class="tag">
109 109 %if c.pull_request.target_ref_parts.type == 'branch':
110 110 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
111 111 %else:
112 112 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
113 113 %endif
114 114 </span>
115 115 <span class="clone-url">
116 116 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
117 117 </span>
118 118 </div>
119 119 </div>
120 120 </div>
121 121
122 122 ## Link to the shadow repository.
123 123 <div class="field">
124 124 <div class="label-summary">
125 125 <label>${_('Merge')}:</label>
126 126 </div>
127 127 <div class="input">
128 128 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
129 129 <div class="pr-mergeinfo">
130 130 %if h.is_hg(c.pull_request.target_repo):
131 131 <input type="text" class="input-monospace" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
132 132 %elif h.is_git(c.pull_request.target_repo):
133 133 <input type="text" class="input-monospace" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
134 134 %endif
135 135 </div>
136 136 % else:
137 137 <div class="">
138 138 ${_('Shadow repository data not available')}.
139 139 </div>
140 140 % endif
141 141 </div>
142 142 </div>
143 143
144 144 <div class="field">
145 145 <div class="label-summary">
146 146 <label>${_('Review')}:</label>
147 147 </div>
148 148 <div class="input">
149 149 %if c.pull_request_review_status:
150 150 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
151 151 <span class="changeset-status-lbl tooltip">
152 152 %if c.pull_request.is_closed():
153 153 ${_('Closed')},
154 154 %endif
155 155 ${h.commit_status_lbl(c.pull_request_review_status)}
156 156 </span>
157 157 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
158 158 %endif
159 159 </div>
160 160 </div>
161 161 <div class="field">
162 162 <div class="pr-description-label label-summary">
163 163 <label>${_('Description')}:</label>
164 164 </div>
165 165 <div id="pr-desc" class="input">
166 166 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
167 167 </div>
168 168 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
169 169 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
170 170 </div>
171 171 </div>
172 172
173 173 <div class="field">
174 174 <div class="label-summary">
175 175 <label>${_('Versions')}:</label>
176 176 </div>
177 177
178 178 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
179 179 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
180 180
181 181 <div class="pr-versions">
182 182 % if c.show_version_changes:
183 183 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
184 184 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
185 185 <a id="show-pr-versions" class="input" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
186 186 data-toggle-on="${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}"
187 187 data-toggle-off="${_('Hide all versions of this pull request')}">
188 188 ${ungettext('{} version available for this pull request, show it.', '{} versions available for this pull request, show them.', len(c.versions)).format(len(c.versions))}
189 189 </a>
190 190 <table>
191 191 ## SHOW ALL VERSIONS OF PR
192 192 <% ver_pr = None %>
193 193
194 194 % for data in reversed(list(enumerate(c.versions, 1))):
195 195 <% ver_pos = data[0] %>
196 196 <% ver = data[1] %>
197 197 <% ver_pr = ver.pull_request_version_id %>
198 198 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
199 199
200 200 <tr class="version-pr" style="display: ${display_row}">
201 201 <td>
202 202 <code>
203 203 <a href="${h.url.current(version=ver_pr or 'latest')}">v${ver_pos}</a>
204 204 </code>
205 205 </td>
206 206 <td>
207 207 <input ${'checked="checked"' if c.from_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
208 208 <input ${'checked="checked"' if c.at_version_num == ver_pr else ''} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
209 209 </td>
210 210 <td>
211 211 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
212 212 <div class="${'flag_status %s' % review_status} tooltip pull-left" title="${_('Your review status at this version')}">
213 213 </div>
214 214 </td>
215 215 <td>
216 216 % if c.at_version_num != ver_pr:
217 217 <i class="icon-comment"></i>
218 218 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline:{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
219 219 G:${len(c.comment_versions[ver_pr]['at'])} / I:${len(c.inline_versions[ver_pr]['at'])}
220 220 </code>
221 221 % endif
222 222 </td>
223 223 <td>
224 224 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
225 225 </td>
226 226 <td>
227 227 ${h.age_component(ver.updated_on, time_is_local=True)}
228 228 </td>
229 229 </tr>
230 230 % endfor
231 231
232 232 <tr>
233 233 <td colspan="6">
234 234 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
235 235 data-label-text-locked="${_('select versions to show changes')}"
236 236 data-label-text-diff="${_('show changes between versions')}"
237 237 data-label-text-show="${_('show pull request for this version')}"
238 238 >
239 239 ${_('select versions to show changes')}
240 240 </button>
241 241 </td>
242 242 </tr>
243 243
244 244 ## show comment/inline comments summary
245 245 <%def name="comments_summary()">
246 246 <tr>
247 247 <td colspan="6" class="comments-summary-td">
248 248
249 249 % if c.at_version:
250 250 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
251 251 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
252 252 ${_('Comments at this version')}:
253 253 % else:
254 254 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
255 255 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
256 256 ${_('Comments for this pull request')}:
257 257 % endif
258 258
259 259
260 260 %if general_comm_count_ver:
261 261 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
262 262 %else:
263 263 ${_("%d General ") % general_comm_count_ver}
264 264 %endif
265 265
266 266 %if inline_comm_count_ver:
267 267 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
268 268 %else:
269 269 , ${_("%d Inline") % inline_comm_count_ver}
270 270 %endif
271 271
272 272 %if outdated_comm_count_ver:
273 273 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
274 274 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
275 275 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
276 276 %else:
277 277 , ${_("%d Outdated") % outdated_comm_count_ver}
278 278 %endif
279 279 </td>
280 280 </tr>
281 281 </%def>
282 282 ${comments_summary()}
283 283 </table>
284 284 % else:
285 285 <div class="input">
286 286 ${_('Pull request versions not available')}.
287 287 </div>
288 288 <div>
289 289 <table>
290 290 ${comments_summary()}
291 291 </table>
292 292 </div>
293 293 % endif
294 294 </div>
295 295 </div>
296 296
297 297 <div id="pr-save" class="field" style="display: none;">
298 298 <div class="label-summary"></div>
299 299 <div class="input">
300 300 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
301 301 </div>
302 302 </div>
303 303 </div>
304 304 </div>
305 305 <div>
306 306 ## AUTHOR
307 307 <div class="reviewers-title block-right">
308 308 <div class="pr-details-title">
309 309 ${_('Author')}
310 310 </div>
311 311 </div>
312 312 <div class="block-right pr-details-content reviewers">
313 313 <ul class="group_members">
314 314 <li>
315 315 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
316 316 </li>
317 317 </ul>
318 318 </div>
319 319 ## REVIEWERS
320 320 <div class="reviewers-title block-right">
321 321 <div class="pr-details-title">
322 322 ${_('Pull request reviewers')}
323 323 %if c.allowed_to_update:
324 324 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
325 325 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
326 326 %endif
327 327 </div>
328 328 </div>
329 329 <div id="reviewers" class="block-right pr-details-content reviewers">
330 330 ## members goes here !
331 331 <input type="hidden" name="__start__" value="review_members:sequence">
332 332 <ul id="review_members" class="group_members">
333 333 %for member,reasons,status in c.pull_request_reviewers:
334 334 <li id="reviewer_${member.user_id}">
335 335 <div class="reviewers_member">
336 336 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
337 337 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
338 338 </div>
339 339 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
340 340 ${self.gravatar_with_user(member.email, 16)}
341 341 </div>
342 342 <input type="hidden" name="__start__" value="reviewer:mapping">
343 343 <input type="hidden" name="__start__" value="reasons:sequence">
344 344 %for reason in reasons:
345 345 <div class="reviewer_reason">- ${reason}</div>
346 346 <input type="hidden" name="reason" value="${reason}">
347 347
348 348 %endfor
349 349 <input type="hidden" name="__end__" value="reasons:sequence">
350 350 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
351 351 <input type="hidden" name="__end__" value="reviewer:mapping">
352 352 %if c.allowed_to_update:
353 353 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
354 354 <i class="icon-remove-sign" ></i>
355 355 </div>
356 356 %endif
357 357 </div>
358 358 </li>
359 359 %endfor
360 360 </ul>
361 361 <input type="hidden" name="__end__" value="review_members:sequence">
362 362 %if not c.pull_request.is_closed():
363 363 <div id="add_reviewer_input" class='ac' style="display: none;">
364 364 %if c.allowed_to_update:
365 365 <div class="reviewer_ac">
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
366 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
367 367 <div id="reviewers_container"></div>
368 368 </div>
369 369 <div>
370 370 <button id="update_pull_request" class="btn btn-small">${_('Save Changes')}</button>
371 371 </div>
372 372 %endif
373 373 </div>
374 374 %endif
375 375 </div>
376 376 </div>
377 377 </div>
378 378 <div class="box">
379 379 ##DIFF
380 380 <div class="table" >
381 381 <div id="changeset_compare_view_content">
382 382 ##CS
383 383 % if c.missing_requirements:
384 384 <div class="box">
385 385 <div class="alert alert-warning">
386 386 <div>
387 387 <strong>${_('Missing requirements:')}</strong>
388 388 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
389 389 </div>
390 390 </div>
391 391 </div>
392 392 % elif c.missing_commits:
393 393 <div class="box">
394 394 <div class="alert alert-warning">
395 395 <div>
396 396 <strong>${_('Missing commits')}:</strong>
397 397 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
398 398 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
399 399 </div>
400 400 </div>
401 401 </div>
402 402 % endif
403 403
404 404 <div class="compare_view_commits_title">
405 405 % if not c.compare_mode:
406 406
407 407 % if c.at_version_pos:
408 408 <h4>
409 409 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
410 410 </h4>
411 411 % endif
412 412
413 413 <div class="pull-left">
414 414 <div class="btn-group">
415 415 <a
416 416 class="btn"
417 417 href="#"
418 418 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
419 419 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
420 420 </a>
421 421 <a
422 422 class="btn"
423 423 href="#"
424 424 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
425 425 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
426 426 </a>
427 427 </div>
428 428 </div>
429 429
430 430 <div class="pull-right">
431 431 % if c.allowed_to_update and not c.pull_request.is_closed():
432 432 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
433 433 % else:
434 434 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
435 435 % endif
436 436
437 437 </div>
438 438 % endif
439 439 </div>
440 440
441 441 % if not c.missing_commits:
442 442 % if c.compare_mode:
443 443 % if c.at_version:
444 444 <h4>
445 445 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
446 446 </h4>
447 447
448 448 <div class="subtitle-compare">
449 449 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
450 450 </div>
451 451
452 452 <div class="container">
453 453 <table class="rctable compare_view_commits">
454 454 <tr>
455 455 <th></th>
456 456 <th>${_('Time')}</th>
457 457 <th>${_('Author')}</th>
458 458 <th>${_('Commit')}</th>
459 459 <th></th>
460 460 <th>${_('Description')}</th>
461 461 </tr>
462 462
463 463 % for c_type, commit in c.commit_changes:
464 464 % if c_type in ['a', 'r']:
465 465 <%
466 466 if c_type == 'a':
467 467 cc_title = _('Commit added in displayed changes')
468 468 elif c_type == 'r':
469 469 cc_title = _('Commit removed in displayed changes')
470 470 else:
471 471 cc_title = ''
472 472 %>
473 473 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
474 474 <td>
475 475 <div class="commit-change-indicator color-${c_type}-border">
476 476 <div class="commit-change-content color-${c_type} tooltip" title="${cc_title}">
477 477 ${c_type.upper()}
478 478 </div>
479 479 </div>
480 480 </td>
481 481 <td class="td-time">
482 482 ${h.age_component(commit.date)}
483 483 </td>
484 484 <td class="td-user">
485 485 ${base.gravatar_with_user(commit.author, 16)}
486 486 </td>
487 487 <td class="td-hash">
488 488 <code>
489 489 <a href="${h.url('changeset_home', repo_name=c.target_repo.repo_name, revision=commit.raw_id)}">
490 490 r${commit.revision}:${h.short_id(commit.raw_id)}
491 491 </a>
492 492 ${h.hidden('revisions', commit.raw_id)}
493 493 </code>
494 494 </td>
495 495 <td class="expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}">
496 496 <div class="show_more_col">
497 497 <i class="show_more"></i>
498 498 </div>
499 499 </td>
500 500 <td class="mid td-description">
501 501 <div class="log-container truncate-wrap">
502 502 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">
503 503 ${h.urlify_commit_message(commit.message, c.repo_name)}
504 504 </div>
505 505 </div>
506 506 </td>
507 507 </tr>
508 508 % endif
509 509 % endfor
510 510 </table>
511 511 </div>
512 512
513 513 <script>
514 514 $('.expand_commit').on('click',function(e){
515 515 var target_expand = $(this);
516 516 var cid = target_expand.data('commitId');
517 517
518 518 if (target_expand.hasClass('open')){
519 519 $('#c-'+cid).css({
520 520 'height': '1.5em',
521 521 'white-space': 'nowrap',
522 522 'text-overflow': 'ellipsis',
523 523 'overflow':'hidden'
524 524 });
525 525 target_expand.removeClass('open');
526 526 }
527 527 else {
528 528 $('#c-'+cid).css({
529 529 'height': 'auto',
530 530 'white-space': 'pre-line',
531 531 'text-overflow': 'initial',
532 532 'overflow':'visible'
533 533 });
534 534 target_expand.addClass('open');
535 535 }
536 536 });
537 537 </script>
538 538
539 539 % endif
540 540
541 541 % else:
542 542 <%include file="/compare/compare_commits.mako" />
543 543 % endif
544 544
545 545 <div class="cs_files">
546 546 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
547 547 ${cbdiffs.render_diffset_menu()}
548 548 ${cbdiffs.render_diffset(
549 549 c.diffset, use_comments=True,
550 550 collapse_when_files_over=30,
551 551 disable_new_comments=not c.allowed_to_comment,
552 552 deleted_files_comments=c.deleted_files_comments)}
553 553 </div>
554 554 % else:
555 555 ## skipping commits we need to clear the view for missing commits
556 556 <div style="clear:both;"></div>
557 557 % endif
558 558
559 559 </div>
560 560 </div>
561 561
562 562 ## template for inline comment form
563 563 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
564 564
565 565 ## render general comments
566 566
567 567 <div id="comment-tr-show">
568 568 <div class="comment">
569 569 % if general_outdated_comm_count_ver:
570 570 <div class="meta">
571 571 % if general_outdated_comm_count_ver == 1:
572 572 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
573 573 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
574 574 % else:
575 575 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
576 576 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
577 577 % endif
578 578 </div>
579 579 % endif
580 580 </div>
581 581 </div>
582 582
583 583 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
584 584
585 585 % if not c.pull_request.is_closed():
586 586 ## merge status, and merge action
587 587 <div class="pull-request-merge">
588 588 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
589 589 </div>
590 590
591 591 ## main comment form and it status
592 592 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
593 593 pull_request_id=c.pull_request.pull_request_id),
594 594 c.pull_request_review_status,
595 595 is_pull_request=True, change_status=c.allowed_to_change_status)}
596 596 %endif
597 597
598 598 <script type="text/javascript">
599 599 if (location.hash) {
600 600 var result = splitDelimitedHash(location.hash);
601 601 var line = $('html').find(result.loc);
602 602 // show hidden comments if we use location.hash
603 603 if (line.hasClass('comment-general')) {
604 604 $(line).show();
605 605 } else if (line.hasClass('comment-inline')) {
606 606 $(line).show();
607 607 var $cb = $(line).closest('.cb');
608 608 $cb.removeClass('cb-collapsed')
609 609 }
610 610 if (line.length > 0){
611 611 offsetScroll(line, 70);
612 612 }
613 613 }
614 614
615 615 versionController = new VersionController();
616 616 versionController.init();
617 617
618 618
619 619 $(function(){
620 ReviewerAutoComplete('user');
620 ReviewerAutoComplete('#user');
621 621 // custom code mirror
622 622 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
623 623
624 624 var PRDetails = {
625 625 editButton: $('#open_edit_pullrequest'),
626 626 closeButton: $('#close_edit_pullrequest'),
627 627 deleteButton: $('#delete_pullrequest'),
628 628 viewFields: $('#pr-desc, #pr-title'),
629 629 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
630 630
631 631 init: function() {
632 632 var that = this;
633 633 this.editButton.on('click', function(e) { that.edit(); });
634 634 this.closeButton.on('click', function(e) { that.view(); });
635 635 },
636 636
637 637 edit: function(event) {
638 638 this.viewFields.hide();
639 639 this.editButton.hide();
640 640 this.deleteButton.hide();
641 641 this.closeButton.show();
642 642 this.editFields.show();
643 643 codeMirrorInstance.refresh();
644 644 },
645 645
646 646 view: function(event) {
647 647 this.editButton.show();
648 648 this.deleteButton.show();
649 649 this.editFields.hide();
650 650 this.closeButton.hide();
651 651 this.viewFields.show();
652 652 }
653 653 };
654 654
655 655 var ReviewersPanel = {
656 656 editButton: $('#open_edit_reviewers'),
657 657 closeButton: $('#close_edit_reviewers'),
658 658 addButton: $('#add_reviewer_input'),
659 659 removeButtons: $('.reviewer_member_remove'),
660 660
661 661 init: function() {
662 662 var that = this;
663 663 this.editButton.on('click', function(e) { that.edit(); });
664 664 this.closeButton.on('click', function(e) { that.close(); });
665 665 },
666 666
667 667 edit: function(event) {
668 668 this.editButton.hide();
669 669 this.closeButton.show();
670 670 this.addButton.show();
671 671 this.removeButtons.css('visibility', 'visible');
672 672 },
673 673
674 674 close: function(event) {
675 675 this.editButton.show();
676 676 this.closeButton.hide();
677 677 this.addButton.hide();
678 678 this.removeButtons.css('visibility', 'hidden');
679 679 }
680 680 };
681 681
682 682 PRDetails.init();
683 683 ReviewersPanel.init();
684 684
685 685 showOutdated = function(self){
686 686 $('.comment-inline.comment-outdated').show();
687 687 $('.filediff-outdated').show();
688 688 $('.showOutdatedComments').hide();
689 689 $('.hideOutdatedComments').show();
690 690 };
691 691
692 692 hideOutdated = function(self){
693 693 $('.comment-inline.comment-outdated').hide();
694 694 $('.filediff-outdated').hide();
695 695 $('.hideOutdatedComments').hide();
696 696 $('.showOutdatedComments').show();
697 697 };
698 698
699 699 refreshMergeChecks = function(){
700 700 var loadUrl = "${h.url.current(merge_checks=1)}";
701 701 $('.pull-request-merge').css('opacity', 0.3);
702 702 $('.action-buttons-extra').css('opacity', 0.3);
703 703
704 704 $('.pull-request-merge').load(
705 705 loadUrl, function() {
706 706 $('.pull-request-merge').css('opacity', 1);
707 707
708 708 $('.action-buttons-extra').css('opacity', 1);
709 709 injectCloseAction();
710 710 }
711 711 );
712 712 };
713 713
714 714 injectCloseAction = function() {
715 715 var closeAction = $('#close-pull-request-action').html();
716 716 var $actionButtons = $('.action-buttons-extra');
717 717 // clear the action before
718 718 $actionButtons.html("");
719 719 $actionButtons.html(closeAction);
720 720 };
721 721
722 722 closePullRequest = function (status) {
723 723 // inject closing flag
724 724 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
725 725 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
726 726 $(generalCommentForm.submitForm).submit();
727 727 };
728 728
729 729 $('#show-outdated-comments').on('click', function(e){
730 730 var button = $(this);
731 731 var outdated = $('.comment-outdated');
732 732
733 733 if (button.html() === "(Show)") {
734 734 button.html("(Hide)");
735 735 outdated.show();
736 736 } else {
737 737 button.html("(Show)");
738 738 outdated.hide();
739 739 }
740 740 });
741 741
742 742 $('.show-inline-comments').on('change', function(e){
743 743 var show = 'none';
744 744 var target = e.currentTarget;
745 745 if(target.checked){
746 746 show = ''
747 747 }
748 748 var boxid = $(target).attr('id_for');
749 749 var comments = $('#{0} .inline-comments'.format(boxid));
750 750 var fn_display = function(idx){
751 751 $(this).css('display', show);
752 752 };
753 753 $(comments).each(fn_display);
754 754 var btns = $('#{0} .inline-comments-button'.format(boxid));
755 755 $(btns).each(fn_display);
756 756 });
757 757
758 758 $('#merge_pull_request_form').submit(function() {
759 759 if (!$('#merge_pull_request').attr('disabled')) {
760 760 $('#merge_pull_request').attr('disabled', 'disabled');
761 761 }
762 762 return true;
763 763 });
764 764
765 765 $('#edit_pull_request').on('click', function(e){
766 766 var title = $('#pr-title-input').val();
767 767 var description = codeMirrorInstance.getValue();
768 768 editPullRequest(
769 769 "${c.repo_name}", "${c.pull_request.pull_request_id}",
770 770 title, description);
771 771 });
772 772
773 773 $('#update_pull_request').on('click', function(e){
774 774 $(this).attr('disabled', 'disabled');
775 775 $(this).addClass('disabled');
776 776 $(this).html(_gettext('Saving...'));
777 777 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
778 778 });
779 779
780 780 $('#update_commits').on('click', function(e){
781 781 var isDisabled = !$(e.currentTarget).attr('disabled');
782 782 $(e.currentTarget).attr('disabled', 'disabled');
783 783 $(e.currentTarget).addClass('disabled');
784 784 $(e.currentTarget).removeClass('btn-primary');
785 785 $(e.currentTarget).text(_gettext('Updating...'));
786 786 if(isDisabled){
787 787 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
788 788 }
789 789 });
790 790 // fixing issue with caches on firefox
791 791 $('#update_commits').removeAttr("disabled");
792 792
793 793 $('#close_pull_request').on('click', function(e){
794 794 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
795 795 });
796 796
797 797 $('.show-inline-comments').on('click', function(e){
798 798 var boxid = $(this).attr('data-comment-id');
799 799 var button = $(this);
800 800
801 801 if(button.hasClass("comments-visible")) {
802 802 $('#{0} .inline-comments'.format(boxid)).each(function(index){
803 803 $(this).hide();
804 804 });
805 805 button.removeClass("comments-visible");
806 806 } else {
807 807 $('#{0} .inline-comments'.format(boxid)).each(function(index){
808 808 $(this).show();
809 809 });
810 810 button.addClass("comments-visible");
811 811 }
812 812 });
813 813
814 814 // register submit callback on commentForm form to track TODOs
815 815 window.commentFormGlobalSubmitSuccessCallback = function(){
816 816 refreshMergeChecks();
817 817 };
818 818 // initial injection
819 819 injectCloseAction();
820 820
821 821 })
822 822 </script>
823 823
824 824 </div>
825 825 </div>
826 826
827 827 </%def>
General Comments 0
You need to be logged in to leave comments. Login now