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