##// END OF EJS Templates
pull-requests: added awaiting my review filter for users pull-requests....
super-admin -
r4690:2e951f8d stable
parent child Browse files
Show More
@@ -1,204 +1,208 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 # -*- coding: utf-8 -*-
20 # -*- coding: utf-8 -*-
21
21
22 # Copyright (C) 2016-2020 RhodeCode GmbH
22 # Copyright (C) 2016-2020 RhodeCode GmbH
23 #
23 #
24 # This program is free software: you can redistribute it and/or modify
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
26 # (only), as published by the Free Software Foundation.
27 #
27 #
28 # This program is distributed in the hope that it will be useful,
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
31 # GNU General Public License for more details.
32 #
32 #
33 # You should have received a copy of the GNU Affero General Public License
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
35 #
36 # This program is dual-licensed. If you wish to learn more about the
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
39
40 import pytest
40 import pytest
41
41
42 from rhodecode.model.db import User
42 from rhodecode.model.db import User
43 from rhodecode.tests import TestController, assert_session_flash
43 from rhodecode.tests import TestController, assert_session_flash
44 from rhodecode.lib import helpers as h
44 from rhodecode.lib import helpers as h
45
45
46
46
47 def route_path(name, params=None, **kwargs):
47 def route_path(name, params=None, **kwargs):
48 import urllib
48 import urllib
49 from rhodecode.apps._base import ADMIN_PREFIX
49 from rhodecode.apps._base import ADMIN_PREFIX
50
50
51 base_url = {
51 base_url = {
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
52 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
53 'my_account_update': ADMIN_PREFIX + '/my_account/update',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
54 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
55 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
56 }[name].format(**kwargs)
56 }[name].format(**kwargs)
57
57
58 if params:
58 if params:
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
59 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
60 return base_url
60 return base_url
61
61
62
62
63 class TestMyAccountEdit(TestController):
63 class TestMyAccountEdit(TestController):
64
64
65 def test_my_account_edit(self):
65 def test_my_account_edit(self):
66 self.log_user()
66 self.log_user()
67 response = self.app.get(route_path('my_account_edit'))
67 response = self.app.get(route_path('my_account_edit'))
68
68
69 response.mustcontain('value="test_admin')
69 response.mustcontain('value="test_admin')
70
70
71 @pytest.mark.backends("git", "hg")
71 @pytest.mark.backends("git", "hg")
72 def test_my_account_my_pullrequests(self, pr_util):
72 def test_my_account_my_pullrequests(self, pr_util):
73 self.log_user()
73 self.log_user()
74 response = self.app.get(route_path('my_account_pullrequests'))
74 response = self.app.get(route_path('my_account_pullrequests'))
75 response.mustcontain('There are currently no open pull '
75 response.mustcontain('There are currently no open pull '
76 'requests requiring your participation.')
76 'requests requiring your participation.')
77
77
78 @pytest.mark.backends("git", "hg")
78 @pytest.mark.backends("git", "hg")
79 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
79 @pytest.mark.parametrize('params, expected_title', [
80 ({'closed': 1}, 'Closed'),
81 ({'awaiting_my_review': 1}, 'Awaiting my review'),
82 ])
83 def test_my_account_my_pullrequests_data(self, pr_util, xhr_header, params, expected_title):
80 self.log_user()
84 self.log_user()
81 response = self.app.get(route_path('my_account_pullrequests_data'),
85 response = self.app.get(route_path('my_account_pullrequests_data'),
82 extra_environ=xhr_header)
86 extra_environ=xhr_header)
83 assert response.json == {
87 assert response.json == {
84 u'data': [], u'draw': None,
88 u'data': [], u'draw': None,
85 u'recordsFiltered': 0, u'recordsTotal': 0}
89 u'recordsFiltered': 0, u'recordsTotal': 0}
86
90
87 pr = pr_util.create_pull_request(title='TestMyAccountPR')
91 pr = pr_util.create_pull_request(title='TestMyAccountPR')
88 expected = {
92 expected = {
89 'author_raw': 'RhodeCode Admin',
93 'author_raw': 'RhodeCode Admin',
90 'name_raw': pr.pull_request_id
94 'name_raw': pr.pull_request_id
91 }
95 }
92 response = self.app.get(route_path('my_account_pullrequests_data'),
96 response = self.app.get(route_path('my_account_pullrequests_data'),
93 extra_environ=xhr_header)
97 extra_environ=xhr_header)
94 assert response.json['recordsTotal'] == 1
98 assert response.json['recordsTotal'] == 1
95 assert response.json['data'][0]['author_raw'] == expected['author_raw']
99 assert response.json['data'][0]['author_raw'] == expected['author_raw']
96
100
97 assert response.json['data'][0]['author_raw'] == expected['author_raw']
101 assert response.json['data'][0]['author_raw'] == expected['author_raw']
98 assert response.json['data'][0]['name_raw'] == expected['name_raw']
102 assert response.json['data'][0]['name_raw'] == expected['name_raw']
99
103
100 @pytest.mark.parametrize(
104 @pytest.mark.parametrize(
101 "name, attrs", [
105 "name, attrs", [
102 ('firstname', {'firstname': 'new_username'}),
106 ('firstname', {'firstname': 'new_username'}),
103 ('lastname', {'lastname': 'new_username'}),
107 ('lastname', {'lastname': 'new_username'}),
104 ('admin', {'admin': True}),
108 ('admin', {'admin': True}),
105 ('admin', {'admin': False}),
109 ('admin', {'admin': False}),
106 ('extern_type', {'extern_type': 'ldap'}),
110 ('extern_type', {'extern_type': 'ldap'}),
107 ('extern_type', {'extern_type': None}),
111 ('extern_type', {'extern_type': None}),
108 # ('extern_name', {'extern_name': 'test'}),
112 # ('extern_name', {'extern_name': 'test'}),
109 # ('extern_name', {'extern_name': None}),
113 # ('extern_name', {'extern_name': None}),
110 ('active', {'active': False}),
114 ('active', {'active': False}),
111 ('active', {'active': True}),
115 ('active', {'active': True}),
112 ('email', {'email': u'some@email.com'}),
116 ('email', {'email': u'some@email.com'}),
113 ])
117 ])
114 def test_my_account_update(self, name, attrs, user_util):
118 def test_my_account_update(self, name, attrs, user_util):
115 usr = user_util.create_user(password='qweqwe')
119 usr = user_util.create_user(password='qweqwe')
116 params = usr.get_api_data() # current user data
120 params = usr.get_api_data() # current user data
117 user_id = usr.user_id
121 user_id = usr.user_id
118 self.log_user(
122 self.log_user(
119 username=usr.username, password='qweqwe')
123 username=usr.username, password='qweqwe')
120
124
121 params.update({'password_confirmation': ''})
125 params.update({'password_confirmation': ''})
122 params.update({'new_password': ''})
126 params.update({'new_password': ''})
123 params.update({'extern_type': u'rhodecode'})
127 params.update({'extern_type': u'rhodecode'})
124 params.update({'extern_name': u'rhodecode'})
128 params.update({'extern_name': u'rhodecode'})
125 params.update({'csrf_token': self.csrf_token})
129 params.update({'csrf_token': self.csrf_token})
126
130
127 params.update(attrs)
131 params.update(attrs)
128 # my account page cannot set language param yet, only for admins
132 # my account page cannot set language param yet, only for admins
129 del params['language']
133 del params['language']
130 if name == 'email':
134 if name == 'email':
131 uem = user_util.create_additional_user_email(usr, attrs['email'])
135 uem = user_util.create_additional_user_email(usr, attrs['email'])
132 email_before = User.get(user_id).email
136 email_before = User.get(user_id).email
133
137
134 response = self.app.post(route_path('my_account_update'), params)
138 response = self.app.post(route_path('my_account_update'), params)
135
139
136 assert_session_flash(
140 assert_session_flash(
137 response, 'Your account was updated successfully')
141 response, 'Your account was updated successfully')
138
142
139 del params['csrf_token']
143 del params['csrf_token']
140
144
141 updated_user = User.get(user_id)
145 updated_user = User.get(user_id)
142 updated_params = updated_user.get_api_data()
146 updated_params = updated_user.get_api_data()
143 updated_params.update({'password_confirmation': ''})
147 updated_params.update({'password_confirmation': ''})
144 updated_params.update({'new_password': ''})
148 updated_params.update({'new_password': ''})
145
149
146 params['last_login'] = updated_params['last_login']
150 params['last_login'] = updated_params['last_login']
147 params['last_activity'] = updated_params['last_activity']
151 params['last_activity'] = updated_params['last_activity']
148 # my account page cannot set language param yet, only for admins
152 # my account page cannot set language param yet, only for admins
149 # but we get this info from API anyway
153 # but we get this info from API anyway
150 params['language'] = updated_params['language']
154 params['language'] = updated_params['language']
151
155
152 if name == 'email':
156 if name == 'email':
153 params['emails'] = [attrs['email'], email_before]
157 params['emails'] = [attrs['email'], email_before]
154 if name == 'extern_type':
158 if name == 'extern_type':
155 # cannot update this via form, expected value is original one
159 # cannot update this via form, expected value is original one
156 params['extern_type'] = "rhodecode"
160 params['extern_type'] = "rhodecode"
157 if name == 'extern_name':
161 if name == 'extern_name':
158 # cannot update this via form, expected value is original one
162 # cannot update this via form, expected value is original one
159 params['extern_name'] = str(user_id)
163 params['extern_name'] = str(user_id)
160 if name == 'active':
164 if name == 'active':
161 # my account cannot deactivate account
165 # my account cannot deactivate account
162 params['active'] = True
166 params['active'] = True
163 if name == 'admin':
167 if name == 'admin':
164 # my account cannot make you an admin !
168 # my account cannot make you an admin !
165 params['admin'] = False
169 params['admin'] = False
166
170
167 assert params == updated_params
171 assert params == updated_params
168
172
169 def test_my_account_update_err_email_not_exists_in_emails(self):
173 def test_my_account_update_err_email_not_exists_in_emails(self):
170 self.log_user()
174 self.log_user()
171
175
172 new_email = 'test_regular@mail.com' # not in emails
176 new_email = 'test_regular@mail.com' # not in emails
173 params = {
177 params = {
174 'username': 'test_admin',
178 'username': 'test_admin',
175 'new_password': 'test12',
179 'new_password': 'test12',
176 'password_confirmation': 'test122',
180 'password_confirmation': 'test122',
177 'firstname': 'NewName',
181 'firstname': 'NewName',
178 'lastname': 'NewLastname',
182 'lastname': 'NewLastname',
179 'email': new_email,
183 'email': new_email,
180 'csrf_token': self.csrf_token,
184 'csrf_token': self.csrf_token,
181 }
185 }
182
186
183 response = self.app.post(route_path('my_account_update'),
187 response = self.app.post(route_path('my_account_update'),
184 params=params)
188 params=params)
185
189
186 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
190 response.mustcontain('"test_regular@mail.com" is not one of test_admin@mail.com')
187
191
188 def test_my_account_update_bad_email_address(self):
192 def test_my_account_update_bad_email_address(self):
189 self.log_user('test_regular2', 'test12')
193 self.log_user('test_regular2', 'test12')
190
194
191 new_email = 'newmail.pl'
195 new_email = 'newmail.pl'
192 params = {
196 params = {
193 'username': 'test_admin',
197 'username': 'test_admin',
194 'new_password': 'test12',
198 'new_password': 'test12',
195 'password_confirmation': 'test122',
199 'password_confirmation': 'test122',
196 'firstname': 'NewName',
200 'firstname': 'NewName',
197 'lastname': 'NewLastname',
201 'lastname': 'NewLastname',
198 'email': new_email,
202 'email': new_email,
199 'csrf_token': self.csrf_token,
203 'csrf_token': self.csrf_token,
200 }
204 }
201 response = self.app.post(route_path('my_account_update'),
205 response = self.app.post(route_path('my_account_update'),
202 params=params)
206 params=params)
203
207
204 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
208 response.mustcontain('"newmail.pl" is not one of test_regular2@mail.com')
@@ -1,752 +1,783 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import string
23 import string
24
24
25 import formencode
25 import formencode
26 import formencode.htmlfill
26 import formencode.htmlfill
27 import peppercorn
27 import peppercorn
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
28 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
29
29
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
30 from rhodecode.apps._base import BaseAppView, DataGridAppView
31 from rhodecode import forms
31 from rhodecode import forms
32 from rhodecode.lib import helpers as h
32 from rhodecode.lib import helpers as h
33 from rhodecode.lib import audit_logger
33 from rhodecode.lib import audit_logger
34 from rhodecode.lib.ext_json import json
34 from rhodecode.lib.ext_json import json
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, NotAnonymous, CSRFRequired,
36 LoginRequired, NotAnonymous, CSRFRequired,
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
37 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
38 from rhodecode.lib.channelstream import (
38 from rhodecode.lib.channelstream import (
39 channelstream_request, ChannelstreamException)
39 channelstream_request, ChannelstreamException)
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 from rhodecode.lib.utils2 import safe_int, md5, str2bool
41 from rhodecode.model.auth_token import AuthTokenModel
41 from rhodecode.model.auth_token import AuthTokenModel
42 from rhodecode.model.comment import CommentsModel
42 from rhodecode.model.comment import CommentsModel
43 from rhodecode.model.db import (
43 from rhodecode.model.db import (
44 IntegrityError, or_, in_filter_generator,
44 IntegrityError, or_, in_filter_generator,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 Repository, UserEmailMap, UserApiKeys, UserFollowing,
46 PullRequest, UserBookmark, RepoGroup)
46 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
47 from rhodecode.model.meta import Session
47 from rhodecode.model.meta import Session
48 from rhodecode.model.pull_request import PullRequestModel
48 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.user import UserModel
49 from rhodecode.model.user import UserModel
50 from rhodecode.model.user_group import UserGroupModel
50 from rhodecode.model.user_group import UserGroupModel
51 from rhodecode.model.validation_schema.schemas import user_schema
51 from rhodecode.model.validation_schema.schemas import user_schema
52
52
53 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
54
54
55
55
56 class MyAccountView(BaseAppView, DataGridAppView):
56 class MyAccountView(BaseAppView, DataGridAppView):
57 ALLOW_SCOPED_TOKENS = False
57 ALLOW_SCOPED_TOKENS = False
58 """
58 """
59 This view has alternative version inside EE, if modified please take a look
59 This view has alternative version inside EE, if modified please take a look
60 in there as well.
60 in there as well.
61 """
61 """
62
62
63 def load_default_context(self):
63 def load_default_context(self):
64 c = self._get_local_tmpl_context()
64 c = self._get_local_tmpl_context()
65 c.user = c.auth_user.get_instance()
65 c.user = c.auth_user.get_instance()
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
67 return c
67 return c
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @NotAnonymous()
70 @NotAnonymous()
71 def my_account_profile(self):
71 def my_account_profile(self):
72 c = self.load_default_context()
72 c = self.load_default_context()
73 c.active = 'profile'
73 c.active = 'profile'
74 c.extern_type = c.user.extern_type
74 c.extern_type = c.user.extern_type
75 return self._get_template_context(c)
75 return self._get_template_context(c)
76
76
77 @LoginRequired()
77 @LoginRequired()
78 @NotAnonymous()
78 @NotAnonymous()
79 def my_account_edit(self):
79 def my_account_edit(self):
80 c = self.load_default_context()
80 c = self.load_default_context()
81 c.active = 'profile_edit'
81 c.active = 'profile_edit'
82 c.extern_type = c.user.extern_type
82 c.extern_type = c.user.extern_type
83 c.extern_name = c.user.extern_name
83 c.extern_name = c.user.extern_name
84
84
85 schema = user_schema.UserProfileSchema().bind(
85 schema = user_schema.UserProfileSchema().bind(
86 username=c.user.username, user_emails=c.user.emails)
86 username=c.user.username, user_emails=c.user.emails)
87 appstruct = {
87 appstruct = {
88 'username': c.user.username,
88 'username': c.user.username,
89 'email': c.user.email,
89 'email': c.user.email,
90 'firstname': c.user.firstname,
90 'firstname': c.user.firstname,
91 'lastname': c.user.lastname,
91 'lastname': c.user.lastname,
92 'description': c.user.description,
92 'description': c.user.description,
93 }
93 }
94 c.form = forms.RcForm(
94 c.form = forms.RcForm(
95 schema, appstruct=appstruct,
95 schema, appstruct=appstruct,
96 action=h.route_path('my_account_update'),
96 action=h.route_path('my_account_update'),
97 buttons=(forms.buttons.save, forms.buttons.reset))
97 buttons=(forms.buttons.save, forms.buttons.reset))
98
98
99 return self._get_template_context(c)
99 return self._get_template_context(c)
100
100
101 @LoginRequired()
101 @LoginRequired()
102 @NotAnonymous()
102 @NotAnonymous()
103 @CSRFRequired()
103 @CSRFRequired()
104 def my_account_update(self):
104 def my_account_update(self):
105 _ = self.request.translate
105 _ = self.request.translate
106 c = self.load_default_context()
106 c = self.load_default_context()
107 c.active = 'profile_edit'
107 c.active = 'profile_edit'
108 c.perm_user = c.auth_user
108 c.perm_user = c.auth_user
109 c.extern_type = c.user.extern_type
109 c.extern_type = c.user.extern_type
110 c.extern_name = c.user.extern_name
110 c.extern_name = c.user.extern_name
111
111
112 schema = user_schema.UserProfileSchema().bind(
112 schema = user_schema.UserProfileSchema().bind(
113 username=c.user.username, user_emails=c.user.emails)
113 username=c.user.username, user_emails=c.user.emails)
114 form = forms.RcForm(
114 form = forms.RcForm(
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 schema, buttons=(forms.buttons.save, forms.buttons.reset))
116
116
117 controls = self.request.POST.items()
117 controls = self.request.POST.items()
118 try:
118 try:
119 valid_data = form.validate(controls)
119 valid_data = form.validate(controls)
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
121 'new_password', 'password_confirmation']
121 'new_password', 'password_confirmation']
122 if c.extern_type != "rhodecode":
122 if c.extern_type != "rhodecode":
123 # forbid updating username for external accounts
123 # forbid updating username for external accounts
124 skip_attrs.append('username')
124 skip_attrs.append('username')
125 old_email = c.user.email
125 old_email = c.user.email
126 UserModel().update_user(
126 UserModel().update_user(
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
128 **valid_data)
128 **valid_data)
129 if old_email != valid_data['email']:
129 if old_email != valid_data['email']:
130 old = UserEmailMap.query() \
130 old = UserEmailMap.query() \
131 .filter(UserEmailMap.user == c.user)\
131 .filter(UserEmailMap.user == c.user)\
132 .filter(UserEmailMap.email == valid_data['email'])\
132 .filter(UserEmailMap.email == valid_data['email'])\
133 .first()
133 .first()
134 old.email = old_email
134 old.email = old_email
135 h.flash(_('Your account was updated successfully'), category='success')
135 h.flash(_('Your account was updated successfully'), category='success')
136 Session().commit()
136 Session().commit()
137 except forms.ValidationFailure as e:
137 except forms.ValidationFailure as e:
138 c.form = e
138 c.form = e
139 return self._get_template_context(c)
139 return self._get_template_context(c)
140 except Exception:
140 except Exception:
141 log.exception("Exception updating user")
141 log.exception("Exception updating user")
142 h.flash(_('Error occurred during update of user'),
142 h.flash(_('Error occurred during update of user'),
143 category='error')
143 category='error')
144 raise HTTPFound(h.route_path('my_account_profile'))
144 raise HTTPFound(h.route_path('my_account_profile'))
145
145
146 @LoginRequired()
146 @LoginRequired()
147 @NotAnonymous()
147 @NotAnonymous()
148 def my_account_password(self):
148 def my_account_password(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150 c.active = 'password'
150 c.active = 'password'
151 c.extern_type = c.user.extern_type
151 c.extern_type = c.user.extern_type
152
152
153 schema = user_schema.ChangePasswordSchema().bind(
153 schema = user_schema.ChangePasswordSchema().bind(
154 username=c.user.username)
154 username=c.user.username)
155
155
156 form = forms.Form(
156 form = forms.Form(
157 schema,
157 schema,
158 action=h.route_path('my_account_password_update'),
158 action=h.route_path('my_account_password_update'),
159 buttons=(forms.buttons.save, forms.buttons.reset))
159 buttons=(forms.buttons.save, forms.buttons.reset))
160
160
161 c.form = form
161 c.form = form
162 return self._get_template_context(c)
162 return self._get_template_context(c)
163
163
164 @LoginRequired()
164 @LoginRequired()
165 @NotAnonymous()
165 @NotAnonymous()
166 @CSRFRequired()
166 @CSRFRequired()
167 def my_account_password_update(self):
167 def my_account_password_update(self):
168 _ = self.request.translate
168 _ = self.request.translate
169 c = self.load_default_context()
169 c = self.load_default_context()
170 c.active = 'password'
170 c.active = 'password'
171 c.extern_type = c.user.extern_type
171 c.extern_type = c.user.extern_type
172
172
173 schema = user_schema.ChangePasswordSchema().bind(
173 schema = user_schema.ChangePasswordSchema().bind(
174 username=c.user.username)
174 username=c.user.username)
175
175
176 form = forms.Form(
176 form = forms.Form(
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 schema, buttons=(forms.buttons.save, forms.buttons.reset))
178
178
179 if c.extern_type != 'rhodecode':
179 if c.extern_type != 'rhodecode':
180 raise HTTPFound(self.request.route_path('my_account_password'))
180 raise HTTPFound(self.request.route_path('my_account_password'))
181
181
182 controls = self.request.POST.items()
182 controls = self.request.POST.items()
183 try:
183 try:
184 valid_data = form.validate(controls)
184 valid_data = form.validate(controls)
185 UserModel().update_user(c.user.user_id, **valid_data)
185 UserModel().update_user(c.user.user_id, **valid_data)
186 c.user.update_userdata(force_password_change=False)
186 c.user.update_userdata(force_password_change=False)
187 Session().commit()
187 Session().commit()
188 except forms.ValidationFailure as e:
188 except forms.ValidationFailure as e:
189 c.form = e
189 c.form = e
190 return self._get_template_context(c)
190 return self._get_template_context(c)
191
191
192 except Exception:
192 except Exception:
193 log.exception("Exception updating password")
193 log.exception("Exception updating password")
194 h.flash(_('Error occurred during update of user password'),
194 h.flash(_('Error occurred during update of user password'),
195 category='error')
195 category='error')
196 else:
196 else:
197 instance = c.auth_user.get_instance()
197 instance = c.auth_user.get_instance()
198 self.session.setdefault('rhodecode_user', {}).update(
198 self.session.setdefault('rhodecode_user', {}).update(
199 {'password': md5(instance.password)})
199 {'password': md5(instance.password)})
200 self.session.save()
200 self.session.save()
201 h.flash(_("Successfully updated password"), category='success')
201 h.flash(_("Successfully updated password"), category='success')
202
202
203 raise HTTPFound(self.request.route_path('my_account_password'))
203 raise HTTPFound(self.request.route_path('my_account_password'))
204
204
205 @LoginRequired()
205 @LoginRequired()
206 @NotAnonymous()
206 @NotAnonymous()
207 def my_account_auth_tokens(self):
207 def my_account_auth_tokens(self):
208 _ = self.request.translate
208 _ = self.request.translate
209
209
210 c = self.load_default_context()
210 c = self.load_default_context()
211 c.active = 'auth_tokens'
211 c.active = 'auth_tokens'
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
212 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
213 c.role_values = [
213 c.role_values = [
214 (x, AuthTokenModel.cls._get_role_name(x))
214 (x, AuthTokenModel.cls._get_role_name(x))
215 for x in AuthTokenModel.cls.ROLES]
215 for x in AuthTokenModel.cls.ROLES]
216 c.role_options = [(c.role_values, _("Role"))]
216 c.role_options = [(c.role_values, _("Role"))]
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
217 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
218 c.user.user_id, show_expired=True)
218 c.user.user_id, show_expired=True)
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
219 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
220 return self._get_template_context(c)
220 return self._get_template_context(c)
221
221
222 @LoginRequired()
222 @LoginRequired()
223 @NotAnonymous()
223 @NotAnonymous()
224 @CSRFRequired()
224 @CSRFRequired()
225 def my_account_auth_tokens_view(self):
225 def my_account_auth_tokens_view(self):
226 _ = self.request.translate
226 _ = self.request.translate
227 c = self.load_default_context()
227 c = self.load_default_context()
228
228
229 auth_token_id = self.request.POST.get('auth_token_id')
229 auth_token_id = self.request.POST.get('auth_token_id')
230
230
231 if auth_token_id:
231 if auth_token_id:
232 token = UserApiKeys.get_or_404(auth_token_id)
232 token = UserApiKeys.get_or_404(auth_token_id)
233 if token.user.user_id != c.user.user_id:
233 if token.user.user_id != c.user.user_id:
234 raise HTTPNotFound()
234 raise HTTPNotFound()
235
235
236 return {
236 return {
237 'auth_token': token.api_key
237 'auth_token': token.api_key
238 }
238 }
239
239
240 def maybe_attach_token_scope(self, token):
240 def maybe_attach_token_scope(self, token):
241 # implemented in EE edition
241 # implemented in EE edition
242 pass
242 pass
243
243
244 @LoginRequired()
244 @LoginRequired()
245 @NotAnonymous()
245 @NotAnonymous()
246 @CSRFRequired()
246 @CSRFRequired()
247 def my_account_auth_tokens_add(self):
247 def my_account_auth_tokens_add(self):
248 _ = self.request.translate
248 _ = self.request.translate
249 c = self.load_default_context()
249 c = self.load_default_context()
250
250
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
251 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
252 description = self.request.POST.get('description')
252 description = self.request.POST.get('description')
253 role = self.request.POST.get('role')
253 role = self.request.POST.get('role')
254
254
255 token = UserModel().add_auth_token(
255 token = UserModel().add_auth_token(
256 user=c.user.user_id,
256 user=c.user.user_id,
257 lifetime_minutes=lifetime, role=role, description=description,
257 lifetime_minutes=lifetime, role=role, description=description,
258 scope_callback=self.maybe_attach_token_scope)
258 scope_callback=self.maybe_attach_token_scope)
259 token_data = token.get_api_data()
259 token_data = token.get_api_data()
260
260
261 audit_logger.store_web(
261 audit_logger.store_web(
262 'user.edit.token.add', action_data={
262 'user.edit.token.add', action_data={
263 'data': {'token': token_data, 'user': 'self'}},
263 'data': {'token': token_data, 'user': 'self'}},
264 user=self._rhodecode_user, )
264 user=self._rhodecode_user, )
265 Session().commit()
265 Session().commit()
266
266
267 h.flash(_("Auth token successfully created"), category='success')
267 h.flash(_("Auth token successfully created"), category='success')
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
268 return HTTPFound(h.route_path('my_account_auth_tokens'))
269
269
270 @LoginRequired()
270 @LoginRequired()
271 @NotAnonymous()
271 @NotAnonymous()
272 @CSRFRequired()
272 @CSRFRequired()
273 def my_account_auth_tokens_delete(self):
273 def my_account_auth_tokens_delete(self):
274 _ = self.request.translate
274 _ = self.request.translate
275 c = self.load_default_context()
275 c = self.load_default_context()
276
276
277 del_auth_token = self.request.POST.get('del_auth_token')
277 del_auth_token = self.request.POST.get('del_auth_token')
278
278
279 if del_auth_token:
279 if del_auth_token:
280 token = UserApiKeys.get_or_404(del_auth_token)
280 token = UserApiKeys.get_or_404(del_auth_token)
281 token_data = token.get_api_data()
281 token_data = token.get_api_data()
282
282
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
283 AuthTokenModel().delete(del_auth_token, c.user.user_id)
284 audit_logger.store_web(
284 audit_logger.store_web(
285 'user.edit.token.delete', action_data={
285 'user.edit.token.delete', action_data={
286 'data': {'token': token_data, 'user': 'self'}},
286 'data': {'token': token_data, 'user': 'self'}},
287 user=self._rhodecode_user,)
287 user=self._rhodecode_user,)
288 Session().commit()
288 Session().commit()
289 h.flash(_("Auth token successfully deleted"), category='success')
289 h.flash(_("Auth token successfully deleted"), category='success')
290
290
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
291 return HTTPFound(h.route_path('my_account_auth_tokens'))
292
292
293 @LoginRequired()
293 @LoginRequired()
294 @NotAnonymous()
294 @NotAnonymous()
295 def my_account_emails(self):
295 def my_account_emails(self):
296 _ = self.request.translate
296 _ = self.request.translate
297
297
298 c = self.load_default_context()
298 c = self.load_default_context()
299 c.active = 'emails'
299 c.active = 'emails'
300
300
301 c.user_email_map = UserEmailMap.query()\
301 c.user_email_map = UserEmailMap.query()\
302 .filter(UserEmailMap.user == c.user).all()
302 .filter(UserEmailMap.user == c.user).all()
303
303
304 schema = user_schema.AddEmailSchema().bind(
304 schema = user_schema.AddEmailSchema().bind(
305 username=c.user.username, user_emails=c.user.emails)
305 username=c.user.username, user_emails=c.user.emails)
306
306
307 form = forms.RcForm(schema,
307 form = forms.RcForm(schema,
308 action=h.route_path('my_account_emails_add'),
308 action=h.route_path('my_account_emails_add'),
309 buttons=(forms.buttons.save, forms.buttons.reset))
309 buttons=(forms.buttons.save, forms.buttons.reset))
310
310
311 c.form = form
311 c.form = form
312 return self._get_template_context(c)
312 return self._get_template_context(c)
313
313
314 @LoginRequired()
314 @LoginRequired()
315 @NotAnonymous()
315 @NotAnonymous()
316 @CSRFRequired()
316 @CSRFRequired()
317 def my_account_emails_add(self):
317 def my_account_emails_add(self):
318 _ = self.request.translate
318 _ = self.request.translate
319 c = self.load_default_context()
319 c = self.load_default_context()
320 c.active = 'emails'
320 c.active = 'emails'
321
321
322 schema = user_schema.AddEmailSchema().bind(
322 schema = user_schema.AddEmailSchema().bind(
323 username=c.user.username, user_emails=c.user.emails)
323 username=c.user.username, user_emails=c.user.emails)
324
324
325 form = forms.RcForm(
325 form = forms.RcForm(
326 schema, action=h.route_path('my_account_emails_add'),
326 schema, action=h.route_path('my_account_emails_add'),
327 buttons=(forms.buttons.save, forms.buttons.reset))
327 buttons=(forms.buttons.save, forms.buttons.reset))
328
328
329 controls = self.request.POST.items()
329 controls = self.request.POST.items()
330 try:
330 try:
331 valid_data = form.validate(controls)
331 valid_data = form.validate(controls)
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
332 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
333 audit_logger.store_web(
333 audit_logger.store_web(
334 'user.edit.email.add', action_data={
334 'user.edit.email.add', action_data={
335 'data': {'email': valid_data['email'], 'user': 'self'}},
335 'data': {'email': valid_data['email'], 'user': 'self'}},
336 user=self._rhodecode_user,)
336 user=self._rhodecode_user,)
337 Session().commit()
337 Session().commit()
338 except formencode.Invalid as error:
338 except formencode.Invalid as error:
339 h.flash(h.escape(error.error_dict['email']), category='error')
339 h.flash(h.escape(error.error_dict['email']), category='error')
340 except forms.ValidationFailure as e:
340 except forms.ValidationFailure as e:
341 c.user_email_map = UserEmailMap.query() \
341 c.user_email_map = UserEmailMap.query() \
342 .filter(UserEmailMap.user == c.user).all()
342 .filter(UserEmailMap.user == c.user).all()
343 c.form = e
343 c.form = e
344 return self._get_template_context(c)
344 return self._get_template_context(c)
345 except Exception:
345 except Exception:
346 log.exception("Exception adding email")
346 log.exception("Exception adding email")
347 h.flash(_('Error occurred during adding email'),
347 h.flash(_('Error occurred during adding email'),
348 category='error')
348 category='error')
349 else:
349 else:
350 h.flash(_("Successfully added email"), category='success')
350 h.flash(_("Successfully added email"), category='success')
351
351
352 raise HTTPFound(self.request.route_path('my_account_emails'))
352 raise HTTPFound(self.request.route_path('my_account_emails'))
353
353
354 @LoginRequired()
354 @LoginRequired()
355 @NotAnonymous()
355 @NotAnonymous()
356 @CSRFRequired()
356 @CSRFRequired()
357 def my_account_emails_delete(self):
357 def my_account_emails_delete(self):
358 _ = self.request.translate
358 _ = self.request.translate
359 c = self.load_default_context()
359 c = self.load_default_context()
360
360
361 del_email_id = self.request.POST.get('del_email_id')
361 del_email_id = self.request.POST.get('del_email_id')
362 if del_email_id:
362 if del_email_id:
363 email = UserEmailMap.get_or_404(del_email_id).email
363 email = UserEmailMap.get_or_404(del_email_id).email
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
364 UserModel().delete_extra_email(c.user.user_id, del_email_id)
365 audit_logger.store_web(
365 audit_logger.store_web(
366 'user.edit.email.delete', action_data={
366 'user.edit.email.delete', action_data={
367 'data': {'email': email, 'user': 'self'}},
367 'data': {'email': email, 'user': 'self'}},
368 user=self._rhodecode_user,)
368 user=self._rhodecode_user,)
369 Session().commit()
369 Session().commit()
370 h.flash(_("Email successfully deleted"),
370 h.flash(_("Email successfully deleted"),
371 category='success')
371 category='success')
372 return HTTPFound(h.route_path('my_account_emails'))
372 return HTTPFound(h.route_path('my_account_emails'))
373
373
374 @LoginRequired()
374 @LoginRequired()
375 @NotAnonymous()
375 @NotAnonymous()
376 @CSRFRequired()
376 @CSRFRequired()
377 def my_account_notifications_test_channelstream(self):
377 def my_account_notifications_test_channelstream(self):
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
378 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
379 self._rhodecode_user.username, datetime.datetime.now())
379 self._rhodecode_user.username, datetime.datetime.now())
380 payload = {
380 payload = {
381 # 'channel': 'broadcast',
381 # 'channel': 'broadcast',
382 'type': 'message',
382 'type': 'message',
383 'timestamp': datetime.datetime.utcnow(),
383 'timestamp': datetime.datetime.utcnow(),
384 'user': 'system',
384 'user': 'system',
385 'pm_users': [self._rhodecode_user.username],
385 'pm_users': [self._rhodecode_user.username],
386 'message': {
386 'message': {
387 'message': message,
387 'message': message,
388 'level': 'info',
388 'level': 'info',
389 'topic': '/notifications'
389 'topic': '/notifications'
390 }
390 }
391 }
391 }
392
392
393 registry = self.request.registry
393 registry = self.request.registry
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
394 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
395 channelstream_config = rhodecode_plugins.get('channelstream', {})
396
396
397 try:
397 try:
398 channelstream_request(channelstream_config, [payload], '/message')
398 channelstream_request(channelstream_config, [payload], '/message')
399 except ChannelstreamException as e:
399 except ChannelstreamException as e:
400 log.exception('Failed to send channelstream data')
400 log.exception('Failed to send channelstream data')
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
401 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
402 return {"response": 'Channelstream data sent. '
402 return {"response": 'Channelstream data sent. '
403 'You should see a new live message now.'}
403 'You should see a new live message now.'}
404
404
405 def _load_my_repos_data(self, watched=False):
405 def _load_my_repos_data(self, watched=False):
406
406
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
407 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
408
408
409 if watched:
409 if watched:
410 # repos user watch
410 # repos user watch
411 repo_list = Session().query(
411 repo_list = Session().query(
412 Repository
412 Repository
413 ) \
413 ) \
414 .join(
414 .join(
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
415 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
416 ) \
416 ) \
417 .filter(
417 .filter(
418 UserFollowing.user_id == self._rhodecode_user.user_id
418 UserFollowing.user_id == self._rhodecode_user.user_id
419 ) \
419 ) \
420 .filter(or_(
420 .filter(or_(
421 # generate multiple IN to fix limitation problems
421 # generate multiple IN to fix limitation problems
422 *in_filter_generator(Repository.repo_id, allowed_ids))
422 *in_filter_generator(Repository.repo_id, allowed_ids))
423 ) \
423 ) \
424 .order_by(Repository.repo_name) \
424 .order_by(Repository.repo_name) \
425 .all()
425 .all()
426
426
427 else:
427 else:
428 # repos user is owner of
428 # repos user is owner of
429 repo_list = Session().query(
429 repo_list = Session().query(
430 Repository
430 Repository
431 ) \
431 ) \
432 .filter(
432 .filter(
433 Repository.user_id == self._rhodecode_user.user_id
433 Repository.user_id == self._rhodecode_user.user_id
434 ) \
434 ) \
435 .filter(or_(
435 .filter(or_(
436 # generate multiple IN to fix limitation problems
436 # generate multiple IN to fix limitation problems
437 *in_filter_generator(Repository.repo_id, allowed_ids))
437 *in_filter_generator(Repository.repo_id, allowed_ids))
438 ) \
438 ) \
439 .order_by(Repository.repo_name) \
439 .order_by(Repository.repo_name) \
440 .all()
440 .all()
441
441
442 _render = self.request.get_partial_renderer(
442 _render = self.request.get_partial_renderer(
443 'rhodecode:templates/data_table/_dt_elements.mako')
443 'rhodecode:templates/data_table/_dt_elements.mako')
444
444
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
445 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
446 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
447 short_name=False, admin=False)
447 short_name=False, admin=False)
448
448
449 repos_data = []
449 repos_data = []
450 for repo in repo_list:
450 for repo in repo_list:
451 row = {
451 row = {
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
452 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
453 repo.private, repo.archived, repo.fork),
453 repo.private, repo.archived, repo.fork),
454 "name_raw": repo.repo_name.lower(),
454 "name_raw": repo.repo_name.lower(),
455 }
455 }
456
456
457 repos_data.append(row)
457 repos_data.append(row)
458
458
459 # json used to render the grid
459 # json used to render the grid
460 return json.dumps(repos_data)
460 return json.dumps(repos_data)
461
461
462 @LoginRequired()
462 @LoginRequired()
463 @NotAnonymous()
463 @NotAnonymous()
464 def my_account_repos(self):
464 def my_account_repos(self):
465 c = self.load_default_context()
465 c = self.load_default_context()
466 c.active = 'repos'
466 c.active = 'repos'
467
467
468 # json used to render the grid
468 # json used to render the grid
469 c.data = self._load_my_repos_data()
469 c.data = self._load_my_repos_data()
470 return self._get_template_context(c)
470 return self._get_template_context(c)
471
471
472 @LoginRequired()
472 @LoginRequired()
473 @NotAnonymous()
473 @NotAnonymous()
474 def my_account_watched(self):
474 def my_account_watched(self):
475 c = self.load_default_context()
475 c = self.load_default_context()
476 c.active = 'watched'
476 c.active = 'watched'
477
477
478 # json used to render the grid
478 # json used to render the grid
479 c.data = self._load_my_repos_data(watched=True)
479 c.data = self._load_my_repos_data(watched=True)
480 return self._get_template_context(c)
480 return self._get_template_context(c)
481
481
482 @LoginRequired()
482 @LoginRequired()
483 @NotAnonymous()
483 @NotAnonymous()
484 def my_account_bookmarks(self):
484 def my_account_bookmarks(self):
485 c = self.load_default_context()
485 c = self.load_default_context()
486 c.active = 'bookmarks'
486 c.active = 'bookmarks'
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
487 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
488 self._rhodecode_db_user.user_id, cache=False)
488 self._rhodecode_db_user.user_id, cache=False)
489 return self._get_template_context(c)
489 return self._get_template_context(c)
490
490
491 def _process_bookmark_entry(self, entry, user_id):
491 def _process_bookmark_entry(self, entry, user_id):
492 position = safe_int(entry.get('position'))
492 position = safe_int(entry.get('position'))
493 cur_position = safe_int(entry.get('cur_position'))
493 cur_position = safe_int(entry.get('cur_position'))
494 if position is None:
494 if position is None:
495 return
495 return
496
496
497 # check if this is an existing entry
497 # check if this is an existing entry
498 is_new = False
498 is_new = False
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
499 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
500
500
501 if db_entry and str2bool(entry.get('remove')):
501 if db_entry and str2bool(entry.get('remove')):
502 log.debug('Marked bookmark %s for deletion', db_entry)
502 log.debug('Marked bookmark %s for deletion', db_entry)
503 Session().delete(db_entry)
503 Session().delete(db_entry)
504 return
504 return
505
505
506 if not db_entry:
506 if not db_entry:
507 # new
507 # new
508 db_entry = UserBookmark()
508 db_entry = UserBookmark()
509 is_new = True
509 is_new = True
510
510
511 should_save = False
511 should_save = False
512 default_redirect_url = ''
512 default_redirect_url = ''
513
513
514 # save repo
514 # save repo
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
515 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
516 repo = Repository.get(entry['bookmark_repo'])
516 repo = Repository.get(entry['bookmark_repo'])
517 perm_check = HasRepoPermissionAny(
517 perm_check = HasRepoPermissionAny(
518 'repository.read', 'repository.write', 'repository.admin')
518 'repository.read', 'repository.write', 'repository.admin')
519 if repo and perm_check(repo_name=repo.repo_name):
519 if repo and perm_check(repo_name=repo.repo_name):
520 db_entry.repository = repo
520 db_entry.repository = repo
521 should_save = True
521 should_save = True
522 default_redirect_url = '${repo_url}'
522 default_redirect_url = '${repo_url}'
523 # save repo group
523 # save repo group
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
524 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
525 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
526 perm_check = HasRepoGroupPermissionAny(
526 perm_check = HasRepoGroupPermissionAny(
527 'group.read', 'group.write', 'group.admin')
527 'group.read', 'group.write', 'group.admin')
528
528
529 if repo_group and perm_check(group_name=repo_group.group_name):
529 if repo_group and perm_check(group_name=repo_group.group_name):
530 db_entry.repository_group = repo_group
530 db_entry.repository_group = repo_group
531 should_save = True
531 should_save = True
532 default_redirect_url = '${repo_group_url}'
532 default_redirect_url = '${repo_group_url}'
533 # save generic info
533 # save generic info
534 elif entry.get('title') and entry.get('redirect_url'):
534 elif entry.get('title') and entry.get('redirect_url'):
535 should_save = True
535 should_save = True
536
536
537 if should_save:
537 if should_save:
538 # mark user and position
538 # mark user and position
539 db_entry.user_id = user_id
539 db_entry.user_id = user_id
540 db_entry.position = position
540 db_entry.position = position
541 db_entry.title = entry.get('title')
541 db_entry.title = entry.get('title')
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
542 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
543 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
544
544
545 Session().add(db_entry)
545 Session().add(db_entry)
546
546
547 @LoginRequired()
547 @LoginRequired()
548 @NotAnonymous()
548 @NotAnonymous()
549 @CSRFRequired()
549 @CSRFRequired()
550 def my_account_bookmarks_update(self):
550 def my_account_bookmarks_update(self):
551 _ = self.request.translate
551 _ = self.request.translate
552 c = self.load_default_context()
552 c = self.load_default_context()
553 c.active = 'bookmarks'
553 c.active = 'bookmarks'
554
554
555 controls = peppercorn.parse(self.request.POST.items())
555 controls = peppercorn.parse(self.request.POST.items())
556 user_id = c.user.user_id
556 user_id = c.user.user_id
557
557
558 # validate positions
558 # validate positions
559 positions = {}
559 positions = {}
560 for entry in controls.get('bookmarks', []):
560 for entry in controls.get('bookmarks', []):
561 position = safe_int(entry['position'])
561 position = safe_int(entry['position'])
562 if position is None:
562 if position is None:
563 continue
563 continue
564
564
565 if position in positions:
565 if position in positions:
566 h.flash(_("Position {} is defined twice. "
566 h.flash(_("Position {} is defined twice. "
567 "Please correct this error.").format(position), category='error')
567 "Please correct this error.").format(position), category='error')
568 return HTTPFound(h.route_path('my_account_bookmarks'))
568 return HTTPFound(h.route_path('my_account_bookmarks'))
569
569
570 entry['position'] = position
570 entry['position'] = position
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
571 entry['cur_position'] = safe_int(entry.get('cur_position'))
572 positions[position] = entry
572 positions[position] = entry
573
573
574 try:
574 try:
575 for entry in positions.values():
575 for entry in positions.values():
576 self._process_bookmark_entry(entry, user_id)
576 self._process_bookmark_entry(entry, user_id)
577
577
578 Session().commit()
578 Session().commit()
579 h.flash(_("Update Bookmarks"), category='success')
579 h.flash(_("Update Bookmarks"), category='success')
580 except IntegrityError:
580 except IntegrityError:
581 h.flash(_("Failed to update bookmarks. "
581 h.flash(_("Failed to update bookmarks. "
582 "Make sure an unique position is used."), category='error')
582 "Make sure an unique position is used."), category='error')
583
583
584 return HTTPFound(h.route_path('my_account_bookmarks'))
584 return HTTPFound(h.route_path('my_account_bookmarks'))
585
585
586 @LoginRequired()
586 @LoginRequired()
587 @NotAnonymous()
587 @NotAnonymous()
588 def my_account_goto_bookmark(self):
588 def my_account_goto_bookmark(self):
589
589
590 bookmark_id = self.request.matchdict['bookmark_id']
590 bookmark_id = self.request.matchdict['bookmark_id']
591 user_bookmark = UserBookmark().query()\
591 user_bookmark = UserBookmark().query()\
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
592 .filter(UserBookmark.user_id == self.request.user.user_id) \
593 .filter(UserBookmark.position == bookmark_id).scalar()
593 .filter(UserBookmark.position == bookmark_id).scalar()
594
594
595 redirect_url = h.route_path('my_account_bookmarks')
595 redirect_url = h.route_path('my_account_bookmarks')
596 if not user_bookmark:
596 if not user_bookmark:
597 raise HTTPFound(redirect_url)
597 raise HTTPFound(redirect_url)
598
598
599 # repository set
599 # repository set
600 if user_bookmark.repository:
600 if user_bookmark.repository:
601 repo_name = user_bookmark.repository.repo_name
601 repo_name = user_bookmark.repository.repo_name
602 base_redirect_url = h.route_path(
602 base_redirect_url = h.route_path(
603 'repo_summary', repo_name=repo_name)
603 'repo_summary', repo_name=repo_name)
604 if user_bookmark.redirect_url and \
604 if user_bookmark.redirect_url and \
605 '${repo_url}' in user_bookmark.redirect_url:
605 '${repo_url}' in user_bookmark.redirect_url:
606 redirect_url = string.Template(user_bookmark.redirect_url)\
606 redirect_url = string.Template(user_bookmark.redirect_url)\
607 .safe_substitute({'repo_url': base_redirect_url})
607 .safe_substitute({'repo_url': base_redirect_url})
608 else:
608 else:
609 redirect_url = base_redirect_url
609 redirect_url = base_redirect_url
610 # repository group set
610 # repository group set
611 elif user_bookmark.repository_group:
611 elif user_bookmark.repository_group:
612 repo_group_name = user_bookmark.repository_group.group_name
612 repo_group_name = user_bookmark.repository_group.group_name
613 base_redirect_url = h.route_path(
613 base_redirect_url = h.route_path(
614 'repo_group_home', repo_group_name=repo_group_name)
614 'repo_group_home', repo_group_name=repo_group_name)
615 if user_bookmark.redirect_url and \
615 if user_bookmark.redirect_url and \
616 '${repo_group_url}' in user_bookmark.redirect_url:
616 '${repo_group_url}' in user_bookmark.redirect_url:
617 redirect_url = string.Template(user_bookmark.redirect_url)\
617 redirect_url = string.Template(user_bookmark.redirect_url)\
618 .safe_substitute({'repo_group_url': base_redirect_url})
618 .safe_substitute({'repo_group_url': base_redirect_url})
619 else:
619 else:
620 redirect_url = base_redirect_url
620 redirect_url = base_redirect_url
621 # custom URL set
621 # custom URL set
622 elif user_bookmark.redirect_url:
622 elif user_bookmark.redirect_url:
623 server_url = h.route_url('home').rstrip('/')
623 server_url = h.route_url('home').rstrip('/')
624 redirect_url = string.Template(user_bookmark.redirect_url) \
624 redirect_url = string.Template(user_bookmark.redirect_url) \
625 .safe_substitute({'server_url': server_url})
625 .safe_substitute({'server_url': server_url})
626
626
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
627 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
628 raise HTTPFound(redirect_url)
628 raise HTTPFound(redirect_url)
629
629
630 @LoginRequired()
630 @LoginRequired()
631 @NotAnonymous()
631 @NotAnonymous()
632 def my_account_perms(self):
632 def my_account_perms(self):
633 c = self.load_default_context()
633 c = self.load_default_context()
634 c.active = 'perms'
634 c.active = 'perms'
635
635
636 c.perm_user = c.auth_user
636 c.perm_user = c.auth_user
637 return self._get_template_context(c)
637 return self._get_template_context(c)
638
638
639 @LoginRequired()
639 @LoginRequired()
640 @NotAnonymous()
640 @NotAnonymous()
641 def my_notifications(self):
641 def my_notifications(self):
642 c = self.load_default_context()
642 c = self.load_default_context()
643 c.active = 'notifications'
643 c.active = 'notifications'
644
644
645 return self._get_template_context(c)
645 return self._get_template_context(c)
646
646
647 @LoginRequired()
647 @LoginRequired()
648 @NotAnonymous()
648 @NotAnonymous()
649 @CSRFRequired()
649 @CSRFRequired()
650 def my_notifications_toggle_visibility(self):
650 def my_notifications_toggle_visibility(self):
651 user = self._rhodecode_db_user
651 user = self._rhodecode_db_user
652 new_status = not user.user_data.get('notification_status', True)
652 new_status = not user.user_data.get('notification_status', True)
653 user.update_userdata(notification_status=new_status)
653 user.update_userdata(notification_status=new_status)
654 Session().commit()
654 Session().commit()
655 return user.user_data['notification_status']
655 return user.user_data['notification_status']
656
656
657 def _get_pull_requests_list(self, statuses):
657 def _get_pull_requests_list(self, statuses, filter_type=None):
658 draw, start, limit = self._extract_chunk(self.request)
658 draw, start, limit = self._extract_chunk(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 search_q, order_by, order_dir = self._extract_ordering(self.request)
660
660
661 _render = self.request.get_partial_renderer(
661 _render = self.request.get_partial_renderer(
662 'rhodecode:templates/data_table/_dt_elements.mako')
662 'rhodecode:templates/data_table/_dt_elements.mako')
663
663
664 if filter_type == 'awaiting_my_review':
665 pull_requests = PullRequestModel().get_im_participating_in_for_review(
666 user_id=self._rhodecode_user.user_id,
667 statuses=statuses, query=search_q,
668 offset=start, length=limit, order_by=order_by,
669 order_dir=order_dir)
670
671 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
672 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
673 else:
664 pull_requests = PullRequestModel().get_im_participating_in(
674 pull_requests = PullRequestModel().get_im_participating_in(
665 user_id=self._rhodecode_user.user_id,
675 user_id=self._rhodecode_user.user_id,
666 statuses=statuses, query=search_q,
676 statuses=statuses, query=search_q,
667 offset=start, length=limit, order_by=order_by,
677 offset=start, length=limit, order_by=order_by,
668 order_dir=order_dir)
678 order_dir=order_dir)
669
679
670 pull_requests_total_count = PullRequestModel().count_im_participating_in(
680 pull_requests_total_count = PullRequestModel().count_im_participating_in(
671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
681 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672
682
673 data = []
683 data = []
674 comments_model = CommentsModel()
684 comments_model = CommentsModel()
675 for pr in pull_requests:
685 for pr in pull_requests:
676 repo_id = pr.target_repo_id
686 repo_id = pr.target_repo_id
677 comments_count = comments_model.get_all_comments(
687 comments_count = comments_model.get_all_comments(
678 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 repo_id, pull_request=pr, include_drafts=False, count_only=True)
679 owned = pr.user_id == self._rhodecode_user.user_id
689 owned = pr.user_id == self._rhodecode_user.user_id
680
690
691 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
692 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
693 if review_statuses and review_statuses[4]:
694 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
695 my_review_status = statuses[0][1].status
696
681 data.append({
697 data.append({
682 'target_repo': _render('pullrequest_target_repo',
698 'target_repo': _render('pullrequest_target_repo',
683 pr.target_repo.repo_name),
699 pr.target_repo.repo_name),
684 'name': _render('pullrequest_name',
700 'name': _render('pullrequest_name',
685 pr.pull_request_id, pr.pull_request_state,
701 pr.pull_request_id, pr.pull_request_state,
686 pr.work_in_progress, pr.target_repo.repo_name,
702 pr.work_in_progress, pr.target_repo.repo_name,
687 short=True),
703 short=True),
688 'name_raw': pr.pull_request_id,
704 'name_raw': pr.pull_request_id,
689 'status': _render('pullrequest_status',
705 'status': _render('pullrequest_status',
690 pr.calculated_review_status()),
706 pr.calculated_review_status()),
707 'my_status': _render('pullrequest_status',
708 my_review_status),
691 'title': _render('pullrequest_title', pr.title, pr.description),
709 'title': _render('pullrequest_title', pr.title, pr.description),
692 'description': h.escape(pr.description),
710 'description': h.escape(pr.description),
693 'updated_on': _render('pullrequest_updated_on',
711 'updated_on': _render('pullrequest_updated_on',
694 h.datetime_to_time(pr.updated_on),
712 h.datetime_to_time(pr.updated_on),
695 pr.versions_count),
713 pr.versions_count),
696 'updated_on_raw': h.datetime_to_time(pr.updated_on),
714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
697 'created_on': _render('pullrequest_updated_on',
715 'created_on': _render('pullrequest_updated_on',
698 h.datetime_to_time(pr.created_on)),
716 h.datetime_to_time(pr.created_on)),
699 'created_on_raw': h.datetime_to_time(pr.created_on),
717 'created_on_raw': h.datetime_to_time(pr.created_on),
700 'state': pr.pull_request_state,
718 'state': pr.pull_request_state,
701 'author': _render('pullrequest_author',
719 'author': _render('pullrequest_author',
702 pr.author.full_contact, ),
720 pr.author.full_contact, ),
703 'author_raw': pr.author.full_name,
721 'author_raw': pr.author.full_name,
704 'comments': _render('pullrequest_comments', comments_count),
722 'comments': _render('pullrequest_comments', comments_count),
705 'comments_raw': comments_count,
723 'comments_raw': comments_count,
706 'closed': pr.is_closed(),
724 'closed': pr.is_closed(),
707 'owned': owned
725 'owned': owned
708 })
726 })
709
727
710 # json used to render the grid
728 # json used to render the grid
711 data = ({
729 data = ({
712 'draw': draw,
730 'draw': draw,
713 'data': data,
731 'data': data,
714 'recordsTotal': pull_requests_total_count,
732 'recordsTotal': pull_requests_total_count,
715 'recordsFiltered': pull_requests_total_count,
733 'recordsFiltered': pull_requests_total_count,
716 })
734 })
717 return data
735 return data
718
736
719 @LoginRequired()
737 @LoginRequired()
720 @NotAnonymous()
738 @NotAnonymous()
721 def my_account_pullrequests(self):
739 def my_account_pullrequests(self):
722 c = self.load_default_context()
740 c = self.load_default_context()
723 c.active = 'pullrequests'
741 c.active = 'pullrequests'
724 req_get = self.request.GET
742 req_get = self.request.GET
725
743
726 c.closed = str2bool(req_get.get('pr_show_closed'))
744 c.closed = str2bool(req_get.get('closed'))
745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
746
747 c.selected_filter = 'all'
748 if c.closed:
749 c.selected_filter = 'all_closed'
750 if c.awaiting_my_review:
751 c.selected_filter = 'awaiting_my_review'
727
752
728 return self._get_template_context(c)
753 return self._get_template_context(c)
729
754
730 @LoginRequired()
755 @LoginRequired()
731 @NotAnonymous()
756 @NotAnonymous()
732 def my_account_pullrequests_data(self):
757 def my_account_pullrequests_data(self):
733 self.load_default_context()
758 self.load_default_context()
734 req_get = self.request.GET
759 req_get = self.request.GET
760
761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
735 closed = str2bool(req_get.get('closed'))
762 closed = str2bool(req_get.get('closed'))
736
763
737 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
738 if closed:
765 if closed:
739 statuses += [PullRequest.STATUS_CLOSED]
766 statuses += [PullRequest.STATUS_CLOSED]
740
767
741 data = self._get_pull_requests_list(statuses=statuses)
768 filter_type = \
769 'awaiting_my_review' if awaiting_my_review \
770 else None
771
772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
742 return data
773 return data
743
774
744 @LoginRequired()
775 @LoginRequired()
745 @NotAnonymous()
776 @NotAnonymous()
746 def my_account_user_group_membership(self):
777 def my_account_user_group_membership(self):
747 c = self.load_default_context()
778 c = self.load_default_context()
748 c.active = 'user_group_membership'
779 c.active = 'user_group_membership'
749 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
750 for group in self._rhodecode_db_user.group_member]
781 for group in self._rhodecode_db_user.group_member]
751 c.user_groups = json.dumps(groups)
782 c.user_groups = json.dumps(groups)
752 return self._get_template_context(c)
783 return self._get_template_context(c)
@@ -1,84 +1,84 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 import pytest
22 from rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
23
23
24
24
25 def route_path(name, params=None, **kwargs):
25 def route_path(name, params=None, **kwargs):
26 import urllib
26 import urllib
27
27
28 base_url = {
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
31 }[name].format(**kwargs)
32
32
33 if params:
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
35 return base_url
36
36
37
37
38 @pytest.mark.backends("git", "hg")
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
40 class TestPullRequestList(object):
41
41
42 @pytest.mark.parametrize('params, expected_title', [
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed'),
43 ({'source': 0, 'closed': 1}, 'Closed'),
44 ({'source': 0, 'my': 1}, 'Opened by me'),
44 ({'source': 0, 'my': 1}, 'Created by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
45 ({'source': 0, 'awaiting_review': 1}, 'Awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'Awaiting my review'),
47 ({'source': 1}, 'From this repo'),
47 ({'source': 1}, 'From this repo'),
48 ])
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
50 pull_request = pr_util.create_pull_request()
51
51
52 response = self.app.get(
52 response = self.app.get(
53 route_path('pullrequest_show_all',
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
55 params=params))
56
56
57 assert_response = response.assert_response()
57 assert_response = response.assert_response()
58
58
59 element = assert_response.get_element('.title .active')
59 element = assert_response.get_element('.title .active')
60 element_text = element.text_content()
60 element_text = element.text_content()
61 assert expected_title == element_text
61 assert expected_title == element_text
62
62
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
64 pull_request = pr_util.create_pull_request()
64 pull_request = pr_util.create_pull_request()
65 response = self.app.get(
65 response = self.app.get(
66 route_path('pullrequest_show_all_data',
66 route_path('pullrequest_show_all_data',
67 repo_name=pull_request.target_repo.repo_name),
67 repo_name=pull_request.target_repo.repo_name),
68 extra_environ=xhr_header)
68 extra_environ=xhr_header)
69
69
70 assert response.json['recordsTotal'] == 1
70 assert response.json['recordsTotal'] == 1
71 assert response.json['data'][0]['description'] == 'Description'
71 assert response.json['data'][0]['description'] == 'Description'
72
72
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
74 xss_description = "<script>alert('Hi!')</script>"
74 xss_description = "<script>alert('Hi!')</script>"
75 pull_request = pr_util.create_pull_request(description=xss_description)
75 pull_request = pr_util.create_pull_request(description=xss_description)
76
76
77 response = self.app.get(
77 response = self.app.get(
78 route_path('pullrequest_show_all_data',
78 route_path('pullrequest_show_all_data',
79 repo_name=pull_request.target_repo.repo_name),
79 repo_name=pull_request.target_repo.repo_name),
80 extra_environ=xhr_header)
80 extra_environ=xhr_header)
81
81
82 assert response.json['recordsTotal'] == 1
82 assert response.json['recordsTotal'] == 1
83 assert response.json['data'][0]['description'] == \
83 assert response.json['data'][0]['description'] == \
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
84 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -1,1861 +1,1868 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name,
83 statuses=statuses, offset=start, length=limit,
83 search_q=search_q, statuses=statuses,
84 order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name,
87 opened_by=opened_by)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, self._rhodecode_user.user_id,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
95 search_q=search_q, statuses=statuses)
97 else:
96 else:
98 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
103 opened_by=opened_by)
105
104
106 data = []
105 data = []
107 comments_model = CommentsModel()
106 comments_model = CommentsModel()
108 for pr in pull_requests:
107 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
112
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
117
113 data.append({
118 data.append({
114 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
116 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
117 short=True),
122 short=True),
118 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
127 my_review_status),
121 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
132 pr.versions_count),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
128 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
130 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
131 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
132 pr.author.full_contact, ),
139 pr.author.full_contact, ),
133 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
134 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
135 'comments_raw': comments_count,
142 'comments_raw': comments_count,
136 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
137 })
144 })
138
145
139 data = ({
146 data = ({
140 'draw': draw,
147 'draw': draw,
141 'data': data,
148 'data': data,
142 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
144 })
151 })
145 return data
152 return data
146
153
147 @LoginRequired()
154 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
150 def pull_request_list(self):
157 def pull_request_list(self):
151 c = self.load_default_context()
158 c = self.load_default_context()
152
159
153 req_get = self.request.GET
160 req_get = self.request.GET
154 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
155 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
156 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
157 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
158 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
159
166
160 c.active = 'open'
167 c.active = 'open'
161 if c.my:
168 if c.my:
162 c.active = 'my'
169 c.active = 'my'
163 if c.closed:
170 if c.closed:
164 c.active = 'closed'
171 c.active = 'closed'
165 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
166 c.active = 'awaiting'
173 c.active = 'awaiting'
167 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
168 c.active = 'source'
175 c.active = 'source'
169 if c.awaiting_my_review:
176 if c.awaiting_my_review:
170 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
171
178
172 return self._get_template_context(c)
179 return self._get_template_context(c)
173
180
174 @LoginRequired()
181 @LoginRequired()
175 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
176 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
177 def pull_request_list_data(self):
184 def pull_request_list_data(self):
178 self.load_default_context()
185 self.load_default_context()
179
186
180 # additional filters
187 # additional filters
181 req_get = self.request.GET
188 req_get = self.request.GET
182 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
183 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
184 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
185 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187
194
188 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
189 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
190 else None
197 else None
191
198
192 opened_by = None
199 opened_by = None
193 if my:
200 if my:
194 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
195
202
196 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 if closed:
204 if closed:
198 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
199
206
200 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
201 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
202 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203
210
204 return data
211 return data
205
212
206 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
207 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
208 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
209 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
210 return caching_enabled
217 return caching_enabled
211
218
212 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
213 ancestor_commit,
220 ancestor_commit,
214 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
215 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217
224
218 target_commit_final = target_commit
225 target_commit_final = target_commit
219 source_commit_final = source_commit
226 source_commit_final = source_commit
220
227
221 if use_ancestor:
228 if use_ancestor:
222 # we might want to not use it for versions
229 # we might want to not use it for versions
223 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
224 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
225
232
226 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
227 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
228 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
229
236
230 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
231 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
232 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
233
240
234 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
235
242
236 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
237 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
238 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
239 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
240 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
241 )
248 )
242 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
243 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
244
251
245 return diffset
252 return diffset
246
253
247 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
248 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
249 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
250 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
251 commit1, commit2,
258 commit1, commit2,
252 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
253 context=diff_context)
260 context=diff_context)
254
261
255 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
256 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
257 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
258
265
259 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
260
267
261 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
262 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
263 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
264 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
265
272
266 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
267 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
268
275
269 return diffset
276 return diffset
270
277
271 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
272 comments_model = CommentsModel()
279 comments_model = CommentsModel()
273
280
274 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
275 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
276 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
277 if not include_drafts:
284 if not include_drafts:
278 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
279 general_comments = q
286 general_comments = q
280
287
281 # pick comments we want to render at current version
288 # pick comments we want to render at current version
282 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
283 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
284
291
285 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
286 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
287 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
288 if not include_drafts:
295 if not include_drafts:
289 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
290 inline_comments = q
297 inline_comments = q
291
298
292 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
293 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
294
301
295 # Comments inline+general
302 # Comments inline+general
296 if c.at_version:
303 if c.at_version:
297 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
298 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
299 else:
306 else:
300 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
301 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
302
309
303 return general_comments, inline_comments
310 return general_comments, inline_comments
304
311
305 @LoginRequired()
312 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
308 def pull_request_show(self):
315 def pull_request_show(self):
309 _ = self.request.translate
316 _ = self.request.translate
310 c = self.load_default_context()
317 c = self.load_default_context()
311
318
312 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
313 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
314 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
315
322
316 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
317 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
318
325
319 _new_state = {
326 _new_state = {
320 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
321 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
322
329
323 if c.is_super_admin and _new_state:
330 if c.is_super_admin and _new_state:
324 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
325 h.flash(
332 h.flash(
326 _('Pull Request state was force changed to `{}`').format(_new_state),
333 _('Pull Request state was force changed to `{}`').format(_new_state),
327 category='success')
334 category='success')
328 Session().commit()
335 Session().commit()
329
336
330 raise HTTPFound(h.route_path(
337 raise HTTPFound(h.route_path(
331 'pullrequest_show', repo_name=self.db_repo_name,
338 'pullrequest_show', repo_name=self.db_repo_name,
332 pull_request_id=pull_request_id))
339 pull_request_id=pull_request_id))
333
340
334 version = self.request.GET.get('version')
341 version = self.request.GET.get('version')
335 from_version = self.request.GET.get('from_version') or version
342 from_version = self.request.GET.get('from_version') or version
336 merge_checks = self.request.GET.get('merge_checks')
343 merge_checks = self.request.GET.get('merge_checks')
337 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
338 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
339 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
340
347
341 # fetch global flags of ignore ws or context lines
348 # fetch global flags of ignore ws or context lines
342 diff_context = diffs.get_diff_context(self.request)
349 diff_context = diffs.get_diff_context(self.request)
343 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
344
351
345 (pull_request_latest,
352 (pull_request_latest,
346 pull_request_at_ver,
353 pull_request_at_ver,
347 pull_request_display_obj,
354 pull_request_display_obj,
348 at_version) = PullRequestModel().get_pr_version(
355 at_version) = PullRequestModel().get_pr_version(
349 pull_request_id, version=version)
356 pull_request_id, version=version)
350
357
351 pr_closed = pull_request_latest.is_closed()
358 pr_closed = pull_request_latest.is_closed()
352
359
353 if pr_closed and (version or from_version):
360 if pr_closed and (version or from_version):
354 # not allow to browse versions for closed PR
361 # not allow to browse versions for closed PR
355 raise HTTPFound(h.route_path(
362 raise HTTPFound(h.route_path(
356 'pullrequest_show', repo_name=self.db_repo_name,
363 'pullrequest_show', repo_name=self.db_repo_name,
357 pull_request_id=pull_request_id))
364 pull_request_id=pull_request_id))
358
365
359 versions = pull_request_display_obj.versions()
366 versions = pull_request_display_obj.versions()
360
367
361 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
362
369
363 # used to store per-commit range diffs
370 # used to store per-commit range diffs
364 c.changes = collections.OrderedDict()
371 c.changes = collections.OrderedDict()
365
372
366 c.at_version = at_version
373 c.at_version = at_version
367 c.at_version_num = (at_version
374 c.at_version_num = (at_version
368 if at_version and at_version != PullRequest.LATEST_VER
375 if at_version and at_version != PullRequest.LATEST_VER
369 else None)
376 else None)
370
377
371 c.at_version_index = ChangesetComment.get_index_from_version(
378 c.at_version_index = ChangesetComment.get_index_from_version(
372 c.at_version_num, versions)
379 c.at_version_num, versions)
373
380
374 (prev_pull_request_latest,
381 (prev_pull_request_latest,
375 prev_pull_request_at_ver,
382 prev_pull_request_at_ver,
376 prev_pull_request_display_obj,
383 prev_pull_request_display_obj,
377 prev_at_version) = PullRequestModel().get_pr_version(
384 prev_at_version) = PullRequestModel().get_pr_version(
378 pull_request_id, version=from_version)
385 pull_request_id, version=from_version)
379
386
380 c.from_version = prev_at_version
387 c.from_version = prev_at_version
381 c.from_version_num = (prev_at_version
388 c.from_version_num = (prev_at_version
382 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
383 else None)
390 else None)
384 c.from_version_index = ChangesetComment.get_index_from_version(
391 c.from_version_index = ChangesetComment.get_index_from_version(
385 c.from_version_num, versions)
392 c.from_version_num, versions)
386
393
387 # define if we're in COMPARE mode or VIEW at version mode
394 # define if we're in COMPARE mode or VIEW at version mode
388 compare = at_version != prev_at_version
395 compare = at_version != prev_at_version
389
396
390 # pull_requests repo_name we opened it against
397 # pull_requests repo_name we opened it against
391 # ie. target_repo must match
398 # ie. target_repo must match
392 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
393 log.warning('Mismatch between the current repo: %s, and target %s',
400 log.warning('Mismatch between the current repo: %s, and target %s',
394 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
395 raise HTTPNotFound()
402 raise HTTPNotFound()
396
403
397 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
398
405
399 c.pull_request = pull_request_display_obj
406 c.pull_request = pull_request_display_obj
400 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
401 c.pull_request_latest = pull_request_latest
408 c.pull_request_latest = pull_request_latest
402
409
403 # inject latest version
410 # inject latest version
404 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
405 c.versions = versions + [latest_ver]
412 c.versions = versions + [latest_ver]
406
413
407 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
408 c.allowed_to_change_status = False
415 c.allowed_to_change_status = False
409 c.allowed_to_update = False
416 c.allowed_to_update = False
410 c.allowed_to_merge = False
417 c.allowed_to_merge = False
411 c.allowed_to_delete = False
418 c.allowed_to_delete = False
412 c.allowed_to_comment = False
419 c.allowed_to_comment = False
413 c.allowed_to_close = False
420 c.allowed_to_close = False
414 else:
421 else:
415 can_change_status = PullRequestModel().check_user_change_status(
422 can_change_status = PullRequestModel().check_user_change_status(
416 pull_request_at_ver, self._rhodecode_user)
423 pull_request_at_ver, self._rhodecode_user)
417 c.allowed_to_change_status = can_change_status and not pr_closed
424 c.allowed_to_change_status = can_change_status and not pr_closed
418
425
419 c.allowed_to_update = PullRequestModel().check_user_update(
426 c.allowed_to_update = PullRequestModel().check_user_update(
420 pull_request_latest, self._rhodecode_user) and not pr_closed
427 pull_request_latest, self._rhodecode_user) and not pr_closed
421 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
429 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
431 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_comment = not pr_closed
432 c.allowed_to_comment = not pr_closed
426 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
427
434
428 c.forbid_adding_reviewers = False
435 c.forbid_adding_reviewers = False
429
436
430 if pull_request_latest.reviewer_data and \
437 if pull_request_latest.reviewer_data and \
431 'rules' in pull_request_latest.reviewer_data:
438 'rules' in pull_request_latest.reviewer_data:
432 rules = pull_request_latest.reviewer_data['rules'] or {}
439 rules = pull_request_latest.reviewer_data['rules'] or {}
433 try:
440 try:
434 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
435 except Exception:
442 except Exception:
436 pass
443 pass
437
444
438 # check merge capabilities
445 # check merge capabilities
439 _merge_check = MergeCheck.validate(
446 _merge_check = MergeCheck.validate(
440 pull_request_latest, auth_user=self._rhodecode_user,
447 pull_request_latest, auth_user=self._rhodecode_user,
441 translator=self.request.translate,
448 translator=self.request.translate,
442 force_shadow_repo_refresh=force_refresh)
449 force_shadow_repo_refresh=force_refresh)
443
450
444 c.pr_merge_errors = _merge_check.error_details
451 c.pr_merge_errors = _merge_check.error_details
445 c.pr_merge_possible = not _merge_check.failed
452 c.pr_merge_possible = not _merge_check.failed
446 c.pr_merge_message = _merge_check.merge_msg
453 c.pr_merge_message = _merge_check.merge_msg
447 c.pr_merge_source_commit = _merge_check.source_commit
454 c.pr_merge_source_commit = _merge_check.source_commit
448 c.pr_merge_target_commit = _merge_check.target_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
449
456
450 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
451 pull_request_latest, translator=self.request.translate)
458 pull_request_latest, translator=self.request.translate)
452
459
453 c.pull_request_review_status = _merge_check.review_status
460 c.pull_request_review_status = _merge_check.review_status
454 if merge_checks:
461 if merge_checks:
455 self.request.override_renderer = \
462 self.request.override_renderer = \
456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
457 return self._get_template_context(c)
464 return self._get_template_context(c)
458
465
459 c.reviewers_count = pull_request.reviewers_count
466 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
467 c.observers_count = pull_request.observers_count
461
468
462 # reviewers and statuses
469 # reviewers and statuses
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
466
473
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
468 member_reviewer = h.reviewer_as_json(
475 member_reviewer = h.reviewer_as_json(
469 member, reasons=reasons, mandatory=mandatory,
476 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
477 role=review_obj.role,
471 user_group=review_obj.rule_user_group_data()
478 user_group=review_obj.rule_user_group_data()
472 )
479 )
473
480
474 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
475 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status'] = current_review_status
476 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
477 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
478 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
479
486
480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
481
488
482 for observer_obj, member in pull_request_at_ver.observers():
489 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
490 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
491 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
492 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
493 user_group=observer_obj.rule_user_group_data()
487 )
494 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
495 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
497
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
499
493 general_comments, inline_comments = \
500 general_comments, inline_comments = \
494 self.register_comments_vars(c, pull_request_latest, versions)
501 self.register_comments_vars(c, pull_request_latest, versions)
495
502
496 # TODOs
503 # TODOs
497 c.unresolved_comments = CommentsModel() \
504 c.unresolved_comments = CommentsModel() \
498 .get_pull_request_unresolved_todos(pull_request_latest)
505 .get_pull_request_unresolved_todos(pull_request_latest)
499 c.resolved_comments = CommentsModel() \
506 c.resolved_comments = CommentsModel() \
500 .get_pull_request_resolved_todos(pull_request_latest)
507 .get_pull_request_resolved_todos(pull_request_latest)
501
508
502 # Drafts
509 # Drafts
503 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
504 self._rhodecode_db_user.user_id,
511 self._rhodecode_db_user.user_id,
505 pull_request_latest)
512 pull_request_latest)
506
513
507 # if we use version, then do not show later comments
514 # if we use version, then do not show later comments
508 # than current version
515 # than current version
509 display_inline_comments = collections.defaultdict(
516 display_inline_comments = collections.defaultdict(
510 lambda: collections.defaultdict(list))
517 lambda: collections.defaultdict(list))
511 for co in inline_comments:
518 for co in inline_comments:
512 if c.at_version_num:
519 if c.at_version_num:
513 # pick comments that are at least UPTO given version, so we
520 # pick comments that are at least UPTO given version, so we
514 # don't render comments for higher version
521 # don't render comments for higher version
515 should_render = co.pull_request_version_id and \
522 should_render = co.pull_request_version_id and \
516 co.pull_request_version_id <= c.at_version_num
523 co.pull_request_version_id <= c.at_version_num
517 else:
524 else:
518 # showing all, for 'latest'
525 # showing all, for 'latest'
519 should_render = True
526 should_render = True
520
527
521 if should_render:
528 if should_render:
522 display_inline_comments[co.f_path][co.line_no].append(co)
529 display_inline_comments[co.f_path][co.line_no].append(co)
523
530
524 # load diff data into template context, if we use compare mode then
531 # load diff data into template context, if we use compare mode then
525 # diff is calculated based on changes between versions of PR
532 # diff is calculated based on changes between versions of PR
526
533
527 source_repo = pull_request_at_ver.source_repo
534 source_repo = pull_request_at_ver.source_repo
528 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
529
536
530 target_repo = pull_request_at_ver.target_repo
537 target_repo = pull_request_at_ver.target_repo
531 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
532
539
533 if compare:
540 if compare:
534 # in compare switch the diff base to latest commit from prev version
541 # in compare switch the diff base to latest commit from prev version
535 target_ref_id = prev_pull_request_display_obj.revisions[0]
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
536
543
537 # despite opening commits for bookmarks/branches/tags, we always
544 # despite opening commits for bookmarks/branches/tags, we always
538 # convert this to rev to prevent changes after bookmark or branch change
545 # convert this to rev to prevent changes after bookmark or branch change
539 c.source_ref_type = 'rev'
546 c.source_ref_type = 'rev'
540 c.source_ref = source_ref_id
547 c.source_ref = source_ref_id
541
548
542 c.target_ref_type = 'rev'
549 c.target_ref_type = 'rev'
543 c.target_ref = target_ref_id
550 c.target_ref = target_ref_id
544
551
545 c.source_repo = source_repo
552 c.source_repo = source_repo
546 c.target_repo = target_repo
553 c.target_repo = target_repo
547
554
548 c.commit_ranges = []
555 c.commit_ranges = []
549 source_commit = EmptyCommit()
556 source_commit = EmptyCommit()
550 target_commit = EmptyCommit()
557 target_commit = EmptyCommit()
551 c.missing_requirements = False
558 c.missing_requirements = False
552
559
553 source_scm = source_repo.scm_instance()
560 source_scm = source_repo.scm_instance()
554 target_scm = target_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
555
562
556 shadow_scm = None
563 shadow_scm = None
557 try:
564 try:
558 shadow_scm = pull_request_latest.get_shadow_repo()
565 shadow_scm = pull_request_latest.get_shadow_repo()
559 except Exception:
566 except Exception:
560 log.debug('Failed to get shadow repo', exc_info=True)
567 log.debug('Failed to get shadow repo', exc_info=True)
561 # try first the existing source_repo, and then shadow
568 # try first the existing source_repo, and then shadow
562 # repo if we can obtain one
569 # repo if we can obtain one
563 commits_source_repo = source_scm
570 commits_source_repo = source_scm
564 if shadow_scm:
571 if shadow_scm:
565 commits_source_repo = shadow_scm
572 commits_source_repo = shadow_scm
566
573
567 c.commits_source_repo = commits_source_repo
574 c.commits_source_repo = commits_source_repo
568 c.ancestor = None # set it to None, to hide it from PR view
575 c.ancestor = None # set it to None, to hide it from PR view
569
576
570 # empty version means latest, so we keep this to prevent
577 # empty version means latest, so we keep this to prevent
571 # double caching
578 # double caching
572 version_normalized = version or PullRequest.LATEST_VER
579 version_normalized = version or PullRequest.LATEST_VER
573 from_version_normalized = from_version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
574
581
575 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
576 cache_file_path = diff_cache_exist(
583 cache_file_path = diff_cache_exist(
577 cache_path, 'pull_request', pull_request_id, version_normalized,
584 cache_path, 'pull_request', pull_request_id, version_normalized,
578 from_version_normalized, source_ref_id, target_ref_id,
585 from_version_normalized, source_ref_id, target_ref_id,
579 hide_whitespace_changes, diff_context, c.fulldiff)
586 hide_whitespace_changes, diff_context, c.fulldiff)
580
587
581 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
582 force_recache = self.get_recache_flag()
589 force_recache = self.get_recache_flag()
583
590
584 cached_diff = None
591 cached_diff = None
585 if caching_enabled:
592 if caching_enabled:
586 cached_diff = load_cached_diff(cache_file_path)
593 cached_diff = load_cached_diff(cache_file_path)
587
594
588 has_proper_commit_cache = (
595 has_proper_commit_cache = (
589 cached_diff and cached_diff.get('commits')
596 cached_diff and cached_diff.get('commits')
590 and len(cached_diff.get('commits', [])) == 5
597 and len(cached_diff.get('commits', [])) == 5
591 and cached_diff.get('commits')[0]
598 and cached_diff.get('commits')[0]
592 and cached_diff.get('commits')[3])
599 and cached_diff.get('commits')[3])
593
600
594 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
595 diff_commit_cache = \
602 diff_commit_cache = \
596 (ancestor_commit, commit_cache, missing_requirements,
603 (ancestor_commit, commit_cache, missing_requirements,
597 source_commit, target_commit) = cached_diff['commits']
604 source_commit, target_commit) = cached_diff['commits']
598 else:
605 else:
599 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
600 # merge errors resulting in potentially hidden commits in the shadow repo.
607 # merge errors resulting in potentially hidden commits in the shadow repo.
601 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
602 and _merge_check.merge_response
609 and _merge_check.merge_response
603 maybe_unreachable = maybe_unreachable \
610 maybe_unreachable = maybe_unreachable \
604 and _merge_check.merge_response.metadata.get('unresolved_files')
611 and _merge_check.merge_response.metadata.get('unresolved_files')
605 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
606 diff_commit_cache = \
613 diff_commit_cache = \
607 (ancestor_commit, commit_cache, missing_requirements,
614 (ancestor_commit, commit_cache, missing_requirements,
608 source_commit, target_commit) = self.get_commits(
615 source_commit, target_commit) = self.get_commits(
609 commits_source_repo,
616 commits_source_repo,
610 pull_request_at_ver,
617 pull_request_at_ver,
611 source_commit,
618 source_commit,
612 source_ref_id,
619 source_ref_id,
613 source_scm,
620 source_scm,
614 target_commit,
621 target_commit,
615 target_ref_id,
622 target_ref_id,
616 target_scm,
623 target_scm,
617 maybe_unreachable=maybe_unreachable)
624 maybe_unreachable=maybe_unreachable)
618
625
619 # register our commit range
626 # register our commit range
620 for comm in commit_cache.values():
627 for comm in commit_cache.values():
621 c.commit_ranges.append(comm)
628 c.commit_ranges.append(comm)
622
629
623 c.missing_requirements = missing_requirements
630 c.missing_requirements = missing_requirements
624 c.ancestor_commit = ancestor_commit
631 c.ancestor_commit = ancestor_commit
625 c.statuses = source_repo.statuses(
632 c.statuses = source_repo.statuses(
626 [x.raw_id for x in c.commit_ranges])
633 [x.raw_id for x in c.commit_ranges])
627
634
628 # auto collapse if we have more than limit
635 # auto collapse if we have more than limit
629 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
630 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
631 c.compare_mode = compare
638 c.compare_mode = compare
632
639
633 # diff_limit is the old behavior, will cut off the whole diff
640 # diff_limit is the old behavior, will cut off the whole diff
634 # if the limit is applied otherwise will just hide the
641 # if the limit is applied otherwise will just hide the
635 # big files from the front-end
642 # big files from the front-end
636 diff_limit = c.visual.cut_off_limit_diff
643 diff_limit = c.visual.cut_off_limit_diff
637 file_limit = c.visual.cut_off_limit_file
644 file_limit = c.visual.cut_off_limit_file
638
645
639 c.missing_commits = False
646 c.missing_commits = False
640 if (c.missing_requirements
647 if (c.missing_requirements
641 or isinstance(source_commit, EmptyCommit)
648 or isinstance(source_commit, EmptyCommit)
642 or source_commit == target_commit):
649 or source_commit == target_commit):
643
650
644 c.missing_commits = True
651 c.missing_commits = True
645 else:
652 else:
646 c.inline_comments = display_inline_comments
653 c.inline_comments = display_inline_comments
647
654
648 use_ancestor = True
655 use_ancestor = True
649 if from_version_normalized != version_normalized:
656 if from_version_normalized != version_normalized:
650 use_ancestor = False
657 use_ancestor = False
651
658
652 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
653 if not force_recache and has_proper_diff_cache:
660 if not force_recache and has_proper_diff_cache:
654 c.diffset = cached_diff['diff']
661 c.diffset = cached_diff['diff']
655 else:
662 else:
656 try:
663 try:
657 c.diffset = self._get_diffset(
664 c.diffset = self._get_diffset(
658 c.source_repo.repo_name, commits_source_repo,
665 c.source_repo.repo_name, commits_source_repo,
659 c.ancestor_commit,
666 c.ancestor_commit,
660 source_ref_id, target_ref_id,
667 source_ref_id, target_ref_id,
661 target_commit, source_commit,
668 target_commit, source_commit,
662 diff_limit, file_limit, c.fulldiff,
669 diff_limit, file_limit, c.fulldiff,
663 hide_whitespace_changes, diff_context,
670 hide_whitespace_changes, diff_context,
664 use_ancestor=use_ancestor
671 use_ancestor=use_ancestor
665 )
672 )
666
673
667 # save cached diff
674 # save cached diff
668 if caching_enabled:
675 if caching_enabled:
669 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
670 except CommitDoesNotExistError:
677 except CommitDoesNotExistError:
671 log.exception('Failed to generate diffset')
678 log.exception('Failed to generate diffset')
672 c.missing_commits = True
679 c.missing_commits = True
673
680
674 if not c.missing_commits:
681 if not c.missing_commits:
675
682
676 c.limited_diff = c.diffset.limited_diff
683 c.limited_diff = c.diffset.limited_diff
677
684
678 # calculate removed files that are bound to comments
685 # calculate removed files that are bound to comments
679 comment_deleted_files = [
686 comment_deleted_files = [
680 fname for fname in display_inline_comments
687 fname for fname in display_inline_comments
681 if fname not in c.diffset.file_stats]
688 if fname not in c.diffset.file_stats]
682
689
683 c.deleted_files_comments = collections.defaultdict(dict)
690 c.deleted_files_comments = collections.defaultdict(dict)
684 for fname, per_line_comments in display_inline_comments.items():
691 for fname, per_line_comments in display_inline_comments.items():
685 if fname in comment_deleted_files:
692 if fname in comment_deleted_files:
686 c.deleted_files_comments[fname]['stats'] = 0
693 c.deleted_files_comments[fname]['stats'] = 0
687 c.deleted_files_comments[fname]['comments'] = list()
694 c.deleted_files_comments[fname]['comments'] = list()
688 for lno, comments in per_line_comments.items():
695 for lno, comments in per_line_comments.items():
689 c.deleted_files_comments[fname]['comments'].extend(comments)
696 c.deleted_files_comments[fname]['comments'].extend(comments)
690
697
691 # maybe calculate the range diff
698 # maybe calculate the range diff
692 if c.range_diff_on:
699 if c.range_diff_on:
693 # TODO(marcink): set whitespace/context
700 # TODO(marcink): set whitespace/context
694 context_lcl = 3
701 context_lcl = 3
695 ign_whitespace_lcl = False
702 ign_whitespace_lcl = False
696
703
697 for commit in c.commit_ranges:
704 for commit in c.commit_ranges:
698 commit2 = commit
705 commit2 = commit
699 commit1 = commit.first_parent
706 commit1 = commit.first_parent
700
707
701 range_diff_cache_file_path = diff_cache_exist(
708 range_diff_cache_file_path = diff_cache_exist(
702 cache_path, 'diff', commit.raw_id,
709 cache_path, 'diff', commit.raw_id,
703 ign_whitespace_lcl, context_lcl, c.fulldiff)
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
704
711
705 cached_diff = None
712 cached_diff = None
706 if caching_enabled:
713 if caching_enabled:
707 cached_diff = load_cached_diff(range_diff_cache_file_path)
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
708
715
709 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
710 if not force_recache and has_proper_diff_cache:
717 if not force_recache and has_proper_diff_cache:
711 diffset = cached_diff['diff']
718 diffset = cached_diff['diff']
712 else:
719 else:
713 diffset = self._get_range_diffset(
720 diffset = self._get_range_diffset(
714 commits_source_repo, source_repo,
721 commits_source_repo, source_repo,
715 commit1, commit2, diff_limit, file_limit,
722 commit1, commit2, diff_limit, file_limit,
716 c.fulldiff, ign_whitespace_lcl, context_lcl
723 c.fulldiff, ign_whitespace_lcl, context_lcl
717 )
724 )
718
725
719 # save cached diff
726 # save cached diff
720 if caching_enabled:
727 if caching_enabled:
721 cache_diff(range_diff_cache_file_path, diffset, None)
728 cache_diff(range_diff_cache_file_path, diffset, None)
722
729
723 c.changes[commit.raw_id] = diffset
730 c.changes[commit.raw_id] = diffset
724
731
725 # this is a hack to properly display links, when creating PR, the
732 # this is a hack to properly display links, when creating PR, the
726 # compare view and others uses different notation, and
733 # compare view and others uses different notation, and
727 # compare_commits.mako renders links based on the target_repo.
734 # compare_commits.mako renders links based on the target_repo.
728 # We need to swap that here to generate it properly on the html side
735 # We need to swap that here to generate it properly on the html side
729 c.target_repo = c.source_repo
736 c.target_repo = c.source_repo
730
737
731 c.commit_statuses = ChangesetStatus.STATUSES
738 c.commit_statuses = ChangesetStatus.STATUSES
732
739
733 c.show_version_changes = not pr_closed
740 c.show_version_changes = not pr_closed
734 if c.show_version_changes:
741 if c.show_version_changes:
735 cur_obj = pull_request_at_ver
742 cur_obj = pull_request_at_ver
736 prev_obj = prev_pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
737
744
738 old_commit_ids = prev_obj.revisions
745 old_commit_ids = prev_obj.revisions
739 new_commit_ids = cur_obj.revisions
746 new_commit_ids = cur_obj.revisions
740 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
741 old_commit_ids, new_commit_ids)
748 old_commit_ids, new_commit_ids)
742 c.commit_changes_summary = commit_changes
749 c.commit_changes_summary = commit_changes
743
750
744 # calculate the diff for commits between versions
751 # calculate the diff for commits between versions
745 c.commit_changes = []
752 c.commit_changes = []
746
753
747 def mark(cs, fw):
754 def mark(cs, fw):
748 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
749
756
750 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
751 + mark(commit_changes.removed, 'r') \
758 + mark(commit_changes.removed, 'r') \
752 + mark(commit_changes.common, 'c'):
759 + mark(commit_changes.common, 'c'):
753
760
754 if raw_id in commit_cache:
761 if raw_id in commit_cache:
755 commit = commit_cache[raw_id]
762 commit = commit_cache[raw_id]
756 else:
763 else:
757 try:
764 try:
758 commit = commits_source_repo.get_commit(raw_id)
765 commit = commits_source_repo.get_commit(raw_id)
759 except CommitDoesNotExistError:
766 except CommitDoesNotExistError:
760 # in case we fail extracting still use "dummy" commit
767 # in case we fail extracting still use "dummy" commit
761 # for display in commit diff
768 # for display in commit diff
762 commit = h.AttributeDict(
769 commit = h.AttributeDict(
763 {'raw_id': raw_id,
770 {'raw_id': raw_id,
764 'message': 'EMPTY or MISSING COMMIT'})
771 'message': 'EMPTY or MISSING COMMIT'})
765 c.commit_changes.append([c_type, commit])
772 c.commit_changes.append([c_type, commit])
766
773
767 # current user review statuses for each version
774 # current user review statuses for each version
768 c.review_versions = {}
775 c.review_versions = {}
769 is_reviewer = PullRequestModel().is_user_reviewer(
776 is_reviewer = PullRequestModel().is_user_reviewer(
770 pull_request, self._rhodecode_user)
777 pull_request, self._rhodecode_user)
771 if is_reviewer:
778 if is_reviewer:
772 for co in general_comments:
779 for co in general_comments:
773 if co.author.user_id == self._rhodecode_user.user_id:
780 if co.author.user_id == self._rhodecode_user.user_id:
774 status = co.status_change
781 status = co.status_change
775 if status:
782 if status:
776 _ver_pr = status[0].comment.pull_request_version_id
783 _ver_pr = status[0].comment.pull_request_version_id
777 c.review_versions[_ver_pr] = status[0]
784 c.review_versions[_ver_pr] = status[0]
778
785
779 return self._get_template_context(c)
786 return self._get_template_context(c)
780
787
781 def get_commits(
788 def get_commits(
782 self, commits_source_repo, pull_request_at_ver, source_commit,
789 self, commits_source_repo, pull_request_at_ver, source_commit,
783 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
784 maybe_unreachable=False):
791 maybe_unreachable=False):
785
792
786 commit_cache = collections.OrderedDict()
793 commit_cache = collections.OrderedDict()
787 missing_requirements = False
794 missing_requirements = False
788
795
789 try:
796 try:
790 pre_load = ["author", "date", "message", "branch", "parents"]
797 pre_load = ["author", "date", "message", "branch", "parents"]
791
798
792 pull_request_commits = pull_request_at_ver.revisions
799 pull_request_commits = pull_request_at_ver.revisions
793 log.debug('Loading %s commits from %s',
800 log.debug('Loading %s commits from %s',
794 len(pull_request_commits), commits_source_repo)
801 len(pull_request_commits), commits_source_repo)
795
802
796 for rev in pull_request_commits:
803 for rev in pull_request_commits:
797 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
798 maybe_unreachable=maybe_unreachable)
805 maybe_unreachable=maybe_unreachable)
799 commit_cache[comm.raw_id] = comm
806 commit_cache[comm.raw_id] = comm
800
807
801 # Order here matters, we first need to get target, and then
808 # Order here matters, we first need to get target, and then
802 # the source
809 # the source
803 target_commit = commits_source_repo.get_commit(
810 target_commit = commits_source_repo.get_commit(
804 commit_id=safe_str(target_ref_id))
811 commit_id=safe_str(target_ref_id))
805
812
806 source_commit = commits_source_repo.get_commit(
813 source_commit = commits_source_repo.get_commit(
807 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
808 except CommitDoesNotExistError:
815 except CommitDoesNotExistError:
809 log.warning('Failed to get commit from `{}` repo'.format(
816 log.warning('Failed to get commit from `{}` repo'.format(
810 commits_source_repo), exc_info=True)
817 commits_source_repo), exc_info=True)
811 except RepositoryRequirementError:
818 except RepositoryRequirementError:
812 log.warning('Failed to get all required data from repo', exc_info=True)
819 log.warning('Failed to get all required data from repo', exc_info=True)
813 missing_requirements = True
820 missing_requirements = True
814
821
815 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
816
823
817 try:
824 try:
818 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
819 except Exception:
826 except Exception:
820 ancestor_commit = None
827 ancestor_commit = None
821
828
822 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
823
830
824 def assure_not_empty_repo(self):
831 def assure_not_empty_repo(self):
825 _ = self.request.translate
832 _ = self.request.translate
826
833
827 try:
834 try:
828 self.db_repo.scm_instance().get_commit()
835 self.db_repo.scm_instance().get_commit()
829 except EmptyRepositoryError:
836 except EmptyRepositoryError:
830 h.flash(h.literal(_('There are no commits yet')),
837 h.flash(h.literal(_('There are no commits yet')),
831 category='warning')
838 category='warning')
832 raise HTTPFound(
839 raise HTTPFound(
833 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
834
841
835 @LoginRequired()
842 @LoginRequired()
836 @NotAnonymous()
843 @NotAnonymous()
837 @HasRepoPermissionAnyDecorator(
844 @HasRepoPermissionAnyDecorator(
838 'repository.read', 'repository.write', 'repository.admin')
845 'repository.read', 'repository.write', 'repository.admin')
839 def pull_request_new(self):
846 def pull_request_new(self):
840 _ = self.request.translate
847 _ = self.request.translate
841 c = self.load_default_context()
848 c = self.load_default_context()
842
849
843 self.assure_not_empty_repo()
850 self.assure_not_empty_repo()
844 source_repo = self.db_repo
851 source_repo = self.db_repo
845
852
846 commit_id = self.request.GET.get('commit')
853 commit_id = self.request.GET.get('commit')
847 branch_ref = self.request.GET.get('branch')
854 branch_ref = self.request.GET.get('branch')
848 bookmark_ref = self.request.GET.get('bookmark')
855 bookmark_ref = self.request.GET.get('bookmark')
849
856
850 try:
857 try:
851 source_repo_data = PullRequestModel().generate_repo_data(
858 source_repo_data = PullRequestModel().generate_repo_data(
852 source_repo, commit_id=commit_id,
859 source_repo, commit_id=commit_id,
853 branch=branch_ref, bookmark=bookmark_ref,
860 branch=branch_ref, bookmark=bookmark_ref,
854 translator=self.request.translate)
861 translator=self.request.translate)
855 except CommitDoesNotExistError as e:
862 except CommitDoesNotExistError as e:
856 log.exception(e)
863 log.exception(e)
857 h.flash(_('Commit does not exist'), 'error')
864 h.flash(_('Commit does not exist'), 'error')
858 raise HTTPFound(
865 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860
867
861 default_target_repo = source_repo
868 default_target_repo = source_repo
862
869
863 if source_repo.parent and c.has_origin_repo_read_perm:
870 if source_repo.parent and c.has_origin_repo_read_perm:
864 parent_vcs_obj = source_repo.parent.scm_instance()
871 parent_vcs_obj = source_repo.parent.scm_instance()
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 # change default if we have a parent repo
873 # change default if we have a parent repo
867 default_target_repo = source_repo.parent
874 default_target_repo = source_repo.parent
868
875
869 target_repo_data = PullRequestModel().generate_repo_data(
876 target_repo_data = PullRequestModel().generate_repo_data(
870 default_target_repo, translator=self.request.translate)
877 default_target_repo, translator=self.request.translate)
871
878
872 selected_source_ref = source_repo_data['refs']['selected_ref']
879 selected_source_ref = source_repo_data['refs']['selected_ref']
873 title_source_ref = ''
880 title_source_ref = ''
874 if selected_source_ref:
881 if selected_source_ref:
875 title_source_ref = selected_source_ref.split(':', 2)[1]
882 title_source_ref = selected_source_ref.split(':', 2)[1]
876 c.default_title = PullRequestModel().generate_pullrequest_title(
883 c.default_title = PullRequestModel().generate_pullrequest_title(
877 source=source_repo.repo_name,
884 source=source_repo.repo_name,
878 source_ref=title_source_ref,
885 source_ref=title_source_ref,
879 target=default_target_repo.repo_name
886 target=default_target_repo.repo_name
880 )
887 )
881
888
882 c.default_repo_data = {
889 c.default_repo_data = {
883 'source_repo_name': source_repo.repo_name,
890 'source_repo_name': source_repo.repo_name,
884 'source_refs_json': json.dumps(source_repo_data),
891 'source_refs_json': json.dumps(source_repo_data),
885 'target_repo_name': default_target_repo.repo_name,
892 'target_repo_name': default_target_repo.repo_name,
886 'target_refs_json': json.dumps(target_repo_data),
893 'target_refs_json': json.dumps(target_repo_data),
887 }
894 }
888 c.default_source_ref = selected_source_ref
895 c.default_source_ref = selected_source_ref
889
896
890 return self._get_template_context(c)
897 return self._get_template_context(c)
891
898
892 @LoginRequired()
899 @LoginRequired()
893 @NotAnonymous()
900 @NotAnonymous()
894 @HasRepoPermissionAnyDecorator(
901 @HasRepoPermissionAnyDecorator(
895 'repository.read', 'repository.write', 'repository.admin')
902 'repository.read', 'repository.write', 'repository.admin')
896 def pull_request_repo_refs(self):
903 def pull_request_repo_refs(self):
897 self.load_default_context()
904 self.load_default_context()
898 target_repo_name = self.request.matchdict['target_repo_name']
905 target_repo_name = self.request.matchdict['target_repo_name']
899 repo = Repository.get_by_repo_name(target_repo_name)
906 repo = Repository.get_by_repo_name(target_repo_name)
900 if not repo:
907 if not repo:
901 raise HTTPNotFound()
908 raise HTTPNotFound()
902
909
903 target_perm = HasRepoPermissionAny(
910 target_perm = HasRepoPermissionAny(
904 'repository.read', 'repository.write', 'repository.admin')(
911 'repository.read', 'repository.write', 'repository.admin')(
905 target_repo_name)
912 target_repo_name)
906 if not target_perm:
913 if not target_perm:
907 raise HTTPNotFound()
914 raise HTTPNotFound()
908
915
909 return PullRequestModel().generate_repo_data(
916 return PullRequestModel().generate_repo_data(
910 repo, translator=self.request.translate)
917 repo, translator=self.request.translate)
911
918
912 @LoginRequired()
919 @LoginRequired()
913 @NotAnonymous()
920 @NotAnonymous()
914 @HasRepoPermissionAnyDecorator(
921 @HasRepoPermissionAnyDecorator(
915 'repository.read', 'repository.write', 'repository.admin')
922 'repository.read', 'repository.write', 'repository.admin')
916 def pullrequest_repo_targets(self):
923 def pullrequest_repo_targets(self):
917 _ = self.request.translate
924 _ = self.request.translate
918 filter_query = self.request.GET.get('query')
925 filter_query = self.request.GET.get('query')
919
926
920 # get the parents
927 # get the parents
921 parent_target_repos = []
928 parent_target_repos = []
922 if self.db_repo.parent:
929 if self.db_repo.parent:
923 parents_query = Repository.query() \
930 parents_query = Repository.query() \
924 .order_by(func.length(Repository.repo_name)) \
931 .order_by(func.length(Repository.repo_name)) \
925 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
926
933
927 if filter_query:
934 if filter_query:
928 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
929 parents_query = parents_query.filter(
936 parents_query = parents_query.filter(
930 Repository.repo_name.ilike(ilike_expression))
937 Repository.repo_name.ilike(ilike_expression))
931 parents = parents_query.limit(20).all()
938 parents = parents_query.limit(20).all()
932
939
933 for parent in parents:
940 for parent in parents:
934 parent_vcs_obj = parent.scm_instance()
941 parent_vcs_obj = parent.scm_instance()
935 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
936 parent_target_repos.append(parent)
943 parent_target_repos.append(parent)
937
944
938 # get other forks, and repo itself
945 # get other forks, and repo itself
939 query = Repository.query() \
946 query = Repository.query() \
940 .order_by(func.length(Repository.repo_name)) \
947 .order_by(func.length(Repository.repo_name)) \
941 .filter(
948 .filter(
942 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
943 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
944 ) \
951 ) \
945 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
946
953
947 if filter_query:
954 if filter_query:
948 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
949 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
950
957
951 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
952 target_repos = query.limit(limit).all()
959 target_repos = query.limit(limit).all()
953
960
954 all_target_repos = target_repos + parent_target_repos
961 all_target_repos = target_repos + parent_target_repos
955
962
956 repos = []
963 repos = []
957 # This checks permissions to the repositories
964 # This checks permissions to the repositories
958 for obj in ScmModel().get_repos(all_target_repos):
965 for obj in ScmModel().get_repos(all_target_repos):
959 repos.append({
966 repos.append({
960 'id': obj['name'],
967 'id': obj['name'],
961 'text': obj['name'],
968 'text': obj['name'],
962 'type': 'repo',
969 'type': 'repo',
963 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_id': obj['dbrepo']['repo_id'],
964 'repo_type': obj['dbrepo']['repo_type'],
971 'repo_type': obj['dbrepo']['repo_type'],
965 'private': obj['dbrepo']['private'],
972 'private': obj['dbrepo']['private'],
966
973
967 })
974 })
968
975
969 data = {
976 data = {
970 'more': False,
977 'more': False,
971 'results': [{
978 'results': [{
972 'text': _('Repositories'),
979 'text': _('Repositories'),
973 'children': repos
980 'children': repos
974 }] if repos else []
981 }] if repos else []
975 }
982 }
976 return data
983 return data
977
984
978 @classmethod
985 @classmethod
979 def get_comment_ids(cls, post_data):
986 def get_comment_ids(cls, post_data):
980 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
981
988
982 @LoginRequired()
989 @LoginRequired()
983 @NotAnonymous()
990 @NotAnonymous()
984 @HasRepoPermissionAnyDecorator(
991 @HasRepoPermissionAnyDecorator(
985 'repository.read', 'repository.write', 'repository.admin')
992 'repository.read', 'repository.write', 'repository.admin')
986 def pullrequest_comments(self):
993 def pullrequest_comments(self):
987 self.load_default_context()
994 self.load_default_context()
988
995
989 pull_request = PullRequest.get_or_404(
996 pull_request = PullRequest.get_or_404(
990 self.request.matchdict['pull_request_id'])
997 self.request.matchdict['pull_request_id'])
991 pull_request_id = pull_request.pull_request_id
998 pull_request_id = pull_request.pull_request_id
992 version = self.request.GET.get('version')
999 version = self.request.GET.get('version')
993
1000
994 _render = self.request.get_partial_renderer(
1001 _render = self.request.get_partial_renderer(
995 'rhodecode:templates/base/sidebar.mako')
1002 'rhodecode:templates/base/sidebar.mako')
996 c = _render.get_call_context()
1003 c = _render.get_call_context()
997
1004
998 (pull_request_latest,
1005 (pull_request_latest,
999 pull_request_at_ver,
1006 pull_request_at_ver,
1000 pull_request_display_obj,
1007 pull_request_display_obj,
1001 at_version) = PullRequestModel().get_pr_version(
1008 at_version) = PullRequestModel().get_pr_version(
1002 pull_request_id, version=version)
1009 pull_request_id, version=version)
1003 versions = pull_request_display_obj.versions()
1010 versions = pull_request_display_obj.versions()
1004 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1005 c.versions = versions + [latest_ver]
1012 c.versions = versions + [latest_ver]
1006
1013
1007 c.at_version = at_version
1014 c.at_version = at_version
1008 c.at_version_num = (at_version
1015 c.at_version_num = (at_version
1009 if at_version and at_version != PullRequest.LATEST_VER
1016 if at_version and at_version != PullRequest.LATEST_VER
1010 else None)
1017 else None)
1011
1018
1012 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1013 all_comments = c.inline_comments_flat + c.comments
1020 all_comments = c.inline_comments_flat + c.comments
1014
1021
1015 existing_ids = self.get_comment_ids(self.request.POST)
1022 existing_ids = self.get_comment_ids(self.request.POST)
1016 return _render('comments_table', all_comments, len(all_comments),
1023 return _render('comments_table', all_comments, len(all_comments),
1017 existing_ids=existing_ids)
1024 existing_ids=existing_ids)
1018
1025
1019 @LoginRequired()
1026 @LoginRequired()
1020 @NotAnonymous()
1027 @NotAnonymous()
1021 @HasRepoPermissionAnyDecorator(
1028 @HasRepoPermissionAnyDecorator(
1022 'repository.read', 'repository.write', 'repository.admin')
1029 'repository.read', 'repository.write', 'repository.admin')
1023 def pullrequest_todos(self):
1030 def pullrequest_todos(self):
1024 self.load_default_context()
1031 self.load_default_context()
1025
1032
1026 pull_request = PullRequest.get_or_404(
1033 pull_request = PullRequest.get_or_404(
1027 self.request.matchdict['pull_request_id'])
1034 self.request.matchdict['pull_request_id'])
1028 pull_request_id = pull_request.pull_request_id
1035 pull_request_id = pull_request.pull_request_id
1029 version = self.request.GET.get('version')
1036 version = self.request.GET.get('version')
1030
1037
1031 _render = self.request.get_partial_renderer(
1038 _render = self.request.get_partial_renderer(
1032 'rhodecode:templates/base/sidebar.mako')
1039 'rhodecode:templates/base/sidebar.mako')
1033 c = _render.get_call_context()
1040 c = _render.get_call_context()
1034 (pull_request_latest,
1041 (pull_request_latest,
1035 pull_request_at_ver,
1042 pull_request_at_ver,
1036 pull_request_display_obj,
1043 pull_request_display_obj,
1037 at_version) = PullRequestModel().get_pr_version(
1044 at_version) = PullRequestModel().get_pr_version(
1038 pull_request_id, version=version)
1045 pull_request_id, version=version)
1039 versions = pull_request_display_obj.versions()
1046 versions = pull_request_display_obj.versions()
1040 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1041 c.versions = versions + [latest_ver]
1048 c.versions = versions + [latest_ver]
1042
1049
1043 c.at_version = at_version
1050 c.at_version = at_version
1044 c.at_version_num = (at_version
1051 c.at_version_num = (at_version
1045 if at_version and at_version != PullRequest.LATEST_VER
1052 if at_version and at_version != PullRequest.LATEST_VER
1046 else None)
1053 else None)
1047
1054
1048 c.unresolved_comments = CommentsModel() \
1055 c.unresolved_comments = CommentsModel() \
1049 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1050 c.resolved_comments = CommentsModel() \
1057 c.resolved_comments = CommentsModel() \
1051 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1052
1059
1053 all_comments = c.unresolved_comments + c.resolved_comments
1060 all_comments = c.unresolved_comments + c.resolved_comments
1054 existing_ids = self.get_comment_ids(self.request.POST)
1061 existing_ids = self.get_comment_ids(self.request.POST)
1055 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1056 todo_comments=True, existing_ids=existing_ids)
1063 todo_comments=True, existing_ids=existing_ids)
1057
1064
1058 @LoginRequired()
1065 @LoginRequired()
1059 @NotAnonymous()
1066 @NotAnonymous()
1060 @HasRepoPermissionAnyDecorator(
1067 @HasRepoPermissionAnyDecorator(
1061 'repository.read', 'repository.write', 'repository.admin')
1068 'repository.read', 'repository.write', 'repository.admin')
1062 def pullrequest_drafts(self):
1069 def pullrequest_drafts(self):
1063 self.load_default_context()
1070 self.load_default_context()
1064
1071
1065 pull_request = PullRequest.get_or_404(
1072 pull_request = PullRequest.get_or_404(
1066 self.request.matchdict['pull_request_id'])
1073 self.request.matchdict['pull_request_id'])
1067 pull_request_id = pull_request.pull_request_id
1074 pull_request_id = pull_request.pull_request_id
1068 version = self.request.GET.get('version')
1075 version = self.request.GET.get('version')
1069
1076
1070 _render = self.request.get_partial_renderer(
1077 _render = self.request.get_partial_renderer(
1071 'rhodecode:templates/base/sidebar.mako')
1078 'rhodecode:templates/base/sidebar.mako')
1072 c = _render.get_call_context()
1079 c = _render.get_call_context()
1073
1080
1074 (pull_request_latest,
1081 (pull_request_latest,
1075 pull_request_at_ver,
1082 pull_request_at_ver,
1076 pull_request_display_obj,
1083 pull_request_display_obj,
1077 at_version) = PullRequestModel().get_pr_version(
1084 at_version) = PullRequestModel().get_pr_version(
1078 pull_request_id, version=version)
1085 pull_request_id, version=version)
1079 versions = pull_request_display_obj.versions()
1086 versions = pull_request_display_obj.versions()
1080 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1081 c.versions = versions + [latest_ver]
1088 c.versions = versions + [latest_ver]
1082
1089
1083 c.at_version = at_version
1090 c.at_version = at_version
1084 c.at_version_num = (at_version
1091 c.at_version_num = (at_version
1085 if at_version and at_version != PullRequest.LATEST_VER
1092 if at_version and at_version != PullRequest.LATEST_VER
1086 else None)
1093 else None)
1087
1094
1088 c.draft_comments = CommentsModel() \
1095 c.draft_comments = CommentsModel() \
1089 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1090
1097
1091 all_comments = c.draft_comments
1098 all_comments = c.draft_comments
1092
1099
1093 existing_ids = self.get_comment_ids(self.request.POST)
1100 existing_ids = self.get_comment_ids(self.request.POST)
1094 return _render('comments_table', all_comments, len(all_comments),
1101 return _render('comments_table', all_comments, len(all_comments),
1095 existing_ids=existing_ids, draft_comments=True)
1102 existing_ids=existing_ids, draft_comments=True)
1096
1103
1097 @LoginRequired()
1104 @LoginRequired()
1098 @NotAnonymous()
1105 @NotAnonymous()
1099 @HasRepoPermissionAnyDecorator(
1106 @HasRepoPermissionAnyDecorator(
1100 'repository.read', 'repository.write', 'repository.admin')
1107 'repository.read', 'repository.write', 'repository.admin')
1101 @CSRFRequired()
1108 @CSRFRequired()
1102 def pull_request_create(self):
1109 def pull_request_create(self):
1103 _ = self.request.translate
1110 _ = self.request.translate
1104 self.assure_not_empty_repo()
1111 self.assure_not_empty_repo()
1105 self.load_default_context()
1112 self.load_default_context()
1106
1113
1107 controls = peppercorn.parse(self.request.POST.items())
1114 controls = peppercorn.parse(self.request.POST.items())
1108
1115
1109 try:
1116 try:
1110 form = PullRequestForm(
1117 form = PullRequestForm(
1111 self.request.translate, self.db_repo.repo_id)()
1118 self.request.translate, self.db_repo.repo_id)()
1112 _form = form.to_python(controls)
1119 _form = form.to_python(controls)
1113 except formencode.Invalid as errors:
1120 except formencode.Invalid as errors:
1114 if errors.error_dict.get('revisions'):
1121 if errors.error_dict.get('revisions'):
1115 msg = 'Revisions: %s' % errors.error_dict['revisions']
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1116 elif errors.error_dict.get('pullrequest_title'):
1123 elif errors.error_dict.get('pullrequest_title'):
1117 msg = errors.error_dict.get('pullrequest_title')
1124 msg = errors.error_dict.get('pullrequest_title')
1118 else:
1125 else:
1119 msg = _('Error creating pull request: {}').format(errors)
1126 msg = _('Error creating pull request: {}').format(errors)
1120 log.exception(msg)
1127 log.exception(msg)
1121 h.flash(msg, 'error')
1128 h.flash(msg, 'error')
1122
1129
1123 # would rather just go back to form ...
1130 # would rather just go back to form ...
1124 raise HTTPFound(
1131 raise HTTPFound(
1125 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1126
1133
1127 source_repo = _form['source_repo']
1134 source_repo = _form['source_repo']
1128 source_ref = _form['source_ref']
1135 source_ref = _form['source_ref']
1129 target_repo = _form['target_repo']
1136 target_repo = _form['target_repo']
1130 target_ref = _form['target_ref']
1137 target_ref = _form['target_ref']
1131 commit_ids = _form['revisions'][::-1]
1138 commit_ids = _form['revisions'][::-1]
1132 common_ancestor_id = _form['common_ancestor']
1139 common_ancestor_id = _form['common_ancestor']
1133
1140
1134 # find the ancestor for this pr
1141 # find the ancestor for this pr
1135 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1136 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1137
1144
1138 if not (source_db_repo or target_db_repo):
1145 if not (source_db_repo or target_db_repo):
1139 h.flash(_('source_repo or target repo not found'), category='error')
1146 h.flash(_('source_repo or target repo not found'), category='error')
1140 raise HTTPFound(
1147 raise HTTPFound(
1141 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1142
1149
1143 # re-check permissions again here
1150 # re-check permissions again here
1144 # source_repo we must have read permissions
1151 # source_repo we must have read permissions
1145
1152
1146 source_perm = HasRepoPermissionAny(
1153 source_perm = HasRepoPermissionAny(
1147 'repository.read', 'repository.write', 'repository.admin')(
1154 'repository.read', 'repository.write', 'repository.admin')(
1148 source_db_repo.repo_name)
1155 source_db_repo.repo_name)
1149 if not source_perm:
1156 if not source_perm:
1150 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1151 source_db_repo.repo_name))
1158 source_db_repo.repo_name))
1152 h.flash(msg, category='error')
1159 h.flash(msg, category='error')
1153 # copy the args back to redirect
1160 # copy the args back to redirect
1154 org_query = self.request.GET.mixed()
1161 org_query = self.request.GET.mixed()
1155 raise HTTPFound(
1162 raise HTTPFound(
1156 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1157 _query=org_query))
1164 _query=org_query))
1158
1165
1159 # target repo we must have read permissions, and also later on
1166 # target repo we must have read permissions, and also later on
1160 # we want to check branch permissions here
1167 # we want to check branch permissions here
1161 target_perm = HasRepoPermissionAny(
1168 target_perm = HasRepoPermissionAny(
1162 'repository.read', 'repository.write', 'repository.admin')(
1169 'repository.read', 'repository.write', 'repository.admin')(
1163 target_db_repo.repo_name)
1170 target_db_repo.repo_name)
1164 if not target_perm:
1171 if not target_perm:
1165 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1166 target_db_repo.repo_name))
1173 target_db_repo.repo_name))
1167 h.flash(msg, category='error')
1174 h.flash(msg, category='error')
1168 # copy the args back to redirect
1175 # copy the args back to redirect
1169 org_query = self.request.GET.mixed()
1176 org_query = self.request.GET.mixed()
1170 raise HTTPFound(
1177 raise HTTPFound(
1171 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1172 _query=org_query))
1179 _query=org_query))
1173
1180
1174 source_scm = source_db_repo.scm_instance()
1181 source_scm = source_db_repo.scm_instance()
1175 target_scm = target_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1176
1183
1177 source_ref_obj = unicode_to_reference(source_ref)
1184 source_ref_obj = unicode_to_reference(source_ref)
1178 target_ref_obj = unicode_to_reference(target_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1179
1186
1180 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1181 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1182
1189
1183 ancestor = source_scm.get_common_ancestor(
1190 ancestor = source_scm.get_common_ancestor(
1184 source_commit.raw_id, target_commit.raw_id, target_scm)
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1185
1192
1186 # recalculate target ref based on ancestor
1193 # recalculate target ref based on ancestor
1187 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1188
1195
1189 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1190 PullRequestModel().get_reviewer_functions()
1197 PullRequestModel().get_reviewer_functions()
1191
1198
1192 # recalculate reviewers logic, to make sure we can validate this
1199 # recalculate reviewers logic, to make sure we can validate this
1193 reviewer_rules = get_default_reviewers_data(
1200 reviewer_rules = get_default_reviewers_data(
1194 self._rhodecode_db_user,
1201 self._rhodecode_db_user,
1195 source_db_repo,
1202 source_db_repo,
1196 source_ref_obj,
1203 source_ref_obj,
1197 target_db_repo,
1204 target_db_repo,
1198 target_ref_obj,
1205 target_ref_obj,
1199 include_diff_info=False)
1206 include_diff_info=False)
1200
1207
1201 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1202 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1203
1210
1204 pullrequest_title = _form['pullrequest_title']
1211 pullrequest_title = _form['pullrequest_title']
1205 title_source_ref = source_ref_obj.name
1212 title_source_ref = source_ref_obj.name
1206 if not pullrequest_title:
1213 if not pullrequest_title:
1207 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1208 source=source_repo,
1215 source=source_repo,
1209 source_ref=title_source_ref,
1216 source_ref=title_source_ref,
1210 target=target_repo
1217 target=target_repo
1211 )
1218 )
1212
1219
1213 description = _form['pullrequest_desc']
1220 description = _form['pullrequest_desc']
1214 description_renderer = _form['description_renderer']
1221 description_renderer = _form['description_renderer']
1215
1222
1216 try:
1223 try:
1217 pull_request = PullRequestModel().create(
1224 pull_request = PullRequestModel().create(
1218 created_by=self._rhodecode_user.user_id,
1225 created_by=self._rhodecode_user.user_id,
1219 source_repo=source_repo,
1226 source_repo=source_repo,
1220 source_ref=source_ref,
1227 source_ref=source_ref,
1221 target_repo=target_repo,
1228 target_repo=target_repo,
1222 target_ref=target_ref,
1229 target_ref=target_ref,
1223 revisions=commit_ids,
1230 revisions=commit_ids,
1224 common_ancestor_id=common_ancestor_id,
1231 common_ancestor_id=common_ancestor_id,
1225 reviewers=reviewers,
1232 reviewers=reviewers,
1226 observers=observers,
1233 observers=observers,
1227 title=pullrequest_title,
1234 title=pullrequest_title,
1228 description=description,
1235 description=description,
1229 description_renderer=description_renderer,
1236 description_renderer=description_renderer,
1230 reviewer_data=reviewer_rules,
1237 reviewer_data=reviewer_rules,
1231 auth_user=self._rhodecode_user
1238 auth_user=self._rhodecode_user
1232 )
1239 )
1233 Session().commit()
1240 Session().commit()
1234
1241
1235 h.flash(_('Successfully opened new pull request'),
1242 h.flash(_('Successfully opened new pull request'),
1236 category='success')
1243 category='success')
1237 except Exception:
1244 except Exception:
1238 msg = _('Error occurred during creation of this pull request.')
1245 msg = _('Error occurred during creation of this pull request.')
1239 log.exception(msg)
1246 log.exception(msg)
1240 h.flash(msg, category='error')
1247 h.flash(msg, category='error')
1241
1248
1242 # copy the args back to redirect
1249 # copy the args back to redirect
1243 org_query = self.request.GET.mixed()
1250 org_query = self.request.GET.mixed()
1244 raise HTTPFound(
1251 raise HTTPFound(
1245 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1246 _query=org_query))
1253 _query=org_query))
1247
1254
1248 raise HTTPFound(
1255 raise HTTPFound(
1249 h.route_path('pullrequest_show', repo_name=target_repo,
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1250 pull_request_id=pull_request.pull_request_id))
1257 pull_request_id=pull_request.pull_request_id))
1251
1258
1252 @LoginRequired()
1259 @LoginRequired()
1253 @NotAnonymous()
1260 @NotAnonymous()
1254 @HasRepoPermissionAnyDecorator(
1261 @HasRepoPermissionAnyDecorator(
1255 'repository.read', 'repository.write', 'repository.admin')
1262 'repository.read', 'repository.write', 'repository.admin')
1256 @CSRFRequired()
1263 @CSRFRequired()
1257 def pull_request_update(self):
1264 def pull_request_update(self):
1258 pull_request = PullRequest.get_or_404(
1265 pull_request = PullRequest.get_or_404(
1259 self.request.matchdict['pull_request_id'])
1266 self.request.matchdict['pull_request_id'])
1260 _ = self.request.translate
1267 _ = self.request.translate
1261
1268
1262 c = self.load_default_context()
1269 c = self.load_default_context()
1263 redirect_url = None
1270 redirect_url = None
1264
1271
1265 if pull_request.is_closed():
1272 if pull_request.is_closed():
1266 log.debug('update: forbidden because pull request is closed')
1273 log.debug('update: forbidden because pull request is closed')
1267 msg = _(u'Cannot update closed pull requests.')
1274 msg = _(u'Cannot update closed pull requests.')
1268 h.flash(msg, category='error')
1275 h.flash(msg, category='error')
1269 return {'response': True,
1276 return {'response': True,
1270 'redirect_url': redirect_url}
1277 'redirect_url': redirect_url}
1271
1278
1272 is_state_changing = pull_request.is_state_changing()
1279 is_state_changing = pull_request.is_state_changing()
1273 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1274
1281
1275 # only owner or admin can update it
1282 # only owner or admin can update it
1276 allowed_to_update = PullRequestModel().check_user_update(
1283 allowed_to_update = PullRequestModel().check_user_update(
1277 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1278
1285
1279 if allowed_to_update:
1286 if allowed_to_update:
1280 controls = peppercorn.parse(self.request.POST.items())
1287 controls = peppercorn.parse(self.request.POST.items())
1281 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1282
1289
1283 if 'review_members' in controls:
1290 if 'review_members' in controls:
1284 self._update_reviewers(
1291 self._update_reviewers(
1285 c,
1292 c,
1286 pull_request, controls['review_members'],
1293 pull_request, controls['review_members'],
1287 pull_request.reviewer_data,
1294 pull_request.reviewer_data,
1288 PullRequestReviewers.ROLE_REVIEWER)
1295 PullRequestReviewers.ROLE_REVIEWER)
1289 elif 'observer_members' in controls:
1296 elif 'observer_members' in controls:
1290 self._update_reviewers(
1297 self._update_reviewers(
1291 c,
1298 c,
1292 pull_request, controls['observer_members'],
1299 pull_request, controls['observer_members'],
1293 pull_request.reviewer_data,
1300 pull_request.reviewer_data,
1294 PullRequestReviewers.ROLE_OBSERVER)
1301 PullRequestReviewers.ROLE_OBSERVER)
1295 elif str2bool(self.request.POST.get('update_commits', 'false')):
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1296 if is_state_changing:
1303 if is_state_changing:
1297 log.debug('commits update: forbidden because pull request is in state %s',
1304 log.debug('commits update: forbidden because pull request is in state %s',
1298 pull_request.pull_request_state)
1305 pull_request.pull_request_state)
1299 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1300 u'Current state is: `{}`').format(
1307 u'Current state is: `{}`').format(
1301 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1302 h.flash(msg, category='error')
1309 h.flash(msg, category='error')
1303 return {'response': True,
1310 return {'response': True,
1304 'redirect_url': redirect_url}
1311 'redirect_url': redirect_url}
1305
1312
1306 self._update_commits(c, pull_request)
1313 self._update_commits(c, pull_request)
1307 if force_refresh:
1314 if force_refresh:
1308 redirect_url = h.route_path(
1315 redirect_url = h.route_path(
1309 'pullrequest_show', repo_name=self.db_repo_name,
1316 'pullrequest_show', repo_name=self.db_repo_name,
1310 pull_request_id=pull_request.pull_request_id,
1317 pull_request_id=pull_request.pull_request_id,
1311 _query={"force_refresh": 1})
1318 _query={"force_refresh": 1})
1312 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1313 self._edit_pull_request(pull_request)
1320 self._edit_pull_request(pull_request)
1314 else:
1321 else:
1315 log.error('Unhandled update data.')
1322 log.error('Unhandled update data.')
1316 raise HTTPBadRequest()
1323 raise HTTPBadRequest()
1317
1324
1318 return {'response': True,
1325 return {'response': True,
1319 'redirect_url': redirect_url}
1326 'redirect_url': redirect_url}
1320 raise HTTPForbidden()
1327 raise HTTPForbidden()
1321
1328
1322 def _edit_pull_request(self, pull_request):
1329 def _edit_pull_request(self, pull_request):
1323 """
1330 """
1324 Edit title and description
1331 Edit title and description
1325 """
1332 """
1326 _ = self.request.translate
1333 _ = self.request.translate
1327
1334
1328 try:
1335 try:
1329 PullRequestModel().edit(
1336 PullRequestModel().edit(
1330 pull_request,
1337 pull_request,
1331 self.request.POST.get('title'),
1338 self.request.POST.get('title'),
1332 self.request.POST.get('description'),
1339 self.request.POST.get('description'),
1333 self.request.POST.get('description_renderer'),
1340 self.request.POST.get('description_renderer'),
1334 self._rhodecode_user)
1341 self._rhodecode_user)
1335 except ValueError:
1342 except ValueError:
1336 msg = _(u'Cannot update closed pull requests.')
1343 msg = _(u'Cannot update closed pull requests.')
1337 h.flash(msg, category='error')
1344 h.flash(msg, category='error')
1338 return
1345 return
1339 else:
1346 else:
1340 Session().commit()
1347 Session().commit()
1341
1348
1342 msg = _(u'Pull request title & description updated.')
1349 msg = _(u'Pull request title & description updated.')
1343 h.flash(msg, category='success')
1350 h.flash(msg, category='success')
1344 return
1351 return
1345
1352
1346 def _update_commits(self, c, pull_request):
1353 def _update_commits(self, c, pull_request):
1347 _ = self.request.translate
1354 _ = self.request.translate
1348
1355
1349 with pull_request.set_state(PullRequest.STATE_UPDATING):
1356 with pull_request.set_state(PullRequest.STATE_UPDATING):
1350 resp = PullRequestModel().update_commits(
1357 resp = PullRequestModel().update_commits(
1351 pull_request, self._rhodecode_db_user)
1358 pull_request, self._rhodecode_db_user)
1352
1359
1353 if resp.executed:
1360 if resp.executed:
1354
1361
1355 if resp.target_changed and resp.source_changed:
1362 if resp.target_changed and resp.source_changed:
1356 changed = 'target and source repositories'
1363 changed = 'target and source repositories'
1357 elif resp.target_changed and not resp.source_changed:
1364 elif resp.target_changed and not resp.source_changed:
1358 changed = 'target repository'
1365 changed = 'target repository'
1359 elif not resp.target_changed and resp.source_changed:
1366 elif not resp.target_changed and resp.source_changed:
1360 changed = 'source repository'
1367 changed = 'source repository'
1361 else:
1368 else:
1362 changed = 'nothing'
1369 changed = 'nothing'
1363
1370
1364 msg = _(u'Pull request updated to "{source_commit_id}" with '
1371 msg = _(u'Pull request updated to "{source_commit_id}" with '
1365 u'{count_added} added, {count_removed} removed commits. '
1372 u'{count_added} added, {count_removed} removed commits. '
1366 u'Source of changes: {change_source}.')
1373 u'Source of changes: {change_source}.')
1367 msg = msg.format(
1374 msg = msg.format(
1368 source_commit_id=pull_request.source_ref_parts.commit_id,
1375 source_commit_id=pull_request.source_ref_parts.commit_id,
1369 count_added=len(resp.changes.added),
1376 count_added=len(resp.changes.added),
1370 count_removed=len(resp.changes.removed),
1377 count_removed=len(resp.changes.removed),
1371 change_source=changed)
1378 change_source=changed)
1372 h.flash(msg, category='success')
1379 h.flash(msg, category='success')
1373 channelstream.pr_update_channelstream_push(
1380 channelstream.pr_update_channelstream_push(
1374 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1375 else:
1382 else:
1376 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1377 warning_reasons = [
1384 warning_reasons = [
1378 UpdateFailureReason.NO_CHANGE,
1385 UpdateFailureReason.NO_CHANGE,
1379 UpdateFailureReason.WRONG_REF_TYPE,
1386 UpdateFailureReason.WRONG_REF_TYPE,
1380 ]
1387 ]
1381 category = 'warning' if resp.reason in warning_reasons else 'error'
1388 category = 'warning' if resp.reason in warning_reasons else 'error'
1382 h.flash(msg, category=category)
1389 h.flash(msg, category=category)
1383
1390
1384 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1391 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1385 _ = self.request.translate
1392 _ = self.request.translate
1386
1393
1387 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1394 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1388 PullRequestModel().get_reviewer_functions()
1395 PullRequestModel().get_reviewer_functions()
1389
1396
1390 if role == PullRequestReviewers.ROLE_REVIEWER:
1397 if role == PullRequestReviewers.ROLE_REVIEWER:
1391 try:
1398 try:
1392 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1399 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1393 except ValueError as e:
1400 except ValueError as e:
1394 log.error('Reviewers Validation: {}'.format(e))
1401 log.error('Reviewers Validation: {}'.format(e))
1395 h.flash(e, category='error')
1402 h.flash(e, category='error')
1396 return
1403 return
1397
1404
1398 old_calculated_status = pull_request.calculated_review_status()
1405 old_calculated_status = pull_request.calculated_review_status()
1399 PullRequestModel().update_reviewers(
1406 PullRequestModel().update_reviewers(
1400 pull_request, reviewers, self._rhodecode_db_user)
1407 pull_request, reviewers, self._rhodecode_db_user)
1401
1408
1402 Session().commit()
1409 Session().commit()
1403
1410
1404 msg = _('Pull request reviewers updated.')
1411 msg = _('Pull request reviewers updated.')
1405 h.flash(msg, category='success')
1412 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1413 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1414 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1415
1409 # trigger status changed if change in reviewers changes the status
1416 # trigger status changed if change in reviewers changes the status
1410 calculated_status = pull_request.calculated_review_status()
1417 calculated_status = pull_request.calculated_review_status()
1411 if old_calculated_status != calculated_status:
1418 if old_calculated_status != calculated_status:
1412 PullRequestModel().trigger_pull_request_hook(
1419 PullRequestModel().trigger_pull_request_hook(
1413 pull_request, self._rhodecode_user, 'review_status_change',
1420 pull_request, self._rhodecode_user, 'review_status_change',
1414 data={'status': calculated_status})
1421 data={'status': calculated_status})
1415
1422
1416 elif role == PullRequestReviewers.ROLE_OBSERVER:
1423 elif role == PullRequestReviewers.ROLE_OBSERVER:
1417 try:
1424 try:
1418 observers = validate_observers(review_members, reviewer_rules)
1425 observers = validate_observers(review_members, reviewer_rules)
1419 except ValueError as e:
1426 except ValueError as e:
1420 log.error('Observers Validation: {}'.format(e))
1427 log.error('Observers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1428 h.flash(e, category='error')
1422 return
1429 return
1423
1430
1424 PullRequestModel().update_observers(
1431 PullRequestModel().update_observers(
1425 pull_request, observers, self._rhodecode_db_user)
1432 pull_request, observers, self._rhodecode_db_user)
1426
1433
1427 Session().commit()
1434 Session().commit()
1428 msg = _('Pull request observers updated.')
1435 msg = _('Pull request observers updated.')
1429 h.flash(msg, category='success')
1436 h.flash(msg, category='success')
1430 channelstream.pr_update_channelstream_push(
1437 channelstream.pr_update_channelstream_push(
1431 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1438 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1432
1439
1433 @LoginRequired()
1440 @LoginRequired()
1434 @NotAnonymous()
1441 @NotAnonymous()
1435 @HasRepoPermissionAnyDecorator(
1442 @HasRepoPermissionAnyDecorator(
1436 'repository.read', 'repository.write', 'repository.admin')
1443 'repository.read', 'repository.write', 'repository.admin')
1437 @CSRFRequired()
1444 @CSRFRequired()
1438 def pull_request_merge(self):
1445 def pull_request_merge(self):
1439 """
1446 """
1440 Merge will perform a server-side merge of the specified
1447 Merge will perform a server-side merge of the specified
1441 pull request, if the pull request is approved and mergeable.
1448 pull request, if the pull request is approved and mergeable.
1442 After successful merging, the pull request is automatically
1449 After successful merging, the pull request is automatically
1443 closed, with a relevant comment.
1450 closed, with a relevant comment.
1444 """
1451 """
1445 pull_request = PullRequest.get_or_404(
1452 pull_request = PullRequest.get_or_404(
1446 self.request.matchdict['pull_request_id'])
1453 self.request.matchdict['pull_request_id'])
1447 _ = self.request.translate
1454 _ = self.request.translate
1448
1455
1449 if pull_request.is_state_changing():
1456 if pull_request.is_state_changing():
1450 log.debug('show: forbidden because pull request is in state %s',
1457 log.debug('show: forbidden because pull request is in state %s',
1451 pull_request.pull_request_state)
1458 pull_request.pull_request_state)
1452 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1459 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1453 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1460 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1454 pull_request.pull_request_state)
1461 pull_request.pull_request_state)
1455 h.flash(msg, category='error')
1462 h.flash(msg, category='error')
1456 raise HTTPFound(
1463 raise HTTPFound(
1457 h.route_path('pullrequest_show',
1464 h.route_path('pullrequest_show',
1458 repo_name=pull_request.target_repo.repo_name,
1465 repo_name=pull_request.target_repo.repo_name,
1459 pull_request_id=pull_request.pull_request_id))
1466 pull_request_id=pull_request.pull_request_id))
1460
1467
1461 self.load_default_context()
1468 self.load_default_context()
1462
1469
1463 with pull_request.set_state(PullRequest.STATE_UPDATING):
1470 with pull_request.set_state(PullRequest.STATE_UPDATING):
1464 check = MergeCheck.validate(
1471 check = MergeCheck.validate(
1465 pull_request, auth_user=self._rhodecode_user,
1472 pull_request, auth_user=self._rhodecode_user,
1466 translator=self.request.translate)
1473 translator=self.request.translate)
1467 merge_possible = not check.failed
1474 merge_possible = not check.failed
1468
1475
1469 for err_type, error_msg in check.errors:
1476 for err_type, error_msg in check.errors:
1470 h.flash(error_msg, category=err_type)
1477 h.flash(error_msg, category=err_type)
1471
1478
1472 if merge_possible:
1479 if merge_possible:
1473 log.debug("Pre-conditions checked, trying to merge.")
1480 log.debug("Pre-conditions checked, trying to merge.")
1474 extras = vcs_operation_context(
1481 extras = vcs_operation_context(
1475 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1482 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1476 username=self._rhodecode_db_user.username, action='push',
1483 username=self._rhodecode_db_user.username, action='push',
1477 scm=pull_request.target_repo.repo_type)
1484 scm=pull_request.target_repo.repo_type)
1478 with pull_request.set_state(PullRequest.STATE_UPDATING):
1485 with pull_request.set_state(PullRequest.STATE_UPDATING):
1479 self._merge_pull_request(
1486 self._merge_pull_request(
1480 pull_request, self._rhodecode_db_user, extras)
1487 pull_request, self._rhodecode_db_user, extras)
1481 else:
1488 else:
1482 log.debug("Pre-conditions failed, NOT merging.")
1489 log.debug("Pre-conditions failed, NOT merging.")
1483
1490
1484 raise HTTPFound(
1491 raise HTTPFound(
1485 h.route_path('pullrequest_show',
1492 h.route_path('pullrequest_show',
1486 repo_name=pull_request.target_repo.repo_name,
1493 repo_name=pull_request.target_repo.repo_name,
1487 pull_request_id=pull_request.pull_request_id))
1494 pull_request_id=pull_request.pull_request_id))
1488
1495
1489 def _merge_pull_request(self, pull_request, user, extras):
1496 def _merge_pull_request(self, pull_request, user, extras):
1490 _ = self.request.translate
1497 _ = self.request.translate
1491 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1498 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1492
1499
1493 if merge_resp.executed:
1500 if merge_resp.executed:
1494 log.debug("The merge was successful, closing the pull request.")
1501 log.debug("The merge was successful, closing the pull request.")
1495 PullRequestModel().close_pull_request(
1502 PullRequestModel().close_pull_request(
1496 pull_request.pull_request_id, user)
1503 pull_request.pull_request_id, user)
1497 Session().commit()
1504 Session().commit()
1498 msg = _('Pull request was successfully merged and closed.')
1505 msg = _('Pull request was successfully merged and closed.')
1499 h.flash(msg, category='success')
1506 h.flash(msg, category='success')
1500 else:
1507 else:
1501 log.debug(
1508 log.debug(
1502 "The merge was not successful. Merge response: %s", merge_resp)
1509 "The merge was not successful. Merge response: %s", merge_resp)
1503 msg = merge_resp.merge_status_message
1510 msg = merge_resp.merge_status_message
1504 h.flash(msg, category='error')
1511 h.flash(msg, category='error')
1505
1512
1506 @LoginRequired()
1513 @LoginRequired()
1507 @NotAnonymous()
1514 @NotAnonymous()
1508 @HasRepoPermissionAnyDecorator(
1515 @HasRepoPermissionAnyDecorator(
1509 'repository.read', 'repository.write', 'repository.admin')
1516 'repository.read', 'repository.write', 'repository.admin')
1510 @CSRFRequired()
1517 @CSRFRequired()
1511 def pull_request_delete(self):
1518 def pull_request_delete(self):
1512 _ = self.request.translate
1519 _ = self.request.translate
1513
1520
1514 pull_request = PullRequest.get_or_404(
1521 pull_request = PullRequest.get_or_404(
1515 self.request.matchdict['pull_request_id'])
1522 self.request.matchdict['pull_request_id'])
1516 self.load_default_context()
1523 self.load_default_context()
1517
1524
1518 pr_closed = pull_request.is_closed()
1525 pr_closed = pull_request.is_closed()
1519 allowed_to_delete = PullRequestModel().check_user_delete(
1526 allowed_to_delete = PullRequestModel().check_user_delete(
1520 pull_request, self._rhodecode_user) and not pr_closed
1527 pull_request, self._rhodecode_user) and not pr_closed
1521
1528
1522 # only owner can delete it !
1529 # only owner can delete it !
1523 if allowed_to_delete:
1530 if allowed_to_delete:
1524 PullRequestModel().delete(pull_request, self._rhodecode_user)
1531 PullRequestModel().delete(pull_request, self._rhodecode_user)
1525 Session().commit()
1532 Session().commit()
1526 h.flash(_('Successfully deleted pull request'),
1533 h.flash(_('Successfully deleted pull request'),
1527 category='success')
1534 category='success')
1528 raise HTTPFound(h.route_path('pullrequest_show_all',
1535 raise HTTPFound(h.route_path('pullrequest_show_all',
1529 repo_name=self.db_repo_name))
1536 repo_name=self.db_repo_name))
1530
1537
1531 log.warning('user %s tried to delete pull request without access',
1538 log.warning('user %s tried to delete pull request without access',
1532 self._rhodecode_user)
1539 self._rhodecode_user)
1533 raise HTTPNotFound()
1540 raise HTTPNotFound()
1534
1541
1535 def _pull_request_comments_create(self, pull_request, comments):
1542 def _pull_request_comments_create(self, pull_request, comments):
1536 _ = self.request.translate
1543 _ = self.request.translate
1537 data = {}
1544 data = {}
1538 if not comments:
1545 if not comments:
1539 return
1546 return
1540 pull_request_id = pull_request.pull_request_id
1547 pull_request_id = pull_request.pull_request_id
1541
1548
1542 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1549 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1543
1550
1544 for entry in comments:
1551 for entry in comments:
1545 c = self.load_default_context()
1552 c = self.load_default_context()
1546 comment_type = entry['comment_type']
1553 comment_type = entry['comment_type']
1547 text = entry['text']
1554 text = entry['text']
1548 status = entry['status']
1555 status = entry['status']
1549 is_draft = str2bool(entry['is_draft'])
1556 is_draft = str2bool(entry['is_draft'])
1550 resolves_comment_id = entry['resolves_comment_id']
1557 resolves_comment_id = entry['resolves_comment_id']
1551 close_pull_request = entry['close_pull_request']
1558 close_pull_request = entry['close_pull_request']
1552 f_path = entry['f_path']
1559 f_path = entry['f_path']
1553 line_no = entry['line']
1560 line_no = entry['line']
1554 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1561 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1555
1562
1556 # the logic here should work like following, if we submit close
1563 # the logic here should work like following, if we submit close
1557 # pr comment, use `close_pull_request_with_comment` function
1564 # pr comment, use `close_pull_request_with_comment` function
1558 # else handle regular comment logic
1565 # else handle regular comment logic
1559
1566
1560 if close_pull_request:
1567 if close_pull_request:
1561 # only owner or admin or person with write permissions
1568 # only owner or admin or person with write permissions
1562 allowed_to_close = PullRequestModel().check_user_update(
1569 allowed_to_close = PullRequestModel().check_user_update(
1563 pull_request, self._rhodecode_user)
1570 pull_request, self._rhodecode_user)
1564 if not allowed_to_close:
1571 if not allowed_to_close:
1565 log.debug('comment: forbidden because not allowed to close '
1572 log.debug('comment: forbidden because not allowed to close '
1566 'pull request %s', pull_request_id)
1573 'pull request %s', pull_request_id)
1567 raise HTTPForbidden()
1574 raise HTTPForbidden()
1568
1575
1569 # This also triggers `review_status_change`
1576 # This also triggers `review_status_change`
1570 comment, status = PullRequestModel().close_pull_request_with_comment(
1577 comment, status = PullRequestModel().close_pull_request_with_comment(
1571 pull_request, self._rhodecode_user, self.db_repo, message=text,
1578 pull_request, self._rhodecode_user, self.db_repo, message=text,
1572 auth_user=self._rhodecode_user)
1579 auth_user=self._rhodecode_user)
1573 Session().flush()
1580 Session().flush()
1574 is_inline = comment.is_inline
1581 is_inline = comment.is_inline
1575
1582
1576 PullRequestModel().trigger_pull_request_hook(
1583 PullRequestModel().trigger_pull_request_hook(
1577 pull_request, self._rhodecode_user, 'comment',
1584 pull_request, self._rhodecode_user, 'comment',
1578 data={'comment': comment})
1585 data={'comment': comment})
1579
1586
1580 else:
1587 else:
1581 # regular comment case, could be inline, or one with status.
1588 # regular comment case, could be inline, or one with status.
1582 # for that one we check also permissions
1589 # for that one we check also permissions
1583 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1590 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1584 allowed_to_change_status = PullRequestModel().check_user_change_status(
1591 allowed_to_change_status = PullRequestModel().check_user_change_status(
1585 pull_request, self._rhodecode_user) and not is_draft
1592 pull_request, self._rhodecode_user) and not is_draft
1586
1593
1587 if status and allowed_to_change_status:
1594 if status and allowed_to_change_status:
1588 message = (_('Status change %(transition_icon)s %(status)s')
1595 message = (_('Status change %(transition_icon)s %(status)s')
1589 % {'transition_icon': '>',
1596 % {'transition_icon': '>',
1590 'status': ChangesetStatus.get_status_lbl(status)})
1597 'status': ChangesetStatus.get_status_lbl(status)})
1591 text = text or message
1598 text = text or message
1592
1599
1593 comment = CommentsModel().create(
1600 comment = CommentsModel().create(
1594 text=text,
1601 text=text,
1595 repo=self.db_repo.repo_id,
1602 repo=self.db_repo.repo_id,
1596 user=self._rhodecode_user.user_id,
1603 user=self._rhodecode_user.user_id,
1597 pull_request=pull_request,
1604 pull_request=pull_request,
1598 f_path=f_path,
1605 f_path=f_path,
1599 line_no=line_no,
1606 line_no=line_no,
1600 status_change=(ChangesetStatus.get_status_lbl(status)
1607 status_change=(ChangesetStatus.get_status_lbl(status)
1601 if status and allowed_to_change_status else None),
1608 if status and allowed_to_change_status else None),
1602 status_change_type=(status
1609 status_change_type=(status
1603 if status and allowed_to_change_status else None),
1610 if status and allowed_to_change_status else None),
1604 comment_type=comment_type,
1611 comment_type=comment_type,
1605 is_draft=is_draft,
1612 is_draft=is_draft,
1606 resolves_comment_id=resolves_comment_id,
1613 resolves_comment_id=resolves_comment_id,
1607 auth_user=self._rhodecode_user,
1614 auth_user=self._rhodecode_user,
1608 send_email=not is_draft, # skip notification for draft comments
1615 send_email=not is_draft, # skip notification for draft comments
1609 )
1616 )
1610 is_inline = comment.is_inline
1617 is_inline = comment.is_inline
1611
1618
1612 if allowed_to_change_status:
1619 if allowed_to_change_status:
1613 # calculate old status before we change it
1620 # calculate old status before we change it
1614 old_calculated_status = pull_request.calculated_review_status()
1621 old_calculated_status = pull_request.calculated_review_status()
1615
1622
1616 # get status if set !
1623 # get status if set !
1617 if status:
1624 if status:
1618 ChangesetStatusModel().set_status(
1625 ChangesetStatusModel().set_status(
1619 self.db_repo.repo_id,
1626 self.db_repo.repo_id,
1620 status,
1627 status,
1621 self._rhodecode_user.user_id,
1628 self._rhodecode_user.user_id,
1622 comment,
1629 comment,
1623 pull_request=pull_request
1630 pull_request=pull_request
1624 )
1631 )
1625
1632
1626 Session().flush()
1633 Session().flush()
1627 # this is somehow required to get access to some relationship
1634 # this is somehow required to get access to some relationship
1628 # loaded on comment
1635 # loaded on comment
1629 Session().refresh(comment)
1636 Session().refresh(comment)
1630
1637
1631 # skip notifications for drafts
1638 # skip notifications for drafts
1632 if not is_draft:
1639 if not is_draft:
1633 PullRequestModel().trigger_pull_request_hook(
1640 PullRequestModel().trigger_pull_request_hook(
1634 pull_request, self._rhodecode_user, 'comment',
1641 pull_request, self._rhodecode_user, 'comment',
1635 data={'comment': comment})
1642 data={'comment': comment})
1636
1643
1637 # we now calculate the status of pull request, and based on that
1644 # we now calculate the status of pull request, and based on that
1638 # calculation we set the commits status
1645 # calculation we set the commits status
1639 calculated_status = pull_request.calculated_review_status()
1646 calculated_status = pull_request.calculated_review_status()
1640 if old_calculated_status != calculated_status:
1647 if old_calculated_status != calculated_status:
1641 PullRequestModel().trigger_pull_request_hook(
1648 PullRequestModel().trigger_pull_request_hook(
1642 pull_request, self._rhodecode_user, 'review_status_change',
1649 pull_request, self._rhodecode_user, 'review_status_change',
1643 data={'status': calculated_status})
1650 data={'status': calculated_status})
1644
1651
1645 comment_id = comment.comment_id
1652 comment_id = comment.comment_id
1646 data[comment_id] = {
1653 data[comment_id] = {
1647 'target_id': target_elem_id
1654 'target_id': target_elem_id
1648 }
1655 }
1649 Session().flush()
1656 Session().flush()
1650
1657
1651 c.co = comment
1658 c.co = comment
1652 c.at_version_num = None
1659 c.at_version_num = None
1653 c.is_new = True
1660 c.is_new = True
1654 rendered_comment = render(
1661 rendered_comment = render(
1655 'rhodecode:templates/changeset/changeset_comment_block.mako',
1662 'rhodecode:templates/changeset/changeset_comment_block.mako',
1656 self._get_template_context(c), self.request)
1663 self._get_template_context(c), self.request)
1657
1664
1658 data[comment_id].update(comment.get_dict())
1665 data[comment_id].update(comment.get_dict())
1659 data[comment_id].update({'rendered_text': rendered_comment})
1666 data[comment_id].update({'rendered_text': rendered_comment})
1660
1667
1661 Session().commit()
1668 Session().commit()
1662
1669
1663 # skip channelstream for draft comments
1670 # skip channelstream for draft comments
1664 if not all_drafts:
1671 if not all_drafts:
1665 comment_broadcast_channel = channelstream.comment_channel(
1672 comment_broadcast_channel = channelstream.comment_channel(
1666 self.db_repo_name, pull_request_obj=pull_request)
1673 self.db_repo_name, pull_request_obj=pull_request)
1667
1674
1668 comment_data = data
1675 comment_data = data
1669 posted_comment_type = 'inline' if is_inline else 'general'
1676 posted_comment_type = 'inline' if is_inline else 'general'
1670 if len(data) == 1:
1677 if len(data) == 1:
1671 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1678 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1672 else:
1679 else:
1673 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1680 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1674
1681
1675 channelstream.comment_channelstream_push(
1682 channelstream.comment_channelstream_push(
1676 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1683 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1677 comment_data=comment_data)
1684 comment_data=comment_data)
1678
1685
1679 return data
1686 return data
1680
1687
1681 @LoginRequired()
1688 @LoginRequired()
1682 @NotAnonymous()
1689 @NotAnonymous()
1683 @HasRepoPermissionAnyDecorator(
1690 @HasRepoPermissionAnyDecorator(
1684 'repository.read', 'repository.write', 'repository.admin')
1691 'repository.read', 'repository.write', 'repository.admin')
1685 @CSRFRequired()
1692 @CSRFRequired()
1686 def pull_request_comment_create(self):
1693 def pull_request_comment_create(self):
1687 _ = self.request.translate
1694 _ = self.request.translate
1688
1695
1689 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1696 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1690
1697
1691 if pull_request.is_closed():
1698 if pull_request.is_closed():
1692 log.debug('comment: forbidden because pull request is closed')
1699 log.debug('comment: forbidden because pull request is closed')
1693 raise HTTPForbidden()
1700 raise HTTPForbidden()
1694
1701
1695 allowed_to_comment = PullRequestModel().check_user_comment(
1702 allowed_to_comment = PullRequestModel().check_user_comment(
1696 pull_request, self._rhodecode_user)
1703 pull_request, self._rhodecode_user)
1697 if not allowed_to_comment:
1704 if not allowed_to_comment:
1698 log.debug('comment: forbidden because pull request is from forbidden repo')
1705 log.debug('comment: forbidden because pull request is from forbidden repo')
1699 raise HTTPForbidden()
1706 raise HTTPForbidden()
1700
1707
1701 comment_data = {
1708 comment_data = {
1702 'comment_type': self.request.POST.get('comment_type'),
1709 'comment_type': self.request.POST.get('comment_type'),
1703 'text': self.request.POST.get('text'),
1710 'text': self.request.POST.get('text'),
1704 'status': self.request.POST.get('changeset_status', None),
1711 'status': self.request.POST.get('changeset_status', None),
1705 'is_draft': self.request.POST.get('draft'),
1712 'is_draft': self.request.POST.get('draft'),
1706 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1713 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1707 'close_pull_request': self.request.POST.get('close_pull_request'),
1714 'close_pull_request': self.request.POST.get('close_pull_request'),
1708 'f_path': self.request.POST.get('f_path'),
1715 'f_path': self.request.POST.get('f_path'),
1709 'line': self.request.POST.get('line'),
1716 'line': self.request.POST.get('line'),
1710 }
1717 }
1711 data = self._pull_request_comments_create(pull_request, [comment_data])
1718 data = self._pull_request_comments_create(pull_request, [comment_data])
1712
1719
1713 return data
1720 return data
1714
1721
1715 @LoginRequired()
1722 @LoginRequired()
1716 @NotAnonymous()
1723 @NotAnonymous()
1717 @HasRepoPermissionAnyDecorator(
1724 @HasRepoPermissionAnyDecorator(
1718 'repository.read', 'repository.write', 'repository.admin')
1725 'repository.read', 'repository.write', 'repository.admin')
1719 @CSRFRequired()
1726 @CSRFRequired()
1720 def pull_request_comment_delete(self):
1727 def pull_request_comment_delete(self):
1721 pull_request = PullRequest.get_or_404(
1728 pull_request = PullRequest.get_or_404(
1722 self.request.matchdict['pull_request_id'])
1729 self.request.matchdict['pull_request_id'])
1723
1730
1724 comment = ChangesetComment.get_or_404(
1731 comment = ChangesetComment.get_or_404(
1725 self.request.matchdict['comment_id'])
1732 self.request.matchdict['comment_id'])
1726 comment_id = comment.comment_id
1733 comment_id = comment.comment_id
1727
1734
1728 if comment.immutable:
1735 if comment.immutable:
1729 # don't allow deleting comments that are immutable
1736 # don't allow deleting comments that are immutable
1730 raise HTTPForbidden()
1737 raise HTTPForbidden()
1731
1738
1732 if pull_request.is_closed():
1739 if pull_request.is_closed():
1733 log.debug('comment: forbidden because pull request is closed')
1740 log.debug('comment: forbidden because pull request is closed')
1734 raise HTTPForbidden()
1741 raise HTTPForbidden()
1735
1742
1736 if not comment:
1743 if not comment:
1737 log.debug('Comment with id:%s not found, skipping', comment_id)
1744 log.debug('Comment with id:%s not found, skipping', comment_id)
1738 # comment already deleted in another call probably
1745 # comment already deleted in another call probably
1739 return True
1746 return True
1740
1747
1741 if comment.pull_request.is_closed():
1748 if comment.pull_request.is_closed():
1742 # don't allow deleting comments on closed pull request
1749 # don't allow deleting comments on closed pull request
1743 raise HTTPForbidden()
1750 raise HTTPForbidden()
1744
1751
1745 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1752 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1746 super_admin = h.HasPermissionAny('hg.admin')()
1753 super_admin = h.HasPermissionAny('hg.admin')()
1747 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1754 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1748 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1755 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1749 comment_repo_admin = is_repo_admin and is_repo_comment
1756 comment_repo_admin = is_repo_admin and is_repo_comment
1750
1757
1751 if comment.draft and not comment_owner:
1758 if comment.draft and not comment_owner:
1752 # We never allow to delete draft comments for other than owners
1759 # We never allow to delete draft comments for other than owners
1753 raise HTTPNotFound()
1760 raise HTTPNotFound()
1754
1761
1755 if super_admin or comment_owner or comment_repo_admin:
1762 if super_admin or comment_owner or comment_repo_admin:
1756 old_calculated_status = comment.pull_request.calculated_review_status()
1763 old_calculated_status = comment.pull_request.calculated_review_status()
1757 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1764 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1758 Session().commit()
1765 Session().commit()
1759 calculated_status = comment.pull_request.calculated_review_status()
1766 calculated_status = comment.pull_request.calculated_review_status()
1760 if old_calculated_status != calculated_status:
1767 if old_calculated_status != calculated_status:
1761 PullRequestModel().trigger_pull_request_hook(
1768 PullRequestModel().trigger_pull_request_hook(
1762 comment.pull_request, self._rhodecode_user, 'review_status_change',
1769 comment.pull_request, self._rhodecode_user, 'review_status_change',
1763 data={'status': calculated_status})
1770 data={'status': calculated_status})
1764 return True
1771 return True
1765 else:
1772 else:
1766 log.warning('No permissions for user %s to delete comment_id: %s',
1773 log.warning('No permissions for user %s to delete comment_id: %s',
1767 self._rhodecode_db_user, comment_id)
1774 self._rhodecode_db_user, comment_id)
1768 raise HTTPNotFound()
1775 raise HTTPNotFound()
1769
1776
1770 @LoginRequired()
1777 @LoginRequired()
1771 @NotAnonymous()
1778 @NotAnonymous()
1772 @HasRepoPermissionAnyDecorator(
1779 @HasRepoPermissionAnyDecorator(
1773 'repository.read', 'repository.write', 'repository.admin')
1780 'repository.read', 'repository.write', 'repository.admin')
1774 @CSRFRequired()
1781 @CSRFRequired()
1775 def pull_request_comment_edit(self):
1782 def pull_request_comment_edit(self):
1776 self.load_default_context()
1783 self.load_default_context()
1777
1784
1778 pull_request = PullRequest.get_or_404(
1785 pull_request = PullRequest.get_or_404(
1779 self.request.matchdict['pull_request_id']
1786 self.request.matchdict['pull_request_id']
1780 )
1787 )
1781 comment = ChangesetComment.get_or_404(
1788 comment = ChangesetComment.get_or_404(
1782 self.request.matchdict['comment_id']
1789 self.request.matchdict['comment_id']
1783 )
1790 )
1784 comment_id = comment.comment_id
1791 comment_id = comment.comment_id
1785
1792
1786 if comment.immutable:
1793 if comment.immutable:
1787 # don't allow deleting comments that are immutable
1794 # don't allow deleting comments that are immutable
1788 raise HTTPForbidden()
1795 raise HTTPForbidden()
1789
1796
1790 if pull_request.is_closed():
1797 if pull_request.is_closed():
1791 log.debug('comment: forbidden because pull request is closed')
1798 log.debug('comment: forbidden because pull request is closed')
1792 raise HTTPForbidden()
1799 raise HTTPForbidden()
1793
1800
1794 if comment.pull_request.is_closed():
1801 if comment.pull_request.is_closed():
1795 # don't allow deleting comments on closed pull request
1802 # don't allow deleting comments on closed pull request
1796 raise HTTPForbidden()
1803 raise HTTPForbidden()
1797
1804
1798 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1805 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1799 super_admin = h.HasPermissionAny('hg.admin')()
1806 super_admin = h.HasPermissionAny('hg.admin')()
1800 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1807 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1801 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1808 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1802 comment_repo_admin = is_repo_admin and is_repo_comment
1809 comment_repo_admin = is_repo_admin and is_repo_comment
1803
1810
1804 if super_admin or comment_owner or comment_repo_admin:
1811 if super_admin or comment_owner or comment_repo_admin:
1805 text = self.request.POST.get('text')
1812 text = self.request.POST.get('text')
1806 version = self.request.POST.get('version')
1813 version = self.request.POST.get('version')
1807 if text == comment.text:
1814 if text == comment.text:
1808 log.warning(
1815 log.warning(
1809 'Comment(PR): '
1816 'Comment(PR): '
1810 'Trying to create new version '
1817 'Trying to create new version '
1811 'with the same comment body {}'.format(
1818 'with the same comment body {}'.format(
1812 comment_id,
1819 comment_id,
1813 )
1820 )
1814 )
1821 )
1815 raise HTTPNotFound()
1822 raise HTTPNotFound()
1816
1823
1817 if version.isdigit():
1824 if version.isdigit():
1818 version = int(version)
1825 version = int(version)
1819 else:
1826 else:
1820 log.warning(
1827 log.warning(
1821 'Comment(PR): Wrong version type {} {} '
1828 'Comment(PR): Wrong version type {} {} '
1822 'for comment {}'.format(
1829 'for comment {}'.format(
1823 version,
1830 version,
1824 type(version),
1831 type(version),
1825 comment_id,
1832 comment_id,
1826 )
1833 )
1827 )
1834 )
1828 raise HTTPNotFound()
1835 raise HTTPNotFound()
1829
1836
1830 try:
1837 try:
1831 comment_history = CommentsModel().edit(
1838 comment_history = CommentsModel().edit(
1832 comment_id=comment_id,
1839 comment_id=comment_id,
1833 text=text,
1840 text=text,
1834 auth_user=self._rhodecode_user,
1841 auth_user=self._rhodecode_user,
1835 version=version,
1842 version=version,
1836 )
1843 )
1837 except CommentVersionMismatch:
1844 except CommentVersionMismatch:
1838 raise HTTPConflict()
1845 raise HTTPConflict()
1839
1846
1840 if not comment_history:
1847 if not comment_history:
1841 raise HTTPNotFound()
1848 raise HTTPNotFound()
1842
1849
1843 Session().commit()
1850 Session().commit()
1844 if not comment.draft:
1851 if not comment.draft:
1845 PullRequestModel().trigger_pull_request_hook(
1852 PullRequestModel().trigger_pull_request_hook(
1846 pull_request, self._rhodecode_user, 'comment_edit',
1853 pull_request, self._rhodecode_user, 'comment_edit',
1847 data={'comment': comment})
1854 data={'comment': comment})
1848
1855
1849 return {
1856 return {
1850 'comment_history_id': comment_history.comment_history_id,
1857 'comment_history_id': comment_history.comment_history_id,
1851 'comment_id': comment.comment_id,
1858 'comment_id': comment.comment_id,
1852 'comment_version': comment_history.version,
1859 'comment_version': comment_history.version,
1853 'comment_author_username': comment_history.author.username,
1860 'comment_author_username': comment_history.author.username,
1854 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1861 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1855 'comment_created_on': h.age_component(comment_history.created_on,
1862 'comment_created_on': h.age_component(comment_history.created_on,
1856 time_is_local=True),
1863 time_is_local=True),
1857 }
1864 }
1858 else:
1865 else:
1859 log.warning('No permissions for user %s to edit comment_id: %s',
1866 log.warning('No permissions for user %s to edit comment_id: %s',
1860 self._rhodecode_db_user, comment_id)
1867 self._rhodecode_db_user, comment_id)
1861 raise HTTPNotFound()
1868 raise HTTPNotFound()
@@ -1,396 +1,403 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import itertools
22 import itertools
23 import logging
23 import logging
24 import collections
24 import collections
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class ChangesetStatusModel(BaseModel):
36 class ChangesetStatusModel(BaseModel):
37
37
38 cls = ChangesetStatus
38 cls = ChangesetStatus
39
39
40 def __get_changeset_status(self, changeset_status):
40 def __get_changeset_status(self, changeset_status):
41 return self._get_instance(ChangesetStatus, changeset_status)
41 return self._get_instance(ChangesetStatus, changeset_status)
42
42
43 def __get_pull_request(self, pull_request):
43 def __get_pull_request(self, pull_request):
44 return self._get_instance(PullRequest, pull_request)
44 return self._get_instance(PullRequest, pull_request)
45
45
46 def _get_status_query(self, repo, revision, pull_request,
46 def _get_status_query(self, repo, revision, pull_request,
47 with_revisions=False):
47 with_revisions=False):
48 repo = self._get_repo(repo)
48 repo = self._get_repo(repo)
49
49
50 q = ChangesetStatus.query()\
50 q = ChangesetStatus.query()\
51 .filter(ChangesetStatus.repo == repo)
51 .filter(ChangesetStatus.repo == repo)
52 if not with_revisions:
52 if not with_revisions:
53 q = q.filter(ChangesetStatus.version == 0)
53 q = q.filter(ChangesetStatus.version == 0)
54
54
55 if revision:
55 if revision:
56 q = q.filter(ChangesetStatus.revision == revision)
56 q = q.filter(ChangesetStatus.revision == revision)
57 elif pull_request:
57 elif pull_request:
58 pull_request = self.__get_pull_request(pull_request)
58 pull_request = self.__get_pull_request(pull_request)
59 # TODO: johbo: Think about the impact of this join, there must
59 # TODO: johbo: Think about the impact of this join, there must
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
60 # be a reason why ChangesetStatus and ChanagesetComment is linked
61 # to the pull request. Might be that we want to do the same for
61 # to the pull request. Might be that we want to do the same for
62 # the pull_request_version_id.
62 # the pull_request_version_id.
63 q = q.join(ChangesetComment).filter(
63 q = q.join(ChangesetComment).filter(
64 ChangesetStatus.pull_request == pull_request,
64 ChangesetStatus.pull_request == pull_request,
65 ChangesetComment.pull_request_version_id == None)
65 ChangesetComment.pull_request_version_id == None)
66 else:
66 else:
67 raise Exception('Please specify revision or pull_request')
67 raise Exception('Please specify revision or pull_request')
68 q = q.order_by(ChangesetStatus.version.asc())
68 q = q.order_by(ChangesetStatus.version.asc())
69 return q
69 return q
70
70
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
71 def calculate_group_vote(self, group_id, group_statuses_by_reviewers,
72 trim_votes=True):
72 trim_votes=True):
73 """
73 """
74 Calculate status based on given group members, and voting rule
74 Calculate status based on given group members, and voting rule
75
75
76
76
77 group1 - 4 members, 3 required for approval
77 group1 - 4 members, 3 required for approval
78 user1 - approved
78 user1 - approved
79 user2 - reject
79 user2 - reject
80 user3 - approved
80 user3 - approved
81 user4 - rejected
81 user4 - rejected
82
82
83 final_state: rejected, reasons not at least 3 votes
83 final_state: rejected, reasons not at least 3 votes
84
84
85
85
86 group1 - 4 members, 2 required for approval
86 group1 - 4 members, 2 required for approval
87 user1 - approved
87 user1 - approved
88 user2 - reject
88 user2 - reject
89 user3 - approved
89 user3 - approved
90 user4 - rejected
90 user4 - rejected
91
91
92 final_state: approved, reasons got at least 2 approvals
92 final_state: approved, reasons got at least 2 approvals
93
93
94 group1 - 4 members, ALL required for approval
94 group1 - 4 members, ALL required for approval
95 user1 - approved
95 user1 - approved
96 user2 - reject
96 user2 - reject
97 user3 - approved
97 user3 - approved
98 user4 - rejected
98 user4 - rejected
99
99
100 final_state: rejected, reasons not all approvals
100 final_state: rejected, reasons not all approvals
101
101
102
102
103 group1 - 4 members, ALL required for approval
103 group1 - 4 members, ALL required for approval
104 user1 - approved
104 user1 - approved
105 user2 - approved
105 user2 - approved
106 user3 - approved
106 user3 - approved
107 user4 - approved
107 user4 - approved
108
108
109 final_state: approved, reason all approvals received
109 final_state: approved, reason all approvals received
110
110
111 group1 - 4 members, 5 required for approval
111 group1 - 4 members, 5 required for approval
112 (approval should be shorted to number of actual members)
112 (approval should be shorted to number of actual members)
113
113
114 user1 - approved
114 user1 - approved
115 user2 - approved
115 user2 - approved
116 user3 - approved
116 user3 - approved
117 user4 - approved
117 user4 - approved
118
118
119 final_state: approved, reason all approvals received
119 final_state: approved, reason all approvals received
120
120
121 """
121 """
122 group_vote_data = {}
122 group_vote_data = {}
123 got_rule = False
123 got_rule = False
124 members = collections.OrderedDict()
124 members = collections.OrderedDict()
125 for review_obj, user, reasons, mandatory, statuses \
125 for review_obj, user, reasons, mandatory, statuses \
126 in group_statuses_by_reviewers:
126 in group_statuses_by_reviewers:
127
127
128 if not got_rule:
128 if not got_rule:
129 group_vote_data = review_obj.rule_user_group_data()
129 group_vote_data = review_obj.rule_user_group_data()
130 got_rule = bool(group_vote_data)
130 got_rule = bool(group_vote_data)
131
131
132 members[user.user_id] = statuses
132 members[user.user_id] = statuses
133
133
134 if not group_vote_data:
134 if not group_vote_data:
135 return []
135 return []
136
136
137 required_votes = group_vote_data['vote_rule']
137 required_votes = group_vote_data['vote_rule']
138 if required_votes == -1:
138 if required_votes == -1:
139 # -1 means all required, so we replace it with how many people
139 # -1 means all required, so we replace it with how many people
140 # are in the members
140 # are in the members
141 required_votes = len(members)
141 required_votes = len(members)
142
142
143 if trim_votes and required_votes > len(members):
143 if trim_votes and required_votes > len(members):
144 # we require more votes than we have members in the group
144 # we require more votes than we have members in the group
145 # in this case we trim the required votes to the number of members
145 # in this case we trim the required votes to the number of members
146 required_votes = len(members)
146 required_votes = len(members)
147
147
148 approvals = sum([
148 approvals = sum([
149 1 for statuses in members.values()
149 1 for statuses in members.values()
150 if statuses and
150 if statuses and
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
151 statuses[0][1].status == ChangesetStatus.STATUS_APPROVED])
152
152
153 calculated_votes = []
153 calculated_votes = []
154 # we have all votes from users, now check if we have enough votes
154 # we have all votes from users, now check if we have enough votes
155 # to fill other
155 # to fill other
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
156 fill_in = ChangesetStatus.STATUS_UNDER_REVIEW
157 if approvals >= required_votes:
157 if approvals >= required_votes:
158 fill_in = ChangesetStatus.STATUS_APPROVED
158 fill_in = ChangesetStatus.STATUS_APPROVED
159
159
160 for member, statuses in members.items():
160 for member, statuses in members.items():
161 if statuses:
161 if statuses:
162 ver, latest = statuses[0]
162 ver, latest = statuses[0]
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
163 if fill_in == ChangesetStatus.STATUS_APPROVED:
164 calculated_votes.append(fill_in)
164 calculated_votes.append(fill_in)
165 else:
165 else:
166 calculated_votes.append(latest.status)
166 calculated_votes.append(latest.status)
167 else:
167 else:
168 calculated_votes.append(fill_in)
168 calculated_votes.append(fill_in)
169
169
170 return calculated_votes
170 return calculated_votes
171
171
172 def calculate_status(self, statuses_by_reviewers):
172 def calculate_status(self, statuses_by_reviewers):
173 """
173 """
174 Given the approval statuses from reviewers, calculates final approval
174 Given the approval statuses from reviewers, calculates final approval
175 status. There can only be 3 results, all approved, all rejected. If
175 status. There can only be 3 results, all approved, all rejected. If
176 there is no consensus the PR is under review.
176 there is no consensus the PR is under review.
177
177
178 :param statuses_by_reviewers:
178 :param statuses_by_reviewers:
179 """
179 """
180
180
181 def group_rule(element):
181 def group_rule(element):
182 review_obj = element[0]
182 review_obj = element[0]
183 rule_data = review_obj.rule_user_group_data()
183 rule_data = review_obj.rule_user_group_data()
184 if rule_data and rule_data['id']:
184 if rule_data and rule_data['id']:
185 return rule_data['id']
185 return rule_data['id']
186
186
187 voting_groups = itertools.groupby(
187 voting_groups = itertools.groupby(
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
188 sorted(statuses_by_reviewers, key=group_rule), group_rule)
189
189
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
190 voting_by_groups = [(x, list(y)) for x, y in voting_groups]
191
191
192 reviewers_number = len(statuses_by_reviewers)
192 reviewers_number = len(statuses_by_reviewers)
193 votes = collections.defaultdict(int)
193 votes = collections.defaultdict(int)
194 for group, group_statuses_by_reviewers in voting_by_groups:
194 for group, group_statuses_by_reviewers in voting_by_groups:
195 if group:
195 if group:
196 # calculate how the "group" voted
196 # calculate how the "group" voted
197 for vote_status in self.calculate_group_vote(
197 for vote_status in self.calculate_group_vote(
198 group, group_statuses_by_reviewers):
198 group, group_statuses_by_reviewers):
199 votes[vote_status] += 1
199 votes[vote_status] += 1
200 else:
200 else:
201
201
202 for review_obj, user, reasons, mandatory, statuses \
202 for review_obj, user, reasons, mandatory, statuses \
203 in group_statuses_by_reviewers:
203 in group_statuses_by_reviewers:
204 # individual vote
204 # individual vote
205 if statuses:
205 if statuses:
206 ver, latest = statuses[0]
206 ver, latest = statuses[0]
207 votes[latest.status] += 1
207 votes[latest.status] += 1
208
208
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
209 approved_votes_count = votes[ChangesetStatus.STATUS_APPROVED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
210 rejected_votes_count = votes[ChangesetStatus.STATUS_REJECTED]
211
211
212 # TODO(marcink): with group voting, how does rejected work,
212 # TODO(marcink): with group voting, how does rejected work,
213 # do we ever get rejected state ?
213 # do we ever get rejected state ?
214
214
215 if approved_votes_count and (approved_votes_count == reviewers_number):
215 if approved_votes_count and (approved_votes_count == reviewers_number):
216 return ChangesetStatus.STATUS_APPROVED
216 return ChangesetStatus.STATUS_APPROVED
217
217
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
218 if rejected_votes_count and (rejected_votes_count == reviewers_number):
219 return ChangesetStatus.STATUS_REJECTED
219 return ChangesetStatus.STATUS_REJECTED
220
220
221 return ChangesetStatus.STATUS_UNDER_REVIEW
221 return ChangesetStatus.STATUS_UNDER_REVIEW
222
222
223 def get_statuses(self, repo, revision=None, pull_request=None,
223 def get_statuses(self, repo, revision=None, pull_request=None,
224 with_revisions=False):
224 with_revisions=False):
225 q = self._get_status_query(repo, revision, pull_request,
225 q = self._get_status_query(repo, revision, pull_request,
226 with_revisions)
226 with_revisions)
227 return q.all()
227 return q.all()
228
228
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
229 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
230 """
230 """
231 Returns latest status of changeset for given revision or for given
231 Returns latest status of changeset for given revision or for given
232 pull request. Statuses are versioned inside a table itself and
232 pull request. Statuses are versioned inside a table itself and
233 version == 0 is always the current one
233 version == 0 is always the current one
234
234
235 :param repo:
235 :param repo:
236 :param revision: 40char hash or None
236 :param revision: 40char hash or None
237 :param pull_request: pull_request reference
237 :param pull_request: pull_request reference
238 :param as_str: return status as string not object
238 :param as_str: return status as string not object
239 """
239 """
240 q = self._get_status_query(repo, revision, pull_request)
240 q = self._get_status_query(repo, revision, pull_request)
241
241
242 # need to use first here since there can be multiple statuses
242 # need to use first here since there can be multiple statuses
243 # returned from pull_request
243 # returned from pull_request
244 status = q.first()
244 status = q.first()
245 if as_str:
245 if as_str:
246 status = status.status if status else status
246 status = status.status if status else status
247 st = status or ChangesetStatus.DEFAULT
247 st = status or ChangesetStatus.DEFAULT
248 return str(st)
248 return str(st)
249 return status
249 return status
250
250
251 def _render_auto_status_message(
251 def _render_auto_status_message(
252 self, status, commit_id=None, pull_request=None):
252 self, status, commit_id=None, pull_request=None):
253 """
253 """
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
254 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
255 so it's always looking the same disregarding on which default
255 so it's always looking the same disregarding on which default
256 renderer system is using.
256 renderer system is using.
257
257
258 :param status: status text to change into
258 :param status: status text to change into
259 :param commit_id: the commit_id we change the status for
259 :param commit_id: the commit_id we change the status for
260 :param pull_request: the pull request we change the status for
260 :param pull_request: the pull request we change the status for
261 """
261 """
262
262
263 new_status = ChangesetStatus.get_status_lbl(status)
263 new_status = ChangesetStatus.get_status_lbl(status)
264
264
265 params = {
265 params = {
266 'new_status_label': new_status,
266 'new_status_label': new_status,
267 'pull_request': pull_request,
267 'pull_request': pull_request,
268 'commit_id': commit_id,
268 'commit_id': commit_id,
269 }
269 }
270 renderer = RstTemplateRenderer()
270 renderer = RstTemplateRenderer()
271 return renderer.render('auto_status_change.mako', **params)
271 return renderer.render('auto_status_change.mako', **params)
272
272
273 def set_status(self, repo, status, user, comment=None, revision=None,
273 def set_status(self, repo, status, user, comment=None, revision=None,
274 pull_request=None, dont_allow_on_closed_pull_request=False):
274 pull_request=None, dont_allow_on_closed_pull_request=False):
275 """
275 """
276 Creates new status for changeset or updates the old ones bumping their
276 Creates new status for changeset or updates the old ones bumping their
277 version, leaving the current status at
277 version, leaving the current status at
278
278
279 :param repo:
279 :param repo:
280 :param revision:
280 :param revision:
281 :param status:
281 :param status:
282 :param user:
282 :param user:
283 :param comment:
283 :param comment:
284 :param dont_allow_on_closed_pull_request: don't allow a status change
284 :param dont_allow_on_closed_pull_request: don't allow a status change
285 if last status was for pull request and it's closed. We shouldn't
285 if last status was for pull request and it's closed. We shouldn't
286 mess around this manually
286 mess around this manually
287 """
287 """
288 repo = self._get_repo(repo)
288 repo = self._get_repo(repo)
289
289
290 q = ChangesetStatus.query()
290 q = ChangesetStatus.query()
291
291
292 if revision:
292 if revision:
293 q = q.filter(ChangesetStatus.repo == repo)
293 q = q.filter(ChangesetStatus.repo == repo)
294 q = q.filter(ChangesetStatus.revision == revision)
294 q = q.filter(ChangesetStatus.revision == revision)
295 elif pull_request:
295 elif pull_request:
296 pull_request = self.__get_pull_request(pull_request)
296 pull_request = self.__get_pull_request(pull_request)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
297 q = q.filter(ChangesetStatus.repo == pull_request.source_repo)
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
298 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
299 cur_statuses = q.all()
299 cur_statuses = q.all()
300
300
301 # if statuses exists and last is associated with a closed pull request
301 # if statuses exists and last is associated with a closed pull request
302 # we need to check if we can allow this status change
302 # we need to check if we can allow this status change
303 if (dont_allow_on_closed_pull_request and cur_statuses
303 if (dont_allow_on_closed_pull_request and cur_statuses
304 and getattr(cur_statuses[0].pull_request, 'status', '')
304 and getattr(cur_statuses[0].pull_request, 'status', '')
305 == PullRequest.STATUS_CLOSED):
305 == PullRequest.STATUS_CLOSED):
306 raise StatusChangeOnClosedPullRequestError(
306 raise StatusChangeOnClosedPullRequestError(
307 'Changing status on closed pull request is not allowed'
307 'Changing status on closed pull request is not allowed'
308 )
308 )
309
309
310 # update all current statuses with older version
310 # update all current statuses with older version
311 if cur_statuses:
311 if cur_statuses:
312 for st in cur_statuses:
312 for st in cur_statuses:
313 st.version += 1
313 st.version += 1
314 Session().add(st)
314 Session().add(st)
315 Session().flush()
315 Session().flush()
316
316
317 def _create_status(user, repo, status, comment, revision, pull_request):
317 def _create_status(user, repo, status, comment, revision, pull_request):
318 new_status = ChangesetStatus()
318 new_status = ChangesetStatus()
319 new_status.author = self._get_user(user)
319 new_status.author = self._get_user(user)
320 new_status.repo = self._get_repo(repo)
320 new_status.repo = self._get_repo(repo)
321 new_status.status = status
321 new_status.status = status
322 new_status.comment = comment
322 new_status.comment = comment
323 new_status.revision = revision
323 new_status.revision = revision
324 new_status.pull_request = pull_request
324 new_status.pull_request = pull_request
325 return new_status
325 return new_status
326
326
327 if not comment:
327 if not comment:
328 from rhodecode.model.comment import CommentsModel
328 from rhodecode.model.comment import CommentsModel
329 comment = CommentsModel().create(
329 comment = CommentsModel().create(
330 text=self._render_auto_status_message(
330 text=self._render_auto_status_message(
331 status, commit_id=revision, pull_request=pull_request),
331 status, commit_id=revision, pull_request=pull_request),
332 repo=repo,
332 repo=repo,
333 user=user,
333 user=user,
334 pull_request=pull_request,
334 pull_request=pull_request,
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
335 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER
336 )
336 )
337
337
338 if revision:
338 if revision:
339 new_status = _create_status(
339 new_status = _create_status(
340 user=user, repo=repo, status=status, comment=comment,
340 user=user, repo=repo, status=status, comment=comment,
341 revision=revision, pull_request=pull_request)
341 revision=revision, pull_request=pull_request)
342 Session().add(new_status)
342 Session().add(new_status)
343 return new_status
343 return new_status
344 elif pull_request:
344 elif pull_request:
345 # pull request can have more than one revision associated to it
345 # pull request can have more than one revision associated to it
346 # we need to create new version for each one
346 # we need to create new version for each one
347 new_statuses = []
347 new_statuses = []
348 repo = pull_request.source_repo
348 repo = pull_request.source_repo
349 for rev in pull_request.revisions:
349 for rev in pull_request.revisions:
350 new_status = _create_status(
350 new_status = _create_status(
351 user=user, repo=repo, status=status, comment=comment,
351 user=user, repo=repo, status=status, comment=comment,
352 revision=rev, pull_request=pull_request)
352 revision=rev, pull_request=pull_request)
353 new_statuses.append(new_status)
353 new_statuses.append(new_status)
354 Session().add(new_status)
354 Session().add(new_status)
355 return new_statuses
355 return new_statuses
356
356
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data):
357 def aggregate_votes_by_user(self, commit_statuses, reviewers_data, user=None):
358
358
359 commit_statuses_map = collections.defaultdict(list)
359 commit_statuses_map = collections.defaultdict(list)
360 for st in commit_statuses:
360 for st in commit_statuses:
361 commit_statuses_map[st.author.username] += [st]
361 commit_statuses_map[st.author.username] += [st]
362
362
363 reviewers = []
363 reviewers = []
364
364
365 def version(commit_status):
365 def version(commit_status):
366 return commit_status.version
366 return commit_status.version
367
367
368 for obj in reviewers_data:
368 for obj in reviewers_data:
369 if not obj.user:
369 if not obj.user:
370 continue
370 continue
371 if user and obj.user.username != user.username:
372 # single user filter
373 continue
374
371 statuses = commit_statuses_map.get(obj.user.username, None)
375 statuses = commit_statuses_map.get(obj.user.username, None)
372 if statuses:
376 if statuses:
373 status_groups = itertools.groupby(
377 status_groups = itertools.groupby(
374 sorted(statuses, key=version), version)
378 sorted(statuses, key=version), version)
375 statuses = [(x, list(y)[0]) for x, y in status_groups]
379 statuses = [(x, list(y)[0]) for x, y in status_groups]
376
380
377 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
381 reviewers.append((obj, obj.user, obj.reasons, obj.mandatory, statuses))
378
382
383 if user:
384 return reviewers[0] if reviewers else reviewers
385 else:
379 return reviewers
386 return reviewers
380
387
381 def reviewers_statuses(self, pull_request):
388 def reviewers_statuses(self, pull_request, user=None):
382 _commit_statuses = self.get_statuses(
389 _commit_statuses = self.get_statuses(
383 pull_request.source_repo,
390 pull_request.source_repo,
384 pull_request=pull_request,
391 pull_request=pull_request,
385 with_revisions=True)
392 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
393 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
394 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
395 return self.aggregate_votes_by_user(_commit_statuses, reviewers, user=user)
389
396
390 def calculated_review_status(self, pull_request):
397 def calculated_review_status(self, pull_request):
391 """
398 """
392 calculate pull request status based on reviewers, it should be a list
399 calculate pull request status based on reviewers, it should be a list
393 of two element lists.
400 of two element lists.
394 """
401 """
395 reviewers = self.reviewers_statuses(pull_request)
402 reviewers = self.reviewers_statuses(pull_request)
396 return self.calculate_status(reviewers)
403 return self.calculate_status(reviewers)
@@ -1,5836 +1,5836 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case, null
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, lazyload, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
66 JsonRaw
66 JsonRaw
67 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.ext_json import json
68 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.caching_query import FromCache
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
70 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.encrypt2 import Encryptor
71 from rhodecode.lib.exceptions import (
71 from rhodecode.lib.exceptions import (
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 from rhodecode.model.meta import Base, Session
73 from rhodecode.model.meta import Base, Session
74
74
75 URL_SEP = '/'
75 URL_SEP = '/'
76 log = logging.getLogger(__name__)
76 log = logging.getLogger(__name__)
77
77
78 # =============================================================================
78 # =============================================================================
79 # BASE CLASSES
79 # BASE CLASSES
80 # =============================================================================
80 # =============================================================================
81
81
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # beaker.session.secret if first is not set.
83 # beaker.session.secret if first is not set.
84 # and initialized at environment.py
84 # and initialized at environment.py
85 ENCRYPTION_KEY = None
85 ENCRYPTION_KEY = None
86
86
87 # used to sort permissions by types, '#' used here is not allowed to be in
87 # used to sort permissions by types, '#' used here is not allowed to be in
88 # usernames, and it's very early in sorted string.printable table.
88 # usernames, and it's very early in sorted string.printable table.
89 PERMISSION_TYPE_SORT = {
89 PERMISSION_TYPE_SORT = {
90 'admin': '####',
90 'admin': '####',
91 'write': '###',
91 'write': '###',
92 'read': '##',
92 'read': '##',
93 'none': '#',
93 'none': '#',
94 }
94 }
95
95
96
96
97 def display_user_sort(obj):
97 def display_user_sort(obj):
98 """
98 """
99 Sort function used to sort permissions in .permissions() function of
99 Sort function used to sort permissions in .permissions() function of
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 of all other resources
101 of all other resources
102 """
102 """
103
103
104 if obj.username == User.DEFAULT_USER:
104 if obj.username == User.DEFAULT_USER:
105 return '#####'
105 return '#####'
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 extra_sort_num = '1' # default
107 extra_sort_num = '1' # default
108
108
109 # NOTE(dan): inactive duplicates goes last
109 # NOTE(dan): inactive duplicates goes last
110 if getattr(obj, 'duplicate_perm', None):
110 if getattr(obj, 'duplicate_perm', None):
111 extra_sort_num = '9'
111 extra_sort_num = '9'
112 return prefix + extra_sort_num + obj.username
112 return prefix + extra_sort_num + obj.username
113
113
114
114
115 def display_user_group_sort(obj):
115 def display_user_group_sort(obj):
116 """
116 """
117 Sort function used to sort permissions in .permissions() function of
117 Sort function used to sort permissions in .permissions() function of
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 of all other resources
119 of all other resources
120 """
120 """
121
121
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 return prefix + obj.users_group_name
123 return prefix + obj.users_group_name
124
124
125
125
126 def _hash_key(k):
126 def _hash_key(k):
127 return sha1_safe(k)
127 return sha1_safe(k)
128
128
129
129
130 def in_filter_generator(qry, items, limit=500):
130 def in_filter_generator(qry, items, limit=500):
131 """
131 """
132 Splits IN() into multiple with OR
132 Splits IN() into multiple with OR
133 e.g.::
133 e.g.::
134 cnt = Repository.query().filter(
134 cnt = Repository.query().filter(
135 or_(
135 or_(
136 *in_filter_generator(Repository.repo_id, range(100000))
136 *in_filter_generator(Repository.repo_id, range(100000))
137 )).count()
137 )).count()
138 """
138 """
139 if not items:
139 if not items:
140 # empty list will cause empty query which might cause security issues
140 # empty list will cause empty query which might cause security issues
141 # this can lead to hidden unpleasant results
141 # this can lead to hidden unpleasant results
142 items = [-1]
142 items = [-1]
143
143
144 parts = []
144 parts = []
145 for chunk in xrange(0, len(items), limit):
145 for chunk in xrange(0, len(items), limit):
146 parts.append(
146 parts.append(
147 qry.in_(items[chunk: chunk + limit])
147 qry.in_(items[chunk: chunk + limit])
148 )
148 )
149
149
150 return parts
150 return parts
151
151
152
152
153 base_table_args = {
153 base_table_args = {
154 'extend_existing': True,
154 'extend_existing': True,
155 'mysql_engine': 'InnoDB',
155 'mysql_engine': 'InnoDB',
156 'mysql_charset': 'utf8',
156 'mysql_charset': 'utf8',
157 'sqlite_autoincrement': True
157 'sqlite_autoincrement': True
158 }
158 }
159
159
160
160
161 class EncryptedTextValue(TypeDecorator):
161 class EncryptedTextValue(TypeDecorator):
162 """
162 """
163 Special column for encrypted long text data, use like::
163 Special column for encrypted long text data, use like::
164
164
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166
166
167 This column is intelligent so if value is in unencrypted form it return
167 This column is intelligent so if value is in unencrypted form it return
168 unencrypted form, but on save it always encrypts
168 unencrypted form, but on save it always encrypts
169 """
169 """
170 impl = Text
170 impl = Text
171
171
172 def process_bind_param(self, value, dialect):
172 def process_bind_param(self, value, dialect):
173 """
173 """
174 Setter for storing value
174 Setter for storing value
175 """
175 """
176 import rhodecode
176 import rhodecode
177 if not value:
177 if not value:
178 return value
178 return value
179
179
180 # protect against double encrypting if values is already encrypted
180 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
181 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
183 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
184 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
185 'ie. not starting with enc$ or enc2$')
186
186
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 if algo == 'aes':
188 if algo == 'aes':
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
190 elif algo == 'fernet':
190 elif algo == 'fernet':
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
192 else:
192 else:
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
194
194
195 def process_result_value(self, value, dialect):
195 def process_result_value(self, value, dialect):
196 """
196 """
197 Getter for retrieving value
197 Getter for retrieving value
198 """
198 """
199
199
200 import rhodecode
200 import rhodecode
201 if not value:
201 if not value:
202 return value
202 return value
203
203
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
206 if algo == 'aes':
206 if algo == 'aes':
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
208 elif algo == 'fernet':
208 elif algo == 'fernet':
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
210 else:
210 else:
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
212 return decrypted_data
212 return decrypted_data
213
213
214
214
215 class BaseModel(object):
215 class BaseModel(object):
216 """
216 """
217 Base Model for all classes
217 Base Model for all classes
218 """
218 """
219
219
220 @classmethod
220 @classmethod
221 def _get_keys(cls):
221 def _get_keys(cls):
222 """return column names for this model """
222 """return column names for this model """
223 return class_mapper(cls).c.keys()
223 return class_mapper(cls).c.keys()
224
224
225 def get_dict(self):
225 def get_dict(self):
226 """
226 """
227 return dict with keys and values corresponding
227 return dict with keys and values corresponding
228 to this model data """
228 to this model data """
229
229
230 d = {}
230 d = {}
231 for k in self._get_keys():
231 for k in self._get_keys():
232 d[k] = getattr(self, k)
232 d[k] = getattr(self, k)
233
233
234 # also use __json__() if present to get additional fields
234 # also use __json__() if present to get additional fields
235 _json_attr = getattr(self, '__json__', None)
235 _json_attr = getattr(self, '__json__', None)
236 if _json_attr:
236 if _json_attr:
237 # update with attributes from __json__
237 # update with attributes from __json__
238 if callable(_json_attr):
238 if callable(_json_attr):
239 _json_attr = _json_attr()
239 _json_attr = _json_attr()
240 for k, val in _json_attr.iteritems():
240 for k, val in _json_attr.iteritems():
241 d[k] = val
241 d[k] = val
242 return d
242 return d
243
243
244 def get_appstruct(self):
244 def get_appstruct(self):
245 """return list with keys and values tuples corresponding
245 """return list with keys and values tuples corresponding
246 to this model data """
246 to this model data """
247
247
248 lst = []
248 lst = []
249 for k in self._get_keys():
249 for k in self._get_keys():
250 lst.append((k, getattr(self, k),))
250 lst.append((k, getattr(self, k),))
251 return lst
251 return lst
252
252
253 def populate_obj(self, populate_dict):
253 def populate_obj(self, populate_dict):
254 """populate model with data from given populate_dict"""
254 """populate model with data from given populate_dict"""
255
255
256 for k in self._get_keys():
256 for k in self._get_keys():
257 if k in populate_dict:
257 if k in populate_dict:
258 setattr(self, k, populate_dict[k])
258 setattr(self, k, populate_dict[k])
259
259
260 @classmethod
260 @classmethod
261 def query(cls):
261 def query(cls):
262 return Session().query(cls)
262 return Session().query(cls)
263
263
264 @classmethod
264 @classmethod
265 def get(cls, id_):
265 def get(cls, id_):
266 if id_:
266 if id_:
267 return cls.query().get(id_)
267 return cls.query().get(id_)
268
268
269 @classmethod
269 @classmethod
270 def get_or_404(cls, id_):
270 def get_or_404(cls, id_):
271 from pyramid.httpexceptions import HTTPNotFound
271 from pyramid.httpexceptions import HTTPNotFound
272
272
273 try:
273 try:
274 id_ = int(id_)
274 id_ = int(id_)
275 except (TypeError, ValueError):
275 except (TypeError, ValueError):
276 raise HTTPNotFound()
276 raise HTTPNotFound()
277
277
278 res = cls.query().get(id_)
278 res = cls.query().get(id_)
279 if not res:
279 if not res:
280 raise HTTPNotFound()
280 raise HTTPNotFound()
281 return res
281 return res
282
282
283 @classmethod
283 @classmethod
284 def getAll(cls):
284 def getAll(cls):
285 # deprecated and left for backward compatibility
285 # deprecated and left for backward compatibility
286 return cls.get_all()
286 return cls.get_all()
287
287
288 @classmethod
288 @classmethod
289 def get_all(cls):
289 def get_all(cls):
290 return cls.query().all()
290 return cls.query().all()
291
291
292 @classmethod
292 @classmethod
293 def delete(cls, id_):
293 def delete(cls, id_):
294 obj = cls.query().get(id_)
294 obj = cls.query().get(id_)
295 Session().delete(obj)
295 Session().delete(obj)
296
296
297 @classmethod
297 @classmethod
298 def identity_cache(cls, session, attr_name, value):
298 def identity_cache(cls, session, attr_name, value):
299 exist_in_session = []
299 exist_in_session = []
300 for (item_cls, pkey), instance in session.identity_map.items():
300 for (item_cls, pkey), instance in session.identity_map.items():
301 if cls == item_cls and getattr(instance, attr_name) == value:
301 if cls == item_cls and getattr(instance, attr_name) == value:
302 exist_in_session.append(instance)
302 exist_in_session.append(instance)
303 if exist_in_session:
303 if exist_in_session:
304 if len(exist_in_session) == 1:
304 if len(exist_in_session) == 1:
305 return exist_in_session[0]
305 return exist_in_session[0]
306 log.exception(
306 log.exception(
307 'multiple objects with attr %s and '
307 'multiple objects with attr %s and '
308 'value %s found with same name: %r',
308 'value %s found with same name: %r',
309 attr_name, value, exist_in_session)
309 attr_name, value, exist_in_session)
310
310
311 def __repr__(self):
311 def __repr__(self):
312 if hasattr(self, '__unicode__'):
312 if hasattr(self, '__unicode__'):
313 # python repr needs to return str
313 # python repr needs to return str
314 try:
314 try:
315 return safe_str(self.__unicode__())
315 return safe_str(self.__unicode__())
316 except UnicodeDecodeError:
316 except UnicodeDecodeError:
317 pass
317 pass
318 return '<DB:%s>' % (self.__class__.__name__)
318 return '<DB:%s>' % (self.__class__.__name__)
319
319
320
320
321 class RhodeCodeSetting(Base, BaseModel):
321 class RhodeCodeSetting(Base, BaseModel):
322 __tablename__ = 'rhodecode_settings'
322 __tablename__ = 'rhodecode_settings'
323 __table_args__ = (
323 __table_args__ = (
324 UniqueConstraint('app_settings_name'),
324 UniqueConstraint('app_settings_name'),
325 base_table_args
325 base_table_args
326 )
326 )
327
327
328 SETTINGS_TYPES = {
328 SETTINGS_TYPES = {
329 'str': safe_str,
329 'str': safe_str,
330 'int': safe_int,
330 'int': safe_int,
331 'unicode': safe_unicode,
331 'unicode': safe_unicode,
332 'bool': str2bool,
332 'bool': str2bool,
333 'list': functools.partial(aslist, sep=',')
333 'list': functools.partial(aslist, sep=',')
334 }
334 }
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
336 GLOBAL_CONF_KEY = 'app_settings'
336 GLOBAL_CONF_KEY = 'app_settings'
337
337
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
342
342
343 def __init__(self, key='', val='', type='unicode'):
343 def __init__(self, key='', val='', type='unicode'):
344 self.app_settings_name = key
344 self.app_settings_name = key
345 self.app_settings_type = type
345 self.app_settings_type = type
346 self.app_settings_value = val
346 self.app_settings_value = val
347
347
348 @validates('_app_settings_value')
348 @validates('_app_settings_value')
349 def validate_settings_value(self, key, val):
349 def validate_settings_value(self, key, val):
350 assert type(val) == unicode
350 assert type(val) == unicode
351 return val
351 return val
352
352
353 @hybrid_property
353 @hybrid_property
354 def app_settings_value(self):
354 def app_settings_value(self):
355 v = self._app_settings_value
355 v = self._app_settings_value
356 _type = self.app_settings_type
356 _type = self.app_settings_type
357 if _type:
357 if _type:
358 _type = self.app_settings_type.split('.')[0]
358 _type = self.app_settings_type.split('.')[0]
359 # decode the encrypted value
359 # decode the encrypted value
360 if 'encrypted' in self.app_settings_type:
360 if 'encrypted' in self.app_settings_type:
361 cipher = EncryptedTextValue()
361 cipher = EncryptedTextValue()
362 v = safe_unicode(cipher.process_result_value(v, None))
362 v = safe_unicode(cipher.process_result_value(v, None))
363
363
364 converter = self.SETTINGS_TYPES.get(_type) or \
364 converter = self.SETTINGS_TYPES.get(_type) or \
365 self.SETTINGS_TYPES['unicode']
365 self.SETTINGS_TYPES['unicode']
366 return converter(v)
366 return converter(v)
367
367
368 @app_settings_value.setter
368 @app_settings_value.setter
369 def app_settings_value(self, val):
369 def app_settings_value(self, val):
370 """
370 """
371 Setter that will always make sure we use unicode in app_settings_value
371 Setter that will always make sure we use unicode in app_settings_value
372
372
373 :param val:
373 :param val:
374 """
374 """
375 val = safe_unicode(val)
375 val = safe_unicode(val)
376 # encode the encrypted value
376 # encode the encrypted value
377 if 'encrypted' in self.app_settings_type:
377 if 'encrypted' in self.app_settings_type:
378 cipher = EncryptedTextValue()
378 cipher = EncryptedTextValue()
379 val = safe_unicode(cipher.process_bind_param(val, None))
379 val = safe_unicode(cipher.process_bind_param(val, None))
380 self._app_settings_value = val
380 self._app_settings_value = val
381
381
382 @hybrid_property
382 @hybrid_property
383 def app_settings_type(self):
383 def app_settings_type(self):
384 return self._app_settings_type
384 return self._app_settings_type
385
385
386 @app_settings_type.setter
386 @app_settings_type.setter
387 def app_settings_type(self, val):
387 def app_settings_type(self, val):
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
389 raise Exception('type must be one of %s got %s'
389 raise Exception('type must be one of %s got %s'
390 % (self.SETTINGS_TYPES.keys(), val))
390 % (self.SETTINGS_TYPES.keys(), val))
391 self._app_settings_type = val
391 self._app_settings_type = val
392
392
393 @classmethod
393 @classmethod
394 def get_by_prefix(cls, prefix):
394 def get_by_prefix(cls, prefix):
395 return RhodeCodeSetting.query()\
395 return RhodeCodeSetting.query()\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
397 .all()
397 .all()
398
398
399 def __unicode__(self):
399 def __unicode__(self):
400 return u"<%s('%s:%s[%s]')>" % (
400 return u"<%s('%s:%s[%s]')>" % (
401 self.__class__.__name__,
401 self.__class__.__name__,
402 self.app_settings_name, self.app_settings_value,
402 self.app_settings_name, self.app_settings_value,
403 self.app_settings_type
403 self.app_settings_type
404 )
404 )
405
405
406
406
407 class RhodeCodeUi(Base, BaseModel):
407 class RhodeCodeUi(Base, BaseModel):
408 __tablename__ = 'rhodecode_ui'
408 __tablename__ = 'rhodecode_ui'
409 __table_args__ = (
409 __table_args__ = (
410 UniqueConstraint('ui_key'),
410 UniqueConstraint('ui_key'),
411 base_table_args
411 base_table_args
412 )
412 )
413
413
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
415 # HG
415 # HG
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
417 HOOK_PULL = 'outgoing.pull_logger'
417 HOOK_PULL = 'outgoing.pull_logger'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
420 HOOK_PUSH = 'changegroup.push_logger'
420 HOOK_PUSH = 'changegroup.push_logger'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
422
422
423 HOOKS_BUILTIN = [
423 HOOKS_BUILTIN = [
424 HOOK_PRE_PULL,
424 HOOK_PRE_PULL,
425 HOOK_PULL,
425 HOOK_PULL,
426 HOOK_PRE_PUSH,
426 HOOK_PRE_PUSH,
427 HOOK_PRETX_PUSH,
427 HOOK_PRETX_PUSH,
428 HOOK_PUSH,
428 HOOK_PUSH,
429 HOOK_PUSH_KEY,
429 HOOK_PUSH_KEY,
430 ]
430 ]
431
431
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
433 # git part is currently hardcoded.
433 # git part is currently hardcoded.
434
434
435 # SVN PATTERNS
435 # SVN PATTERNS
436 SVN_BRANCH_ID = 'vcs_svn_branch'
436 SVN_BRANCH_ID = 'vcs_svn_branch'
437 SVN_TAG_ID = 'vcs_svn_tag'
437 SVN_TAG_ID = 'vcs_svn_tag'
438
438
439 ui_id = Column(
439 ui_id = Column(
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
441 primary_key=True)
441 primary_key=True)
442 ui_section = Column(
442 ui_section = Column(
443 "ui_section", String(255), nullable=True, unique=None, default=None)
443 "ui_section", String(255), nullable=True, unique=None, default=None)
444 ui_key = Column(
444 ui_key = Column(
445 "ui_key", String(255), nullable=True, unique=None, default=None)
445 "ui_key", String(255), nullable=True, unique=None, default=None)
446 ui_value = Column(
446 ui_value = Column(
447 "ui_value", String(255), nullable=True, unique=None, default=None)
447 "ui_value", String(255), nullable=True, unique=None, default=None)
448 ui_active = Column(
448 ui_active = Column(
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
450
450
451 def __repr__(self):
451 def __repr__(self):
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
453 self.ui_key, self.ui_value)
453 self.ui_key, self.ui_value)
454
454
455
455
456 class RepoRhodeCodeSetting(Base, BaseModel):
456 class RepoRhodeCodeSetting(Base, BaseModel):
457 __tablename__ = 'repo_rhodecode_settings'
457 __tablename__ = 'repo_rhodecode_settings'
458 __table_args__ = (
458 __table_args__ = (
459 UniqueConstraint(
459 UniqueConstraint(
460 'app_settings_name', 'repository_id',
460 'app_settings_name', 'repository_id',
461 name='uq_repo_rhodecode_setting_name_repo_id'),
461 name='uq_repo_rhodecode_setting_name_repo_id'),
462 base_table_args
462 base_table_args
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 app_settings_id = Column(
468 app_settings_id = Column(
469 "app_settings_id", Integer(), nullable=False, unique=True,
469 "app_settings_id", Integer(), nullable=False, unique=True,
470 default=None, primary_key=True)
470 default=None, primary_key=True)
471 app_settings_name = Column(
471 app_settings_name = Column(
472 "app_settings_name", String(255), nullable=True, unique=None,
472 "app_settings_name", String(255), nullable=True, unique=None,
473 default=None)
473 default=None)
474 _app_settings_value = Column(
474 _app_settings_value = Column(
475 "app_settings_value", String(4096), nullable=True, unique=None,
475 "app_settings_value", String(4096), nullable=True, unique=None,
476 default=None)
476 default=None)
477 _app_settings_type = Column(
477 _app_settings_type = Column(
478 "app_settings_type", String(255), nullable=True, unique=None,
478 "app_settings_type", String(255), nullable=True, unique=None,
479 default=None)
479 default=None)
480
480
481 repository = relationship('Repository')
481 repository = relationship('Repository')
482
482
483 def __init__(self, repository_id, key='', val='', type='unicode'):
483 def __init__(self, repository_id, key='', val='', type='unicode'):
484 self.repository_id = repository_id
484 self.repository_id = repository_id
485 self.app_settings_name = key
485 self.app_settings_name = key
486 self.app_settings_type = type
486 self.app_settings_type = type
487 self.app_settings_value = val
487 self.app_settings_value = val
488
488
489 @validates('_app_settings_value')
489 @validates('_app_settings_value')
490 def validate_settings_value(self, key, val):
490 def validate_settings_value(self, key, val):
491 assert type(val) == unicode
491 assert type(val) == unicode
492 return val
492 return val
493
493
494 @hybrid_property
494 @hybrid_property
495 def app_settings_value(self):
495 def app_settings_value(self):
496 v = self._app_settings_value
496 v = self._app_settings_value
497 type_ = self.app_settings_type
497 type_ = self.app_settings_type
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
500 return converter(v)
500 return converter(v)
501
501
502 @app_settings_value.setter
502 @app_settings_value.setter
503 def app_settings_value(self, val):
503 def app_settings_value(self, val):
504 """
504 """
505 Setter that will always make sure we use unicode in app_settings_value
505 Setter that will always make sure we use unicode in app_settings_value
506
506
507 :param val:
507 :param val:
508 """
508 """
509 self._app_settings_value = safe_unicode(val)
509 self._app_settings_value = safe_unicode(val)
510
510
511 @hybrid_property
511 @hybrid_property
512 def app_settings_type(self):
512 def app_settings_type(self):
513 return self._app_settings_type
513 return self._app_settings_type
514
514
515 @app_settings_type.setter
515 @app_settings_type.setter
516 def app_settings_type(self, val):
516 def app_settings_type(self, val):
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 if val not in SETTINGS_TYPES:
518 if val not in SETTINGS_TYPES:
519 raise Exception('type must be one of %s got %s'
519 raise Exception('type must be one of %s got %s'
520 % (SETTINGS_TYPES.keys(), val))
520 % (SETTINGS_TYPES.keys(), val))
521 self._app_settings_type = val
521 self._app_settings_type = val
522
522
523 def __unicode__(self):
523 def __unicode__(self):
524 return u"<%s('%s:%s:%s[%s]')>" % (
524 return u"<%s('%s:%s:%s[%s]')>" % (
525 self.__class__.__name__, self.repository.repo_name,
525 self.__class__.__name__, self.repository.repo_name,
526 self.app_settings_name, self.app_settings_value,
526 self.app_settings_name, self.app_settings_value,
527 self.app_settings_type
527 self.app_settings_type
528 )
528 )
529
529
530
530
531 class RepoRhodeCodeUi(Base, BaseModel):
531 class RepoRhodeCodeUi(Base, BaseModel):
532 __tablename__ = 'repo_rhodecode_ui'
532 __tablename__ = 'repo_rhodecode_ui'
533 __table_args__ = (
533 __table_args__ = (
534 UniqueConstraint(
534 UniqueConstraint(
535 'repository_id', 'ui_section', 'ui_key',
535 'repository_id', 'ui_section', 'ui_key',
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
537 base_table_args
537 base_table_args
538 )
538 )
539
539
540 repository_id = Column(
540 repository_id = Column(
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
542 nullable=False)
542 nullable=False)
543 ui_id = Column(
543 ui_id = Column(
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
545 primary_key=True)
545 primary_key=True)
546 ui_section = Column(
546 ui_section = Column(
547 "ui_section", String(255), nullable=True, unique=None, default=None)
547 "ui_section", String(255), nullable=True, unique=None, default=None)
548 ui_key = Column(
548 ui_key = Column(
549 "ui_key", String(255), nullable=True, unique=None, default=None)
549 "ui_key", String(255), nullable=True, unique=None, default=None)
550 ui_value = Column(
550 ui_value = Column(
551 "ui_value", String(255), nullable=True, unique=None, default=None)
551 "ui_value", String(255), nullable=True, unique=None, default=None)
552 ui_active = Column(
552 ui_active = Column(
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
554
554
555 repository = relationship('Repository')
555 repository = relationship('Repository')
556
556
557 def __repr__(self):
557 def __repr__(self):
558 return '<%s[%s:%s]%s=>%s]>' % (
558 return '<%s[%s:%s]%s=>%s]>' % (
559 self.__class__.__name__, self.repository.repo_name,
559 self.__class__.__name__, self.repository.repo_name,
560 self.ui_section, self.ui_key, self.ui_value)
560 self.ui_section, self.ui_key, self.ui_value)
561
561
562
562
563 class User(Base, BaseModel):
563 class User(Base, BaseModel):
564 __tablename__ = 'users'
564 __tablename__ = 'users'
565 __table_args__ = (
565 __table_args__ = (
566 UniqueConstraint('username'), UniqueConstraint('email'),
566 UniqueConstraint('username'), UniqueConstraint('email'),
567 Index('u_username_idx', 'username'),
567 Index('u_username_idx', 'username'),
568 Index('u_email_idx', 'email'),
568 Index('u_email_idx', 'email'),
569 base_table_args
569 base_table_args
570 )
570 )
571
571
572 DEFAULT_USER = 'default'
572 DEFAULT_USER = 'default'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
575
575
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
587
587
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
594
594
595 user_log = relationship('UserLog')
595 user_log = relationship('UserLog')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
597
597
598 repositories = relationship('Repository')
598 repositories = relationship('Repository')
599 repository_groups = relationship('RepoGroup')
599 repository_groups = relationship('RepoGroup')
600 user_groups = relationship('UserGroup')
600 user_groups = relationship('UserGroup')
601
601
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
604
604
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
608
608
609 group_member = relationship('UserGroupMember', cascade='all')
609 group_member = relationship('UserGroupMember', cascade='all')
610
610
611 notifications = relationship('UserNotification', cascade='all')
611 notifications = relationship('UserNotification', cascade='all')
612 # notifications assigned to this user
612 # notifications assigned to this user
613 user_created_notifications = relationship('Notification', cascade='all')
613 user_created_notifications = relationship('Notification', cascade='all')
614 # comments created by this user
614 # comments created by this user
615 user_comments = relationship('ChangesetComment', cascade='all')
615 user_comments = relationship('ChangesetComment', cascade='all')
616 # user profile extra info
616 # user profile extra info
617 user_emails = relationship('UserEmailMap', cascade='all')
617 user_emails = relationship('UserEmailMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
621
621
622 # gists
622 # gists
623 user_gists = relationship('Gist', cascade='all')
623 user_gists = relationship('Gist', cascade='all')
624 # user pull requests
624 # user pull requests
625 user_pull_requests = relationship('PullRequest', cascade='all')
625 user_pull_requests = relationship('PullRequest', cascade='all')
626
626
627 # external identities
627 # external identities
628 external_identities = relationship(
628 external_identities = relationship(
629 'ExternalIdentity',
629 'ExternalIdentity',
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
631 cascade='all')
631 cascade='all')
632 # review rules
632 # review rules
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
634
634
635 # artifacts owned
635 # artifacts owned
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
637
637
638 # no cascade, set NULL
638 # no cascade, set NULL
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
640
640
641 def __unicode__(self):
641 def __unicode__(self):
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
643 self.user_id, self.username)
643 self.user_id, self.username)
644
644
645 @hybrid_property
645 @hybrid_property
646 def email(self):
646 def email(self):
647 return self._email
647 return self._email
648
648
649 @email.setter
649 @email.setter
650 def email(self, val):
650 def email(self, val):
651 self._email = val.lower() if val else None
651 self._email = val.lower() if val else None
652
652
653 @hybrid_property
653 @hybrid_property
654 def first_name(self):
654 def first_name(self):
655 from rhodecode.lib import helpers as h
655 from rhodecode.lib import helpers as h
656 if self.name:
656 if self.name:
657 return h.escape(self.name)
657 return h.escape(self.name)
658 return self.name
658 return self.name
659
659
660 @hybrid_property
660 @hybrid_property
661 def last_name(self):
661 def last_name(self):
662 from rhodecode.lib import helpers as h
662 from rhodecode.lib import helpers as h
663 if self.lastname:
663 if self.lastname:
664 return h.escape(self.lastname)
664 return h.escape(self.lastname)
665 return self.lastname
665 return self.lastname
666
666
667 @hybrid_property
667 @hybrid_property
668 def api_key(self):
668 def api_key(self):
669 """
669 """
670 Fetch if exist an auth-token with role ALL connected to this user
670 Fetch if exist an auth-token with role ALL connected to this user
671 """
671 """
672 user_auth_token = UserApiKeys.query()\
672 user_auth_token = UserApiKeys.query()\
673 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(UserApiKeys.user_id == self.user_id)\
674 .filter(or_(UserApiKeys.expires == -1,
674 .filter(or_(UserApiKeys.expires == -1,
675 UserApiKeys.expires >= time.time()))\
675 UserApiKeys.expires >= time.time()))\
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
677 if user_auth_token:
677 if user_auth_token:
678 user_auth_token = user_auth_token.api_key
678 user_auth_token = user_auth_token.api_key
679
679
680 return user_auth_token
680 return user_auth_token
681
681
682 @api_key.setter
682 @api_key.setter
683 def api_key(self, val):
683 def api_key(self, val):
684 # don't allow to set API key this is deprecated for now
684 # don't allow to set API key this is deprecated for now
685 self._api_key = None
685 self._api_key = None
686
686
687 @property
687 @property
688 def reviewer_pull_requests(self):
688 def reviewer_pull_requests(self):
689 return PullRequestReviewers.query() \
689 return PullRequestReviewers.query() \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
692 .all()
692 .all()
693
693
694 @property
694 @property
695 def firstname(self):
695 def firstname(self):
696 # alias for future
696 # alias for future
697 return self.name
697 return self.name
698
698
699 @property
699 @property
700 def emails(self):
700 def emails(self):
701 other = UserEmailMap.query()\
701 other = UserEmailMap.query()\
702 .filter(UserEmailMap.user == self) \
702 .filter(UserEmailMap.user == self) \
703 .order_by(UserEmailMap.email_id.asc()) \
703 .order_by(UserEmailMap.email_id.asc()) \
704 .all()
704 .all()
705 return [self.email] + [x.email for x in other]
705 return [self.email] + [x.email for x in other]
706
706
707 def emails_cached(self):
707 def emails_cached(self):
708 emails = UserEmailMap.query()\
708 emails = UserEmailMap.query()\
709 .filter(UserEmailMap.user == self) \
709 .filter(UserEmailMap.user == self) \
710 .order_by(UserEmailMap.email_id.asc())
710 .order_by(UserEmailMap.email_id.asc())
711
711
712 emails = emails.options(
712 emails = emails.options(
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
714 )
714 )
715
715
716 return [self.email] + [x.email for x in emails]
716 return [self.email] + [x.email for x in emails]
717
717
718 @property
718 @property
719 def auth_tokens(self):
719 def auth_tokens(self):
720 auth_tokens = self.get_auth_tokens()
720 auth_tokens = self.get_auth_tokens()
721 return [x.api_key for x in auth_tokens]
721 return [x.api_key for x in auth_tokens]
722
722
723 def get_auth_tokens(self):
723 def get_auth_tokens(self):
724 return UserApiKeys.query()\
724 return UserApiKeys.query()\
725 .filter(UserApiKeys.user == self)\
725 .filter(UserApiKeys.user == self)\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
727 .all()
727 .all()
728
728
729 @LazyProperty
729 @LazyProperty
730 def feed_token(self):
730 def feed_token(self):
731 return self.get_feed_token()
731 return self.get_feed_token()
732
732
733 def get_feed_token(self, cache=True):
733 def get_feed_token(self, cache=True):
734 feed_tokens = UserApiKeys.query()\
734 feed_tokens = UserApiKeys.query()\
735 .filter(UserApiKeys.user == self)\
735 .filter(UserApiKeys.user == self)\
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
737 if cache:
737 if cache:
738 feed_tokens = feed_tokens.options(
738 feed_tokens = feed_tokens.options(
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
740
740
741 feed_tokens = feed_tokens.all()
741 feed_tokens = feed_tokens.all()
742 if feed_tokens:
742 if feed_tokens:
743 return feed_tokens[0].api_key
743 return feed_tokens[0].api_key
744 return 'NO_FEED_TOKEN_AVAILABLE'
744 return 'NO_FEED_TOKEN_AVAILABLE'
745
745
746 @LazyProperty
746 @LazyProperty
747 def artifact_token(self):
747 def artifact_token(self):
748 return self.get_artifact_token()
748 return self.get_artifact_token()
749
749
750 def get_artifact_token(self, cache=True):
750 def get_artifact_token(self, cache=True):
751 artifacts_tokens = UserApiKeys.query()\
751 artifacts_tokens = UserApiKeys.query()\
752 .filter(UserApiKeys.user == self) \
752 .filter(UserApiKeys.user == self) \
753 .filter(or_(UserApiKeys.expires == -1,
753 .filter(or_(UserApiKeys.expires == -1,
754 UserApiKeys.expires >= time.time())) \
754 UserApiKeys.expires >= time.time())) \
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
756
756
757 if cache:
757 if cache:
758 artifacts_tokens = artifacts_tokens.options(
758 artifacts_tokens = artifacts_tokens.options(
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
760
760
761 artifacts_tokens = artifacts_tokens.all()
761 artifacts_tokens = artifacts_tokens.all()
762 if artifacts_tokens:
762 if artifacts_tokens:
763 return artifacts_tokens[0].api_key
763 return artifacts_tokens[0].api_key
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
765
765
766 def get_or_create_artifact_token(self):
766 def get_or_create_artifact_token(self):
767 artifacts_tokens = UserApiKeys.query()\
767 artifacts_tokens = UserApiKeys.query()\
768 .filter(UserApiKeys.user == self) \
768 .filter(UserApiKeys.user == self) \
769 .filter(or_(UserApiKeys.expires == -1,
769 .filter(or_(UserApiKeys.expires == -1,
770 UserApiKeys.expires >= time.time())) \
770 UserApiKeys.expires >= time.time())) \
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772
772
773 artifacts_tokens = artifacts_tokens.all()
773 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
774 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
775 return artifacts_tokens[0].api_key
776 else:
776 else:
777 from rhodecode.model.auth_token import AuthTokenModel
777 from rhodecode.model.auth_token import AuthTokenModel
778 artifact_token = AuthTokenModel().create(
778 artifact_token = AuthTokenModel().create(
779 self, 'auto-generated-artifact-token',
779 self, 'auto-generated-artifact-token',
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
781 Session.commit()
781 Session.commit()
782 return artifact_token.api_key
782 return artifact_token.api_key
783
783
784 @classmethod
784 @classmethod
785 def get(cls, user_id, cache=False):
785 def get(cls, user_id, cache=False):
786 if not user_id:
786 if not user_id:
787 return
787 return
788
788
789 user = cls.query()
789 user = cls.query()
790 if cache:
790 if cache:
791 user = user.options(
791 user = user.options(
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
793 return user.get(user_id)
793 return user.get(user_id)
794
794
795 @classmethod
795 @classmethod
796 def extra_valid_auth_tokens(cls, user, role=None):
796 def extra_valid_auth_tokens(cls, user, role=None):
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
798 .filter(or_(UserApiKeys.expires == -1,
798 .filter(or_(UserApiKeys.expires == -1,
799 UserApiKeys.expires >= time.time()))
799 UserApiKeys.expires >= time.time()))
800 if role:
800 if role:
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
803 return tokens.all()
803 return tokens.all()
804
804
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
806 from rhodecode.lib import auth
806 from rhodecode.lib import auth
807
807
808 log.debug('Trying to authenticate user: %s via auth-token, '
808 log.debug('Trying to authenticate user: %s via auth-token, '
809 'and roles: %s', self, roles)
809 'and roles: %s', self, roles)
810
810
811 if not auth_token:
811 if not auth_token:
812 return False
812 return False
813
813
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
815 tokens_q = UserApiKeys.query()\
815 tokens_q = UserApiKeys.query()\
816 .filter(UserApiKeys.user_id == self.user_id)\
816 .filter(UserApiKeys.user_id == self.user_id)\
817 .filter(or_(UserApiKeys.expires == -1,
817 .filter(or_(UserApiKeys.expires == -1,
818 UserApiKeys.expires >= time.time()))
818 UserApiKeys.expires >= time.time()))
819
819
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
821
821
822 crypto_backend = auth.crypto_backend()
822 crypto_backend = auth.crypto_backend()
823 enc_token_map = {}
823 enc_token_map = {}
824 plain_token_map = {}
824 plain_token_map = {}
825 for token in tokens_q:
825 for token in tokens_q:
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
827 enc_token_map[token.api_key] = token
827 enc_token_map[token.api_key] = token
828 else:
828 else:
829 plain_token_map[token.api_key] = token
829 plain_token_map[token.api_key] = token
830 log.debug(
830 log.debug(
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
832 len(plain_token_map), len(enc_token_map))
832 len(plain_token_map), len(enc_token_map))
833
833
834 # plain token match comes first
834 # plain token match comes first
835 match = plain_token_map.get(auth_token)
835 match = plain_token_map.get(auth_token)
836
836
837 # check encrypted tokens now
837 # check encrypted tokens now
838 if not match:
838 if not match:
839 for token_hash, token in enc_token_map.items():
839 for token_hash, token in enc_token_map.items():
840 # NOTE(marcink): this is expensive to calculate, but most secure
840 # NOTE(marcink): this is expensive to calculate, but most secure
841 if crypto_backend.hash_check(auth_token, token_hash):
841 if crypto_backend.hash_check(auth_token, token_hash):
842 match = token
842 match = token
843 break
843 break
844
844
845 if match:
845 if match:
846 log.debug('Found matching token %s', match)
846 log.debug('Found matching token %s', match)
847 if match.repo_id:
847 if match.repo_id:
848 log.debug('Found scope, checking for scope match of token %s', match)
848 log.debug('Found scope, checking for scope match of token %s', match)
849 if match.repo_id == scope_repo_id:
849 if match.repo_id == scope_repo_id:
850 return True
850 return True
851 else:
851 else:
852 log.debug(
852 log.debug(
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
854 'and calling scope is:%s, skipping further checks',
854 'and calling scope is:%s, skipping further checks',
855 match.repo, scope_repo_id)
855 match.repo, scope_repo_id)
856 return False
856 return False
857 else:
857 else:
858 return True
858 return True
859
859
860 return False
860 return False
861
861
862 @property
862 @property
863 def ip_addresses(self):
863 def ip_addresses(self):
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
865 return [x.ip_addr for x in ret]
865 return [x.ip_addr for x in ret]
866
866
867 @property
867 @property
868 def username_and_name(self):
868 def username_and_name(self):
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
870
870
871 @property
871 @property
872 def username_or_name_or_email(self):
872 def username_or_name_or_email(self):
873 full_name = self.full_name if self.full_name is not ' ' else None
873 full_name = self.full_name if self.full_name is not ' ' else None
874 return self.username or full_name or self.email
874 return self.username or full_name or self.email
875
875
876 @property
876 @property
877 def full_name(self):
877 def full_name(self):
878 return '%s %s' % (self.first_name, self.last_name)
878 return '%s %s' % (self.first_name, self.last_name)
879
879
880 @property
880 @property
881 def full_name_or_username(self):
881 def full_name_or_username(self):
882 return ('%s %s' % (self.first_name, self.last_name)
882 return ('%s %s' % (self.first_name, self.last_name)
883 if (self.first_name and self.last_name) else self.username)
883 if (self.first_name and self.last_name) else self.username)
884
884
885 @property
885 @property
886 def full_contact(self):
886 def full_contact(self):
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
888
888
889 @property
889 @property
890 def short_contact(self):
890 def short_contact(self):
891 return '%s %s' % (self.first_name, self.last_name)
891 return '%s %s' % (self.first_name, self.last_name)
892
892
893 @property
893 @property
894 def is_admin(self):
894 def is_admin(self):
895 return self.admin
895 return self.admin
896
896
897 @property
897 @property
898 def language(self):
898 def language(self):
899 return self.user_data.get('language')
899 return self.user_data.get('language')
900
900
901 def AuthUser(self, **kwargs):
901 def AuthUser(self, **kwargs):
902 """
902 """
903 Returns instance of AuthUser for this user
903 Returns instance of AuthUser for this user
904 """
904 """
905 from rhodecode.lib.auth import AuthUser
905 from rhodecode.lib.auth import AuthUser
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
907
907
908 @hybrid_property
908 @hybrid_property
909 def user_data(self):
909 def user_data(self):
910 if not self._user_data:
910 if not self._user_data:
911 return {}
911 return {}
912
912
913 try:
913 try:
914 return json.loads(self._user_data) or {}
914 return json.loads(self._user_data) or {}
915 except TypeError:
915 except TypeError:
916 return {}
916 return {}
917
917
918 @user_data.setter
918 @user_data.setter
919 def user_data(self, val):
919 def user_data(self, val):
920 if not isinstance(val, dict):
920 if not isinstance(val, dict):
921 raise Exception('user_data must be dict, got %s' % type(val))
921 raise Exception('user_data must be dict, got %s' % type(val))
922 try:
922 try:
923 self._user_data = json.dumps(val)
923 self._user_data = json.dumps(val)
924 except Exception:
924 except Exception:
925 log.error(traceback.format_exc())
925 log.error(traceback.format_exc())
926
926
927 @classmethod
927 @classmethod
928 def get_by_username(cls, username, case_insensitive=False,
928 def get_by_username(cls, username, case_insensitive=False,
929 cache=False, identity_cache=False):
929 cache=False, identity_cache=False):
930 session = Session()
930 session = Session()
931
931
932 if case_insensitive:
932 if case_insensitive:
933 q = cls.query().filter(
933 q = cls.query().filter(
934 func.lower(cls.username) == func.lower(username))
934 func.lower(cls.username) == func.lower(username))
935 else:
935 else:
936 q = cls.query().filter(cls.username == username)
936 q = cls.query().filter(cls.username == username)
937
937
938 if cache:
938 if cache:
939 if identity_cache:
939 if identity_cache:
940 val = cls.identity_cache(session, 'username', username)
940 val = cls.identity_cache(session, 'username', username)
941 if val:
941 if val:
942 return val
942 return val
943 else:
943 else:
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
945 q = q.options(
945 q = q.options(
946 FromCache("sql_cache_short", cache_key))
946 FromCache("sql_cache_short", cache_key))
947
947
948 return q.scalar()
948 return q.scalar()
949
949
950 @classmethod
950 @classmethod
951 def get_by_auth_token(cls, auth_token, cache=False):
951 def get_by_auth_token(cls, auth_token, cache=False):
952 q = UserApiKeys.query()\
952 q = UserApiKeys.query()\
953 .filter(UserApiKeys.api_key == auth_token)\
953 .filter(UserApiKeys.api_key == auth_token)\
954 .filter(or_(UserApiKeys.expires == -1,
954 .filter(or_(UserApiKeys.expires == -1,
955 UserApiKeys.expires >= time.time()))
955 UserApiKeys.expires >= time.time()))
956 if cache:
956 if cache:
957 q = q.options(
957 q = q.options(
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
959
959
960 match = q.first()
960 match = q.first()
961 if match:
961 if match:
962 return match.user
962 return match.user
963
963
964 @classmethod
964 @classmethod
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
966
966
967 if case_insensitive:
967 if case_insensitive:
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
969
969
970 else:
970 else:
971 q = cls.query().filter(cls.email == email)
971 q = cls.query().filter(cls.email == email)
972
972
973 email_key = _hash_key(email)
973 email_key = _hash_key(email)
974 if cache:
974 if cache:
975 q = q.options(
975 q = q.options(
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
977
977
978 ret = q.scalar()
978 ret = q.scalar()
979 if ret is None:
979 if ret is None:
980 q = UserEmailMap.query()
980 q = UserEmailMap.query()
981 # try fetching in alternate email map
981 # try fetching in alternate email map
982 if case_insensitive:
982 if case_insensitive:
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
984 else:
984 else:
985 q = q.filter(UserEmailMap.email == email)
985 q = q.filter(UserEmailMap.email == email)
986 q = q.options(joinedload(UserEmailMap.user))
986 q = q.options(joinedload(UserEmailMap.user))
987 if cache:
987 if cache:
988 q = q.options(
988 q = q.options(
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
990 ret = getattr(q.scalar(), 'user', None)
990 ret = getattr(q.scalar(), 'user', None)
991
991
992 return ret
992 return ret
993
993
994 @classmethod
994 @classmethod
995 def get_from_cs_author(cls, author):
995 def get_from_cs_author(cls, author):
996 """
996 """
997 Tries to get User objects out of commit author string
997 Tries to get User objects out of commit author string
998
998
999 :param author:
999 :param author:
1000 """
1000 """
1001 from rhodecode.lib.helpers import email, author_name
1001 from rhodecode.lib.helpers import email, author_name
1002 # Valid email in the attribute passed, see if they're in the system
1002 # Valid email in the attribute passed, see if they're in the system
1003 _email = email(author)
1003 _email = email(author)
1004 if _email:
1004 if _email:
1005 user = cls.get_by_email(_email, case_insensitive=True)
1005 user = cls.get_by_email(_email, case_insensitive=True)
1006 if user:
1006 if user:
1007 return user
1007 return user
1008 # Maybe we can match by username?
1008 # Maybe we can match by username?
1009 _author = author_name(author)
1009 _author = author_name(author)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1011 if user:
1011 if user:
1012 return user
1012 return user
1013
1013
1014 def update_userdata(self, **kwargs):
1014 def update_userdata(self, **kwargs):
1015 usr = self
1015 usr = self
1016 old = usr.user_data
1016 old = usr.user_data
1017 old.update(**kwargs)
1017 old.update(**kwargs)
1018 usr.user_data = old
1018 usr.user_data = old
1019 Session().add(usr)
1019 Session().add(usr)
1020 log.debug('updated userdata with %s', kwargs)
1020 log.debug('updated userdata with %s', kwargs)
1021
1021
1022 def update_lastlogin(self):
1022 def update_lastlogin(self):
1023 """Update user lastlogin"""
1023 """Update user lastlogin"""
1024 self.last_login = datetime.datetime.now()
1024 self.last_login = datetime.datetime.now()
1025 Session().add(self)
1025 Session().add(self)
1026 log.debug('updated user %s lastlogin', self.username)
1026 log.debug('updated user %s lastlogin', self.username)
1027
1027
1028 def update_password(self, new_password):
1028 def update_password(self, new_password):
1029 from rhodecode.lib.auth import get_crypt_password
1029 from rhodecode.lib.auth import get_crypt_password
1030
1030
1031 self.password = get_crypt_password(new_password)
1031 self.password = get_crypt_password(new_password)
1032 Session().add(self)
1032 Session().add(self)
1033
1033
1034 @classmethod
1034 @classmethod
1035 def get_first_super_admin(cls):
1035 def get_first_super_admin(cls):
1036 user = User.query()\
1036 user = User.query()\
1037 .filter(User.admin == true()) \
1037 .filter(User.admin == true()) \
1038 .order_by(User.user_id.asc()) \
1038 .order_by(User.user_id.asc()) \
1039 .first()
1039 .first()
1040
1040
1041 if user is None:
1041 if user is None:
1042 raise Exception('FATAL: Missing administrative account!')
1042 raise Exception('FATAL: Missing administrative account!')
1043 return user
1043 return user
1044
1044
1045 @classmethod
1045 @classmethod
1046 def get_all_super_admins(cls, only_active=False):
1046 def get_all_super_admins(cls, only_active=False):
1047 """
1047 """
1048 Returns all admin accounts sorted by username
1048 Returns all admin accounts sorted by username
1049 """
1049 """
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1051 if only_active:
1051 if only_active:
1052 qry = qry.filter(User.active == true())
1052 qry = qry.filter(User.active == true())
1053 return qry.all()
1053 return qry.all()
1054
1054
1055 @classmethod
1055 @classmethod
1056 def get_all_user_ids(cls, only_active=True):
1056 def get_all_user_ids(cls, only_active=True):
1057 """
1057 """
1058 Returns all users IDs
1058 Returns all users IDs
1059 """
1059 """
1060 qry = Session().query(User.user_id)
1060 qry = Session().query(User.user_id)
1061
1061
1062 if only_active:
1062 if only_active:
1063 qry = qry.filter(User.active == true())
1063 qry = qry.filter(User.active == true())
1064 return [x.user_id for x in qry]
1064 return [x.user_id for x in qry]
1065
1065
1066 @classmethod
1066 @classmethod
1067 def get_default_user(cls, cache=False, refresh=False):
1067 def get_default_user(cls, cache=False, refresh=False):
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1069 if user is None:
1069 if user is None:
1070 raise Exception('FATAL: Missing default account!')
1070 raise Exception('FATAL: Missing default account!')
1071 if refresh:
1071 if refresh:
1072 # The default user might be based on outdated state which
1072 # The default user might be based on outdated state which
1073 # has been loaded from the cache.
1073 # has been loaded from the cache.
1074 # A call to refresh() ensures that the
1074 # A call to refresh() ensures that the
1075 # latest state from the database is used.
1075 # latest state from the database is used.
1076 Session().refresh(user)
1076 Session().refresh(user)
1077 return user
1077 return user
1078
1078
1079 @classmethod
1079 @classmethod
1080 def get_default_user_id(cls):
1080 def get_default_user_id(cls):
1081 import rhodecode
1081 import rhodecode
1082 return rhodecode.CONFIG['default_user_id']
1082 return rhodecode.CONFIG['default_user_id']
1083
1083
1084 def _get_default_perms(self, user, suffix=''):
1084 def _get_default_perms(self, user, suffix=''):
1085 from rhodecode.model.permission import PermissionModel
1085 from rhodecode.model.permission import PermissionModel
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1087
1087
1088 def get_default_perms(self, suffix=''):
1088 def get_default_perms(self, suffix=''):
1089 return self._get_default_perms(self, suffix)
1089 return self._get_default_perms(self, suffix)
1090
1090
1091 def get_api_data(self, include_secrets=False, details='full'):
1091 def get_api_data(self, include_secrets=False, details='full'):
1092 """
1092 """
1093 Common function for generating user related data for API
1093 Common function for generating user related data for API
1094
1094
1095 :param include_secrets: By default secrets in the API data will be replaced
1095 :param include_secrets: By default secrets in the API data will be replaced
1096 by a placeholder value to prevent exposing this data by accident. In case
1096 by a placeholder value to prevent exposing this data by accident. In case
1097 this data shall be exposed, set this flag to ``True``.
1097 this data shall be exposed, set this flag to ``True``.
1098
1098
1099 :param details: details can be 'basic|full' basic gives only a subset of
1099 :param details: details can be 'basic|full' basic gives only a subset of
1100 the available user information that includes user_id, name and emails.
1100 the available user information that includes user_id, name and emails.
1101 """
1101 """
1102 user = self
1102 user = self
1103 user_data = self.user_data
1103 user_data = self.user_data
1104 data = {
1104 data = {
1105 'user_id': user.user_id,
1105 'user_id': user.user_id,
1106 'username': user.username,
1106 'username': user.username,
1107 'firstname': user.name,
1107 'firstname': user.name,
1108 'lastname': user.lastname,
1108 'lastname': user.lastname,
1109 'description': user.description,
1109 'description': user.description,
1110 'email': user.email,
1110 'email': user.email,
1111 'emails': user.emails,
1111 'emails': user.emails,
1112 }
1112 }
1113 if details == 'basic':
1113 if details == 'basic':
1114 return data
1114 return data
1115
1115
1116 auth_token_length = 40
1116 auth_token_length = 40
1117 auth_token_replacement = '*' * auth_token_length
1117 auth_token_replacement = '*' * auth_token_length
1118
1118
1119 extras = {
1119 extras = {
1120 'auth_tokens': [auth_token_replacement],
1120 'auth_tokens': [auth_token_replacement],
1121 'active': user.active,
1121 'active': user.active,
1122 'admin': user.admin,
1122 'admin': user.admin,
1123 'extern_type': user.extern_type,
1123 'extern_type': user.extern_type,
1124 'extern_name': user.extern_name,
1124 'extern_name': user.extern_name,
1125 'last_login': user.last_login,
1125 'last_login': user.last_login,
1126 'last_activity': user.last_activity,
1126 'last_activity': user.last_activity,
1127 'ip_addresses': user.ip_addresses,
1127 'ip_addresses': user.ip_addresses,
1128 'language': user_data.get('language')
1128 'language': user_data.get('language')
1129 }
1129 }
1130 data.update(extras)
1130 data.update(extras)
1131
1131
1132 if include_secrets:
1132 if include_secrets:
1133 data['auth_tokens'] = user.auth_tokens
1133 data['auth_tokens'] = user.auth_tokens
1134 return data
1134 return data
1135
1135
1136 def __json__(self):
1136 def __json__(self):
1137 data = {
1137 data = {
1138 'full_name': self.full_name,
1138 'full_name': self.full_name,
1139 'full_name_or_username': self.full_name_or_username,
1139 'full_name_or_username': self.full_name_or_username,
1140 'short_contact': self.short_contact,
1140 'short_contact': self.short_contact,
1141 'full_contact': self.full_contact,
1141 'full_contact': self.full_contact,
1142 }
1142 }
1143 data.update(self.get_api_data())
1143 data.update(self.get_api_data())
1144 return data
1144 return data
1145
1145
1146
1146
1147 class UserApiKeys(Base, BaseModel):
1147 class UserApiKeys(Base, BaseModel):
1148 __tablename__ = 'user_api_keys'
1148 __tablename__ = 'user_api_keys'
1149 __table_args__ = (
1149 __table_args__ = (
1150 Index('uak_api_key_idx', 'api_key'),
1150 Index('uak_api_key_idx', 'api_key'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1152 base_table_args
1152 base_table_args
1153 )
1153 )
1154 __mapper_args__ = {}
1154 __mapper_args__ = {}
1155
1155
1156 # ApiKey role
1156 # ApiKey role
1157 ROLE_ALL = 'token_role_all'
1157 ROLE_ALL = 'token_role_all'
1158 ROLE_VCS = 'token_role_vcs'
1158 ROLE_VCS = 'token_role_vcs'
1159 ROLE_API = 'token_role_api'
1159 ROLE_API = 'token_role_api'
1160 ROLE_HTTP = 'token_role_http'
1160 ROLE_HTTP = 'token_role_http'
1161 ROLE_FEED = 'token_role_feed'
1161 ROLE_FEED = 'token_role_feed'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1163 # The last one is ignored in the list as we only
1163 # The last one is ignored in the list as we only
1164 # use it for one action, and cannot be created by users
1164 # use it for one action, and cannot be created by users
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1166
1166
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1168
1168
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1173 expires = Column('expires', Float(53), nullable=False)
1173 expires = Column('expires', Float(53), nullable=False)
1174 role = Column('role', String(255), nullable=True)
1174 role = Column('role', String(255), nullable=True)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1176
1176
1177 # scope columns
1177 # scope columns
1178 repo_id = Column(
1178 repo_id = Column(
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1180 nullable=True, unique=None, default=None)
1180 nullable=True, unique=None, default=None)
1181 repo = relationship('Repository', lazy='joined')
1181 repo = relationship('Repository', lazy='joined')
1182
1182
1183 repo_group_id = Column(
1183 repo_group_id = Column(
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1185 nullable=True, unique=None, default=None)
1185 nullable=True, unique=None, default=None)
1186 repo_group = relationship('RepoGroup', lazy='joined')
1186 repo_group = relationship('RepoGroup', lazy='joined')
1187
1187
1188 user = relationship('User', lazy='joined')
1188 user = relationship('User', lazy='joined')
1189
1189
1190 def __unicode__(self):
1190 def __unicode__(self):
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1192
1192
1193 def __json__(self):
1193 def __json__(self):
1194 data = {
1194 data = {
1195 'auth_token': self.api_key,
1195 'auth_token': self.api_key,
1196 'role': self.role,
1196 'role': self.role,
1197 'scope': self.scope_humanized,
1197 'scope': self.scope_humanized,
1198 'expired': self.expired
1198 'expired': self.expired
1199 }
1199 }
1200 return data
1200 return data
1201
1201
1202 def get_api_data(self, include_secrets=False):
1202 def get_api_data(self, include_secrets=False):
1203 data = self.__json__()
1203 data = self.__json__()
1204 if include_secrets:
1204 if include_secrets:
1205 return data
1205 return data
1206 else:
1206 else:
1207 data['auth_token'] = self.token_obfuscated
1207 data['auth_token'] = self.token_obfuscated
1208 return data
1208 return data
1209
1209
1210 @hybrid_property
1210 @hybrid_property
1211 def description_safe(self):
1211 def description_safe(self):
1212 from rhodecode.lib import helpers as h
1212 from rhodecode.lib import helpers as h
1213 return h.escape(self.description)
1213 return h.escape(self.description)
1214
1214
1215 @property
1215 @property
1216 def expired(self):
1216 def expired(self):
1217 if self.expires == -1:
1217 if self.expires == -1:
1218 return False
1218 return False
1219 return time.time() > self.expires
1219 return time.time() > self.expires
1220
1220
1221 @classmethod
1221 @classmethod
1222 def _get_role_name(cls, role):
1222 def _get_role_name(cls, role):
1223 return {
1223 return {
1224 cls.ROLE_ALL: _('all'),
1224 cls.ROLE_ALL: _('all'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1227 cls.ROLE_API: _('api calls'),
1227 cls.ROLE_API: _('api calls'),
1228 cls.ROLE_FEED: _('feed access'),
1228 cls.ROLE_FEED: _('feed access'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1230 }.get(role, role)
1230 }.get(role, role)
1231
1231
1232 @classmethod
1232 @classmethod
1233 def _get_role_description(cls, role):
1233 def _get_role_description(cls, role):
1234 return {
1234 return {
1235 cls.ROLE_ALL: _('Token for all actions.'),
1235 cls.ROLE_ALL: _('Token for all actions.'),
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1237 'login using `api_access_controllers_whitelist` functionality.'),
1237 'login using `api_access_controllers_whitelist` functionality.'),
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1239 'Requires auth_token authentication plugin to be active. <br/>'
1239 'Requires auth_token authentication plugin to be active. <br/>'
1240 'Such Token should be used then instead of a password to '
1240 'Such Token should be used then instead of a password to '
1241 'interact with a repository, and additionally can be '
1241 'interact with a repository, and additionally can be '
1242 'limited to single repository using repo scope.'),
1242 'limited to single repository using repo scope.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1246 }.get(role, role)
1246 }.get(role, role)
1247
1247
1248 @property
1248 @property
1249 def role_humanized(self):
1249 def role_humanized(self):
1250 return self._get_role_name(self.role)
1250 return self._get_role_name(self.role)
1251
1251
1252 def _get_scope(self):
1252 def _get_scope(self):
1253 if self.repo:
1253 if self.repo:
1254 return 'Repository: {}'.format(self.repo.repo_name)
1254 return 'Repository: {}'.format(self.repo.repo_name)
1255 if self.repo_group:
1255 if self.repo_group:
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1257 return 'Global'
1257 return 'Global'
1258
1258
1259 @property
1259 @property
1260 def scope_humanized(self):
1260 def scope_humanized(self):
1261 return self._get_scope()
1261 return self._get_scope()
1262
1262
1263 @property
1263 @property
1264 def token_obfuscated(self):
1264 def token_obfuscated(self):
1265 if self.api_key:
1265 if self.api_key:
1266 return self.api_key[:4] + "****"
1266 return self.api_key[:4] + "****"
1267
1267
1268
1268
1269 class UserEmailMap(Base, BaseModel):
1269 class UserEmailMap(Base, BaseModel):
1270 __tablename__ = 'user_email_map'
1270 __tablename__ = 'user_email_map'
1271 __table_args__ = (
1271 __table_args__ = (
1272 Index('uem_email_idx', 'email'),
1272 Index('uem_email_idx', 'email'),
1273 UniqueConstraint('email'),
1273 UniqueConstraint('email'),
1274 base_table_args
1274 base_table_args
1275 )
1275 )
1276 __mapper_args__ = {}
1276 __mapper_args__ = {}
1277
1277
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1281 user = relationship('User', lazy='joined')
1281 user = relationship('User', lazy='joined')
1282
1282
1283 @validates('_email')
1283 @validates('_email')
1284 def validate_email(self, key, email):
1284 def validate_email(self, key, email):
1285 # check if this email is not main one
1285 # check if this email is not main one
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1287 if main_email is not None:
1287 if main_email is not None:
1288 raise AttributeError('email %s is present is user table' % email)
1288 raise AttributeError('email %s is present is user table' % email)
1289 return email
1289 return email
1290
1290
1291 @hybrid_property
1291 @hybrid_property
1292 def email(self):
1292 def email(self):
1293 return self._email
1293 return self._email
1294
1294
1295 @email.setter
1295 @email.setter
1296 def email(self, val):
1296 def email(self, val):
1297 self._email = val.lower() if val else None
1297 self._email = val.lower() if val else None
1298
1298
1299
1299
1300 class UserIpMap(Base, BaseModel):
1300 class UserIpMap(Base, BaseModel):
1301 __tablename__ = 'user_ip_map'
1301 __tablename__ = 'user_ip_map'
1302 __table_args__ = (
1302 __table_args__ = (
1303 UniqueConstraint('user_id', 'ip_addr'),
1303 UniqueConstraint('user_id', 'ip_addr'),
1304 base_table_args
1304 base_table_args
1305 )
1305 )
1306 __mapper_args__ = {}
1306 __mapper_args__ = {}
1307
1307
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1313 user = relationship('User', lazy='joined')
1313 user = relationship('User', lazy='joined')
1314
1314
1315 @hybrid_property
1315 @hybrid_property
1316 def description_safe(self):
1316 def description_safe(self):
1317 from rhodecode.lib import helpers as h
1317 from rhodecode.lib import helpers as h
1318 return h.escape(self.description)
1318 return h.escape(self.description)
1319
1319
1320 @classmethod
1320 @classmethod
1321 def _get_ip_range(cls, ip_addr):
1321 def _get_ip_range(cls, ip_addr):
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1323 return [str(net.network_address), str(net.broadcast_address)]
1323 return [str(net.network_address), str(net.broadcast_address)]
1324
1324
1325 def __json__(self):
1325 def __json__(self):
1326 return {
1326 return {
1327 'ip_addr': self.ip_addr,
1327 'ip_addr': self.ip_addr,
1328 'ip_range': self._get_ip_range(self.ip_addr),
1328 'ip_range': self._get_ip_range(self.ip_addr),
1329 }
1329 }
1330
1330
1331 def __unicode__(self):
1331 def __unicode__(self):
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1333 self.user_id, self.ip_addr)
1333 self.user_id, self.ip_addr)
1334
1334
1335
1335
1336 class UserSshKeys(Base, BaseModel):
1336 class UserSshKeys(Base, BaseModel):
1337 __tablename__ = 'user_ssh_keys'
1337 __tablename__ = 'user_ssh_keys'
1338 __table_args__ = (
1338 __table_args__ = (
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1340
1340
1341 UniqueConstraint('ssh_key_fingerprint'),
1341 UniqueConstraint('ssh_key_fingerprint'),
1342
1342
1343 base_table_args
1343 base_table_args
1344 )
1344 )
1345 __mapper_args__ = {}
1345 __mapper_args__ = {}
1346
1346
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1350
1350
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1352
1352
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1356
1356
1357 user = relationship('User', lazy='joined')
1357 user = relationship('User', lazy='joined')
1358
1358
1359 def __json__(self):
1359 def __json__(self):
1360 data = {
1360 data = {
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1362 'description': self.description,
1362 'description': self.description,
1363 'created_on': self.created_on
1363 'created_on': self.created_on
1364 }
1364 }
1365 return data
1365 return data
1366
1366
1367 def get_api_data(self):
1367 def get_api_data(self):
1368 data = self.__json__()
1368 data = self.__json__()
1369 return data
1369 return data
1370
1370
1371
1371
1372 class UserLog(Base, BaseModel):
1372 class UserLog(Base, BaseModel):
1373 __tablename__ = 'user_logs'
1373 __tablename__ = 'user_logs'
1374 __table_args__ = (
1374 __table_args__ = (
1375 base_table_args,
1375 base_table_args,
1376 )
1376 )
1377
1377
1378 VERSION_1 = 'v1'
1378 VERSION_1 = 'v1'
1379 VERSION_2 = 'v2'
1379 VERSION_2 = 'v2'
1380 VERSIONS = [VERSION_1, VERSION_2]
1380 VERSIONS = [VERSION_1, VERSION_2]
1381
1381
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1390
1390
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1394
1394
1395 def __unicode__(self):
1395 def __unicode__(self):
1396 return u"<%s('id:%s:%s')>" % (
1396 return u"<%s('id:%s:%s')>" % (
1397 self.__class__.__name__, self.repository_name, self.action)
1397 self.__class__.__name__, self.repository_name, self.action)
1398
1398
1399 def __json__(self):
1399 def __json__(self):
1400 return {
1400 return {
1401 'user_id': self.user_id,
1401 'user_id': self.user_id,
1402 'username': self.username,
1402 'username': self.username,
1403 'repository_id': self.repository_id,
1403 'repository_id': self.repository_id,
1404 'repository_name': self.repository_name,
1404 'repository_name': self.repository_name,
1405 'user_ip': self.user_ip,
1405 'user_ip': self.user_ip,
1406 'action_date': self.action_date,
1406 'action_date': self.action_date,
1407 'action': self.action,
1407 'action': self.action,
1408 }
1408 }
1409
1409
1410 @hybrid_property
1410 @hybrid_property
1411 def entry_id(self):
1411 def entry_id(self):
1412 return self.user_log_id
1412 return self.user_log_id
1413
1413
1414 @property
1414 @property
1415 def action_as_day(self):
1415 def action_as_day(self):
1416 return datetime.date(*self.action_date.timetuple()[:3])
1416 return datetime.date(*self.action_date.timetuple()[:3])
1417
1417
1418 user = relationship('User')
1418 user = relationship('User')
1419 repository = relationship('Repository', cascade='')
1419 repository = relationship('Repository', cascade='')
1420
1420
1421
1421
1422 class UserGroup(Base, BaseModel):
1422 class UserGroup(Base, BaseModel):
1423 __tablename__ = 'users_groups'
1423 __tablename__ = 'users_groups'
1424 __table_args__ = (
1424 __table_args__ = (
1425 base_table_args,
1425 base_table_args,
1426 )
1426 )
1427
1427
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1436
1436
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1443
1443
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1446
1446
1447 @classmethod
1447 @classmethod
1448 def _load_group_data(cls, column):
1448 def _load_group_data(cls, column):
1449 if not column:
1449 if not column:
1450 return {}
1450 return {}
1451
1451
1452 try:
1452 try:
1453 return json.loads(column) or {}
1453 return json.loads(column) or {}
1454 except TypeError:
1454 except TypeError:
1455 return {}
1455 return {}
1456
1456
1457 @hybrid_property
1457 @hybrid_property
1458 def description_safe(self):
1458 def description_safe(self):
1459 from rhodecode.lib import helpers as h
1459 from rhodecode.lib import helpers as h
1460 return h.escape(self.user_group_description)
1460 return h.escape(self.user_group_description)
1461
1461
1462 @hybrid_property
1462 @hybrid_property
1463 def group_data(self):
1463 def group_data(self):
1464 return self._load_group_data(self._group_data)
1464 return self._load_group_data(self._group_data)
1465
1465
1466 @group_data.expression
1466 @group_data.expression
1467 def group_data(self, **kwargs):
1467 def group_data(self, **kwargs):
1468 return self._group_data
1468 return self._group_data
1469
1469
1470 @group_data.setter
1470 @group_data.setter
1471 def group_data(self, val):
1471 def group_data(self, val):
1472 try:
1472 try:
1473 self._group_data = json.dumps(val)
1473 self._group_data = json.dumps(val)
1474 except Exception:
1474 except Exception:
1475 log.error(traceback.format_exc())
1475 log.error(traceback.format_exc())
1476
1476
1477 @classmethod
1477 @classmethod
1478 def _load_sync(cls, group_data):
1478 def _load_sync(cls, group_data):
1479 if group_data:
1479 if group_data:
1480 return group_data.get('extern_type')
1480 return group_data.get('extern_type')
1481
1481
1482 @property
1482 @property
1483 def sync(self):
1483 def sync(self):
1484 return self._load_sync(self.group_data)
1484 return self._load_sync(self.group_data)
1485
1485
1486 def __unicode__(self):
1486 def __unicode__(self):
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1488 self.users_group_id,
1488 self.users_group_id,
1489 self.users_group_name)
1489 self.users_group_name)
1490
1490
1491 @classmethod
1491 @classmethod
1492 def get_by_group_name(cls, group_name, cache=False,
1492 def get_by_group_name(cls, group_name, cache=False,
1493 case_insensitive=False):
1493 case_insensitive=False):
1494 if case_insensitive:
1494 if case_insensitive:
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1496 func.lower(group_name))
1496 func.lower(group_name))
1497
1497
1498 else:
1498 else:
1499 q = cls.query().filter(cls.users_group_name == group_name)
1499 q = cls.query().filter(cls.users_group_name == group_name)
1500 if cache:
1500 if cache:
1501 q = q.options(
1501 q = q.options(
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1503 return q.scalar()
1503 return q.scalar()
1504
1504
1505 @classmethod
1505 @classmethod
1506 def get(cls, user_group_id, cache=False):
1506 def get(cls, user_group_id, cache=False):
1507 if not user_group_id:
1507 if not user_group_id:
1508 return
1508 return
1509
1509
1510 user_group = cls.query()
1510 user_group = cls.query()
1511 if cache:
1511 if cache:
1512 user_group = user_group.options(
1512 user_group = user_group.options(
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1514 return user_group.get(user_group_id)
1514 return user_group.get(user_group_id)
1515
1515
1516 def permissions(self, with_admins=True, with_owner=True,
1516 def permissions(self, with_admins=True, with_owner=True,
1517 expand_from_user_groups=False):
1517 expand_from_user_groups=False):
1518 """
1518 """
1519 Permissions for user groups
1519 Permissions for user groups
1520 """
1520 """
1521 _admin_perm = 'usergroup.admin'
1521 _admin_perm = 'usergroup.admin'
1522
1522
1523 owner_row = []
1523 owner_row = []
1524 if with_owner:
1524 if with_owner:
1525 usr = AttributeDict(self.user.get_dict())
1525 usr = AttributeDict(self.user.get_dict())
1526 usr.owner_row = True
1526 usr.owner_row = True
1527 usr.permission = _admin_perm
1527 usr.permission = _admin_perm
1528 owner_row.append(usr)
1528 owner_row.append(usr)
1529
1529
1530 super_admin_ids = []
1530 super_admin_ids = []
1531 super_admin_rows = []
1531 super_admin_rows = []
1532 if with_admins:
1532 if with_admins:
1533 for usr in User.get_all_super_admins():
1533 for usr in User.get_all_super_admins():
1534 super_admin_ids.append(usr.user_id)
1534 super_admin_ids.append(usr.user_id)
1535 # if this admin is also owner, don't double the record
1535 # if this admin is also owner, don't double the record
1536 if usr.user_id == owner_row[0].user_id:
1536 if usr.user_id == owner_row[0].user_id:
1537 owner_row[0].admin_row = True
1537 owner_row[0].admin_row = True
1538 else:
1538 else:
1539 usr = AttributeDict(usr.get_dict())
1539 usr = AttributeDict(usr.get_dict())
1540 usr.admin_row = True
1540 usr.admin_row = True
1541 usr.permission = _admin_perm
1541 usr.permission = _admin_perm
1542 super_admin_rows.append(usr)
1542 super_admin_rows.append(usr)
1543
1543
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1546 joinedload(UserUserGroupToPerm.user),
1546 joinedload(UserUserGroupToPerm.user),
1547 joinedload(UserUserGroupToPerm.permission),)
1547 joinedload(UserUserGroupToPerm.permission),)
1548
1548
1549 # get owners and admins and permissions. We do a trick of re-writing
1549 # get owners and admins and permissions. We do a trick of re-writing
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1551 # has a global reference and changing one object propagates to all
1551 # has a global reference and changing one object propagates to all
1552 # others. This means if admin is also an owner admin_row that change
1552 # others. This means if admin is also an owner admin_row that change
1553 # would propagate to both objects
1553 # would propagate to both objects
1554 perm_rows = []
1554 perm_rows = []
1555 for _usr in q.all():
1555 for _usr in q.all():
1556 usr = AttributeDict(_usr.user.get_dict())
1556 usr = AttributeDict(_usr.user.get_dict())
1557 # if this user is also owner/admin, mark as duplicate record
1557 # if this user is also owner/admin, mark as duplicate record
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1559 usr.duplicate_perm = True
1559 usr.duplicate_perm = True
1560 usr.permission = _usr.permission.permission_name
1560 usr.permission = _usr.permission.permission_name
1561 perm_rows.append(usr)
1561 perm_rows.append(usr)
1562
1562
1563 # filter the perm rows by 'default' first and then sort them by
1563 # filter the perm rows by 'default' first and then sort them by
1564 # admin,write,read,none permissions sorted again alphabetically in
1564 # admin,write,read,none permissions sorted again alphabetically in
1565 # each group
1565 # each group
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1567
1567
1568 user_groups_rows = []
1568 user_groups_rows = []
1569 if expand_from_user_groups:
1569 if expand_from_user_groups:
1570 for ug in self.permission_user_groups(with_members=True):
1570 for ug in self.permission_user_groups(with_members=True):
1571 for user_data in ug.members:
1571 for user_data in ug.members:
1572 user_groups_rows.append(user_data)
1572 user_groups_rows.append(user_data)
1573
1573
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1575
1575
1576 def permission_user_groups(self, with_members=False):
1576 def permission_user_groups(self, with_members=False):
1577 q = UserGroupUserGroupToPerm.query()\
1577 q = UserGroupUserGroupToPerm.query()\
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1582
1582
1583 perm_rows = []
1583 perm_rows = []
1584 for _user_group in q.all():
1584 for _user_group in q.all():
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1586 entry.permission = _user_group.permission.permission_name
1586 entry.permission = _user_group.permission.permission_name
1587 if with_members:
1587 if with_members:
1588 entry.members = [x.user.get_dict()
1588 entry.members = [x.user.get_dict()
1589 for x in _user_group.user_group.members]
1589 for x in _user_group.user_group.members]
1590 perm_rows.append(entry)
1590 perm_rows.append(entry)
1591
1591
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1593 return perm_rows
1593 return perm_rows
1594
1594
1595 def _get_default_perms(self, user_group, suffix=''):
1595 def _get_default_perms(self, user_group, suffix=''):
1596 from rhodecode.model.permission import PermissionModel
1596 from rhodecode.model.permission import PermissionModel
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1598
1598
1599 def get_default_perms(self, suffix=''):
1599 def get_default_perms(self, suffix=''):
1600 return self._get_default_perms(self, suffix)
1600 return self._get_default_perms(self, suffix)
1601
1601
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1603 """
1603 """
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1605 basically forwarded.
1605 basically forwarded.
1606
1606
1607 """
1607 """
1608 user_group = self
1608 user_group = self
1609 data = {
1609 data = {
1610 'users_group_id': user_group.users_group_id,
1610 'users_group_id': user_group.users_group_id,
1611 'group_name': user_group.users_group_name,
1611 'group_name': user_group.users_group_name,
1612 'group_description': user_group.user_group_description,
1612 'group_description': user_group.user_group_description,
1613 'active': user_group.users_group_active,
1613 'active': user_group.users_group_active,
1614 'owner': user_group.user.username,
1614 'owner': user_group.user.username,
1615 'sync': user_group.sync,
1615 'sync': user_group.sync,
1616 'owner_email': user_group.user.email,
1616 'owner_email': user_group.user.email,
1617 }
1617 }
1618
1618
1619 if with_group_members:
1619 if with_group_members:
1620 users = []
1620 users = []
1621 for user in user_group.members:
1621 for user in user_group.members:
1622 user = user.user
1622 user = user.user
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1624 data['users'] = users
1624 data['users'] = users
1625
1625
1626 return data
1626 return data
1627
1627
1628
1628
1629 class UserGroupMember(Base, BaseModel):
1629 class UserGroupMember(Base, BaseModel):
1630 __tablename__ = 'users_groups_members'
1630 __tablename__ = 'users_groups_members'
1631 __table_args__ = (
1631 __table_args__ = (
1632 base_table_args,
1632 base_table_args,
1633 )
1633 )
1634
1634
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1638
1638
1639 user = relationship('User', lazy='joined')
1639 user = relationship('User', lazy='joined')
1640 users_group = relationship('UserGroup')
1640 users_group = relationship('UserGroup')
1641
1641
1642 def __init__(self, gr_id='', u_id=''):
1642 def __init__(self, gr_id='', u_id=''):
1643 self.users_group_id = gr_id
1643 self.users_group_id = gr_id
1644 self.user_id = u_id
1644 self.user_id = u_id
1645
1645
1646
1646
1647 class RepositoryField(Base, BaseModel):
1647 class RepositoryField(Base, BaseModel):
1648 __tablename__ = 'repositories_fields'
1648 __tablename__ = 'repositories_fields'
1649 __table_args__ = (
1649 __table_args__ = (
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1651 base_table_args,
1651 base_table_args,
1652 )
1652 )
1653
1653
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1655
1655
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1658 field_key = Column("field_key", String(250))
1658 field_key = Column("field_key", String(250))
1659 field_label = Column("field_label", String(1024), nullable=False)
1659 field_label = Column("field_label", String(1024), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1664
1664
1665 repository = relationship('Repository')
1665 repository = relationship('Repository')
1666
1666
1667 @property
1667 @property
1668 def field_key_prefixed(self):
1668 def field_key_prefixed(self):
1669 return 'ex_%s' % self.field_key
1669 return 'ex_%s' % self.field_key
1670
1670
1671 @classmethod
1671 @classmethod
1672 def un_prefix_key(cls, key):
1672 def un_prefix_key(cls, key):
1673 if key.startswith(cls.PREFIX):
1673 if key.startswith(cls.PREFIX):
1674 return key[len(cls.PREFIX):]
1674 return key[len(cls.PREFIX):]
1675 return key
1675 return key
1676
1676
1677 @classmethod
1677 @classmethod
1678 def get_by_key_name(cls, key, repo):
1678 def get_by_key_name(cls, key, repo):
1679 row = cls.query()\
1679 row = cls.query()\
1680 .filter(cls.repository == repo)\
1680 .filter(cls.repository == repo)\
1681 .filter(cls.field_key == key).scalar()
1681 .filter(cls.field_key == key).scalar()
1682 return row
1682 return row
1683
1683
1684
1684
1685 class Repository(Base, BaseModel):
1685 class Repository(Base, BaseModel):
1686 __tablename__ = 'repositories'
1686 __tablename__ = 'repositories'
1687 __table_args__ = (
1687 __table_args__ = (
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1689 base_table_args,
1689 base_table_args,
1690 )
1690 )
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1694
1694
1695 STATE_CREATED = 'repo_state_created'
1695 STATE_CREATED = 'repo_state_created'
1696 STATE_PENDING = 'repo_state_pending'
1696 STATE_PENDING = 'repo_state_pending'
1697 STATE_ERROR = 'repo_state_error'
1697 STATE_ERROR = 'repo_state_error'
1698
1698
1699 LOCK_AUTOMATIC = 'lock_auto'
1699 LOCK_AUTOMATIC = 'lock_auto'
1700 LOCK_API = 'lock_api'
1700 LOCK_API = 'lock_api'
1701 LOCK_WEB = 'lock_web'
1701 LOCK_WEB = 'lock_web'
1702 LOCK_PULL = 'lock_pull'
1702 LOCK_PULL = 'lock_pull'
1703
1703
1704 NAME_SEP = URL_SEP
1704 NAME_SEP = URL_SEP
1705
1705
1706 repo_id = Column(
1706 repo_id = Column(
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1708 primary_key=True)
1708 primary_key=True)
1709 _repo_name = Column(
1709 _repo_name = Column(
1710 "repo_name", Text(), nullable=False, default=None)
1710 "repo_name", Text(), nullable=False, default=None)
1711 repo_name_hash = Column(
1711 repo_name_hash = Column(
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1714
1714
1715 clone_uri = Column(
1715 clone_uri = Column(
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1717 default=None)
1717 default=None)
1718 push_uri = Column(
1718 push_uri = Column(
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1720 default=None)
1720 default=None)
1721 repo_type = Column(
1721 repo_type = Column(
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1723 user_id = Column(
1723 user_id = Column(
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1725 unique=False, default=None)
1725 unique=False, default=None)
1726 private = Column(
1726 private = Column(
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1728 archived = Column(
1728 archived = Column(
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1730 enable_statistics = Column(
1730 enable_statistics = Column(
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1732 enable_downloads = Column(
1732 enable_downloads = Column(
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1734 description = Column(
1734 description = Column(
1735 "description", String(10000), nullable=True, unique=None, default=None)
1735 "description", String(10000), nullable=True, unique=None, default=None)
1736 created_on = Column(
1736 created_on = Column(
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1738 default=datetime.datetime.now)
1738 default=datetime.datetime.now)
1739 updated_on = Column(
1739 updated_on = Column(
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1741 default=datetime.datetime.now)
1741 default=datetime.datetime.now)
1742 _landing_revision = Column(
1742 _landing_revision = Column(
1743 "landing_revision", String(255), nullable=False, unique=False,
1743 "landing_revision", String(255), nullable=False, unique=False,
1744 default=None)
1744 default=None)
1745 enable_locking = Column(
1745 enable_locking = Column(
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1747 default=False)
1747 default=False)
1748 _locked = Column(
1748 _locked = Column(
1749 "locked", String(255), nullable=True, unique=False, default=None)
1749 "locked", String(255), nullable=True, unique=False, default=None)
1750 _changeset_cache = Column(
1750 _changeset_cache = Column(
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1752
1752
1753 fork_id = Column(
1753 fork_id = Column(
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1755 nullable=True, unique=False, default=None)
1755 nullable=True, unique=False, default=None)
1756 group_id = Column(
1756 group_id = Column(
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1758 unique=False, default=None)
1758 unique=False, default=None)
1759
1759
1760 user = relationship('User', lazy='joined')
1760 user = relationship('User', lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1763 repo_to_perm = relationship(
1763 repo_to_perm = relationship(
1764 'UserRepoToPerm', cascade='all',
1764 'UserRepoToPerm', cascade='all',
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1768
1768
1769 followers = relationship(
1769 followers = relationship(
1770 'UserFollowing',
1770 'UserFollowing',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1772 cascade='all')
1772 cascade='all')
1773 extra_fields = relationship(
1773 extra_fields = relationship(
1774 'RepositoryField', cascade="all, delete-orphan")
1774 'RepositoryField', cascade="all, delete-orphan")
1775 logs = relationship('UserLog')
1775 logs = relationship('UserLog')
1776 comments = relationship(
1776 comments = relationship(
1777 'ChangesetComment', cascade="all, delete-orphan")
1777 'ChangesetComment', cascade="all, delete-orphan")
1778 pull_requests_source = relationship(
1778 pull_requests_source = relationship(
1779 'PullRequest',
1779 'PullRequest',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 cascade="all, delete-orphan")
1781 cascade="all, delete-orphan")
1782 pull_requests_target = relationship(
1782 pull_requests_target = relationship(
1783 'PullRequest',
1783 'PullRequest',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1785 cascade="all, delete-orphan")
1785 cascade="all, delete-orphan")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1789
1789
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1791
1791
1792 # no cascade, set NULL
1792 # no cascade, set NULL
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1794
1794
1795 def __unicode__(self):
1795 def __unicode__(self):
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1797 safe_unicode(self.repo_name))
1797 safe_unicode(self.repo_name))
1798
1798
1799 @hybrid_property
1799 @hybrid_property
1800 def description_safe(self):
1800 def description_safe(self):
1801 from rhodecode.lib import helpers as h
1801 from rhodecode.lib import helpers as h
1802 return h.escape(self.description)
1802 return h.escape(self.description)
1803
1803
1804 @hybrid_property
1804 @hybrid_property
1805 def landing_rev(self):
1805 def landing_rev(self):
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1807 if self._landing_revision:
1807 if self._landing_revision:
1808 _rev_info = self._landing_revision.split(':')
1808 _rev_info = self._landing_revision.split(':')
1809 if len(_rev_info) < 2:
1809 if len(_rev_info) < 2:
1810 _rev_info.insert(0, 'rev')
1810 _rev_info.insert(0, 'rev')
1811 return [_rev_info[0], _rev_info[1]]
1811 return [_rev_info[0], _rev_info[1]]
1812 return [None, None]
1812 return [None, None]
1813
1813
1814 @property
1814 @property
1815 def landing_ref_type(self):
1815 def landing_ref_type(self):
1816 return self.landing_rev[0]
1816 return self.landing_rev[0]
1817
1817
1818 @property
1818 @property
1819 def landing_ref_name(self):
1819 def landing_ref_name(self):
1820 return self.landing_rev[1]
1820 return self.landing_rev[1]
1821
1821
1822 @landing_rev.setter
1822 @landing_rev.setter
1823 def landing_rev(self, val):
1823 def landing_rev(self, val):
1824 if ':' not in val:
1824 if ':' not in val:
1825 raise ValueError('value must be delimited with `:` and consist '
1825 raise ValueError('value must be delimited with `:` and consist '
1826 'of <rev_type>:<rev>, got %s instead' % val)
1826 'of <rev_type>:<rev>, got %s instead' % val)
1827 self._landing_revision = val
1827 self._landing_revision = val
1828
1828
1829 @hybrid_property
1829 @hybrid_property
1830 def locked(self):
1830 def locked(self):
1831 if self._locked:
1831 if self._locked:
1832 user_id, timelocked, reason = self._locked.split(':')
1832 user_id, timelocked, reason = self._locked.split(':')
1833 lock_values = int(user_id), timelocked, reason
1833 lock_values = int(user_id), timelocked, reason
1834 else:
1834 else:
1835 lock_values = [None, None, None]
1835 lock_values = [None, None, None]
1836 return lock_values
1836 return lock_values
1837
1837
1838 @locked.setter
1838 @locked.setter
1839 def locked(self, val):
1839 def locked(self, val):
1840 if val and isinstance(val, (list, tuple)):
1840 if val and isinstance(val, (list, tuple)):
1841 self._locked = ':'.join(map(str, val))
1841 self._locked = ':'.join(map(str, val))
1842 else:
1842 else:
1843 self._locked = None
1843 self._locked = None
1844
1844
1845 @classmethod
1845 @classmethod
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1848 dummy = EmptyCommit().__json__()
1848 dummy = EmptyCommit().__json__()
1849 if not changeset_cache_raw:
1849 if not changeset_cache_raw:
1850 dummy['source_repo_id'] = repo_id
1850 dummy['source_repo_id'] = repo_id
1851 return json.loads(json.dumps(dummy))
1851 return json.loads(json.dumps(dummy))
1852
1852
1853 try:
1853 try:
1854 return json.loads(changeset_cache_raw)
1854 return json.loads(changeset_cache_raw)
1855 except TypeError:
1855 except TypeError:
1856 return dummy
1856 return dummy
1857 except Exception:
1857 except Exception:
1858 log.error(traceback.format_exc())
1858 log.error(traceback.format_exc())
1859 return dummy
1859 return dummy
1860
1860
1861 @hybrid_property
1861 @hybrid_property
1862 def changeset_cache(self):
1862 def changeset_cache(self):
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1864
1864
1865 @changeset_cache.setter
1865 @changeset_cache.setter
1866 def changeset_cache(self, val):
1866 def changeset_cache(self, val):
1867 try:
1867 try:
1868 self._changeset_cache = json.dumps(val)
1868 self._changeset_cache = json.dumps(val)
1869 except Exception:
1869 except Exception:
1870 log.error(traceback.format_exc())
1870 log.error(traceback.format_exc())
1871
1871
1872 @hybrid_property
1872 @hybrid_property
1873 def repo_name(self):
1873 def repo_name(self):
1874 return self._repo_name
1874 return self._repo_name
1875
1875
1876 @repo_name.setter
1876 @repo_name.setter
1877 def repo_name(self, value):
1877 def repo_name(self, value):
1878 self._repo_name = value
1878 self._repo_name = value
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1880
1880
1881 @classmethod
1881 @classmethod
1882 def normalize_repo_name(cls, repo_name):
1882 def normalize_repo_name(cls, repo_name):
1883 """
1883 """
1884 Normalizes os specific repo_name to the format internally stored inside
1884 Normalizes os specific repo_name to the format internally stored inside
1885 database using URL_SEP
1885 database using URL_SEP
1886
1886
1887 :param cls:
1887 :param cls:
1888 :param repo_name:
1888 :param repo_name:
1889 """
1889 """
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1891
1891
1892 @classmethod
1892 @classmethod
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1894 session = Session()
1894 session = Session()
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1896
1896
1897 if cache:
1897 if cache:
1898 if identity_cache:
1898 if identity_cache:
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1900 if val:
1900 if val:
1901 return val
1901 return val
1902 else:
1902 else:
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1904 q = q.options(
1904 q = q.options(
1905 FromCache("sql_cache_short", cache_key))
1905 FromCache("sql_cache_short", cache_key))
1906
1906
1907 return q.scalar()
1907 return q.scalar()
1908
1908
1909 @classmethod
1909 @classmethod
1910 def get_by_id_or_repo_name(cls, repoid):
1910 def get_by_id_or_repo_name(cls, repoid):
1911 if isinstance(repoid, (int, long)):
1911 if isinstance(repoid, (int, long)):
1912 try:
1912 try:
1913 repo = cls.get(repoid)
1913 repo = cls.get(repoid)
1914 except ValueError:
1914 except ValueError:
1915 repo = None
1915 repo = None
1916 else:
1916 else:
1917 repo = cls.get_by_repo_name(repoid)
1917 repo = cls.get_by_repo_name(repoid)
1918 return repo
1918 return repo
1919
1919
1920 @classmethod
1920 @classmethod
1921 def get_by_full_path(cls, repo_full_path):
1921 def get_by_full_path(cls, repo_full_path):
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1923 repo_name = cls.normalize_repo_name(repo_name)
1923 repo_name = cls.normalize_repo_name(repo_name)
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1925
1925
1926 @classmethod
1926 @classmethod
1927 def get_repo_forks(cls, repo_id):
1927 def get_repo_forks(cls, repo_id):
1928 return cls.query().filter(Repository.fork_id == repo_id)
1928 return cls.query().filter(Repository.fork_id == repo_id)
1929
1929
1930 @classmethod
1930 @classmethod
1931 def base_path(cls):
1931 def base_path(cls):
1932 """
1932 """
1933 Returns base path when all repos are stored
1933 Returns base path when all repos are stored
1934
1934
1935 :param cls:
1935 :param cls:
1936 """
1936 """
1937 q = Session().query(RhodeCodeUi)\
1937 q = Session().query(RhodeCodeUi)\
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1940 return q.one().ui_value
1940 return q.one().ui_value
1941
1941
1942 @classmethod
1942 @classmethod
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1944 case_insensitive=True, archived=False):
1944 case_insensitive=True, archived=False):
1945 q = Repository.query()
1945 q = Repository.query()
1946
1946
1947 if not archived:
1947 if not archived:
1948 q = q.filter(Repository.archived.isnot(true()))
1948 q = q.filter(Repository.archived.isnot(true()))
1949
1949
1950 if not isinstance(user_id, Optional):
1950 if not isinstance(user_id, Optional):
1951 q = q.filter(Repository.user_id == user_id)
1951 q = q.filter(Repository.user_id == user_id)
1952
1952
1953 if not isinstance(group_id, Optional):
1953 if not isinstance(group_id, Optional):
1954 q = q.filter(Repository.group_id == group_id)
1954 q = q.filter(Repository.group_id == group_id)
1955
1955
1956 if case_insensitive:
1956 if case_insensitive:
1957 q = q.order_by(func.lower(Repository.repo_name))
1957 q = q.order_by(func.lower(Repository.repo_name))
1958 else:
1958 else:
1959 q = q.order_by(Repository.repo_name)
1959 q = q.order_by(Repository.repo_name)
1960
1960
1961 return q.all()
1961 return q.all()
1962
1962
1963 @property
1963 @property
1964 def repo_uid(self):
1964 def repo_uid(self):
1965 return '_{}'.format(self.repo_id)
1965 return '_{}'.format(self.repo_id)
1966
1966
1967 @property
1967 @property
1968 def forks(self):
1968 def forks(self):
1969 """
1969 """
1970 Return forks of this repo
1970 Return forks of this repo
1971 """
1971 """
1972 return Repository.get_repo_forks(self.repo_id)
1972 return Repository.get_repo_forks(self.repo_id)
1973
1973
1974 @property
1974 @property
1975 def parent(self):
1975 def parent(self):
1976 """
1976 """
1977 Returns fork parent
1977 Returns fork parent
1978 """
1978 """
1979 return self.fork
1979 return self.fork
1980
1980
1981 @property
1981 @property
1982 def just_name(self):
1982 def just_name(self):
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1984
1984
1985 @property
1985 @property
1986 def groups_with_parents(self):
1986 def groups_with_parents(self):
1987 groups = []
1987 groups = []
1988 if self.group is None:
1988 if self.group is None:
1989 return groups
1989 return groups
1990
1990
1991 cur_gr = self.group
1991 cur_gr = self.group
1992 groups.insert(0, cur_gr)
1992 groups.insert(0, cur_gr)
1993 while 1:
1993 while 1:
1994 gr = getattr(cur_gr, 'parent_group', None)
1994 gr = getattr(cur_gr, 'parent_group', None)
1995 cur_gr = cur_gr.parent_group
1995 cur_gr = cur_gr.parent_group
1996 if gr is None:
1996 if gr is None:
1997 break
1997 break
1998 groups.insert(0, gr)
1998 groups.insert(0, gr)
1999
1999
2000 return groups
2000 return groups
2001
2001
2002 @property
2002 @property
2003 def groups_and_repo(self):
2003 def groups_and_repo(self):
2004 return self.groups_with_parents, self
2004 return self.groups_with_parents, self
2005
2005
2006 @LazyProperty
2006 @LazyProperty
2007 def repo_path(self):
2007 def repo_path(self):
2008 """
2008 """
2009 Returns base full path for that repository means where it actually
2009 Returns base full path for that repository means where it actually
2010 exists on a filesystem
2010 exists on a filesystem
2011 """
2011 """
2012 q = Session().query(RhodeCodeUi).filter(
2012 q = Session().query(RhodeCodeUi).filter(
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2015 return q.one().ui_value
2015 return q.one().ui_value
2016
2016
2017 @property
2017 @property
2018 def repo_full_path(self):
2018 def repo_full_path(self):
2019 p = [self.repo_path]
2019 p = [self.repo_path]
2020 # we need to split the name by / since this is how we store the
2020 # we need to split the name by / since this is how we store the
2021 # names in the database, but that eventually needs to be converted
2021 # names in the database, but that eventually needs to be converted
2022 # into a valid system path
2022 # into a valid system path
2023 p += self.repo_name.split(self.NAME_SEP)
2023 p += self.repo_name.split(self.NAME_SEP)
2024 return os.path.join(*map(safe_unicode, p))
2024 return os.path.join(*map(safe_unicode, p))
2025
2025
2026 @property
2026 @property
2027 def cache_keys(self):
2027 def cache_keys(self):
2028 """
2028 """
2029 Returns associated cache keys for that repo
2029 Returns associated cache keys for that repo
2030 """
2030 """
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2032 repo_id=self.repo_id)
2032 repo_id=self.repo_id)
2033 return CacheKey.query()\
2033 return CacheKey.query()\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2035 .order_by(CacheKey.cache_key)\
2035 .order_by(CacheKey.cache_key)\
2036 .all()
2036 .all()
2037
2037
2038 @property
2038 @property
2039 def cached_diffs_relative_dir(self):
2039 def cached_diffs_relative_dir(self):
2040 """
2040 """
2041 Return a relative to the repository store path of cached diffs
2041 Return a relative to the repository store path of cached diffs
2042 used for safe display for users, who shouldn't know the absolute store
2042 used for safe display for users, who shouldn't know the absolute store
2043 path
2043 path
2044 """
2044 """
2045 return os.path.join(
2045 return os.path.join(
2046 os.path.dirname(self.repo_name),
2046 os.path.dirname(self.repo_name),
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2048
2048
2049 @property
2049 @property
2050 def cached_diffs_dir(self):
2050 def cached_diffs_dir(self):
2051 path = self.repo_full_path
2051 path = self.repo_full_path
2052 return os.path.join(
2052 return os.path.join(
2053 os.path.dirname(path),
2053 os.path.dirname(path),
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2055
2055
2056 def cached_diffs(self):
2056 def cached_diffs(self):
2057 diff_cache_dir = self.cached_diffs_dir
2057 diff_cache_dir = self.cached_diffs_dir
2058 if os.path.isdir(diff_cache_dir):
2058 if os.path.isdir(diff_cache_dir):
2059 return os.listdir(diff_cache_dir)
2059 return os.listdir(diff_cache_dir)
2060 return []
2060 return []
2061
2061
2062 def shadow_repos(self):
2062 def shadow_repos(self):
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2064 return [
2064 return [
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2066 if x.startswith(shadow_repos_pattern)]
2066 if x.startswith(shadow_repos_pattern)]
2067
2067
2068 def get_new_name(self, repo_name):
2068 def get_new_name(self, repo_name):
2069 """
2069 """
2070 returns new full repository name based on assigned group and new new
2070 returns new full repository name based on assigned group and new new
2071
2071
2072 :param group_name:
2072 :param group_name:
2073 """
2073 """
2074 path_prefix = self.group.full_path_splitted if self.group else []
2074 path_prefix = self.group.full_path_splitted if self.group else []
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2076
2076
2077 @property
2077 @property
2078 def _config(self):
2078 def _config(self):
2079 """
2079 """
2080 Returns db based config object.
2080 Returns db based config object.
2081 """
2081 """
2082 from rhodecode.lib.utils import make_db_config
2082 from rhodecode.lib.utils import make_db_config
2083 return make_db_config(clear_session=False, repo=self)
2083 return make_db_config(clear_session=False, repo=self)
2084
2084
2085 def permissions(self, with_admins=True, with_owner=True,
2085 def permissions(self, with_admins=True, with_owner=True,
2086 expand_from_user_groups=False):
2086 expand_from_user_groups=False):
2087 """
2087 """
2088 Permissions for repositories
2088 Permissions for repositories
2089 """
2089 """
2090 _admin_perm = 'repository.admin'
2090 _admin_perm = 'repository.admin'
2091
2091
2092 owner_row = []
2092 owner_row = []
2093 if with_owner:
2093 if with_owner:
2094 usr = AttributeDict(self.user.get_dict())
2094 usr = AttributeDict(self.user.get_dict())
2095 usr.owner_row = True
2095 usr.owner_row = True
2096 usr.permission = _admin_perm
2096 usr.permission = _admin_perm
2097 usr.permission_id = None
2097 usr.permission_id = None
2098 owner_row.append(usr)
2098 owner_row.append(usr)
2099
2099
2100 super_admin_ids = []
2100 super_admin_ids = []
2101 super_admin_rows = []
2101 super_admin_rows = []
2102 if with_admins:
2102 if with_admins:
2103 for usr in User.get_all_super_admins():
2103 for usr in User.get_all_super_admins():
2104 super_admin_ids.append(usr.user_id)
2104 super_admin_ids.append(usr.user_id)
2105 # if this admin is also owner, don't double the record
2105 # if this admin is also owner, don't double the record
2106 if usr.user_id == owner_row[0].user_id:
2106 if usr.user_id == owner_row[0].user_id:
2107 owner_row[0].admin_row = True
2107 owner_row[0].admin_row = True
2108 else:
2108 else:
2109 usr = AttributeDict(usr.get_dict())
2109 usr = AttributeDict(usr.get_dict())
2110 usr.admin_row = True
2110 usr.admin_row = True
2111 usr.permission = _admin_perm
2111 usr.permission = _admin_perm
2112 usr.permission_id = None
2112 usr.permission_id = None
2113 super_admin_rows.append(usr)
2113 super_admin_rows.append(usr)
2114
2114
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2117 joinedload(UserRepoToPerm.user),
2117 joinedload(UserRepoToPerm.user),
2118 joinedload(UserRepoToPerm.permission),)
2118 joinedload(UserRepoToPerm.permission),)
2119
2119
2120 # get owners and admins and permissions. We do a trick of re-writing
2120 # get owners and admins and permissions. We do a trick of re-writing
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2122 # has a global reference and changing one object propagates to all
2122 # has a global reference and changing one object propagates to all
2123 # others. This means if admin is also an owner admin_row that change
2123 # others. This means if admin is also an owner admin_row that change
2124 # would propagate to both objects
2124 # would propagate to both objects
2125 perm_rows = []
2125 perm_rows = []
2126 for _usr in q.all():
2126 for _usr in q.all():
2127 usr = AttributeDict(_usr.user.get_dict())
2127 usr = AttributeDict(_usr.user.get_dict())
2128 # if this user is also owner/admin, mark as duplicate record
2128 # if this user is also owner/admin, mark as duplicate record
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2130 usr.duplicate_perm = True
2130 usr.duplicate_perm = True
2131 # also check if this permission is maybe used by branch_permissions
2131 # also check if this permission is maybe used by branch_permissions
2132 if _usr.branch_perm_entry:
2132 if _usr.branch_perm_entry:
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2134
2134
2135 usr.permission = _usr.permission.permission_name
2135 usr.permission = _usr.permission.permission_name
2136 usr.permission_id = _usr.repo_to_perm_id
2136 usr.permission_id = _usr.repo_to_perm_id
2137 perm_rows.append(usr)
2137 perm_rows.append(usr)
2138
2138
2139 # filter the perm rows by 'default' first and then sort them by
2139 # filter the perm rows by 'default' first and then sort them by
2140 # admin,write,read,none permissions sorted again alphabetically in
2140 # admin,write,read,none permissions sorted again alphabetically in
2141 # each group
2141 # each group
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2143
2143
2144 user_groups_rows = []
2144 user_groups_rows = []
2145 if expand_from_user_groups:
2145 if expand_from_user_groups:
2146 for ug in self.permission_user_groups(with_members=True):
2146 for ug in self.permission_user_groups(with_members=True):
2147 for user_data in ug.members:
2147 for user_data in ug.members:
2148 user_groups_rows.append(user_data)
2148 user_groups_rows.append(user_data)
2149
2149
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2151
2151
2152 def permission_user_groups(self, with_members=True):
2152 def permission_user_groups(self, with_members=True):
2153 q = UserGroupRepoToPerm.query()\
2153 q = UserGroupRepoToPerm.query()\
2154 .filter(UserGroupRepoToPerm.repository == self)
2154 .filter(UserGroupRepoToPerm.repository == self)
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2156 joinedload(UserGroupRepoToPerm.users_group),
2156 joinedload(UserGroupRepoToPerm.users_group),
2157 joinedload(UserGroupRepoToPerm.permission),)
2157 joinedload(UserGroupRepoToPerm.permission),)
2158
2158
2159 perm_rows = []
2159 perm_rows = []
2160 for _user_group in q.all():
2160 for _user_group in q.all():
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2162 entry.permission = _user_group.permission.permission_name
2162 entry.permission = _user_group.permission.permission_name
2163 if with_members:
2163 if with_members:
2164 entry.members = [x.user.get_dict()
2164 entry.members = [x.user.get_dict()
2165 for x in _user_group.users_group.members]
2165 for x in _user_group.users_group.members]
2166 perm_rows.append(entry)
2166 perm_rows.append(entry)
2167
2167
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2169 return perm_rows
2169 return perm_rows
2170
2170
2171 def get_api_data(self, include_secrets=False):
2171 def get_api_data(self, include_secrets=False):
2172 """
2172 """
2173 Common function for generating repo api data
2173 Common function for generating repo api data
2174
2174
2175 :param include_secrets: See :meth:`User.get_api_data`.
2175 :param include_secrets: See :meth:`User.get_api_data`.
2176
2176
2177 """
2177 """
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2179 # move this methods on models level.
2179 # move this methods on models level.
2180 from rhodecode.model.settings import SettingsModel
2180 from rhodecode.model.settings import SettingsModel
2181 from rhodecode.model.repo import RepoModel
2181 from rhodecode.model.repo import RepoModel
2182
2182
2183 repo = self
2183 repo = self
2184 _user_id, _time, _reason = self.locked
2184 _user_id, _time, _reason = self.locked
2185
2185
2186 data = {
2186 data = {
2187 'repo_id': repo.repo_id,
2187 'repo_id': repo.repo_id,
2188 'repo_name': repo.repo_name,
2188 'repo_name': repo.repo_name,
2189 'repo_type': repo.repo_type,
2189 'repo_type': repo.repo_type,
2190 'clone_uri': repo.clone_uri or '',
2190 'clone_uri': repo.clone_uri or '',
2191 'push_uri': repo.push_uri or '',
2191 'push_uri': repo.push_uri or '',
2192 'url': RepoModel().get_url(self),
2192 'url': RepoModel().get_url(self),
2193 'private': repo.private,
2193 'private': repo.private,
2194 'created_on': repo.created_on,
2194 'created_on': repo.created_on,
2195 'description': repo.description_safe,
2195 'description': repo.description_safe,
2196 'landing_rev': repo.landing_rev,
2196 'landing_rev': repo.landing_rev,
2197 'owner': repo.user.username,
2197 'owner': repo.user.username,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2200 'enable_statistics': repo.enable_statistics,
2200 'enable_statistics': repo.enable_statistics,
2201 'enable_locking': repo.enable_locking,
2201 'enable_locking': repo.enable_locking,
2202 'enable_downloads': repo.enable_downloads,
2202 'enable_downloads': repo.enable_downloads,
2203 'last_changeset': repo.changeset_cache,
2203 'last_changeset': repo.changeset_cache,
2204 'locked_by': User.get(_user_id).get_api_data(
2204 'locked_by': User.get(_user_id).get_api_data(
2205 include_secrets=include_secrets) if _user_id else None,
2205 include_secrets=include_secrets) if _user_id else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2207 'lock_reason': _reason if _reason else None,
2207 'lock_reason': _reason if _reason else None,
2208 }
2208 }
2209
2209
2210 # TODO: mikhail: should be per-repo settings here
2210 # TODO: mikhail: should be per-repo settings here
2211 rc_config = SettingsModel().get_all_settings()
2211 rc_config = SettingsModel().get_all_settings()
2212 repository_fields = str2bool(
2212 repository_fields = str2bool(
2213 rc_config.get('rhodecode_repository_fields'))
2213 rc_config.get('rhodecode_repository_fields'))
2214 if repository_fields:
2214 if repository_fields:
2215 for f in self.extra_fields:
2215 for f in self.extra_fields:
2216 data[f.field_key_prefixed] = f.field_value
2216 data[f.field_key_prefixed] = f.field_value
2217
2217
2218 return data
2218 return data
2219
2219
2220 @classmethod
2220 @classmethod
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2222 if not lock_time:
2222 if not lock_time:
2223 lock_time = time.time()
2223 lock_time = time.time()
2224 if not lock_reason:
2224 if not lock_reason:
2225 lock_reason = cls.LOCK_AUTOMATIC
2225 lock_reason = cls.LOCK_AUTOMATIC
2226 repo.locked = [user_id, lock_time, lock_reason]
2226 repo.locked = [user_id, lock_time, lock_reason]
2227 Session().add(repo)
2227 Session().add(repo)
2228 Session().commit()
2228 Session().commit()
2229
2229
2230 @classmethod
2230 @classmethod
2231 def unlock(cls, repo):
2231 def unlock(cls, repo):
2232 repo.locked = None
2232 repo.locked = None
2233 Session().add(repo)
2233 Session().add(repo)
2234 Session().commit()
2234 Session().commit()
2235
2235
2236 @classmethod
2236 @classmethod
2237 def getlock(cls, repo):
2237 def getlock(cls, repo):
2238 return repo.locked
2238 return repo.locked
2239
2239
2240 def is_user_lock(self, user_id):
2240 def is_user_lock(self, user_id):
2241 if self.lock[0]:
2241 if self.lock[0]:
2242 lock_user_id = safe_int(self.lock[0])
2242 lock_user_id = safe_int(self.lock[0])
2243 user_id = safe_int(user_id)
2243 user_id = safe_int(user_id)
2244 # both are ints, and they are equal
2244 # both are ints, and they are equal
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2246
2246
2247 return False
2247 return False
2248
2248
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 """
2250 """
2251 Checks locking on this repository, if locking is enabled and lock is
2251 Checks locking on this repository, if locking is enabled and lock is
2252 present returns a tuple of make_lock, locked, locked_by.
2252 present returns a tuple of make_lock, locked, locked_by.
2253 make_lock can have 3 states None (do nothing) True, make lock
2253 make_lock can have 3 states None (do nothing) True, make lock
2254 False release lock, This value is later propagated to hooks, which
2254 False release lock, This value is later propagated to hooks, which
2255 do the locking. Think about this as signals passed to hooks what to do.
2255 do the locking. Think about this as signals passed to hooks what to do.
2256
2256
2257 """
2257 """
2258 # TODO: johbo: This is part of the business logic and should be moved
2258 # TODO: johbo: This is part of the business logic and should be moved
2259 # into the RepositoryModel.
2259 # into the RepositoryModel.
2260
2260
2261 if action not in ('push', 'pull'):
2261 if action not in ('push', 'pull'):
2262 raise ValueError("Invalid action value: %s" % repr(action))
2262 raise ValueError("Invalid action value: %s" % repr(action))
2263
2263
2264 # defines if locked error should be thrown to user
2264 # defines if locked error should be thrown to user
2265 currently_locked = False
2265 currently_locked = False
2266 # defines if new lock should be made, tri-state
2266 # defines if new lock should be made, tri-state
2267 make_lock = None
2267 make_lock = None
2268 repo = self
2268 repo = self
2269 user = User.get(user_id)
2269 user = User.get(user_id)
2270
2270
2271 lock_info = repo.locked
2271 lock_info = repo.locked
2272
2272
2273 if repo and (repo.enable_locking or not only_when_enabled):
2273 if repo and (repo.enable_locking or not only_when_enabled):
2274 if action == 'push':
2274 if action == 'push':
2275 # check if it's already locked !, if it is compare users
2275 # check if it's already locked !, if it is compare users
2276 locked_by_user_id = lock_info[0]
2276 locked_by_user_id = lock_info[0]
2277 if user.user_id == locked_by_user_id:
2277 if user.user_id == locked_by_user_id:
2278 log.debug(
2278 log.debug(
2279 'Got `push` action from user %s, now unlocking', user)
2279 'Got `push` action from user %s, now unlocking', user)
2280 # unlock if we have push from user who locked
2280 # unlock if we have push from user who locked
2281 make_lock = False
2281 make_lock = False
2282 else:
2282 else:
2283 # we're not the same user who locked, ban with
2283 # we're not the same user who locked, ban with
2284 # code defined in settings (default is 423 HTTP Locked) !
2284 # code defined in settings (default is 423 HTTP Locked) !
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2286 currently_locked = True
2286 currently_locked = True
2287 elif action == 'pull':
2287 elif action == 'pull':
2288 # [0] user [1] date
2288 # [0] user [1] date
2289 if lock_info[0] and lock_info[1]:
2289 if lock_info[0] and lock_info[1]:
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2291 currently_locked = True
2291 currently_locked = True
2292 else:
2292 else:
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2294 make_lock = True
2294 make_lock = True
2295
2295
2296 else:
2296 else:
2297 log.debug('Repository %s do not have locking enabled', repo)
2297 log.debug('Repository %s do not have locking enabled', repo)
2298
2298
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 make_lock, currently_locked, lock_info)
2300 make_lock, currently_locked, lock_info)
2301
2301
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 # if we don't have at least write permission we cannot make a lock
2305 # if we don't have at least write permission we cannot make a lock
2306 log.debug('lock state reset back to FALSE due to lack '
2306 log.debug('lock state reset back to FALSE due to lack '
2307 'of at least read permission')
2307 'of at least read permission')
2308 make_lock = False
2308 make_lock = False
2309
2309
2310 return make_lock, currently_locked, lock_info
2310 return make_lock, currently_locked, lock_info
2311
2311
2312 @property
2312 @property
2313 def last_commit_cache_update_diff(self):
2313 def last_commit_cache_update_diff(self):
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315
2315
2316 @classmethod
2316 @classmethod
2317 def _load_commit_change(cls, last_commit_cache):
2317 def _load_commit_change(cls, last_commit_cache):
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 empty_date = datetime.datetime.fromtimestamp(0)
2319 empty_date = datetime.datetime.fromtimestamp(0)
2320 date_latest = last_commit_cache.get('date', empty_date)
2320 date_latest = last_commit_cache.get('date', empty_date)
2321 try:
2321 try:
2322 return parse_datetime(date_latest)
2322 return parse_datetime(date_latest)
2323 except Exception:
2323 except Exception:
2324 return empty_date
2324 return empty_date
2325
2325
2326 @property
2326 @property
2327 def last_commit_change(self):
2327 def last_commit_change(self):
2328 return self._load_commit_change(self.changeset_cache)
2328 return self._load_commit_change(self.changeset_cache)
2329
2329
2330 @property
2330 @property
2331 def last_db_change(self):
2331 def last_db_change(self):
2332 return self.updated_on
2332 return self.updated_on
2333
2333
2334 @property
2334 @property
2335 def clone_uri_hidden(self):
2335 def clone_uri_hidden(self):
2336 clone_uri = self.clone_uri
2336 clone_uri = self.clone_uri
2337 if clone_uri:
2337 if clone_uri:
2338 import urlobject
2338 import urlobject
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 if url_obj.password:
2340 if url_obj.password:
2341 clone_uri = url_obj.with_password('*****')
2341 clone_uri = url_obj.with_password('*****')
2342 return clone_uri
2342 return clone_uri
2343
2343
2344 @property
2344 @property
2345 def push_uri_hidden(self):
2345 def push_uri_hidden(self):
2346 push_uri = self.push_uri
2346 push_uri = self.push_uri
2347 if push_uri:
2347 if push_uri:
2348 import urlobject
2348 import urlobject
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 if url_obj.password:
2350 if url_obj.password:
2351 push_uri = url_obj.with_password('*****')
2351 push_uri = url_obj.with_password('*****')
2352 return push_uri
2352 return push_uri
2353
2353
2354 def clone_url(self, **override):
2354 def clone_url(self, **override):
2355 from rhodecode.model.settings import SettingsModel
2355 from rhodecode.model.settings import SettingsModel
2356
2356
2357 uri_tmpl = None
2357 uri_tmpl = None
2358 if 'with_id' in override:
2358 if 'with_id' in override:
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 del override['with_id']
2360 del override['with_id']
2361
2361
2362 if 'uri_tmpl' in override:
2362 if 'uri_tmpl' in override:
2363 uri_tmpl = override['uri_tmpl']
2363 uri_tmpl = override['uri_tmpl']
2364 del override['uri_tmpl']
2364 del override['uri_tmpl']
2365
2365
2366 ssh = False
2366 ssh = False
2367 if 'ssh' in override:
2367 if 'ssh' in override:
2368 ssh = True
2368 ssh = True
2369 del override['ssh']
2369 del override['ssh']
2370
2370
2371 # we didn't override our tmpl from **overrides
2371 # we didn't override our tmpl from **overrides
2372 request = get_current_request()
2372 request = get_current_request()
2373 if not uri_tmpl:
2373 if not uri_tmpl:
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 rc_config = request.call_context.rc_config
2375 rc_config = request.call_context.rc_config
2376 else:
2376 else:
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2378
2378
2379 if ssh:
2379 if ssh:
2380 uri_tmpl = rc_config.get(
2380 uri_tmpl = rc_config.get(
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382
2382
2383 else:
2383 else:
2384 uri_tmpl = rc_config.get(
2384 uri_tmpl = rc_config.get(
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386
2386
2387 return get_clone_url(request=request,
2387 return get_clone_url(request=request,
2388 uri_tmpl=uri_tmpl,
2388 uri_tmpl=uri_tmpl,
2389 repo_name=self.repo_name,
2389 repo_name=self.repo_name,
2390 repo_id=self.repo_id,
2390 repo_id=self.repo_id,
2391 repo_type=self.repo_type,
2391 repo_type=self.repo_type,
2392 **override)
2392 **override)
2393
2393
2394 def set_state(self, state):
2394 def set_state(self, state):
2395 self.repo_state = state
2395 self.repo_state = state
2396 Session().add(self)
2396 Session().add(self)
2397 #==========================================================================
2397 #==========================================================================
2398 # SCM PROPERTIES
2398 # SCM PROPERTIES
2399 #==========================================================================
2399 #==========================================================================
2400
2400
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2402 return get_commit_safe(
2402 return get_commit_safe(
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2405
2405
2406 def get_changeset(self, rev=None, pre_load=None):
2406 def get_changeset(self, rev=None, pre_load=None):
2407 warnings.warn("Use get_commit", DeprecationWarning)
2407 warnings.warn("Use get_commit", DeprecationWarning)
2408 commit_id = None
2408 commit_id = None
2409 commit_idx = None
2409 commit_idx = None
2410 if isinstance(rev, compat.string_types):
2410 if isinstance(rev, compat.string_types):
2411 commit_id = rev
2411 commit_id = rev
2412 else:
2412 else:
2413 commit_idx = rev
2413 commit_idx = rev
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 pre_load=pre_load)
2415 pre_load=pre_load)
2416
2416
2417 def get_landing_commit(self):
2417 def get_landing_commit(self):
2418 """
2418 """
2419 Returns landing commit, or if that doesn't exist returns the tip
2419 Returns landing commit, or if that doesn't exist returns the tip
2420 """
2420 """
2421 _rev_type, _rev = self.landing_rev
2421 _rev_type, _rev = self.landing_rev
2422 commit = self.get_commit(_rev)
2422 commit = self.get_commit(_rev)
2423 if isinstance(commit, EmptyCommit):
2423 if isinstance(commit, EmptyCommit):
2424 return self.get_commit()
2424 return self.get_commit()
2425 return commit
2425 return commit
2426
2426
2427 def flush_commit_cache(self):
2427 def flush_commit_cache(self):
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 self.update_commit_cache()
2429 self.update_commit_cache()
2430
2430
2431 def update_commit_cache(self, cs_cache=None, config=None):
2431 def update_commit_cache(self, cs_cache=None, config=None):
2432 """
2432 """
2433 Update cache of last commit for repository
2433 Update cache of last commit for repository
2434 cache_keys should be::
2434 cache_keys should be::
2435
2435
2436 source_repo_id
2436 source_repo_id
2437 short_id
2437 short_id
2438 raw_id
2438 raw_id
2439 revision
2439 revision
2440 parents
2440 parents
2441 message
2441 message
2442 date
2442 date
2443 author
2443 author
2444 updated_on
2444 updated_on
2445
2445
2446 """
2446 """
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 empty_date = datetime.datetime.fromtimestamp(0)
2449 empty_date = datetime.datetime.fromtimestamp(0)
2450
2450
2451 if cs_cache is None:
2451 if cs_cache is None:
2452 # use no-cache version here
2452 # use no-cache version here
2453 try:
2453 try:
2454 scm_repo = self.scm_instance(cache=False, config=config)
2454 scm_repo = self.scm_instance(cache=False, config=config)
2455 except VCSError:
2455 except VCSError:
2456 scm_repo = None
2456 scm_repo = None
2457 empty = scm_repo is None or scm_repo.is_empty()
2457 empty = scm_repo is None or scm_repo.is_empty()
2458
2458
2459 if not empty:
2459 if not empty:
2460 cs_cache = scm_repo.get_commit(
2460 cs_cache = scm_repo.get_commit(
2461 pre_load=["author", "date", "message", "parents", "branch"])
2461 pre_load=["author", "date", "message", "parents", "branch"])
2462 else:
2462 else:
2463 cs_cache = EmptyCommit()
2463 cs_cache = EmptyCommit()
2464
2464
2465 if isinstance(cs_cache, BaseChangeset):
2465 if isinstance(cs_cache, BaseChangeset):
2466 cs_cache = cs_cache.__json__()
2466 cs_cache = cs_cache.__json__()
2467
2467
2468 def is_outdated(new_cs_cache):
2468 def is_outdated(new_cs_cache):
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 return True
2471 return True
2472 return False
2472 return False
2473
2473
2474 # check if we have maybe already latest cached revision
2474 # check if we have maybe already latest cached revision
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 _current_datetime = datetime.datetime.utcnow()
2476 _current_datetime = datetime.datetime.utcnow()
2477 last_change = cs_cache.get('date') or _current_datetime
2477 last_change = cs_cache.get('date') or _current_datetime
2478 # we check if last update is newer than the new value
2478 # we check if last update is newer than the new value
2479 # if yes, we use the current timestamp instead. Imagine you get
2479 # if yes, we use the current timestamp instead. Imagine you get
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 last_change_timestamp = datetime_to_time(last_change)
2481 last_change_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2483 if last_change_timestamp > current_timestamp and not empty:
2483 if last_change_timestamp > current_timestamp and not empty:
2484 cs_cache['date'] = _current_datetime
2484 cs_cache['date'] = _current_datetime
2485
2485
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2487 cs_cache['updated_on'] = time.time()
2487 cs_cache['updated_on'] = time.time()
2488 self.changeset_cache = cs_cache
2488 self.changeset_cache = cs_cache
2489 self.updated_on = last_change
2489 self.updated_on = last_change
2490 Session().add(self)
2490 Session().add(self)
2491 Session().commit()
2491 Session().commit()
2492
2492
2493 else:
2493 else:
2494 if empty:
2494 if empty:
2495 cs_cache = EmptyCommit().__json__()
2495 cs_cache = EmptyCommit().__json__()
2496 else:
2496 else:
2497 cs_cache = self.changeset_cache
2497 cs_cache = self.changeset_cache
2498
2498
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2500
2500
2501 cs_cache['updated_on'] = time.time()
2501 cs_cache['updated_on'] = time.time()
2502 self.changeset_cache = cs_cache
2502 self.changeset_cache = cs_cache
2503 self.updated_on = _date_latest
2503 self.updated_on = _date_latest
2504 Session().add(self)
2504 Session().add(self)
2505 Session().commit()
2505 Session().commit()
2506
2506
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2508 self.repo_name, cs_cache, _date_latest)
2508 self.repo_name, cs_cache, _date_latest)
2509
2509
2510 @property
2510 @property
2511 def tip(self):
2511 def tip(self):
2512 return self.get_commit('tip')
2512 return self.get_commit('tip')
2513
2513
2514 @property
2514 @property
2515 def author(self):
2515 def author(self):
2516 return self.tip.author
2516 return self.tip.author
2517
2517
2518 @property
2518 @property
2519 def last_change(self):
2519 def last_change(self):
2520 return self.scm_instance().last_change
2520 return self.scm_instance().last_change
2521
2521
2522 def get_comments(self, revisions=None):
2522 def get_comments(self, revisions=None):
2523 """
2523 """
2524 Returns comments for this repository grouped by revisions
2524 Returns comments for this repository grouped by revisions
2525
2525
2526 :param revisions: filter query by revisions only
2526 :param revisions: filter query by revisions only
2527 """
2527 """
2528 cmts = ChangesetComment.query()\
2528 cmts = ChangesetComment.query()\
2529 .filter(ChangesetComment.repo == self)
2529 .filter(ChangesetComment.repo == self)
2530 if revisions:
2530 if revisions:
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2532 grouped = collections.defaultdict(list)
2532 grouped = collections.defaultdict(list)
2533 for cmt in cmts.all():
2533 for cmt in cmts.all():
2534 grouped[cmt.revision].append(cmt)
2534 grouped[cmt.revision].append(cmt)
2535 return grouped
2535 return grouped
2536
2536
2537 def statuses(self, revisions=None):
2537 def statuses(self, revisions=None):
2538 """
2538 """
2539 Returns statuses for this repository
2539 Returns statuses for this repository
2540
2540
2541 :param revisions: list of revisions to get statuses for
2541 :param revisions: list of revisions to get statuses for
2542 """
2542 """
2543 statuses = ChangesetStatus.query()\
2543 statuses = ChangesetStatus.query()\
2544 .filter(ChangesetStatus.repo == self)\
2544 .filter(ChangesetStatus.repo == self)\
2545 .filter(ChangesetStatus.version == 0)
2545 .filter(ChangesetStatus.version == 0)
2546
2546
2547 if revisions:
2547 if revisions:
2548 # Try doing the filtering in chunks to avoid hitting limits
2548 # Try doing the filtering in chunks to avoid hitting limits
2549 size = 500
2549 size = 500
2550 status_results = []
2550 status_results = []
2551 for chunk in xrange(0, len(revisions), size):
2551 for chunk in xrange(0, len(revisions), size):
2552 status_results += statuses.filter(
2552 status_results += statuses.filter(
2553 ChangesetStatus.revision.in_(
2553 ChangesetStatus.revision.in_(
2554 revisions[chunk: chunk+size])
2554 revisions[chunk: chunk+size])
2555 ).all()
2555 ).all()
2556 else:
2556 else:
2557 status_results = statuses.all()
2557 status_results = statuses.all()
2558
2558
2559 grouped = {}
2559 grouped = {}
2560
2560
2561 # maybe we have open new pullrequest without a status?
2561 # maybe we have open new pullrequest without a status?
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2565 for rev in pr.revisions:
2565 for rev in pr.revisions:
2566 pr_id = pr.pull_request_id
2566 pr_id = pr.pull_request_id
2567 pr_repo = pr.target_repo.repo_name
2567 pr_repo = pr.target_repo.repo_name
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2569
2569
2570 for stat in status_results:
2570 for stat in status_results:
2571 pr_id = pr_repo = None
2571 pr_id = pr_repo = None
2572 if stat.pull_request:
2572 if stat.pull_request:
2573 pr_id = stat.pull_request.pull_request_id
2573 pr_id = stat.pull_request.pull_request_id
2574 pr_repo = stat.pull_request.target_repo.repo_name
2574 pr_repo = stat.pull_request.target_repo.repo_name
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2576 pr_id, pr_repo]
2576 pr_id, pr_repo]
2577 return grouped
2577 return grouped
2578
2578
2579 # ==========================================================================
2579 # ==========================================================================
2580 # SCM CACHE INSTANCE
2580 # SCM CACHE INSTANCE
2581 # ==========================================================================
2581 # ==========================================================================
2582
2582
2583 def scm_instance(self, **kwargs):
2583 def scm_instance(self, **kwargs):
2584 import rhodecode
2584 import rhodecode
2585
2585
2586 # Passing a config will not hit the cache currently only used
2586 # Passing a config will not hit the cache currently only used
2587 # for repo2dbmapper
2587 # for repo2dbmapper
2588 config = kwargs.pop('config', None)
2588 config = kwargs.pop('config', None)
2589 cache = kwargs.pop('cache', None)
2589 cache = kwargs.pop('cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2591 if vcs_full_cache is not None:
2591 if vcs_full_cache is not None:
2592 # allows override global config
2592 # allows override global config
2593 full_cache = vcs_full_cache
2593 full_cache = vcs_full_cache
2594 else:
2594 else:
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2596 # if cache is NOT defined use default global, else we have a full
2596 # if cache is NOT defined use default global, else we have a full
2597 # control over cache behaviour
2597 # control over cache behaviour
2598 if cache is None and full_cache and not config:
2598 if cache is None and full_cache and not config:
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2600 return self._get_instance_cached()
2600 return self._get_instance_cached()
2601
2601
2602 # cache here is sent to the "vcs server"
2602 # cache here is sent to the "vcs server"
2603 return self._get_instance(cache=bool(cache), config=config)
2603 return self._get_instance(cache=bool(cache), config=config)
2604
2604
2605 def _get_instance_cached(self):
2605 def _get_instance_cached(self):
2606 from rhodecode.lib import rc_cache
2606 from rhodecode.lib import rc_cache
2607
2607
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2610 repo_id=self.repo_id)
2610 repo_id=self.repo_id)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2612
2612
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2616
2616
2617 # we must use thread scoped cache here,
2617 # we must use thread scoped cache here,
2618 # because each thread of gevent needs it's own not shared connection and cache
2618 # because each thread of gevent needs it's own not shared connection and cache
2619 # we also alter `args` so the cache key is individual for every green thread.
2619 # we also alter `args` so the cache key is individual for every green thread.
2620 inv_context_manager = rc_cache.InvalidationContext(
2620 inv_context_manager = rc_cache.InvalidationContext(
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2622 thread_scoped=True)
2622 thread_scoped=True)
2623 with inv_context_manager as invalidation_context:
2623 with inv_context_manager as invalidation_context:
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2626
2626
2627 # re-compute and store cache if we get invalidate signal
2627 # re-compute and store cache if we get invalidate signal
2628 if invalidation_context.should_invalidate():
2628 if invalidation_context.should_invalidate():
2629 instance = get_instance_cached.refresh(*args)
2629 instance = get_instance_cached.refresh(*args)
2630 else:
2630 else:
2631 instance = get_instance_cached(*args)
2631 instance = get_instance_cached(*args)
2632
2632
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2634 return instance
2634 return instance
2635
2635
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2638 self.repo_type, self.repo_path, cache)
2638 self.repo_type, self.repo_path, cache)
2639 config = config or self._config
2639 config = config or self._config
2640 custom_wire = {
2640 custom_wire = {
2641 'cache': cache, # controls the vcs.remote cache
2641 'cache': cache, # controls the vcs.remote cache
2642 'repo_state_uid': repo_state_uid
2642 'repo_state_uid': repo_state_uid
2643 }
2643 }
2644 repo = get_vcs_instance(
2644 repo = get_vcs_instance(
2645 repo_path=safe_str(self.repo_full_path),
2645 repo_path=safe_str(self.repo_full_path),
2646 config=config,
2646 config=config,
2647 with_wire=custom_wire,
2647 with_wire=custom_wire,
2648 create=False,
2648 create=False,
2649 _vcs_alias=self.repo_type)
2649 _vcs_alias=self.repo_type)
2650 if repo is not None:
2650 if repo is not None:
2651 repo.count() # cache rebuild
2651 repo.count() # cache rebuild
2652 return repo
2652 return repo
2653
2653
2654 def get_shadow_repository_path(self, workspace_id):
2654 def get_shadow_repository_path(self, workspace_id):
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2657 self.repo_full_path, self.repo_id, workspace_id)
2657 self.repo_full_path, self.repo_id, workspace_id)
2658 return shadow_repo_path
2658 return shadow_repo_path
2659
2659
2660 def __json__(self):
2660 def __json__(self):
2661 return {'landing_rev': self.landing_rev}
2661 return {'landing_rev': self.landing_rev}
2662
2662
2663 def get_dict(self):
2663 def get_dict(self):
2664
2664
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2666 # keep compatibility with the code which uses `repo_name` field.
2666 # keep compatibility with the code which uses `repo_name` field.
2667
2667
2668 result = super(Repository, self).get_dict()
2668 result = super(Repository, self).get_dict()
2669 result['repo_name'] = result.pop('_repo_name', None)
2669 result['repo_name'] = result.pop('_repo_name', None)
2670 return result
2670 return result
2671
2671
2672
2672
2673 class RepoGroup(Base, BaseModel):
2673 class RepoGroup(Base, BaseModel):
2674 __tablename__ = 'groups'
2674 __tablename__ = 'groups'
2675 __table_args__ = (
2675 __table_args__ = (
2676 UniqueConstraint('group_name', 'group_parent_id'),
2676 UniqueConstraint('group_name', 'group_parent_id'),
2677 base_table_args,
2677 base_table_args,
2678 )
2678 )
2679 __mapper_args__ = {'order_by': 'group_name'}
2679 __mapper_args__ = {'order_by': 'group_name'}
2680
2680
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2682
2682
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2694
2694
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2698 user = relationship('User')
2698 user = relationship('User')
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2700
2700
2701 # no cascade, set NULL
2701 # no cascade, set NULL
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2703
2703
2704 def __init__(self, group_name='', parent_group=None):
2704 def __init__(self, group_name='', parent_group=None):
2705 self.group_name = group_name
2705 self.group_name = group_name
2706 self.parent_group = parent_group
2706 self.parent_group = parent_group
2707
2707
2708 def __unicode__(self):
2708 def __unicode__(self):
2709 return u"<%s('id:%s:%s')>" % (
2709 return u"<%s('id:%s:%s')>" % (
2710 self.__class__.__name__, self.group_id, self.group_name)
2710 self.__class__.__name__, self.group_id, self.group_name)
2711
2711
2712 @hybrid_property
2712 @hybrid_property
2713 def group_name(self):
2713 def group_name(self):
2714 return self._group_name
2714 return self._group_name
2715
2715
2716 @group_name.setter
2716 @group_name.setter
2717 def group_name(self, value):
2717 def group_name(self, value):
2718 self._group_name = value
2718 self._group_name = value
2719 self.group_name_hash = self.hash_repo_group_name(value)
2719 self.group_name_hash = self.hash_repo_group_name(value)
2720
2720
2721 @classmethod
2721 @classmethod
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2724 dummy = EmptyCommit().__json__()
2724 dummy = EmptyCommit().__json__()
2725 if not changeset_cache_raw:
2725 if not changeset_cache_raw:
2726 dummy['source_repo_id'] = repo_id
2726 dummy['source_repo_id'] = repo_id
2727 return json.loads(json.dumps(dummy))
2727 return json.loads(json.dumps(dummy))
2728
2728
2729 try:
2729 try:
2730 return json.loads(changeset_cache_raw)
2730 return json.loads(changeset_cache_raw)
2731 except TypeError:
2731 except TypeError:
2732 return dummy
2732 return dummy
2733 except Exception:
2733 except Exception:
2734 log.error(traceback.format_exc())
2734 log.error(traceback.format_exc())
2735 return dummy
2735 return dummy
2736
2736
2737 @hybrid_property
2737 @hybrid_property
2738 def changeset_cache(self):
2738 def changeset_cache(self):
2739 return self._load_changeset_cache('', self._changeset_cache)
2739 return self._load_changeset_cache('', self._changeset_cache)
2740
2740
2741 @changeset_cache.setter
2741 @changeset_cache.setter
2742 def changeset_cache(self, val):
2742 def changeset_cache(self, val):
2743 try:
2743 try:
2744 self._changeset_cache = json.dumps(val)
2744 self._changeset_cache = json.dumps(val)
2745 except Exception:
2745 except Exception:
2746 log.error(traceback.format_exc())
2746 log.error(traceback.format_exc())
2747
2747
2748 @validates('group_parent_id')
2748 @validates('group_parent_id')
2749 def validate_group_parent_id(self, key, val):
2749 def validate_group_parent_id(self, key, val):
2750 """
2750 """
2751 Check cycle references for a parent group to self
2751 Check cycle references for a parent group to self
2752 """
2752 """
2753 if self.group_id and val:
2753 if self.group_id and val:
2754 assert val != self.group_id
2754 assert val != self.group_id
2755
2755
2756 return val
2756 return val
2757
2757
2758 @hybrid_property
2758 @hybrid_property
2759 def description_safe(self):
2759 def description_safe(self):
2760 from rhodecode.lib import helpers as h
2760 from rhodecode.lib import helpers as h
2761 return h.escape(self.group_description)
2761 return h.escape(self.group_description)
2762
2762
2763 @classmethod
2763 @classmethod
2764 def hash_repo_group_name(cls, repo_group_name):
2764 def hash_repo_group_name(cls, repo_group_name):
2765 val = remove_formatting(repo_group_name)
2765 val = remove_formatting(repo_group_name)
2766 val = safe_str(val).lower()
2766 val = safe_str(val).lower()
2767 chars = []
2767 chars = []
2768 for c in val:
2768 for c in val:
2769 if c not in string.ascii_letters:
2769 if c not in string.ascii_letters:
2770 c = str(ord(c))
2770 c = str(ord(c))
2771 chars.append(c)
2771 chars.append(c)
2772
2772
2773 return ''.join(chars)
2773 return ''.join(chars)
2774
2774
2775 @classmethod
2775 @classmethod
2776 def _generate_choice(cls, repo_group):
2776 def _generate_choice(cls, repo_group):
2777 from webhelpers2.html import literal as _literal
2777 from webhelpers2.html import literal as _literal
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2780
2780
2781 @classmethod
2781 @classmethod
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2783 if not groups:
2783 if not groups:
2784 groups = cls.query().all()
2784 groups = cls.query().all()
2785
2785
2786 repo_groups = []
2786 repo_groups = []
2787 if show_empty_group:
2787 if show_empty_group:
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2789
2789
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2791
2791
2792 repo_groups = sorted(
2792 repo_groups = sorted(
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2794 return repo_groups
2794 return repo_groups
2795
2795
2796 @classmethod
2796 @classmethod
2797 def url_sep(cls):
2797 def url_sep(cls):
2798 return URL_SEP
2798 return URL_SEP
2799
2799
2800 @classmethod
2800 @classmethod
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2802 if case_insensitive:
2802 if case_insensitive:
2803 gr = cls.query().filter(func.lower(cls.group_name)
2803 gr = cls.query().filter(func.lower(cls.group_name)
2804 == func.lower(group_name))
2804 == func.lower(group_name))
2805 else:
2805 else:
2806 gr = cls.query().filter(cls.group_name == group_name)
2806 gr = cls.query().filter(cls.group_name == group_name)
2807 if cache:
2807 if cache:
2808 name_key = _hash_key(group_name)
2808 name_key = _hash_key(group_name)
2809 gr = gr.options(
2809 gr = gr.options(
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2811 return gr.scalar()
2811 return gr.scalar()
2812
2812
2813 @classmethod
2813 @classmethod
2814 def get_user_personal_repo_group(cls, user_id):
2814 def get_user_personal_repo_group(cls, user_id):
2815 user = User.get(user_id)
2815 user = User.get(user_id)
2816 if user.username == User.DEFAULT_USER:
2816 if user.username == User.DEFAULT_USER:
2817 return None
2817 return None
2818
2818
2819 return cls.query()\
2819 return cls.query()\
2820 .filter(cls.personal == true()) \
2820 .filter(cls.personal == true()) \
2821 .filter(cls.user == user) \
2821 .filter(cls.user == user) \
2822 .order_by(cls.group_id.asc()) \
2822 .order_by(cls.group_id.asc()) \
2823 .first()
2823 .first()
2824
2824
2825 @classmethod
2825 @classmethod
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2827 case_insensitive=True):
2827 case_insensitive=True):
2828 q = RepoGroup.query()
2828 q = RepoGroup.query()
2829
2829
2830 if not isinstance(user_id, Optional):
2830 if not isinstance(user_id, Optional):
2831 q = q.filter(RepoGroup.user_id == user_id)
2831 q = q.filter(RepoGroup.user_id == user_id)
2832
2832
2833 if not isinstance(group_id, Optional):
2833 if not isinstance(group_id, Optional):
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2835
2835
2836 if case_insensitive:
2836 if case_insensitive:
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2838 else:
2838 else:
2839 q = q.order_by(RepoGroup.group_name)
2839 q = q.order_by(RepoGroup.group_name)
2840 return q.all()
2840 return q.all()
2841
2841
2842 @property
2842 @property
2843 def parents(self, parents_recursion_limit=10):
2843 def parents(self, parents_recursion_limit=10):
2844 groups = []
2844 groups = []
2845 if self.parent_group is None:
2845 if self.parent_group is None:
2846 return groups
2846 return groups
2847 cur_gr = self.parent_group
2847 cur_gr = self.parent_group
2848 groups.insert(0, cur_gr)
2848 groups.insert(0, cur_gr)
2849 cnt = 0
2849 cnt = 0
2850 while 1:
2850 while 1:
2851 cnt += 1
2851 cnt += 1
2852 gr = getattr(cur_gr, 'parent_group', None)
2852 gr = getattr(cur_gr, 'parent_group', None)
2853 cur_gr = cur_gr.parent_group
2853 cur_gr = cur_gr.parent_group
2854 if gr is None:
2854 if gr is None:
2855 break
2855 break
2856 if cnt == parents_recursion_limit:
2856 if cnt == parents_recursion_limit:
2857 # this will prevent accidental infinit loops
2857 # this will prevent accidental infinit loops
2858 log.error('more than %s parents found for group %s, stopping '
2858 log.error('more than %s parents found for group %s, stopping '
2859 'recursive parent fetching', parents_recursion_limit, self)
2859 'recursive parent fetching', parents_recursion_limit, self)
2860 break
2860 break
2861
2861
2862 groups.insert(0, gr)
2862 groups.insert(0, gr)
2863 return groups
2863 return groups
2864
2864
2865 @property
2865 @property
2866 def last_commit_cache_update_diff(self):
2866 def last_commit_cache_update_diff(self):
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2868
2868
2869 @classmethod
2869 @classmethod
2870 def _load_commit_change(cls, last_commit_cache):
2870 def _load_commit_change(cls, last_commit_cache):
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2872 empty_date = datetime.datetime.fromtimestamp(0)
2872 empty_date = datetime.datetime.fromtimestamp(0)
2873 date_latest = last_commit_cache.get('date', empty_date)
2873 date_latest = last_commit_cache.get('date', empty_date)
2874 try:
2874 try:
2875 return parse_datetime(date_latest)
2875 return parse_datetime(date_latest)
2876 except Exception:
2876 except Exception:
2877 return empty_date
2877 return empty_date
2878
2878
2879 @property
2879 @property
2880 def last_commit_change(self):
2880 def last_commit_change(self):
2881 return self._load_commit_change(self.changeset_cache)
2881 return self._load_commit_change(self.changeset_cache)
2882
2882
2883 @property
2883 @property
2884 def last_db_change(self):
2884 def last_db_change(self):
2885 return self.updated_on
2885 return self.updated_on
2886
2886
2887 @property
2887 @property
2888 def children(self):
2888 def children(self):
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2890
2890
2891 @property
2891 @property
2892 def name(self):
2892 def name(self):
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2894
2894
2895 @property
2895 @property
2896 def full_path(self):
2896 def full_path(self):
2897 return self.group_name
2897 return self.group_name
2898
2898
2899 @property
2899 @property
2900 def full_path_splitted(self):
2900 def full_path_splitted(self):
2901 return self.group_name.split(RepoGroup.url_sep())
2901 return self.group_name.split(RepoGroup.url_sep())
2902
2902
2903 @property
2903 @property
2904 def repositories(self):
2904 def repositories(self):
2905 return Repository.query()\
2905 return Repository.query()\
2906 .filter(Repository.group == self)\
2906 .filter(Repository.group == self)\
2907 .order_by(Repository.repo_name)
2907 .order_by(Repository.repo_name)
2908
2908
2909 @property
2909 @property
2910 def repositories_recursive_count(self):
2910 def repositories_recursive_count(self):
2911 cnt = self.repositories.count()
2911 cnt = self.repositories.count()
2912
2912
2913 def children_count(group):
2913 def children_count(group):
2914 cnt = 0
2914 cnt = 0
2915 for child in group.children:
2915 for child in group.children:
2916 cnt += child.repositories.count()
2916 cnt += child.repositories.count()
2917 cnt += children_count(child)
2917 cnt += children_count(child)
2918 return cnt
2918 return cnt
2919
2919
2920 return cnt + children_count(self)
2920 return cnt + children_count(self)
2921
2921
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2923 all_ = []
2923 all_ = []
2924
2924
2925 def _get_members(root_gr):
2925 def _get_members(root_gr):
2926 if include_repos:
2926 if include_repos:
2927 for r in root_gr.repositories:
2927 for r in root_gr.repositories:
2928 all_.append(r)
2928 all_.append(r)
2929 childs = root_gr.children.all()
2929 childs = root_gr.children.all()
2930 if childs:
2930 if childs:
2931 for gr in childs:
2931 for gr in childs:
2932 if include_groups:
2932 if include_groups:
2933 all_.append(gr)
2933 all_.append(gr)
2934 _get_members(gr)
2934 _get_members(gr)
2935
2935
2936 root_group = []
2936 root_group = []
2937 if include_groups:
2937 if include_groups:
2938 root_group = [self]
2938 root_group = [self]
2939
2939
2940 _get_members(self)
2940 _get_members(self)
2941 return root_group + all_
2941 return root_group + all_
2942
2942
2943 def recursive_groups_and_repos(self):
2943 def recursive_groups_and_repos(self):
2944 """
2944 """
2945 Recursive return all groups, with repositories in those groups
2945 Recursive return all groups, with repositories in those groups
2946 """
2946 """
2947 return self._recursive_objects()
2947 return self._recursive_objects()
2948
2948
2949 def recursive_groups(self):
2949 def recursive_groups(self):
2950 """
2950 """
2951 Returns all children groups for this group including children of children
2951 Returns all children groups for this group including children of children
2952 """
2952 """
2953 return self._recursive_objects(include_repos=False)
2953 return self._recursive_objects(include_repos=False)
2954
2954
2955 def recursive_repos(self):
2955 def recursive_repos(self):
2956 """
2956 """
2957 Returns all children repositories for this group
2957 Returns all children repositories for this group
2958 """
2958 """
2959 return self._recursive_objects(include_groups=False)
2959 return self._recursive_objects(include_groups=False)
2960
2960
2961 def get_new_name(self, group_name):
2961 def get_new_name(self, group_name):
2962 """
2962 """
2963 returns new full group name based on parent and new name
2963 returns new full group name based on parent and new name
2964
2964
2965 :param group_name:
2965 :param group_name:
2966 """
2966 """
2967 path_prefix = (self.parent_group.full_path_splitted if
2967 path_prefix = (self.parent_group.full_path_splitted if
2968 self.parent_group else [])
2968 self.parent_group else [])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2970
2970
2971 def update_commit_cache(self, config=None):
2971 def update_commit_cache(self, config=None):
2972 """
2972 """
2973 Update cache of last commit for newest repository inside this repository group.
2973 Update cache of last commit for newest repository inside this repository group.
2974 cache_keys should be::
2974 cache_keys should be::
2975
2975
2976 source_repo_id
2976 source_repo_id
2977 short_id
2977 short_id
2978 raw_id
2978 raw_id
2979 revision
2979 revision
2980 parents
2980 parents
2981 message
2981 message
2982 date
2982 date
2983 author
2983 author
2984
2984
2985 """
2985 """
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2987 empty_date = datetime.datetime.fromtimestamp(0)
2987 empty_date = datetime.datetime.fromtimestamp(0)
2988
2988
2989 def repo_groups_and_repos(root_gr):
2989 def repo_groups_and_repos(root_gr):
2990 for _repo in root_gr.repositories:
2990 for _repo in root_gr.repositories:
2991 yield _repo
2991 yield _repo
2992 for child_group in root_gr.children.all():
2992 for child_group in root_gr.children.all():
2993 yield child_group
2993 yield child_group
2994
2994
2995 latest_repo_cs_cache = {}
2995 latest_repo_cs_cache = {}
2996 for obj in repo_groups_and_repos(self):
2996 for obj in repo_groups_and_repos(self):
2997 repo_cs_cache = obj.changeset_cache
2997 repo_cs_cache = obj.changeset_cache
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3002 latest_repo_cs_cache = repo_cs_cache
3002 latest_repo_cs_cache = repo_cs_cache
3003 if hasattr(obj, 'repo_id'):
3003 if hasattr(obj, 'repo_id'):
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3005 else:
3005 else:
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3007
3007
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3009
3009
3010 latest_repo_cs_cache['updated_on'] = time.time()
3010 latest_repo_cs_cache['updated_on'] = time.time()
3011 self.changeset_cache = latest_repo_cs_cache
3011 self.changeset_cache = latest_repo_cs_cache
3012 self.updated_on = _date_latest
3012 self.updated_on = _date_latest
3013 Session().add(self)
3013 Session().add(self)
3014 Session().commit()
3014 Session().commit()
3015
3015
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3018
3018
3019 def permissions(self, with_admins=True, with_owner=True,
3019 def permissions(self, with_admins=True, with_owner=True,
3020 expand_from_user_groups=False):
3020 expand_from_user_groups=False):
3021 """
3021 """
3022 Permissions for repository groups
3022 Permissions for repository groups
3023 """
3023 """
3024 _admin_perm = 'group.admin'
3024 _admin_perm = 'group.admin'
3025
3025
3026 owner_row = []
3026 owner_row = []
3027 if with_owner:
3027 if with_owner:
3028 usr = AttributeDict(self.user.get_dict())
3028 usr = AttributeDict(self.user.get_dict())
3029 usr.owner_row = True
3029 usr.owner_row = True
3030 usr.permission = _admin_perm
3030 usr.permission = _admin_perm
3031 owner_row.append(usr)
3031 owner_row.append(usr)
3032
3032
3033 super_admin_ids = []
3033 super_admin_ids = []
3034 super_admin_rows = []
3034 super_admin_rows = []
3035 if with_admins:
3035 if with_admins:
3036 for usr in User.get_all_super_admins():
3036 for usr in User.get_all_super_admins():
3037 super_admin_ids.append(usr.user_id)
3037 super_admin_ids.append(usr.user_id)
3038 # if this admin is also owner, don't double the record
3038 # if this admin is also owner, don't double the record
3039 if usr.user_id == owner_row[0].user_id:
3039 if usr.user_id == owner_row[0].user_id:
3040 owner_row[0].admin_row = True
3040 owner_row[0].admin_row = True
3041 else:
3041 else:
3042 usr = AttributeDict(usr.get_dict())
3042 usr = AttributeDict(usr.get_dict())
3043 usr.admin_row = True
3043 usr.admin_row = True
3044 usr.permission = _admin_perm
3044 usr.permission = _admin_perm
3045 super_admin_rows.append(usr)
3045 super_admin_rows.append(usr)
3046
3046
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3049 joinedload(UserRepoGroupToPerm.user),
3049 joinedload(UserRepoGroupToPerm.user),
3050 joinedload(UserRepoGroupToPerm.permission),)
3050 joinedload(UserRepoGroupToPerm.permission),)
3051
3051
3052 # get owners and admins and permissions. We do a trick of re-writing
3052 # get owners and admins and permissions. We do a trick of re-writing
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3054 # has a global reference and changing one object propagates to all
3054 # has a global reference and changing one object propagates to all
3055 # others. This means if admin is also an owner admin_row that change
3055 # others. This means if admin is also an owner admin_row that change
3056 # would propagate to both objects
3056 # would propagate to both objects
3057 perm_rows = []
3057 perm_rows = []
3058 for _usr in q.all():
3058 for _usr in q.all():
3059 usr = AttributeDict(_usr.user.get_dict())
3059 usr = AttributeDict(_usr.user.get_dict())
3060 # if this user is also owner/admin, mark as duplicate record
3060 # if this user is also owner/admin, mark as duplicate record
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3062 usr.duplicate_perm = True
3062 usr.duplicate_perm = True
3063 usr.permission = _usr.permission.permission_name
3063 usr.permission = _usr.permission.permission_name
3064 perm_rows.append(usr)
3064 perm_rows.append(usr)
3065
3065
3066 # filter the perm rows by 'default' first and then sort them by
3066 # filter the perm rows by 'default' first and then sort them by
3067 # admin,write,read,none permissions sorted again alphabetically in
3067 # admin,write,read,none permissions sorted again alphabetically in
3068 # each group
3068 # each group
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3070
3070
3071 user_groups_rows = []
3071 user_groups_rows = []
3072 if expand_from_user_groups:
3072 if expand_from_user_groups:
3073 for ug in self.permission_user_groups(with_members=True):
3073 for ug in self.permission_user_groups(with_members=True):
3074 for user_data in ug.members:
3074 for user_data in ug.members:
3075 user_groups_rows.append(user_data)
3075 user_groups_rows.append(user_data)
3076
3076
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3078
3078
3079 def permission_user_groups(self, with_members=False):
3079 def permission_user_groups(self, with_members=False):
3080 q = UserGroupRepoGroupToPerm.query()\
3080 q = UserGroupRepoGroupToPerm.query()\
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3085
3085
3086 perm_rows = []
3086 perm_rows = []
3087 for _user_group in q.all():
3087 for _user_group in q.all():
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3089 entry.permission = _user_group.permission.permission_name
3089 entry.permission = _user_group.permission.permission_name
3090 if with_members:
3090 if with_members:
3091 entry.members = [x.user.get_dict()
3091 entry.members = [x.user.get_dict()
3092 for x in _user_group.users_group.members]
3092 for x in _user_group.users_group.members]
3093 perm_rows.append(entry)
3093 perm_rows.append(entry)
3094
3094
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3096 return perm_rows
3096 return perm_rows
3097
3097
3098 def get_api_data(self):
3098 def get_api_data(self):
3099 """
3099 """
3100 Common function for generating api data
3100 Common function for generating api data
3101
3101
3102 """
3102 """
3103 group = self
3103 group = self
3104 data = {
3104 data = {
3105 'group_id': group.group_id,
3105 'group_id': group.group_id,
3106 'group_name': group.group_name,
3106 'group_name': group.group_name,
3107 'group_description': group.description_safe,
3107 'group_description': group.description_safe,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3109 'repositories': [x.repo_name for x in group.repositories],
3109 'repositories': [x.repo_name for x in group.repositories],
3110 'owner': group.user.username,
3110 'owner': group.user.username,
3111 }
3111 }
3112 return data
3112 return data
3113
3113
3114 def get_dict(self):
3114 def get_dict(self):
3115 # Since we transformed `group_name` to a hybrid property, we need to
3115 # Since we transformed `group_name` to a hybrid property, we need to
3116 # keep compatibility with the code which uses `group_name` field.
3116 # keep compatibility with the code which uses `group_name` field.
3117 result = super(RepoGroup, self).get_dict()
3117 result = super(RepoGroup, self).get_dict()
3118 result['group_name'] = result.pop('_group_name', None)
3118 result['group_name'] = result.pop('_group_name', None)
3119 return result
3119 return result
3120
3120
3121
3121
3122 class Permission(Base, BaseModel):
3122 class Permission(Base, BaseModel):
3123 __tablename__ = 'permissions'
3123 __tablename__ = 'permissions'
3124 __table_args__ = (
3124 __table_args__ = (
3125 Index('p_perm_name_idx', 'permission_name'),
3125 Index('p_perm_name_idx', 'permission_name'),
3126 base_table_args,
3126 base_table_args,
3127 )
3127 )
3128
3128
3129 PERMS = [
3129 PERMS = [
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3131
3131
3132 ('repository.none', _('Repository no access')),
3132 ('repository.none', _('Repository no access')),
3133 ('repository.read', _('Repository read access')),
3133 ('repository.read', _('Repository read access')),
3134 ('repository.write', _('Repository write access')),
3134 ('repository.write', _('Repository write access')),
3135 ('repository.admin', _('Repository admin access')),
3135 ('repository.admin', _('Repository admin access')),
3136
3136
3137 ('group.none', _('Repository group no access')),
3137 ('group.none', _('Repository group no access')),
3138 ('group.read', _('Repository group read access')),
3138 ('group.read', _('Repository group read access')),
3139 ('group.write', _('Repository group write access')),
3139 ('group.write', _('Repository group write access')),
3140 ('group.admin', _('Repository group admin access')),
3140 ('group.admin', _('Repository group admin access')),
3141
3141
3142 ('usergroup.none', _('User group no access')),
3142 ('usergroup.none', _('User group no access')),
3143 ('usergroup.read', _('User group read access')),
3143 ('usergroup.read', _('User group read access')),
3144 ('usergroup.write', _('User group write access')),
3144 ('usergroup.write', _('User group write access')),
3145 ('usergroup.admin', _('User group admin access')),
3145 ('usergroup.admin', _('User group admin access')),
3146
3146
3147 ('branch.none', _('Branch no permissions')),
3147 ('branch.none', _('Branch no permissions')),
3148 ('branch.merge', _('Branch access by web merge')),
3148 ('branch.merge', _('Branch access by web merge')),
3149 ('branch.push', _('Branch access by push')),
3149 ('branch.push', _('Branch access by push')),
3150 ('branch.push_force', _('Branch access by push with force')),
3150 ('branch.push_force', _('Branch access by push with force')),
3151
3151
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3154
3154
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3157
3157
3158 ('hg.create.none', _('Repository creation disabled')),
3158 ('hg.create.none', _('Repository creation disabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3162
3162
3163 ('hg.fork.none', _('Repository forking disabled')),
3163 ('hg.fork.none', _('Repository forking disabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3165
3165
3166 ('hg.register.none', _('Registration disabled')),
3166 ('hg.register.none', _('Registration disabled')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3169
3169
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3173
3173
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3176
3176
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3179 ]
3179 ]
3180
3180
3181 # definition of system default permissions for DEFAULT user, created on
3181 # definition of system default permissions for DEFAULT user, created on
3182 # system setup
3182 # system setup
3183 DEFAULT_USER_PERMISSIONS = [
3183 DEFAULT_USER_PERMISSIONS = [
3184 # object perms
3184 # object perms
3185 'repository.read',
3185 'repository.read',
3186 'group.read',
3186 'group.read',
3187 'usergroup.read',
3187 'usergroup.read',
3188 # branch, for backward compat we need same value as before so forced pushed
3188 # branch, for backward compat we need same value as before so forced pushed
3189 'branch.push_force',
3189 'branch.push_force',
3190 # global
3190 # global
3191 'hg.create.repository',
3191 'hg.create.repository',
3192 'hg.repogroup.create.false',
3192 'hg.repogroup.create.false',
3193 'hg.usergroup.create.false',
3193 'hg.usergroup.create.false',
3194 'hg.create.write_on_repogroup.true',
3194 'hg.create.write_on_repogroup.true',
3195 'hg.fork.repository',
3195 'hg.fork.repository',
3196 'hg.register.manual_activate',
3196 'hg.register.manual_activate',
3197 'hg.password_reset.enabled',
3197 'hg.password_reset.enabled',
3198 'hg.extern_activate.auto',
3198 'hg.extern_activate.auto',
3199 'hg.inherit_default_perms.true',
3199 'hg.inherit_default_perms.true',
3200 ]
3200 ]
3201
3201
3202 # defines which permissions are more important higher the more important
3202 # defines which permissions are more important higher the more important
3203 # Weight defines which permissions are more important.
3203 # Weight defines which permissions are more important.
3204 # The higher number the more important.
3204 # The higher number the more important.
3205 PERM_WEIGHTS = {
3205 PERM_WEIGHTS = {
3206 'repository.none': 0,
3206 'repository.none': 0,
3207 'repository.read': 1,
3207 'repository.read': 1,
3208 'repository.write': 3,
3208 'repository.write': 3,
3209 'repository.admin': 4,
3209 'repository.admin': 4,
3210
3210
3211 'group.none': 0,
3211 'group.none': 0,
3212 'group.read': 1,
3212 'group.read': 1,
3213 'group.write': 3,
3213 'group.write': 3,
3214 'group.admin': 4,
3214 'group.admin': 4,
3215
3215
3216 'usergroup.none': 0,
3216 'usergroup.none': 0,
3217 'usergroup.read': 1,
3217 'usergroup.read': 1,
3218 'usergroup.write': 3,
3218 'usergroup.write': 3,
3219 'usergroup.admin': 4,
3219 'usergroup.admin': 4,
3220
3220
3221 'branch.none': 0,
3221 'branch.none': 0,
3222 'branch.merge': 1,
3222 'branch.merge': 1,
3223 'branch.push': 3,
3223 'branch.push': 3,
3224 'branch.push_force': 4,
3224 'branch.push_force': 4,
3225
3225
3226 'hg.repogroup.create.false': 0,
3226 'hg.repogroup.create.false': 0,
3227 'hg.repogroup.create.true': 1,
3227 'hg.repogroup.create.true': 1,
3228
3228
3229 'hg.usergroup.create.false': 0,
3229 'hg.usergroup.create.false': 0,
3230 'hg.usergroup.create.true': 1,
3230 'hg.usergroup.create.true': 1,
3231
3231
3232 'hg.fork.none': 0,
3232 'hg.fork.none': 0,
3233 'hg.fork.repository': 1,
3233 'hg.fork.repository': 1,
3234 'hg.create.none': 0,
3234 'hg.create.none': 0,
3235 'hg.create.repository': 1
3235 'hg.create.repository': 1
3236 }
3236 }
3237
3237
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3241
3241
3242 def __unicode__(self):
3242 def __unicode__(self):
3243 return u"<%s('%s:%s')>" % (
3243 return u"<%s('%s:%s')>" % (
3244 self.__class__.__name__, self.permission_id, self.permission_name
3244 self.__class__.__name__, self.permission_id, self.permission_name
3245 )
3245 )
3246
3246
3247 @classmethod
3247 @classmethod
3248 def get_by_key(cls, key):
3248 def get_by_key(cls, key):
3249 return cls.query().filter(cls.permission_name == key).scalar()
3249 return cls.query().filter(cls.permission_name == key).scalar()
3250
3250
3251 @classmethod
3251 @classmethod
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3256 .filter(UserRepoToPerm.user_id == user_id)
3256 .filter(UserRepoToPerm.user_id == user_id)
3257 if repo_id:
3257 if repo_id:
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3259 return q.all()
3259 return q.all()
3260
3260
3261 @classmethod
3261 @classmethod
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3264 .join(
3264 .join(
3265 Permission,
3265 Permission,
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3267 .join(
3267 .join(
3268 UserRepoToPerm,
3268 UserRepoToPerm,
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3270 .filter(UserRepoToPerm.user_id == user_id)
3270 .filter(UserRepoToPerm.user_id == user_id)
3271
3271
3272 if repo_id:
3272 if repo_id:
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3275
3275
3276 @classmethod
3276 @classmethod
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3279 .join(
3279 .join(
3280 Permission,
3280 Permission,
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3282 .join(
3282 .join(
3283 Repository,
3283 Repository,
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3285 .join(
3285 .join(
3286 UserGroup,
3286 UserGroup,
3287 UserGroupRepoToPerm.users_group_id ==
3287 UserGroupRepoToPerm.users_group_id ==
3288 UserGroup.users_group_id)\
3288 UserGroup.users_group_id)\
3289 .join(
3289 .join(
3290 UserGroupMember,
3290 UserGroupMember,
3291 UserGroupRepoToPerm.users_group_id ==
3291 UserGroupRepoToPerm.users_group_id ==
3292 UserGroupMember.users_group_id)\
3292 UserGroupMember.users_group_id)\
3293 .filter(
3293 .filter(
3294 UserGroupMember.user_id == user_id,
3294 UserGroupMember.user_id == user_id,
3295 UserGroup.users_group_active == true())
3295 UserGroup.users_group_active == true())
3296 if repo_id:
3296 if repo_id:
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3298 return q.all()
3298 return q.all()
3299
3299
3300 @classmethod
3300 @classmethod
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3303 .join(
3303 .join(
3304 Permission,
3304 Permission,
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3306 .join(
3306 .join(
3307 UserGroupRepoToPerm,
3307 UserGroupRepoToPerm,
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3309 .join(
3309 .join(
3310 UserGroup,
3310 UserGroup,
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3312 .join(
3312 .join(
3313 UserGroupMember,
3313 UserGroupMember,
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3315 .filter(
3315 .filter(
3316 UserGroupMember.user_id == user_id,
3316 UserGroupMember.user_id == user_id,
3317 UserGroup.users_group_active == true())
3317 UserGroup.users_group_active == true())
3318
3318
3319 if repo_id:
3319 if repo_id:
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3322
3322
3323 @classmethod
3323 @classmethod
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3326 .join(
3326 .join(
3327 Permission,
3327 Permission,
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3329 .join(
3329 .join(
3330 RepoGroup,
3330 RepoGroup,
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3333 if repo_group_id:
3333 if repo_group_id:
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3335 return q.all()
3335 return q.all()
3336
3336
3337 @classmethod
3337 @classmethod
3338 def get_default_group_perms_from_user_group(
3338 def get_default_group_perms_from_user_group(
3339 cls, user_id, repo_group_id=None):
3339 cls, user_id, repo_group_id=None):
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3341 .join(
3341 .join(
3342 Permission,
3342 Permission,
3343 UserGroupRepoGroupToPerm.permission_id ==
3343 UserGroupRepoGroupToPerm.permission_id ==
3344 Permission.permission_id)\
3344 Permission.permission_id)\
3345 .join(
3345 .join(
3346 RepoGroup,
3346 RepoGroup,
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3348 .join(
3348 .join(
3349 UserGroup,
3349 UserGroup,
3350 UserGroupRepoGroupToPerm.users_group_id ==
3350 UserGroupRepoGroupToPerm.users_group_id ==
3351 UserGroup.users_group_id)\
3351 UserGroup.users_group_id)\
3352 .join(
3352 .join(
3353 UserGroupMember,
3353 UserGroupMember,
3354 UserGroupRepoGroupToPerm.users_group_id ==
3354 UserGroupRepoGroupToPerm.users_group_id ==
3355 UserGroupMember.users_group_id)\
3355 UserGroupMember.users_group_id)\
3356 .filter(
3356 .filter(
3357 UserGroupMember.user_id == user_id,
3357 UserGroupMember.user_id == user_id,
3358 UserGroup.users_group_active == true())
3358 UserGroup.users_group_active == true())
3359 if repo_group_id:
3359 if repo_group_id:
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3361 return q.all()
3361 return q.all()
3362
3362
3363 @classmethod
3363 @classmethod
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3369 if user_group_id:
3369 if user_group_id:
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3371 return q.all()
3371 return q.all()
3372
3372
3373 @classmethod
3373 @classmethod
3374 def get_default_user_group_perms_from_user_group(
3374 def get_default_user_group_perms_from_user_group(
3375 cls, user_id, user_group_id=None):
3375 cls, user_id, user_group_id=None):
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3378 .join(
3378 .join(
3379 Permission,
3379 Permission,
3380 UserGroupUserGroupToPerm.permission_id ==
3380 UserGroupUserGroupToPerm.permission_id ==
3381 Permission.permission_id)\
3381 Permission.permission_id)\
3382 .join(
3382 .join(
3383 TargetUserGroup,
3383 TargetUserGroup,
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3385 TargetUserGroup.users_group_id)\
3385 TargetUserGroup.users_group_id)\
3386 .join(
3386 .join(
3387 UserGroup,
3387 UserGroup,
3388 UserGroupUserGroupToPerm.user_group_id ==
3388 UserGroupUserGroupToPerm.user_group_id ==
3389 UserGroup.users_group_id)\
3389 UserGroup.users_group_id)\
3390 .join(
3390 .join(
3391 UserGroupMember,
3391 UserGroupMember,
3392 UserGroupUserGroupToPerm.user_group_id ==
3392 UserGroupUserGroupToPerm.user_group_id ==
3393 UserGroupMember.users_group_id)\
3393 UserGroupMember.users_group_id)\
3394 .filter(
3394 .filter(
3395 UserGroupMember.user_id == user_id,
3395 UserGroupMember.user_id == user_id,
3396 UserGroup.users_group_active == true())
3396 UserGroup.users_group_active == true())
3397 if user_group_id:
3397 if user_group_id:
3398 q = q.filter(
3398 q = q.filter(
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3400
3400
3401 return q.all()
3401 return q.all()
3402
3402
3403
3403
3404 class UserRepoToPerm(Base, BaseModel):
3404 class UserRepoToPerm(Base, BaseModel):
3405 __tablename__ = 'repo_to_perm'
3405 __tablename__ = 'repo_to_perm'
3406 __table_args__ = (
3406 __table_args__ = (
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3408 base_table_args
3408 base_table_args
3409 )
3409 )
3410
3410
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3415
3415
3416 user = relationship('User')
3416 user = relationship('User')
3417 repository = relationship('Repository')
3417 repository = relationship('Repository')
3418 permission = relationship('Permission')
3418 permission = relationship('Permission')
3419
3419
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3421
3421
3422 @classmethod
3422 @classmethod
3423 def create(cls, user, repository, permission):
3423 def create(cls, user, repository, permission):
3424 n = cls()
3424 n = cls()
3425 n.user = user
3425 n.user = user
3426 n.repository = repository
3426 n.repository = repository
3427 n.permission = permission
3427 n.permission = permission
3428 Session().add(n)
3428 Session().add(n)
3429 return n
3429 return n
3430
3430
3431 def __unicode__(self):
3431 def __unicode__(self):
3432 return u'<%s => %s >' % (self.user, self.repository)
3432 return u'<%s => %s >' % (self.user, self.repository)
3433
3433
3434
3434
3435 class UserUserGroupToPerm(Base, BaseModel):
3435 class UserUserGroupToPerm(Base, BaseModel):
3436 __tablename__ = 'user_user_group_to_perm'
3436 __tablename__ = 'user_user_group_to_perm'
3437 __table_args__ = (
3437 __table_args__ = (
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3439 base_table_args
3439 base_table_args
3440 )
3440 )
3441
3441
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3446
3446
3447 user = relationship('User')
3447 user = relationship('User')
3448 user_group = relationship('UserGroup')
3448 user_group = relationship('UserGroup')
3449 permission = relationship('Permission')
3449 permission = relationship('Permission')
3450
3450
3451 @classmethod
3451 @classmethod
3452 def create(cls, user, user_group, permission):
3452 def create(cls, user, user_group, permission):
3453 n = cls()
3453 n = cls()
3454 n.user = user
3454 n.user = user
3455 n.user_group = user_group
3455 n.user_group = user_group
3456 n.permission = permission
3456 n.permission = permission
3457 Session().add(n)
3457 Session().add(n)
3458 return n
3458 return n
3459
3459
3460 def __unicode__(self):
3460 def __unicode__(self):
3461 return u'<%s => %s >' % (self.user, self.user_group)
3461 return u'<%s => %s >' % (self.user, self.user_group)
3462
3462
3463
3463
3464 class UserToPerm(Base, BaseModel):
3464 class UserToPerm(Base, BaseModel):
3465 __tablename__ = 'user_to_perm'
3465 __tablename__ = 'user_to_perm'
3466 __table_args__ = (
3466 __table_args__ = (
3467 UniqueConstraint('user_id', 'permission_id'),
3467 UniqueConstraint('user_id', 'permission_id'),
3468 base_table_args
3468 base_table_args
3469 )
3469 )
3470
3470
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3474
3474
3475 user = relationship('User')
3475 user = relationship('User')
3476 permission = relationship('Permission', lazy='joined')
3476 permission = relationship('Permission', lazy='joined')
3477
3477
3478 def __unicode__(self):
3478 def __unicode__(self):
3479 return u'<%s => %s >' % (self.user, self.permission)
3479 return u'<%s => %s >' % (self.user, self.permission)
3480
3480
3481
3481
3482 class UserGroupRepoToPerm(Base, BaseModel):
3482 class UserGroupRepoToPerm(Base, BaseModel):
3483 __tablename__ = 'users_group_repo_to_perm'
3483 __tablename__ = 'users_group_repo_to_perm'
3484 __table_args__ = (
3484 __table_args__ = (
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3486 base_table_args
3486 base_table_args
3487 )
3487 )
3488
3488
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3493
3493
3494 users_group = relationship('UserGroup')
3494 users_group = relationship('UserGroup')
3495 permission = relationship('Permission')
3495 permission = relationship('Permission')
3496 repository = relationship('Repository')
3496 repository = relationship('Repository')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3498
3498
3499 @classmethod
3499 @classmethod
3500 def create(cls, users_group, repository, permission):
3500 def create(cls, users_group, repository, permission):
3501 n = cls()
3501 n = cls()
3502 n.users_group = users_group
3502 n.users_group = users_group
3503 n.repository = repository
3503 n.repository = repository
3504 n.permission = permission
3504 n.permission = permission
3505 Session().add(n)
3505 Session().add(n)
3506 return n
3506 return n
3507
3507
3508 def __unicode__(self):
3508 def __unicode__(self):
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3510
3510
3511
3511
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3513 __tablename__ = 'user_group_user_group_to_perm'
3513 __tablename__ = 'user_group_user_group_to_perm'
3514 __table_args__ = (
3514 __table_args__ = (
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3517 base_table_args
3517 base_table_args
3518 )
3518 )
3519
3519
3520 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3520 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3524
3524
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3527 permission = relationship('Permission')
3527 permission = relationship('Permission')
3528
3528
3529 @classmethod
3529 @classmethod
3530 def create(cls, target_user_group, user_group, permission):
3530 def create(cls, target_user_group, user_group, permission):
3531 n = cls()
3531 n = cls()
3532 n.target_user_group = target_user_group
3532 n.target_user_group = target_user_group
3533 n.user_group = user_group
3533 n.user_group = user_group
3534 n.permission = permission
3534 n.permission = permission
3535 Session().add(n)
3535 Session().add(n)
3536 return n
3536 return n
3537
3537
3538 def __unicode__(self):
3538 def __unicode__(self):
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3540
3540
3541
3541
3542 class UserGroupToPerm(Base, BaseModel):
3542 class UserGroupToPerm(Base, BaseModel):
3543 __tablename__ = 'users_group_to_perm'
3543 __tablename__ = 'users_group_to_perm'
3544 __table_args__ = (
3544 __table_args__ = (
3545 UniqueConstraint('users_group_id', 'permission_id',),
3545 UniqueConstraint('users_group_id', 'permission_id',),
3546 base_table_args
3546 base_table_args
3547 )
3547 )
3548
3548
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3552
3552
3553 users_group = relationship('UserGroup')
3553 users_group = relationship('UserGroup')
3554 permission = relationship('Permission')
3554 permission = relationship('Permission')
3555
3555
3556
3556
3557 class UserRepoGroupToPerm(Base, BaseModel):
3557 class UserRepoGroupToPerm(Base, BaseModel):
3558 __tablename__ = 'user_repo_group_to_perm'
3558 __tablename__ = 'user_repo_group_to_perm'
3559 __table_args__ = (
3559 __table_args__ = (
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3561 base_table_args
3561 base_table_args
3562 )
3562 )
3563
3563
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3568
3568
3569 user = relationship('User')
3569 user = relationship('User')
3570 group = relationship('RepoGroup')
3570 group = relationship('RepoGroup')
3571 permission = relationship('Permission')
3571 permission = relationship('Permission')
3572
3572
3573 @classmethod
3573 @classmethod
3574 def create(cls, user, repository_group, permission):
3574 def create(cls, user, repository_group, permission):
3575 n = cls()
3575 n = cls()
3576 n.user = user
3576 n.user = user
3577 n.group = repository_group
3577 n.group = repository_group
3578 n.permission = permission
3578 n.permission = permission
3579 Session().add(n)
3579 Session().add(n)
3580 return n
3580 return n
3581
3581
3582
3582
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3584 __tablename__ = 'users_group_repo_group_to_perm'
3584 __tablename__ = 'users_group_repo_group_to_perm'
3585 __table_args__ = (
3585 __table_args__ = (
3586 UniqueConstraint('users_group_id', 'group_id'),
3586 UniqueConstraint('users_group_id', 'group_id'),
3587 base_table_args
3587 base_table_args
3588 )
3588 )
3589
3589
3590 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3590 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3594
3594
3595 users_group = relationship('UserGroup')
3595 users_group = relationship('UserGroup')
3596 permission = relationship('Permission')
3596 permission = relationship('Permission')
3597 group = relationship('RepoGroup')
3597 group = relationship('RepoGroup')
3598
3598
3599 @classmethod
3599 @classmethod
3600 def create(cls, user_group, repository_group, permission):
3600 def create(cls, user_group, repository_group, permission):
3601 n = cls()
3601 n = cls()
3602 n.users_group = user_group
3602 n.users_group = user_group
3603 n.group = repository_group
3603 n.group = repository_group
3604 n.permission = permission
3604 n.permission = permission
3605 Session().add(n)
3605 Session().add(n)
3606 return n
3606 return n
3607
3607
3608 def __unicode__(self):
3608 def __unicode__(self):
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3610
3610
3611
3611
3612 class Statistics(Base, BaseModel):
3612 class Statistics(Base, BaseModel):
3613 __tablename__ = 'statistics'
3613 __tablename__ = 'statistics'
3614 __table_args__ = (
3614 __table_args__ = (
3615 base_table_args
3615 base_table_args
3616 )
3616 )
3617
3617
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3624
3624
3625 repository = relationship('Repository', single_parent=True)
3625 repository = relationship('Repository', single_parent=True)
3626
3626
3627
3627
3628 class UserFollowing(Base, BaseModel):
3628 class UserFollowing(Base, BaseModel):
3629 __tablename__ = 'user_followings'
3629 __tablename__ = 'user_followings'
3630 __table_args__ = (
3630 __table_args__ = (
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3633 base_table_args
3633 base_table_args
3634 )
3634 )
3635
3635
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3641
3641
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3643
3643
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3646
3646
3647 @classmethod
3647 @classmethod
3648 def get_repo_followers(cls, repo_id):
3648 def get_repo_followers(cls, repo_id):
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3650
3650
3651
3651
3652 class CacheKey(Base, BaseModel):
3652 class CacheKey(Base, BaseModel):
3653 __tablename__ = 'cache_invalidation'
3653 __tablename__ = 'cache_invalidation'
3654 __table_args__ = (
3654 __table_args__ = (
3655 UniqueConstraint('cache_key'),
3655 UniqueConstraint('cache_key'),
3656 Index('key_idx', 'cache_key'),
3656 Index('key_idx', 'cache_key'),
3657 base_table_args,
3657 base_table_args,
3658 )
3658 )
3659
3659
3660 CACHE_TYPE_FEED = 'FEED'
3660 CACHE_TYPE_FEED = 'FEED'
3661
3661
3662 # namespaces used to register process/thread aware caches
3662 # namespaces used to register process/thread aware caches
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3665
3665
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3671
3671
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3673 self.cache_key = cache_key
3673 self.cache_key = cache_key
3674 self.cache_args = cache_args
3674 self.cache_args = cache_args
3675 self.cache_active = False
3675 self.cache_active = False
3676 # first key should be same for all entries, since all workers should share it
3676 # first key should be same for all entries, since all workers should share it
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3678
3678
3679 def __unicode__(self):
3679 def __unicode__(self):
3680 return u"<%s('%s:%s[%s]')>" % (
3680 return u"<%s('%s:%s[%s]')>" % (
3681 self.__class__.__name__,
3681 self.__class__.__name__,
3682 self.cache_id, self.cache_key, self.cache_active)
3682 self.cache_id, self.cache_key, self.cache_active)
3683
3683
3684 def _cache_key_partition(self):
3684 def _cache_key_partition(self):
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3686 return prefix, repo_name, suffix
3686 return prefix, repo_name, suffix
3687
3687
3688 def get_prefix(self):
3688 def get_prefix(self):
3689 """
3689 """
3690 Try to extract prefix from existing cache key. The key could consist
3690 Try to extract prefix from existing cache key. The key could consist
3691 of prefix, repo_name, suffix
3691 of prefix, repo_name, suffix
3692 """
3692 """
3693 # this returns prefix, repo_name, suffix
3693 # this returns prefix, repo_name, suffix
3694 return self._cache_key_partition()[0]
3694 return self._cache_key_partition()[0]
3695
3695
3696 def get_suffix(self):
3696 def get_suffix(self):
3697 """
3697 """
3698 get suffix that might have been used in _get_cache_key to
3698 get suffix that might have been used in _get_cache_key to
3699 generate self.cache_key. Only used for informational purposes
3699 generate self.cache_key. Only used for informational purposes
3700 in repo_edit.mako.
3700 in repo_edit.mako.
3701 """
3701 """
3702 # prefix, repo_name, suffix
3702 # prefix, repo_name, suffix
3703 return self._cache_key_partition()[2]
3703 return self._cache_key_partition()[2]
3704
3704
3705 @classmethod
3705 @classmethod
3706 def generate_new_state_uid(cls, based_on=None):
3706 def generate_new_state_uid(cls, based_on=None):
3707 if based_on:
3707 if based_on:
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3709 else:
3709 else:
3710 return str(uuid.uuid4())
3710 return str(uuid.uuid4())
3711
3711
3712 @classmethod
3712 @classmethod
3713 def delete_all_cache(cls):
3713 def delete_all_cache(cls):
3714 """
3714 """
3715 Delete all cache keys from database.
3715 Delete all cache keys from database.
3716 Should only be run when all instances are down and all entries
3716 Should only be run when all instances are down and all entries
3717 thus stale.
3717 thus stale.
3718 """
3718 """
3719 cls.query().delete()
3719 cls.query().delete()
3720 Session().commit()
3720 Session().commit()
3721
3721
3722 @classmethod
3722 @classmethod
3723 def set_invalidate(cls, cache_uid, delete=False):
3723 def set_invalidate(cls, cache_uid, delete=False):
3724 """
3724 """
3725 Mark all caches of a repo as invalid in the database.
3725 Mark all caches of a repo as invalid in the database.
3726 """
3726 """
3727
3727
3728 try:
3728 try:
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3730 if delete:
3730 if delete:
3731 qry.delete()
3731 qry.delete()
3732 log.debug('cache objects deleted for cache args %s',
3732 log.debug('cache objects deleted for cache args %s',
3733 safe_str(cache_uid))
3733 safe_str(cache_uid))
3734 else:
3734 else:
3735 qry.update({"cache_active": False,
3735 qry.update({"cache_active": False,
3736 "cache_state_uid": cls.generate_new_state_uid()})
3736 "cache_state_uid": cls.generate_new_state_uid()})
3737 log.debug('cache objects marked as invalid for cache args %s',
3737 log.debug('cache objects marked as invalid for cache args %s',
3738 safe_str(cache_uid))
3738 safe_str(cache_uid))
3739
3739
3740 Session().commit()
3740 Session().commit()
3741 except Exception:
3741 except Exception:
3742 log.exception(
3742 log.exception(
3743 'Cache key invalidation failed for cache args %s',
3743 'Cache key invalidation failed for cache args %s',
3744 safe_str(cache_uid))
3744 safe_str(cache_uid))
3745 Session().rollback()
3745 Session().rollback()
3746
3746
3747 @classmethod
3747 @classmethod
3748 def get_active_cache(cls, cache_key):
3748 def get_active_cache(cls, cache_key):
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3750 if inv_obj:
3750 if inv_obj:
3751 return inv_obj
3751 return inv_obj
3752 return None
3752 return None
3753
3753
3754 @classmethod
3754 @classmethod
3755 def get_namespace_map(cls, namespace):
3755 def get_namespace_map(cls, namespace):
3756 return {
3756 return {
3757 x.cache_key: x
3757 x.cache_key: x
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3759
3759
3760
3760
3761 class ChangesetComment(Base, BaseModel):
3761 class ChangesetComment(Base, BaseModel):
3762 __tablename__ = 'changeset_comments'
3762 __tablename__ = 'changeset_comments'
3763 __table_args__ = (
3763 __table_args__ = (
3764 Index('cc_revision_idx', 'revision'),
3764 Index('cc_revision_idx', 'revision'),
3765 base_table_args,
3765 base_table_args,
3766 )
3766 )
3767
3767
3768 COMMENT_OUTDATED = u'comment_outdated'
3768 COMMENT_OUTDATED = u'comment_outdated'
3769 COMMENT_TYPE_NOTE = u'note'
3769 COMMENT_TYPE_NOTE = u'note'
3770 COMMENT_TYPE_TODO = u'todo'
3770 COMMENT_TYPE_TODO = u'todo'
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3772
3772
3773 OP_IMMUTABLE = u'immutable'
3773 OP_IMMUTABLE = u'immutable'
3774 OP_CHANGEABLE = u'changeable'
3774 OP_CHANGEABLE = u'changeable'
3775
3775
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3778 revision = Column('revision', String(40), nullable=True)
3778 revision = Column('revision', String(40), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3792
3792
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3795
3795
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3798
3798
3799 author = relationship('User', lazy='select')
3799 author = relationship('User', lazy='select')
3800 repo = relationship('Repository')
3800 repo = relationship('Repository')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3805
3805
3806 @classmethod
3806 @classmethod
3807 def get_users(cls, revision=None, pull_request_id=None):
3807 def get_users(cls, revision=None, pull_request_id=None):
3808 """
3808 """
3809 Returns user associated with this ChangesetComment. ie those
3809 Returns user associated with this ChangesetComment. ie those
3810 who actually commented
3810 who actually commented
3811
3811
3812 :param cls:
3812 :param cls:
3813 :param revision:
3813 :param revision:
3814 """
3814 """
3815 q = Session().query(User)\
3815 q = Session().query(User)\
3816 .join(ChangesetComment.author)
3816 .join(ChangesetComment.author)
3817 if revision:
3817 if revision:
3818 q = q.filter(cls.revision == revision)
3818 q = q.filter(cls.revision == revision)
3819 elif pull_request_id:
3819 elif pull_request_id:
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3821 return q.all()
3821 return q.all()
3822
3822
3823 @classmethod
3823 @classmethod
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3825
3825
3826 if versions is not None:
3826 if versions is not None:
3827 num_versions = [x.pull_request_version_id for x in versions]
3827 num_versions = [x.pull_request_version_id for x in versions]
3828
3828
3829 num_versions = num_versions or []
3829 num_versions = num_versions or []
3830 try:
3830 try:
3831 return num_versions.index(pr_version) + 1
3831 return num_versions.index(pr_version) + 1
3832 except (IndexError, ValueError):
3832 except (IndexError, ValueError):
3833 return
3833 return
3834
3834
3835 @property
3835 @property
3836 def outdated(self):
3836 def outdated(self):
3837 return self.display_state == self.COMMENT_OUTDATED
3837 return self.display_state == self.COMMENT_OUTDATED
3838
3838
3839 @property
3839 @property
3840 def outdated_js(self):
3840 def outdated_js(self):
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3842
3842
3843 @property
3843 @property
3844 def immutable(self):
3844 def immutable(self):
3845 return self.immutable_state == self.OP_IMMUTABLE
3845 return self.immutable_state == self.OP_IMMUTABLE
3846
3846
3847 def outdated_at_version(self, version):
3847 def outdated_at_version(self, version):
3848 """
3848 """
3849 Checks if comment is outdated for given pull request version
3849 Checks if comment is outdated for given pull request version
3850 """
3850 """
3851 def version_check():
3851 def version_check():
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3853
3853
3854 if self.is_inline:
3854 if self.is_inline:
3855 return self.outdated and version_check()
3855 return self.outdated and version_check()
3856 else:
3856 else:
3857 # general comments don't have .outdated set, also latest don't have a version
3857 # general comments don't have .outdated set, also latest don't have a version
3858 return version_check()
3858 return version_check()
3859
3859
3860 def outdated_at_version_js(self, version):
3860 def outdated_at_version_js(self, version):
3861 """
3861 """
3862 Checks if comment is outdated for given pull request version
3862 Checks if comment is outdated for given pull request version
3863 """
3863 """
3864 return json.dumps(self.outdated_at_version(version))
3864 return json.dumps(self.outdated_at_version(version))
3865
3865
3866 def older_than_version(self, version):
3866 def older_than_version(self, version):
3867 """
3867 """
3868 Checks if comment is made from previous version than given
3868 Checks if comment is made from previous version than given
3869 """
3869 """
3870 if version is None:
3870 if version is None:
3871 return self.pull_request_version != version
3871 return self.pull_request_version != version
3872
3872
3873 return self.pull_request_version < version
3873 return self.pull_request_version < version
3874
3874
3875 def older_than_version_js(self, version):
3875 def older_than_version_js(self, version):
3876 """
3876 """
3877 Checks if comment is made from previous version than given
3877 Checks if comment is made from previous version than given
3878 """
3878 """
3879 return json.dumps(self.older_than_version(version))
3879 return json.dumps(self.older_than_version(version))
3880
3880
3881 @property
3881 @property
3882 def commit_id(self):
3882 def commit_id(self):
3883 """New style naming to stop using .revision"""
3883 """New style naming to stop using .revision"""
3884 return self.revision
3884 return self.revision
3885
3885
3886 @property
3886 @property
3887 def resolved(self):
3887 def resolved(self):
3888 return self.resolved_by[0] if self.resolved_by else None
3888 return self.resolved_by[0] if self.resolved_by else None
3889
3889
3890 @property
3890 @property
3891 def is_todo(self):
3891 def is_todo(self):
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3893
3893
3894 @property
3894 @property
3895 def is_inline(self):
3895 def is_inline(self):
3896 if self.line_no and self.f_path:
3896 if self.line_no and self.f_path:
3897 return True
3897 return True
3898 return False
3898 return False
3899
3899
3900 @property
3900 @property
3901 def last_version(self):
3901 def last_version(self):
3902 version = 0
3902 version = 0
3903 if self.history:
3903 if self.history:
3904 version = self.history[-1].version
3904 version = self.history[-1].version
3905 return version
3905 return version
3906
3906
3907 def get_index_version(self, versions):
3907 def get_index_version(self, versions):
3908 return self.get_index_from_version(
3908 return self.get_index_from_version(
3909 self.pull_request_version_id, versions)
3909 self.pull_request_version_id, versions)
3910
3910
3911 @property
3911 @property
3912 def review_status(self):
3912 def review_status(self):
3913 if self.status_change:
3913 if self.status_change:
3914 return self.status_change[0].status
3914 return self.status_change[0].status
3915
3915
3916 @property
3916 @property
3917 def review_status_lbl(self):
3917 def review_status_lbl(self):
3918 if self.status_change:
3918 if self.status_change:
3919 return self.status_change[0].status_lbl
3919 return self.status_change[0].status_lbl
3920
3920
3921 def __repr__(self):
3921 def __repr__(self):
3922 if self.comment_id:
3922 if self.comment_id:
3923 return '<DB:Comment #%s>' % self.comment_id
3923 return '<DB:Comment #%s>' % self.comment_id
3924 else:
3924 else:
3925 return '<DB:Comment at %#x>' % id(self)
3925 return '<DB:Comment at %#x>' % id(self)
3926
3926
3927 def get_api_data(self):
3927 def get_api_data(self):
3928 comment = self
3928 comment = self
3929
3929
3930 data = {
3930 data = {
3931 'comment_id': comment.comment_id,
3931 'comment_id': comment.comment_id,
3932 'comment_type': comment.comment_type,
3932 'comment_type': comment.comment_type,
3933 'comment_text': comment.text,
3933 'comment_text': comment.text,
3934 'comment_status': comment.status_change,
3934 'comment_status': comment.status_change,
3935 'comment_f_path': comment.f_path,
3935 'comment_f_path': comment.f_path,
3936 'comment_lineno': comment.line_no,
3936 'comment_lineno': comment.line_no,
3937 'comment_author': comment.author,
3937 'comment_author': comment.author,
3938 'comment_created_on': comment.created_on,
3938 'comment_created_on': comment.created_on,
3939 'comment_resolved_by': self.resolved,
3939 'comment_resolved_by': self.resolved,
3940 'comment_commit_id': comment.revision,
3940 'comment_commit_id': comment.revision,
3941 'comment_pull_request_id': comment.pull_request_id,
3941 'comment_pull_request_id': comment.pull_request_id,
3942 'comment_last_version': self.last_version
3942 'comment_last_version': self.last_version
3943 }
3943 }
3944 return data
3944 return data
3945
3945
3946 def __json__(self):
3946 def __json__(self):
3947 data = dict()
3947 data = dict()
3948 data.update(self.get_api_data())
3948 data.update(self.get_api_data())
3949 return data
3949 return data
3950
3950
3951
3951
3952 class ChangesetCommentHistory(Base, BaseModel):
3952 class ChangesetCommentHistory(Base, BaseModel):
3953 __tablename__ = 'changeset_comments_history'
3953 __tablename__ = 'changeset_comments_history'
3954 __table_args__ = (
3954 __table_args__ = (
3955 Index('cch_comment_id_idx', 'comment_id'),
3955 Index('cch_comment_id_idx', 'comment_id'),
3956 base_table_args,
3956 base_table_args,
3957 )
3957 )
3958
3958
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3961 version = Column("version", Integer(), nullable=False, default=0)
3961 version = Column("version", Integer(), nullable=False, default=0)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3965 deleted = Column('deleted', Boolean(), default=False)
3965 deleted = Column('deleted', Boolean(), default=False)
3966
3966
3967 author = relationship('User', lazy='joined')
3967 author = relationship('User', lazy='joined')
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3969
3969
3970 @classmethod
3970 @classmethod
3971 def get_version(cls, comment_id):
3971 def get_version(cls, comment_id):
3972 q = Session().query(ChangesetCommentHistory).filter(
3972 q = Session().query(ChangesetCommentHistory).filter(
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3974 if q.count() == 0:
3974 if q.count() == 0:
3975 return 1
3975 return 1
3976 elif q.count() >= q[0].version:
3976 elif q.count() >= q[0].version:
3977 return q.count() + 1
3977 return q.count() + 1
3978 else:
3978 else:
3979 return q[0].version + 1
3979 return q[0].version + 1
3980
3980
3981
3981
3982 class ChangesetStatus(Base, BaseModel):
3982 class ChangesetStatus(Base, BaseModel):
3983 __tablename__ = 'changeset_statuses'
3983 __tablename__ = 'changeset_statuses'
3984 __table_args__ = (
3984 __table_args__ = (
3985 Index('cs_revision_idx', 'revision'),
3985 Index('cs_revision_idx', 'revision'),
3986 Index('cs_version_idx', 'version'),
3986 Index('cs_version_idx', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3988 base_table_args
3988 base_table_args
3989 )
3989 )
3990
3990
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3992 STATUS_APPROVED = 'approved'
3992 STATUS_APPROVED = 'approved'
3993 STATUS_REJECTED = 'rejected'
3993 STATUS_REJECTED = 'rejected'
3994 STATUS_UNDER_REVIEW = 'under_review'
3994 STATUS_UNDER_REVIEW = 'under_review'
3995 CheckConstraint,
3995 CheckConstraint,
3996 STATUSES = [
3996 STATUSES = [
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3998 (STATUS_APPROVED, _("Approved")),
3998 (STATUS_APPROVED, _("Approved")),
3999 (STATUS_REJECTED, _("Rejected")),
3999 (STATUS_REJECTED, _("Rejected")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4001 ]
4001 ]
4002
4002
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4006 revision = Column('revision', String(40), nullable=False)
4006 revision = Column('revision', String(40), nullable=False)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4010 version = Column('version', Integer(), nullable=False, default=0)
4010 version = Column('version', Integer(), nullable=False, default=0)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4012
4012
4013 author = relationship('User', lazy='select')
4013 author = relationship('User', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4017
4017
4018 def __unicode__(self):
4018 def __unicode__(self):
4019 return u"<%s('%s[v%s]:%s')>" % (
4019 return u"<%s('%s[v%s]:%s')>" % (
4020 self.__class__.__name__,
4020 self.__class__.__name__,
4021 self.status, self.version, self.author
4021 self.status, self.version, self.author
4022 )
4022 )
4023
4023
4024 @classmethod
4024 @classmethod
4025 def get_status_lbl(cls, value):
4025 def get_status_lbl(cls, value):
4026 return dict(cls.STATUSES).get(value)
4026 return dict(cls.STATUSES).get(value)
4027
4027
4028 @property
4028 @property
4029 def status_lbl(self):
4029 def status_lbl(self):
4030 return ChangesetStatus.get_status_lbl(self.status)
4030 return ChangesetStatus.get_status_lbl(self.status)
4031
4031
4032 def get_api_data(self):
4032 def get_api_data(self):
4033 status = self
4033 status = self
4034 data = {
4034 data = {
4035 'status_id': status.changeset_status_id,
4035 'status_id': status.changeset_status_id,
4036 'status': status.status,
4036 'status': status.status,
4037 }
4037 }
4038 return data
4038 return data
4039
4039
4040 def __json__(self):
4040 def __json__(self):
4041 data = dict()
4041 data = dict()
4042 data.update(self.get_api_data())
4042 data.update(self.get_api_data())
4043 return data
4043 return data
4044
4044
4045
4045
4046 class _SetState(object):
4046 class _SetState(object):
4047 """
4047 """
4048 Context processor allowing changing state for sensitive operation such as
4048 Context processor allowing changing state for sensitive operation such as
4049 pull request update or merge
4049 pull request update or merge
4050 """
4050 """
4051
4051
4052 def __init__(self, pull_request, pr_state, back_state=None):
4052 def __init__(self, pull_request, pr_state, back_state=None):
4053 self._pr = pull_request
4053 self._pr = pull_request
4054 self._org_state = back_state or pull_request.pull_request_state
4054 self._org_state = back_state or pull_request.pull_request_state
4055 self._pr_state = pr_state
4055 self._pr_state = pr_state
4056 self._current_state = None
4056 self._current_state = None
4057
4057
4058 def __enter__(self):
4058 def __enter__(self):
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4060 self._pr, self._pr_state)
4060 self._pr, self._pr_state)
4061 self.set_pr_state(self._pr_state)
4061 self.set_pr_state(self._pr_state)
4062 return self
4062 return self
4063
4063
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4065 if exc_val is not None:
4065 if exc_val is not None:
4066 log.error(traceback.format_exc(exc_tb))
4066 log.error(traceback.format_exc(exc_tb))
4067 return None
4067 return None
4068
4068
4069 self.set_pr_state(self._org_state)
4069 self.set_pr_state(self._org_state)
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4071 self._pr, self._org_state)
4071 self._pr, self._org_state)
4072
4072
4073 @property
4073 @property
4074 def state(self):
4074 def state(self):
4075 return self._current_state
4075 return self._current_state
4076
4076
4077 def set_pr_state(self, pr_state):
4077 def set_pr_state(self, pr_state):
4078 try:
4078 try:
4079 self._pr.pull_request_state = pr_state
4079 self._pr.pull_request_state = pr_state
4080 Session().add(self._pr)
4080 Session().add(self._pr)
4081 Session().commit()
4081 Session().commit()
4082 self._current_state = pr_state
4082 self._current_state = pr_state
4083 except Exception:
4083 except Exception:
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4085 raise
4085 raise
4086
4086
4087
4087
4088 class _PullRequestBase(BaseModel):
4088 class _PullRequestBase(BaseModel):
4089 """
4089 """
4090 Common attributes of pull request and version entries.
4090 Common attributes of pull request and version entries.
4091 """
4091 """
4092
4092
4093 # .status values
4093 # .status values
4094 STATUS_NEW = u'new'
4094 STATUS_NEW = u'new'
4095 STATUS_OPEN = u'open'
4095 STATUS_OPEN = u'open'
4096 STATUS_CLOSED = u'closed'
4096 STATUS_CLOSED = u'closed'
4097
4097
4098 # available states
4098 # available states
4099 STATE_CREATING = u'creating'
4099 STATE_CREATING = u'creating'
4100 STATE_UPDATING = u'updating'
4100 STATE_UPDATING = u'updating'
4101 STATE_MERGING = u'merging'
4101 STATE_MERGING = u'merging'
4102 STATE_CREATED = u'created'
4102 STATE_CREATED = u'created'
4103
4103
4104 title = Column('title', Unicode(255), nullable=True)
4104 title = Column('title', Unicode(255), nullable=True)
4105 description = Column(
4105 description = Column(
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4107 nullable=True)
4107 nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4109
4109
4110 # new/open/closed status of pull request (not approve/reject/etc)
4110 # new/open/closed status of pull request (not approve/reject/etc)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4112 created_on = Column(
4112 created_on = Column(
4113 'created_on', DateTime(timezone=False), nullable=False,
4113 'created_on', DateTime(timezone=False), nullable=False,
4114 default=datetime.datetime.now)
4114 default=datetime.datetime.now)
4115 updated_on = Column(
4115 updated_on = Column(
4116 'updated_on', DateTime(timezone=False), nullable=False,
4116 'updated_on', DateTime(timezone=False), nullable=False,
4117 default=datetime.datetime.now)
4117 default=datetime.datetime.now)
4118
4118
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4120
4120
4121 @declared_attr
4121 @declared_attr
4122 def user_id(cls):
4122 def user_id(cls):
4123 return Column(
4123 return Column(
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4125 unique=None)
4125 unique=None)
4126
4126
4127 # 500 revisions max
4127 # 500 revisions max
4128 _revisions = Column(
4128 _revisions = Column(
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4130
4130
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4132
4132
4133 @declared_attr
4133 @declared_attr
4134 def source_repo_id(cls):
4134 def source_repo_id(cls):
4135 # TODO: dan: rename column to source_repo_id
4135 # TODO: dan: rename column to source_repo_id
4136 return Column(
4136 return Column(
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4138 nullable=False)
4138 nullable=False)
4139
4139
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4141
4141
4142 @hybrid_property
4142 @hybrid_property
4143 def source_ref(self):
4143 def source_ref(self):
4144 return self._source_ref
4144 return self._source_ref
4145
4145
4146 @source_ref.setter
4146 @source_ref.setter
4147 def source_ref(self, val):
4147 def source_ref(self, val):
4148 parts = (val or '').split(':')
4148 parts = (val or '').split(':')
4149 if len(parts) != 3:
4149 if len(parts) != 3:
4150 raise ValueError(
4150 raise ValueError(
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4152 self._source_ref = safe_unicode(val)
4152 self._source_ref = safe_unicode(val)
4153
4153
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4155
4155
4156 @hybrid_property
4156 @hybrid_property
4157 def target_ref(self):
4157 def target_ref(self):
4158 return self._target_ref
4158 return self._target_ref
4159
4159
4160 @target_ref.setter
4160 @target_ref.setter
4161 def target_ref(self, val):
4161 def target_ref(self, val):
4162 parts = (val or '').split(':')
4162 parts = (val or '').split(':')
4163 if len(parts) != 3:
4163 if len(parts) != 3:
4164 raise ValueError(
4164 raise ValueError(
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4166 self._target_ref = safe_unicode(val)
4166 self._target_ref = safe_unicode(val)
4167
4167
4168 @declared_attr
4168 @declared_attr
4169 def target_repo_id(cls):
4169 def target_repo_id(cls):
4170 # TODO: dan: rename column to target_repo_id
4170 # TODO: dan: rename column to target_repo_id
4171 return Column(
4171 return Column(
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4173 nullable=False)
4173 nullable=False)
4174
4174
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4176
4176
4177 # TODO: dan: rename column to last_merge_source_rev
4177 # TODO: dan: rename column to last_merge_source_rev
4178 _last_merge_source_rev = Column(
4178 _last_merge_source_rev = Column(
4179 'last_merge_org_rev', String(40), nullable=True)
4179 'last_merge_org_rev', String(40), nullable=True)
4180 # TODO: dan: rename column to last_merge_target_rev
4180 # TODO: dan: rename column to last_merge_target_rev
4181 _last_merge_target_rev = Column(
4181 _last_merge_target_rev = Column(
4182 'last_merge_other_rev', String(40), nullable=True)
4182 'last_merge_other_rev', String(40), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4184 last_merge_metadata = Column(
4184 last_merge_metadata = Column(
4185 'last_merge_metadata', MutationObj.as_mutable(
4185 'last_merge_metadata', MutationObj.as_mutable(
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4187
4187
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4189
4189
4190 reviewer_data = Column(
4190 reviewer_data = Column(
4191 'reviewer_data_json', MutationObj.as_mutable(
4191 'reviewer_data_json', MutationObj.as_mutable(
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4193
4193
4194 @property
4194 @property
4195 def reviewer_data_json(self):
4195 def reviewer_data_json(self):
4196 return json.dumps(self.reviewer_data)
4196 return json.dumps(self.reviewer_data)
4197
4197
4198 @property
4198 @property
4199 def last_merge_metadata_parsed(self):
4199 def last_merge_metadata_parsed(self):
4200 metadata = {}
4200 metadata = {}
4201 if not self.last_merge_metadata:
4201 if not self.last_merge_metadata:
4202 return metadata
4202 return metadata
4203
4203
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4206 if k in ['target_ref', 'source_ref']:
4206 if k in ['target_ref', 'source_ref']:
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4208 else:
4208 else:
4209 if hasattr(v, 'de_coerce'):
4209 if hasattr(v, 'de_coerce'):
4210 metadata[k] = v.de_coerce()
4210 metadata[k] = v.de_coerce()
4211 else:
4211 else:
4212 metadata[k] = v
4212 metadata[k] = v
4213 return metadata
4213 return metadata
4214
4214
4215 @property
4215 @property
4216 def work_in_progress(self):
4216 def work_in_progress(self):
4217 """checks if pull request is work in progress by checking the title"""
4217 """checks if pull request is work in progress by checking the title"""
4218 title = self.title.upper()
4218 title = self.title.upper()
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4220 return True
4220 return True
4221 return False
4221 return False
4222
4222
4223 @property
4223 @property
4224 def title_safe(self):
4224 def title_safe(self):
4225 return self.title\
4225 return self.title\
4226 .replace('{', '{{')\
4226 .replace('{', '{{')\
4227 .replace('}', '}}')
4227 .replace('}', '}}')
4228
4228
4229 @hybrid_property
4229 @hybrid_property
4230 def description_safe(self):
4230 def description_safe(self):
4231 from rhodecode.lib import helpers as h
4231 from rhodecode.lib import helpers as h
4232 return h.escape(self.description)
4232 return h.escape(self.description)
4233
4233
4234 @hybrid_property
4234 @hybrid_property
4235 def revisions(self):
4235 def revisions(self):
4236 return self._revisions.split(':') if self._revisions else []
4236 return self._revisions.split(':') if self._revisions else []
4237
4237
4238 @revisions.setter
4238 @revisions.setter
4239 def revisions(self, val):
4239 def revisions(self, val):
4240 self._revisions = u':'.join(val)
4240 self._revisions = u':'.join(val)
4241
4241
4242 @hybrid_property
4242 @hybrid_property
4243 def last_merge_status(self):
4243 def last_merge_status(self):
4244 return safe_int(self._last_merge_status)
4244 return safe_int(self._last_merge_status)
4245
4245
4246 @last_merge_status.setter
4246 @last_merge_status.setter
4247 def last_merge_status(self, val):
4247 def last_merge_status(self, val):
4248 self._last_merge_status = val
4248 self._last_merge_status = val
4249
4249
4250 @declared_attr
4250 @declared_attr
4251 def author(cls):
4251 def author(cls):
4252 return relationship('User', lazy='joined')
4252 return relationship('User', lazy='joined')
4253
4253
4254 @declared_attr
4254 @declared_attr
4255 def source_repo(cls):
4255 def source_repo(cls):
4256 return relationship(
4256 return relationship(
4257 'Repository',
4257 'Repository',
4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4259
4259
4260 @property
4260 @property
4261 def source_ref_parts(self):
4261 def source_ref_parts(self):
4262 return self.unicode_to_reference(self.source_ref)
4262 return self.unicode_to_reference(self.source_ref)
4263
4263
4264 @declared_attr
4264 @declared_attr
4265 def target_repo(cls):
4265 def target_repo(cls):
4266 return relationship(
4266 return relationship(
4267 'Repository',
4267 'Repository',
4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4269
4269
4270 @property
4270 @property
4271 def target_ref_parts(self):
4271 def target_ref_parts(self):
4272 return self.unicode_to_reference(self.target_ref)
4272 return self.unicode_to_reference(self.target_ref)
4273
4273
4274 @property
4274 @property
4275 def shadow_merge_ref(self):
4275 def shadow_merge_ref(self):
4276 return self.unicode_to_reference(self._shadow_merge_ref)
4276 return self.unicode_to_reference(self._shadow_merge_ref)
4277
4277
4278 @shadow_merge_ref.setter
4278 @shadow_merge_ref.setter
4279 def shadow_merge_ref(self, ref):
4279 def shadow_merge_ref(self, ref):
4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4281
4281
4282 @staticmethod
4282 @staticmethod
4283 def unicode_to_reference(raw):
4283 def unicode_to_reference(raw):
4284 return unicode_to_reference(raw)
4284 return unicode_to_reference(raw)
4285
4285
4286 @staticmethod
4286 @staticmethod
4287 def reference_to_unicode(ref):
4287 def reference_to_unicode(ref):
4288 return reference_to_unicode(ref)
4288 return reference_to_unicode(ref)
4289
4289
4290 def get_api_data(self, with_merge_state=True):
4290 def get_api_data(self, with_merge_state=True):
4291 from rhodecode.model.pull_request import PullRequestModel
4291 from rhodecode.model.pull_request import PullRequestModel
4292
4292
4293 pull_request = self
4293 pull_request = self
4294 if with_merge_state:
4294 if with_merge_state:
4295 merge_response, merge_status, msg = \
4295 merge_response, merge_status, msg = \
4296 PullRequestModel().merge_status(pull_request)
4296 PullRequestModel().merge_status(pull_request)
4297 merge_state = {
4297 merge_state = {
4298 'status': merge_status,
4298 'status': merge_status,
4299 'message': safe_unicode(msg),
4299 'message': safe_unicode(msg),
4300 }
4300 }
4301 else:
4301 else:
4302 merge_state = {'status': 'not_available',
4302 merge_state = {'status': 'not_available',
4303 'message': 'not_available'}
4303 'message': 'not_available'}
4304
4304
4305 merge_data = {
4305 merge_data = {
4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4307 'reference': (
4307 'reference': (
4308 pull_request.shadow_merge_ref._asdict()
4308 pull_request.shadow_merge_ref._asdict()
4309 if pull_request.shadow_merge_ref else None),
4309 if pull_request.shadow_merge_ref else None),
4310 }
4310 }
4311
4311
4312 data = {
4312 data = {
4313 'pull_request_id': pull_request.pull_request_id,
4313 'pull_request_id': pull_request.pull_request_id,
4314 'url': PullRequestModel().get_url(pull_request),
4314 'url': PullRequestModel().get_url(pull_request),
4315 'title': pull_request.title,
4315 'title': pull_request.title,
4316 'description': pull_request.description,
4316 'description': pull_request.description,
4317 'status': pull_request.status,
4317 'status': pull_request.status,
4318 'state': pull_request.pull_request_state,
4318 'state': pull_request.pull_request_state,
4319 'created_on': pull_request.created_on,
4319 'created_on': pull_request.created_on,
4320 'updated_on': pull_request.updated_on,
4320 'updated_on': pull_request.updated_on,
4321 'commit_ids': pull_request.revisions,
4321 'commit_ids': pull_request.revisions,
4322 'review_status': pull_request.calculated_review_status(),
4322 'review_status': pull_request.calculated_review_status(),
4323 'mergeable': merge_state,
4323 'mergeable': merge_state,
4324 'source': {
4324 'source': {
4325 'clone_url': pull_request.source_repo.clone_url(),
4325 'clone_url': pull_request.source_repo.clone_url(),
4326 'repository': pull_request.source_repo.repo_name,
4326 'repository': pull_request.source_repo.repo_name,
4327 'reference': {
4327 'reference': {
4328 'name': pull_request.source_ref_parts.name,
4328 'name': pull_request.source_ref_parts.name,
4329 'type': pull_request.source_ref_parts.type,
4329 'type': pull_request.source_ref_parts.type,
4330 'commit_id': pull_request.source_ref_parts.commit_id,
4330 'commit_id': pull_request.source_ref_parts.commit_id,
4331 },
4331 },
4332 },
4332 },
4333 'target': {
4333 'target': {
4334 'clone_url': pull_request.target_repo.clone_url(),
4334 'clone_url': pull_request.target_repo.clone_url(),
4335 'repository': pull_request.target_repo.repo_name,
4335 'repository': pull_request.target_repo.repo_name,
4336 'reference': {
4336 'reference': {
4337 'name': pull_request.target_ref_parts.name,
4337 'name': pull_request.target_ref_parts.name,
4338 'type': pull_request.target_ref_parts.type,
4338 'type': pull_request.target_ref_parts.type,
4339 'commit_id': pull_request.target_ref_parts.commit_id,
4339 'commit_id': pull_request.target_ref_parts.commit_id,
4340 },
4340 },
4341 },
4341 },
4342 'merge': merge_data,
4342 'merge': merge_data,
4343 'author': pull_request.author.get_api_data(include_secrets=False,
4343 'author': pull_request.author.get_api_data(include_secrets=False,
4344 details='basic'),
4344 details='basic'),
4345 'reviewers': [
4345 'reviewers': [
4346 {
4346 {
4347 'user': reviewer.get_api_data(include_secrets=False,
4347 'user': reviewer.get_api_data(include_secrets=False,
4348 details='basic'),
4348 details='basic'),
4349 'reasons': reasons,
4349 'reasons': reasons,
4350 'review_status': st[0][1].status if st else 'not_reviewed',
4350 'review_status': st[0][1].status if st else 'not_reviewed',
4351 }
4351 }
4352 for obj, reviewer, reasons, mandatory, st in
4352 for obj, reviewer, reasons, mandatory, st in
4353 pull_request.reviewers_statuses()
4353 pull_request.reviewers_statuses()
4354 ]
4354 ]
4355 }
4355 }
4356
4356
4357 return data
4357 return data
4358
4358
4359 def set_state(self, pull_request_state, final_state=None):
4359 def set_state(self, pull_request_state, final_state=None):
4360 """
4360 """
4361 # goes from initial state to updating to initial state.
4361 # goes from initial state to updating to initial state.
4362 # initial state can be changed by specifying back_state=
4362 # initial state can be changed by specifying back_state=
4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4364 pull_request.merge()
4364 pull_request.merge()
4365
4365
4366 :param pull_request_state:
4366 :param pull_request_state:
4367 :param final_state:
4367 :param final_state:
4368
4368
4369 """
4369 """
4370
4370
4371 return _SetState(self, pull_request_state, back_state=final_state)
4371 return _SetState(self, pull_request_state, back_state=final_state)
4372
4372
4373
4373
4374 class PullRequest(Base, _PullRequestBase):
4374 class PullRequest(Base, _PullRequestBase):
4375 __tablename__ = 'pull_requests'
4375 __tablename__ = 'pull_requests'
4376 __table_args__ = (
4376 __table_args__ = (
4377 base_table_args,
4377 base_table_args,
4378 )
4378 )
4379 LATEST_VER = 'latest'
4379 LATEST_VER = 'latest'
4380
4380
4381 pull_request_id = Column(
4381 pull_request_id = Column(
4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4383
4383
4384 def __repr__(self):
4384 def __repr__(self):
4385 if self.pull_request_id:
4385 if self.pull_request_id:
4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4387 else:
4387 else:
4388 return '<DB:PullRequest at %#x>' % id(self)
4388 return '<DB:PullRequest at %#x>' % id(self)
4389
4389
4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4394 lazy='dynamic')
4394 lazy='dynamic')
4395
4395
4396 @classmethod
4396 @classmethod
4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4398 internal_methods=None):
4398 internal_methods=None):
4399
4399
4400 class PullRequestDisplay(object):
4400 class PullRequestDisplay(object):
4401 """
4401 """
4402 Special object wrapper for showing PullRequest data via Versions
4402 Special object wrapper for showing PullRequest data via Versions
4403 It mimics PR object as close as possible. This is read only object
4403 It mimics PR object as close as possible. This is read only object
4404 just for display
4404 just for display
4405 """
4405 """
4406
4406
4407 def __init__(self, attrs, internal=None):
4407 def __init__(self, attrs, internal=None):
4408 self.attrs = attrs
4408 self.attrs = attrs
4409 # internal have priority over the given ones via attrs
4409 # internal have priority over the given ones via attrs
4410 self.internal = internal or ['versions']
4410 self.internal = internal or ['versions']
4411
4411
4412 def __getattr__(self, item):
4412 def __getattr__(self, item):
4413 if item in self.internal:
4413 if item in self.internal:
4414 return getattr(self, item)
4414 return getattr(self, item)
4415 try:
4415 try:
4416 return self.attrs[item]
4416 return self.attrs[item]
4417 except KeyError:
4417 except KeyError:
4418 raise AttributeError(
4418 raise AttributeError(
4419 '%s object has no attribute %s' % (self, item))
4419 '%s object has no attribute %s' % (self, item))
4420
4420
4421 def __repr__(self):
4421 def __repr__(self):
4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4423
4423
4424 def versions(self):
4424 def versions(self):
4425 return pull_request_obj.versions.order_by(
4425 return pull_request_obj.versions.order_by(
4426 PullRequestVersion.pull_request_version_id).all()
4426 PullRequestVersion.pull_request_version_id).all()
4427
4427
4428 def is_closed(self):
4428 def is_closed(self):
4429 return pull_request_obj.is_closed()
4429 return pull_request_obj.is_closed()
4430
4430
4431 def is_state_changing(self):
4431 def is_state_changing(self):
4432 return pull_request_obj.is_state_changing()
4432 return pull_request_obj.is_state_changing()
4433
4433
4434 @property
4434 @property
4435 def pull_request_version_id(self):
4435 def pull_request_version_id(self):
4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4437
4437
4438 @property
4438 @property
4439 def pull_request_last_version(self):
4439 def pull_request_last_version(self):
4440 return pull_request_obj.pull_request_last_version
4440 return pull_request_obj.pull_request_last_version
4441
4441
4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4443
4443
4444 attrs.author = StrictAttributeDict(
4444 attrs.author = StrictAttributeDict(
4445 pull_request_obj.author.get_api_data())
4445 pull_request_obj.author.get_api_data())
4446 if pull_request_obj.target_repo:
4446 if pull_request_obj.target_repo:
4447 attrs.target_repo = StrictAttributeDict(
4447 attrs.target_repo = StrictAttributeDict(
4448 pull_request_obj.target_repo.get_api_data())
4448 pull_request_obj.target_repo.get_api_data())
4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4450
4450
4451 if pull_request_obj.source_repo:
4451 if pull_request_obj.source_repo:
4452 attrs.source_repo = StrictAttributeDict(
4452 attrs.source_repo = StrictAttributeDict(
4453 pull_request_obj.source_repo.get_api_data())
4453 pull_request_obj.source_repo.get_api_data())
4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4455
4455
4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4458 attrs.revisions = pull_request_obj.revisions
4458 attrs.revisions = pull_request_obj.revisions
4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4463
4463
4464 return PullRequestDisplay(attrs, internal=internal_methods)
4464 return PullRequestDisplay(attrs, internal=internal_methods)
4465
4465
4466 def is_closed(self):
4466 def is_closed(self):
4467 return self.status == self.STATUS_CLOSED
4467 return self.status == self.STATUS_CLOSED
4468
4468
4469 def is_state_changing(self):
4469 def is_state_changing(self):
4470 return self.pull_request_state != PullRequest.STATE_CREATED
4470 return self.pull_request_state != PullRequest.STATE_CREATED
4471
4471
4472 def __json__(self):
4472 def __json__(self):
4473 return {
4473 return {
4474 'revisions': self.revisions,
4474 'revisions': self.revisions,
4475 'versions': self.versions_count
4475 'versions': self.versions_count
4476 }
4476 }
4477
4477
4478 def calculated_review_status(self):
4478 def calculated_review_status(self):
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4480 return ChangesetStatusModel().calculated_review_status(self)
4480 return ChangesetStatusModel().calculated_review_status(self)
4481
4481
4482 def reviewers_statuses(self):
4482 def reviewers_statuses(self, user=None):
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4484 return ChangesetStatusModel().reviewers_statuses(self)
4484 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4485
4485
4486 def get_pull_request_reviewers(self, role=None):
4486 def get_pull_request_reviewers(self, role=None):
4487 qry = PullRequestReviewers.query()\
4487 qry = PullRequestReviewers.query()\
4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4489 if role:
4489 if role:
4490 qry = qry.filter(PullRequestReviewers.role == role)
4490 qry = qry.filter(PullRequestReviewers.role == role)
4491
4491
4492 return qry.all()
4492 return qry.all()
4493
4493
4494 @property
4494 @property
4495 def reviewers_count(self):
4495 def reviewers_count(self):
4496 qry = PullRequestReviewers.query()\
4496 qry = PullRequestReviewers.query()\
4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4499 return qry.count()
4499 return qry.count()
4500
4500
4501 @property
4501 @property
4502 def observers_count(self):
4502 def observers_count(self):
4503 qry = PullRequestReviewers.query()\
4503 qry = PullRequestReviewers.query()\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4506 return qry.count()
4506 return qry.count()
4507
4507
4508 def observers(self):
4508 def observers(self):
4509 qry = PullRequestReviewers.query()\
4509 qry = PullRequestReviewers.query()\
4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4512 .all()
4512 .all()
4513
4513
4514 for entry in qry:
4514 for entry in qry:
4515 yield entry, entry.user
4515 yield entry, entry.user
4516
4516
4517 @property
4517 @property
4518 def workspace_id(self):
4518 def workspace_id(self):
4519 from rhodecode.model.pull_request import PullRequestModel
4519 from rhodecode.model.pull_request import PullRequestModel
4520 return PullRequestModel()._workspace_id(self)
4520 return PullRequestModel()._workspace_id(self)
4521
4521
4522 def get_shadow_repo(self):
4522 def get_shadow_repo(self):
4523 workspace_id = self.workspace_id
4523 workspace_id = self.workspace_id
4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4525 if os.path.isdir(shadow_repository_path):
4525 if os.path.isdir(shadow_repository_path):
4526 vcs_obj = self.target_repo.scm_instance()
4526 vcs_obj = self.target_repo.scm_instance()
4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4528
4528
4529 @property
4529 @property
4530 def versions_count(self):
4530 def versions_count(self):
4531 """
4531 """
4532 return number of versions this PR have, e.g a PR that once been
4532 return number of versions this PR have, e.g a PR that once been
4533 updated will have 2 versions
4533 updated will have 2 versions
4534 """
4534 """
4535 return self.versions.count() + 1
4535 return self.versions.count() + 1
4536
4536
4537 @property
4537 @property
4538 def pull_request_last_version(self):
4538 def pull_request_last_version(self):
4539 return self.versions_count
4539 return self.versions_count
4540
4540
4541
4541
4542 class PullRequestVersion(Base, _PullRequestBase):
4542 class PullRequestVersion(Base, _PullRequestBase):
4543 __tablename__ = 'pull_request_versions'
4543 __tablename__ = 'pull_request_versions'
4544 __table_args__ = (
4544 __table_args__ = (
4545 base_table_args,
4545 base_table_args,
4546 )
4546 )
4547
4547
4548 pull_request_version_id = Column(
4548 pull_request_version_id = Column(
4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4550 pull_request_id = Column(
4550 pull_request_id = Column(
4551 'pull_request_id', Integer(),
4551 'pull_request_id', Integer(),
4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4553 pull_request = relationship('PullRequest')
4553 pull_request = relationship('PullRequest')
4554
4554
4555 def __repr__(self):
4555 def __repr__(self):
4556 if self.pull_request_version_id:
4556 if self.pull_request_version_id:
4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4558 else:
4558 else:
4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4560
4560
4561 @property
4561 @property
4562 def reviewers(self):
4562 def reviewers(self):
4563 return self.pull_request.reviewers
4563 return self.pull_request.reviewers
4564 @property
4564 @property
4565 def reviewers(self):
4565 def reviewers(self):
4566 return self.pull_request.reviewers
4566 return self.pull_request.reviewers
4567
4567
4568 @property
4568 @property
4569 def versions(self):
4569 def versions(self):
4570 return self.pull_request.versions
4570 return self.pull_request.versions
4571
4571
4572 def is_closed(self):
4572 def is_closed(self):
4573 # calculate from original
4573 # calculate from original
4574 return self.pull_request.status == self.STATUS_CLOSED
4574 return self.pull_request.status == self.STATUS_CLOSED
4575
4575
4576 def is_state_changing(self):
4576 def is_state_changing(self):
4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4578
4578
4579 def calculated_review_status(self):
4579 def calculated_review_status(self):
4580 return self.pull_request.calculated_review_status()
4580 return self.pull_request.calculated_review_status()
4581
4581
4582 def reviewers_statuses(self):
4582 def reviewers_statuses(self):
4583 return self.pull_request.reviewers_statuses()
4583 return self.pull_request.reviewers_statuses()
4584
4584
4585 def observers(self):
4585 def observers(self):
4586 return self.pull_request.observers()
4586 return self.pull_request.observers()
4587
4587
4588
4588
4589 class PullRequestReviewers(Base, BaseModel):
4589 class PullRequestReviewers(Base, BaseModel):
4590 __tablename__ = 'pull_request_reviewers'
4590 __tablename__ = 'pull_request_reviewers'
4591 __table_args__ = (
4591 __table_args__ = (
4592 base_table_args,
4592 base_table_args,
4593 )
4593 )
4594 ROLE_REVIEWER = u'reviewer'
4594 ROLE_REVIEWER = u'reviewer'
4595 ROLE_OBSERVER = u'observer'
4595 ROLE_OBSERVER = u'observer'
4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4597
4597
4598 @hybrid_property
4598 @hybrid_property
4599 def reasons(self):
4599 def reasons(self):
4600 if not self._reasons:
4600 if not self._reasons:
4601 return []
4601 return []
4602 return self._reasons
4602 return self._reasons
4603
4603
4604 @reasons.setter
4604 @reasons.setter
4605 def reasons(self, val):
4605 def reasons(self, val):
4606 val = val or []
4606 val = val or []
4607 if any(not isinstance(x, compat.string_types) for x in val):
4607 if any(not isinstance(x, compat.string_types) for x in val):
4608 raise Exception('invalid reasons type, must be list of strings')
4608 raise Exception('invalid reasons type, must be list of strings')
4609 self._reasons = val
4609 self._reasons = val
4610
4610
4611 pull_requests_reviewers_id = Column(
4611 pull_requests_reviewers_id = Column(
4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4613 primary_key=True)
4613 primary_key=True)
4614 pull_request_id = Column(
4614 pull_request_id = Column(
4615 "pull_request_id", Integer(),
4615 "pull_request_id", Integer(),
4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4617 user_id = Column(
4617 user_id = Column(
4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4619 _reasons = Column(
4619 _reasons = Column(
4620 'reason', MutationList.as_mutable(
4620 'reason', MutationList.as_mutable(
4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4622
4622
4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4625
4625
4626 user = relationship('User')
4626 user = relationship('User')
4627 pull_request = relationship('PullRequest')
4627 pull_request = relationship('PullRequest')
4628
4628
4629 rule_data = Column(
4629 rule_data = Column(
4630 'rule_data_json',
4630 'rule_data_json',
4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4632
4632
4633 def rule_user_group_data(self):
4633 def rule_user_group_data(self):
4634 """
4634 """
4635 Returns the voting user group rule data for this reviewer
4635 Returns the voting user group rule data for this reviewer
4636 """
4636 """
4637
4637
4638 if self.rule_data and 'vote_rule' in self.rule_data:
4638 if self.rule_data and 'vote_rule' in self.rule_data:
4639 user_group_data = {}
4639 user_group_data = {}
4640 if 'rule_user_group_entry_id' in self.rule_data:
4640 if 'rule_user_group_entry_id' in self.rule_data:
4641 # means a group with voting rules !
4641 # means a group with voting rules !
4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4643 user_group_data['name'] = self.rule_data['rule_name']
4643 user_group_data['name'] = self.rule_data['rule_name']
4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4645
4645
4646 return user_group_data
4646 return user_group_data
4647
4647
4648 @classmethod
4648 @classmethod
4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4650 qry = PullRequestReviewers.query()\
4650 qry = PullRequestReviewers.query()\
4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4652 if role:
4652 if role:
4653 qry = qry.filter(PullRequestReviewers.role == role)
4653 qry = qry.filter(PullRequestReviewers.role == role)
4654
4654
4655 return qry.all()
4655 return qry.all()
4656
4656
4657 def __unicode__(self):
4657 def __unicode__(self):
4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4659 self.pull_requests_reviewers_id)
4659 self.pull_requests_reviewers_id)
4660
4660
4661
4661
4662 class Notification(Base, BaseModel):
4662 class Notification(Base, BaseModel):
4663 __tablename__ = 'notifications'
4663 __tablename__ = 'notifications'
4664 __table_args__ = (
4664 __table_args__ = (
4665 Index('notification_type_idx', 'type'),
4665 Index('notification_type_idx', 'type'),
4666 base_table_args,
4666 base_table_args,
4667 )
4667 )
4668
4668
4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4670 TYPE_MESSAGE = u'message'
4670 TYPE_MESSAGE = u'message'
4671 TYPE_MENTION = u'mention'
4671 TYPE_MENTION = u'mention'
4672 TYPE_REGISTRATION = u'registration'
4672 TYPE_REGISTRATION = u'registration'
4673 TYPE_PULL_REQUEST = u'pull_request'
4673 TYPE_PULL_REQUEST = u'pull_request'
4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4676
4676
4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4678 subject = Column('subject', Unicode(512), nullable=True)
4678 subject = Column('subject', Unicode(512), nullable=True)
4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4682 type_ = Column('type', Unicode(255))
4682 type_ = Column('type', Unicode(255))
4683
4683
4684 created_by_user = relationship('User')
4684 created_by_user = relationship('User')
4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4686 cascade="all, delete-orphan")
4686 cascade="all, delete-orphan")
4687
4687
4688 @property
4688 @property
4689 def recipients(self):
4689 def recipients(self):
4690 return [x.user for x in UserNotification.query()\
4690 return [x.user for x in UserNotification.query()\
4691 .filter(UserNotification.notification == self)\
4691 .filter(UserNotification.notification == self)\
4692 .order_by(UserNotification.user_id.asc()).all()]
4692 .order_by(UserNotification.user_id.asc()).all()]
4693
4693
4694 @classmethod
4694 @classmethod
4695 def create(cls, created_by, subject, body, recipients, type_=None):
4695 def create(cls, created_by, subject, body, recipients, type_=None):
4696 if type_ is None:
4696 if type_ is None:
4697 type_ = Notification.TYPE_MESSAGE
4697 type_ = Notification.TYPE_MESSAGE
4698
4698
4699 notification = cls()
4699 notification = cls()
4700 notification.created_by_user = created_by
4700 notification.created_by_user = created_by
4701 notification.subject = subject
4701 notification.subject = subject
4702 notification.body = body
4702 notification.body = body
4703 notification.type_ = type_
4703 notification.type_ = type_
4704 notification.created_on = datetime.datetime.now()
4704 notification.created_on = datetime.datetime.now()
4705
4705
4706 # For each recipient link the created notification to his account
4706 # For each recipient link the created notification to his account
4707 for u in recipients:
4707 for u in recipients:
4708 assoc = UserNotification()
4708 assoc = UserNotification()
4709 assoc.user_id = u.user_id
4709 assoc.user_id = u.user_id
4710 assoc.notification = notification
4710 assoc.notification = notification
4711
4711
4712 # if created_by is inside recipients mark his notification
4712 # if created_by is inside recipients mark his notification
4713 # as read
4713 # as read
4714 if u.user_id == created_by.user_id:
4714 if u.user_id == created_by.user_id:
4715 assoc.read = True
4715 assoc.read = True
4716 Session().add(assoc)
4716 Session().add(assoc)
4717
4717
4718 Session().add(notification)
4718 Session().add(notification)
4719
4719
4720 return notification
4720 return notification
4721
4721
4722
4722
4723 class UserNotification(Base, BaseModel):
4723 class UserNotification(Base, BaseModel):
4724 __tablename__ = 'user_to_notification'
4724 __tablename__ = 'user_to_notification'
4725 __table_args__ = (
4725 __table_args__ = (
4726 UniqueConstraint('user_id', 'notification_id'),
4726 UniqueConstraint('user_id', 'notification_id'),
4727 base_table_args
4727 base_table_args
4728 )
4728 )
4729
4729
4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4732 read = Column('read', Boolean, default=False)
4732 read = Column('read', Boolean, default=False)
4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4734
4734
4735 user = relationship('User', lazy="joined")
4735 user = relationship('User', lazy="joined")
4736 notification = relationship('Notification', lazy="joined",
4736 notification = relationship('Notification', lazy="joined",
4737 order_by=lambda: Notification.created_on.desc(),)
4737 order_by=lambda: Notification.created_on.desc(),)
4738
4738
4739 def mark_as_read(self):
4739 def mark_as_read(self):
4740 self.read = True
4740 self.read = True
4741 Session().add(self)
4741 Session().add(self)
4742
4742
4743
4743
4744 class UserNotice(Base, BaseModel):
4744 class UserNotice(Base, BaseModel):
4745 __tablename__ = 'user_notices'
4745 __tablename__ = 'user_notices'
4746 __table_args__ = (
4746 __table_args__ = (
4747 base_table_args
4747 base_table_args
4748 )
4748 )
4749
4749
4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4752
4752
4753 NOTIFICATION_LEVEL_INFO = 'info'
4753 NOTIFICATION_LEVEL_INFO = 'info'
4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4755 NOTIFICATION_LEVEL_ERROR = 'error'
4755 NOTIFICATION_LEVEL_ERROR = 'error'
4756
4756
4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4758
4758
4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4761
4761
4762 notice_read = Column('notice_read', Boolean, default=False)
4762 notice_read = Column('notice_read', Boolean, default=False)
4763
4763
4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4766
4766
4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4769
4769
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4772
4772
4773 @classmethod
4773 @classmethod
4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4775
4775
4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4777 cls.NOTIFICATION_LEVEL_WARNING,
4777 cls.NOTIFICATION_LEVEL_WARNING,
4778 cls.NOTIFICATION_LEVEL_INFO]:
4778 cls.NOTIFICATION_LEVEL_INFO]:
4779 return
4779 return
4780
4780
4781 from rhodecode.model.user import UserModel
4781 from rhodecode.model.user import UserModel
4782 user = UserModel().get_user(user)
4782 user = UserModel().get_user(user)
4783
4783
4784 new_notice = UserNotice()
4784 new_notice = UserNotice()
4785 if not allow_duplicate:
4785 if not allow_duplicate:
4786 existing_msg = UserNotice().query() \
4786 existing_msg = UserNotice().query() \
4787 .filter(UserNotice.user == user) \
4787 .filter(UserNotice.user == user) \
4788 .filter(UserNotice.notice_body == body) \
4788 .filter(UserNotice.notice_body == body) \
4789 .filter(UserNotice.notice_read == false()) \
4789 .filter(UserNotice.notice_read == false()) \
4790 .scalar()
4790 .scalar()
4791 if existing_msg:
4791 if existing_msg:
4792 log.warning('Ignoring duplicate notice for user %s', user)
4792 log.warning('Ignoring duplicate notice for user %s', user)
4793 return
4793 return
4794
4794
4795 new_notice.user = user
4795 new_notice.user = user
4796 new_notice.notice_subject = subject
4796 new_notice.notice_subject = subject
4797 new_notice.notice_body = body
4797 new_notice.notice_body = body
4798 new_notice.notification_level = notice_level
4798 new_notice.notification_level = notice_level
4799 Session().add(new_notice)
4799 Session().add(new_notice)
4800 Session().commit()
4800 Session().commit()
4801
4801
4802
4802
4803 class Gist(Base, BaseModel):
4803 class Gist(Base, BaseModel):
4804 __tablename__ = 'gists'
4804 __tablename__ = 'gists'
4805 __table_args__ = (
4805 __table_args__ = (
4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4807 Index('g_created_on_idx', 'created_on'),
4807 Index('g_created_on_idx', 'created_on'),
4808 base_table_args
4808 base_table_args
4809 )
4809 )
4810
4810
4811 GIST_PUBLIC = u'public'
4811 GIST_PUBLIC = u'public'
4812 GIST_PRIVATE = u'private'
4812 GIST_PRIVATE = u'private'
4813 DEFAULT_FILENAME = u'gistfile1.txt'
4813 DEFAULT_FILENAME = u'gistfile1.txt'
4814
4814
4815 ACL_LEVEL_PUBLIC = u'acl_public'
4815 ACL_LEVEL_PUBLIC = u'acl_public'
4816 ACL_LEVEL_PRIVATE = u'acl_private'
4816 ACL_LEVEL_PRIVATE = u'acl_private'
4817
4817
4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4819 gist_access_id = Column('gist_access_id', Unicode(250))
4819 gist_access_id = Column('gist_access_id', Unicode(250))
4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4827
4827
4828 owner = relationship('User')
4828 owner = relationship('User')
4829
4829
4830 def __repr__(self):
4830 def __repr__(self):
4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4832
4832
4833 @hybrid_property
4833 @hybrid_property
4834 def description_safe(self):
4834 def description_safe(self):
4835 from rhodecode.lib import helpers as h
4835 from rhodecode.lib import helpers as h
4836 return h.escape(self.gist_description)
4836 return h.escape(self.gist_description)
4837
4837
4838 @classmethod
4838 @classmethod
4839 def get_or_404(cls, id_):
4839 def get_or_404(cls, id_):
4840 from pyramid.httpexceptions import HTTPNotFound
4840 from pyramid.httpexceptions import HTTPNotFound
4841
4841
4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4843 if not res:
4843 if not res:
4844 log.debug('WARN: No DB entry with id %s', id_)
4844 log.debug('WARN: No DB entry with id %s', id_)
4845 raise HTTPNotFound()
4845 raise HTTPNotFound()
4846 return res
4846 return res
4847
4847
4848 @classmethod
4848 @classmethod
4849 def get_by_access_id(cls, gist_access_id):
4849 def get_by_access_id(cls, gist_access_id):
4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4851
4851
4852 def gist_url(self):
4852 def gist_url(self):
4853 from rhodecode.model.gist import GistModel
4853 from rhodecode.model.gist import GistModel
4854 return GistModel().get_url(self)
4854 return GistModel().get_url(self)
4855
4855
4856 @classmethod
4856 @classmethod
4857 def base_path(cls):
4857 def base_path(cls):
4858 """
4858 """
4859 Returns base path when all gists are stored
4859 Returns base path when all gists are stored
4860
4860
4861 :param cls:
4861 :param cls:
4862 """
4862 """
4863 from rhodecode.model.gist import GIST_STORE_LOC
4863 from rhodecode.model.gist import GIST_STORE_LOC
4864 q = Session().query(RhodeCodeUi)\
4864 q = Session().query(RhodeCodeUi)\
4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4868
4868
4869 def get_api_data(self):
4869 def get_api_data(self):
4870 """
4870 """
4871 Common function for generating gist related data for API
4871 Common function for generating gist related data for API
4872 """
4872 """
4873 gist = self
4873 gist = self
4874 data = {
4874 data = {
4875 'gist_id': gist.gist_id,
4875 'gist_id': gist.gist_id,
4876 'type': gist.gist_type,
4876 'type': gist.gist_type,
4877 'access_id': gist.gist_access_id,
4877 'access_id': gist.gist_access_id,
4878 'description': gist.gist_description,
4878 'description': gist.gist_description,
4879 'url': gist.gist_url(),
4879 'url': gist.gist_url(),
4880 'expires': gist.gist_expires,
4880 'expires': gist.gist_expires,
4881 'created_on': gist.created_on,
4881 'created_on': gist.created_on,
4882 'modified_at': gist.modified_at,
4882 'modified_at': gist.modified_at,
4883 'content': None,
4883 'content': None,
4884 'acl_level': gist.acl_level,
4884 'acl_level': gist.acl_level,
4885 }
4885 }
4886 return data
4886 return data
4887
4887
4888 def __json__(self):
4888 def __json__(self):
4889 data = dict(
4889 data = dict(
4890 )
4890 )
4891 data.update(self.get_api_data())
4891 data.update(self.get_api_data())
4892 return data
4892 return data
4893 # SCM functions
4893 # SCM functions
4894
4894
4895 def scm_instance(self, **kwargs):
4895 def scm_instance(self, **kwargs):
4896 """
4896 """
4897 Get an instance of VCS Repository
4897 Get an instance of VCS Repository
4898
4898
4899 :param kwargs:
4899 :param kwargs:
4900 """
4900 """
4901 from rhodecode.model.gist import GistModel
4901 from rhodecode.model.gist import GistModel
4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4903 return get_vcs_instance(
4903 return get_vcs_instance(
4904 repo_path=safe_str(full_repo_path), create=False,
4904 repo_path=safe_str(full_repo_path), create=False,
4905 _vcs_alias=GistModel.vcs_backend)
4905 _vcs_alias=GistModel.vcs_backend)
4906
4906
4907
4907
4908 class ExternalIdentity(Base, BaseModel):
4908 class ExternalIdentity(Base, BaseModel):
4909 __tablename__ = 'external_identities'
4909 __tablename__ = 'external_identities'
4910 __table_args__ = (
4910 __table_args__ = (
4911 Index('local_user_id_idx', 'local_user_id'),
4911 Index('local_user_id_idx', 'local_user_id'),
4912 Index('external_id_idx', 'external_id'),
4912 Index('external_id_idx', 'external_id'),
4913 base_table_args
4913 base_table_args
4914 )
4914 )
4915
4915
4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4917 external_username = Column('external_username', Unicode(1024), default=u'')
4917 external_username = Column('external_username', Unicode(1024), default=u'')
4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4920 access_token = Column('access_token', String(1024), default=u'')
4920 access_token = Column('access_token', String(1024), default=u'')
4921 alt_token = Column('alt_token', String(1024), default=u'')
4921 alt_token = Column('alt_token', String(1024), default=u'')
4922 token_secret = Column('token_secret', String(1024), default=u'')
4922 token_secret = Column('token_secret', String(1024), default=u'')
4923
4923
4924 @classmethod
4924 @classmethod
4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4926 """
4926 """
4927 Returns ExternalIdentity instance based on search params
4927 Returns ExternalIdentity instance based on search params
4928
4928
4929 :param external_id:
4929 :param external_id:
4930 :param provider_name:
4930 :param provider_name:
4931 :return: ExternalIdentity
4931 :return: ExternalIdentity
4932 """
4932 """
4933 query = cls.query()
4933 query = cls.query()
4934 query = query.filter(cls.external_id == external_id)
4934 query = query.filter(cls.external_id == external_id)
4935 query = query.filter(cls.provider_name == provider_name)
4935 query = query.filter(cls.provider_name == provider_name)
4936 if local_user_id:
4936 if local_user_id:
4937 query = query.filter(cls.local_user_id == local_user_id)
4937 query = query.filter(cls.local_user_id == local_user_id)
4938 return query.first()
4938 return query.first()
4939
4939
4940 @classmethod
4940 @classmethod
4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4942 """
4942 """
4943 Returns User instance based on search params
4943 Returns User instance based on search params
4944
4944
4945 :param external_id:
4945 :param external_id:
4946 :param provider_name:
4946 :param provider_name:
4947 :return: User
4947 :return: User
4948 """
4948 """
4949 query = User.query()
4949 query = User.query()
4950 query = query.filter(cls.external_id == external_id)
4950 query = query.filter(cls.external_id == external_id)
4951 query = query.filter(cls.provider_name == provider_name)
4951 query = query.filter(cls.provider_name == provider_name)
4952 query = query.filter(User.user_id == cls.local_user_id)
4952 query = query.filter(User.user_id == cls.local_user_id)
4953 return query.first()
4953 return query.first()
4954
4954
4955 @classmethod
4955 @classmethod
4956 def by_local_user_id(cls, local_user_id):
4956 def by_local_user_id(cls, local_user_id):
4957 """
4957 """
4958 Returns all tokens for user
4958 Returns all tokens for user
4959
4959
4960 :param local_user_id:
4960 :param local_user_id:
4961 :return: ExternalIdentity
4961 :return: ExternalIdentity
4962 """
4962 """
4963 query = cls.query()
4963 query = cls.query()
4964 query = query.filter(cls.local_user_id == local_user_id)
4964 query = query.filter(cls.local_user_id == local_user_id)
4965 return query
4965 return query
4966
4966
4967 @classmethod
4967 @classmethod
4968 def load_provider_plugin(cls, plugin_id):
4968 def load_provider_plugin(cls, plugin_id):
4969 from rhodecode.authentication.base import loadplugin
4969 from rhodecode.authentication.base import loadplugin
4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4971 auth_plugin = loadplugin(_plugin_id)
4971 auth_plugin = loadplugin(_plugin_id)
4972 return auth_plugin
4972 return auth_plugin
4973
4973
4974
4974
4975 class Integration(Base, BaseModel):
4975 class Integration(Base, BaseModel):
4976 __tablename__ = 'integrations'
4976 __tablename__ = 'integrations'
4977 __table_args__ = (
4977 __table_args__ = (
4978 base_table_args
4978 base_table_args
4979 )
4979 )
4980
4980
4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4982 integration_type = Column('integration_type', String(255))
4982 integration_type = Column('integration_type', String(255))
4983 enabled = Column('enabled', Boolean(), nullable=False)
4983 enabled = Column('enabled', Boolean(), nullable=False)
4984 name = Column('name', String(255), nullable=False)
4984 name = Column('name', String(255), nullable=False)
4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4986 default=False)
4986 default=False)
4987
4987
4988 settings = Column(
4988 settings = Column(
4989 'settings_json', MutationObj.as_mutable(
4989 'settings_json', MutationObj.as_mutable(
4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4991 repo_id = Column(
4991 repo_id = Column(
4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4993 nullable=True, unique=None, default=None)
4993 nullable=True, unique=None, default=None)
4994 repo = relationship('Repository', lazy='joined')
4994 repo = relationship('Repository', lazy='joined')
4995
4995
4996 repo_group_id = Column(
4996 repo_group_id = Column(
4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4998 nullable=True, unique=None, default=None)
4998 nullable=True, unique=None, default=None)
4999 repo_group = relationship('RepoGroup', lazy='joined')
4999 repo_group = relationship('RepoGroup', lazy='joined')
5000
5000
5001 @property
5001 @property
5002 def scope(self):
5002 def scope(self):
5003 if self.repo:
5003 if self.repo:
5004 return repr(self.repo)
5004 return repr(self.repo)
5005 if self.repo_group:
5005 if self.repo_group:
5006 if self.child_repos_only:
5006 if self.child_repos_only:
5007 return repr(self.repo_group) + ' (child repos only)'
5007 return repr(self.repo_group) + ' (child repos only)'
5008 else:
5008 else:
5009 return repr(self.repo_group) + ' (recursive)'
5009 return repr(self.repo_group) + ' (recursive)'
5010 if self.child_repos_only:
5010 if self.child_repos_only:
5011 return 'root_repos'
5011 return 'root_repos'
5012 return 'global'
5012 return 'global'
5013
5013
5014 def __repr__(self):
5014 def __repr__(self):
5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5016
5016
5017
5017
5018 class RepoReviewRuleUser(Base, BaseModel):
5018 class RepoReviewRuleUser(Base, BaseModel):
5019 __tablename__ = 'repo_review_rules_users'
5019 __tablename__ = 'repo_review_rules_users'
5020 __table_args__ = (
5020 __table_args__ = (
5021 base_table_args
5021 base_table_args
5022 )
5022 )
5023 ROLE_REVIEWER = u'reviewer'
5023 ROLE_REVIEWER = u'reviewer'
5024 ROLE_OBSERVER = u'observer'
5024 ROLE_OBSERVER = u'observer'
5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5026
5026
5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5032 user = relationship('User')
5032 user = relationship('User')
5033
5033
5034 def rule_data(self):
5034 def rule_data(self):
5035 return {
5035 return {
5036 'mandatory': self.mandatory,
5036 'mandatory': self.mandatory,
5037 'role': self.role,
5037 'role': self.role,
5038 }
5038 }
5039
5039
5040
5040
5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5042 __tablename__ = 'repo_review_rules_users_groups'
5042 __tablename__ = 'repo_review_rules_users_groups'
5043 __table_args__ = (
5043 __table_args__ = (
5044 base_table_args
5044 base_table_args
5045 )
5045 )
5046
5046
5047 VOTE_RULE_ALL = -1
5047 VOTE_RULE_ALL = -1
5048 ROLE_REVIEWER = u'reviewer'
5048 ROLE_REVIEWER = u'reviewer'
5049 ROLE_OBSERVER = u'observer'
5049 ROLE_OBSERVER = u'observer'
5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5051
5051
5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5058 users_group = relationship('UserGroup')
5058 users_group = relationship('UserGroup')
5059
5059
5060 def rule_data(self):
5060 def rule_data(self):
5061 return {
5061 return {
5062 'mandatory': self.mandatory,
5062 'mandatory': self.mandatory,
5063 'role': self.role,
5063 'role': self.role,
5064 'vote_rule': self.vote_rule
5064 'vote_rule': self.vote_rule
5065 }
5065 }
5066
5066
5067 @property
5067 @property
5068 def vote_rule_label(self):
5068 def vote_rule_label(self):
5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5070 return 'all must vote'
5070 return 'all must vote'
5071 else:
5071 else:
5072 return 'min. vote {}'.format(self.vote_rule)
5072 return 'min. vote {}'.format(self.vote_rule)
5073
5073
5074
5074
5075 class RepoReviewRule(Base, BaseModel):
5075 class RepoReviewRule(Base, BaseModel):
5076 __tablename__ = 'repo_review_rules'
5076 __tablename__ = 'repo_review_rules'
5077 __table_args__ = (
5077 __table_args__ = (
5078 base_table_args
5078 base_table_args
5079 )
5079 )
5080
5080
5081 repo_review_rule_id = Column(
5081 repo_review_rule_id = Column(
5082 'repo_review_rule_id', Integer(), primary_key=True)
5082 'repo_review_rule_id', Integer(), primary_key=True)
5083 repo_id = Column(
5083 repo_id = Column(
5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5085 repo = relationship('Repository', backref='review_rules')
5085 repo = relationship('Repository', backref='review_rules')
5086
5086
5087 review_rule_name = Column('review_rule_name', String(255))
5087 review_rule_name = Column('review_rule_name', String(255))
5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5091
5091
5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5093
5093
5094 # Legacy fields, just for backward compat
5094 # Legacy fields, just for backward compat
5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5097
5097
5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5100
5100
5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5102
5102
5103 rule_users = relationship('RepoReviewRuleUser')
5103 rule_users = relationship('RepoReviewRuleUser')
5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5105
5105
5106 def _validate_pattern(self, value):
5106 def _validate_pattern(self, value):
5107 re.compile('^' + glob2re(value) + '$')
5107 re.compile('^' + glob2re(value) + '$')
5108
5108
5109 @hybrid_property
5109 @hybrid_property
5110 def source_branch_pattern(self):
5110 def source_branch_pattern(self):
5111 return self._branch_pattern or '*'
5111 return self._branch_pattern or '*'
5112
5112
5113 @source_branch_pattern.setter
5113 @source_branch_pattern.setter
5114 def source_branch_pattern(self, value):
5114 def source_branch_pattern(self, value):
5115 self._validate_pattern(value)
5115 self._validate_pattern(value)
5116 self._branch_pattern = value or '*'
5116 self._branch_pattern = value or '*'
5117
5117
5118 @hybrid_property
5118 @hybrid_property
5119 def target_branch_pattern(self):
5119 def target_branch_pattern(self):
5120 return self._target_branch_pattern or '*'
5120 return self._target_branch_pattern or '*'
5121
5121
5122 @target_branch_pattern.setter
5122 @target_branch_pattern.setter
5123 def target_branch_pattern(self, value):
5123 def target_branch_pattern(self, value):
5124 self._validate_pattern(value)
5124 self._validate_pattern(value)
5125 self._target_branch_pattern = value or '*'
5125 self._target_branch_pattern = value or '*'
5126
5126
5127 @hybrid_property
5127 @hybrid_property
5128 def file_pattern(self):
5128 def file_pattern(self):
5129 return self._file_pattern or '*'
5129 return self._file_pattern or '*'
5130
5130
5131 @file_pattern.setter
5131 @file_pattern.setter
5132 def file_pattern(self, value):
5132 def file_pattern(self, value):
5133 self._validate_pattern(value)
5133 self._validate_pattern(value)
5134 self._file_pattern = value or '*'
5134 self._file_pattern = value or '*'
5135
5135
5136 @hybrid_property
5136 @hybrid_property
5137 def forbid_pr_author_to_review(self):
5137 def forbid_pr_author_to_review(self):
5138 return self.pr_author == 'forbid_pr_author'
5138 return self.pr_author == 'forbid_pr_author'
5139
5139
5140 @hybrid_property
5140 @hybrid_property
5141 def include_pr_author_to_review(self):
5141 def include_pr_author_to_review(self):
5142 return self.pr_author == 'include_pr_author'
5142 return self.pr_author == 'include_pr_author'
5143
5143
5144 @hybrid_property
5144 @hybrid_property
5145 def forbid_commit_author_to_review(self):
5145 def forbid_commit_author_to_review(self):
5146 return self.commit_author == 'forbid_commit_author'
5146 return self.commit_author == 'forbid_commit_author'
5147
5147
5148 @hybrid_property
5148 @hybrid_property
5149 def include_commit_author_to_review(self):
5149 def include_commit_author_to_review(self):
5150 return self.commit_author == 'include_commit_author'
5150 return self.commit_author == 'include_commit_author'
5151
5151
5152 def matches(self, source_branch, target_branch, files_changed):
5152 def matches(self, source_branch, target_branch, files_changed):
5153 """
5153 """
5154 Check if this review rule matches a branch/files in a pull request
5154 Check if this review rule matches a branch/files in a pull request
5155
5155
5156 :param source_branch: source branch name for the commit
5156 :param source_branch: source branch name for the commit
5157 :param target_branch: target branch name for the commit
5157 :param target_branch: target branch name for the commit
5158 :param files_changed: list of file paths changed in the pull request
5158 :param files_changed: list of file paths changed in the pull request
5159 """
5159 """
5160
5160
5161 source_branch = source_branch or ''
5161 source_branch = source_branch or ''
5162 target_branch = target_branch or ''
5162 target_branch = target_branch or ''
5163 files_changed = files_changed or []
5163 files_changed = files_changed or []
5164
5164
5165 branch_matches = True
5165 branch_matches = True
5166 if source_branch or target_branch:
5166 if source_branch or target_branch:
5167 if self.source_branch_pattern == '*':
5167 if self.source_branch_pattern == '*':
5168 source_branch_match = True
5168 source_branch_match = True
5169 else:
5169 else:
5170 if self.source_branch_pattern.startswith('re:'):
5170 if self.source_branch_pattern.startswith('re:'):
5171 source_pattern = self.source_branch_pattern[3:]
5171 source_pattern = self.source_branch_pattern[3:]
5172 else:
5172 else:
5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5174 source_branch_regex = re.compile(source_pattern)
5174 source_branch_regex = re.compile(source_pattern)
5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5176 if self.target_branch_pattern == '*':
5176 if self.target_branch_pattern == '*':
5177 target_branch_match = True
5177 target_branch_match = True
5178 else:
5178 else:
5179 if self.target_branch_pattern.startswith('re:'):
5179 if self.target_branch_pattern.startswith('re:'):
5180 target_pattern = self.target_branch_pattern[3:]
5180 target_pattern = self.target_branch_pattern[3:]
5181 else:
5181 else:
5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5183 target_branch_regex = re.compile(target_pattern)
5183 target_branch_regex = re.compile(target_pattern)
5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5185
5185
5186 branch_matches = source_branch_match and target_branch_match
5186 branch_matches = source_branch_match and target_branch_match
5187
5187
5188 files_matches = True
5188 files_matches = True
5189 if self.file_pattern != '*':
5189 if self.file_pattern != '*':
5190 files_matches = False
5190 files_matches = False
5191 if self.file_pattern.startswith('re:'):
5191 if self.file_pattern.startswith('re:'):
5192 file_pattern = self.file_pattern[3:]
5192 file_pattern = self.file_pattern[3:]
5193 else:
5193 else:
5194 file_pattern = glob2re(self.file_pattern)
5194 file_pattern = glob2re(self.file_pattern)
5195 file_regex = re.compile(file_pattern)
5195 file_regex = re.compile(file_pattern)
5196 for file_data in files_changed:
5196 for file_data in files_changed:
5197 filename = file_data.get('filename')
5197 filename = file_data.get('filename')
5198
5198
5199 if file_regex.search(filename):
5199 if file_regex.search(filename):
5200 files_matches = True
5200 files_matches = True
5201 break
5201 break
5202
5202
5203 return branch_matches and files_matches
5203 return branch_matches and files_matches
5204
5204
5205 @property
5205 @property
5206 def review_users(self):
5206 def review_users(self):
5207 """ Returns the users which this rule applies to """
5207 """ Returns the users which this rule applies to """
5208
5208
5209 users = collections.OrderedDict()
5209 users = collections.OrderedDict()
5210
5210
5211 for rule_user in self.rule_users:
5211 for rule_user in self.rule_users:
5212 if rule_user.user.active:
5212 if rule_user.user.active:
5213 if rule_user.user not in users:
5213 if rule_user.user not in users:
5214 users[rule_user.user.username] = {
5214 users[rule_user.user.username] = {
5215 'user': rule_user.user,
5215 'user': rule_user.user,
5216 'source': 'user',
5216 'source': 'user',
5217 'source_data': {},
5217 'source_data': {},
5218 'data': rule_user.rule_data()
5218 'data': rule_user.rule_data()
5219 }
5219 }
5220
5220
5221 for rule_user_group in self.rule_user_groups:
5221 for rule_user_group in self.rule_user_groups:
5222 source_data = {
5222 source_data = {
5223 'user_group_id': rule_user_group.users_group.users_group_id,
5223 'user_group_id': rule_user_group.users_group.users_group_id,
5224 'name': rule_user_group.users_group.users_group_name,
5224 'name': rule_user_group.users_group.users_group_name,
5225 'members': len(rule_user_group.users_group.members)
5225 'members': len(rule_user_group.users_group.members)
5226 }
5226 }
5227 for member in rule_user_group.users_group.members:
5227 for member in rule_user_group.users_group.members:
5228 if member.user.active:
5228 if member.user.active:
5229 key = member.user.username
5229 key = member.user.username
5230 if key in users:
5230 if key in users:
5231 # skip this member as we have him already
5231 # skip this member as we have him already
5232 # this prevents from override the "first" matched
5232 # this prevents from override the "first" matched
5233 # users with duplicates in multiple groups
5233 # users with duplicates in multiple groups
5234 continue
5234 continue
5235
5235
5236 users[key] = {
5236 users[key] = {
5237 'user': member.user,
5237 'user': member.user,
5238 'source': 'user_group',
5238 'source': 'user_group',
5239 'source_data': source_data,
5239 'source_data': source_data,
5240 'data': rule_user_group.rule_data()
5240 'data': rule_user_group.rule_data()
5241 }
5241 }
5242
5242
5243 return users
5243 return users
5244
5244
5245 def user_group_vote_rule(self, user_id):
5245 def user_group_vote_rule(self, user_id):
5246
5246
5247 rules = []
5247 rules = []
5248 if not self.rule_user_groups:
5248 if not self.rule_user_groups:
5249 return rules
5249 return rules
5250
5250
5251 for user_group in self.rule_user_groups:
5251 for user_group in self.rule_user_groups:
5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5253 if user_id in user_group_members:
5253 if user_id in user_group_members:
5254 rules.append(user_group)
5254 rules.append(user_group)
5255 return rules
5255 return rules
5256
5256
5257 def __repr__(self):
5257 def __repr__(self):
5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5259 self.repo_review_rule_id, self.repo)
5259 self.repo_review_rule_id, self.repo)
5260
5260
5261
5261
5262 class ScheduleEntry(Base, BaseModel):
5262 class ScheduleEntry(Base, BaseModel):
5263 __tablename__ = 'schedule_entries'
5263 __tablename__ = 'schedule_entries'
5264 __table_args__ = (
5264 __table_args__ = (
5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5267 base_table_args,
5267 base_table_args,
5268 )
5268 )
5269
5269
5270 schedule_types = ['crontab', 'timedelta', 'integer']
5270 schedule_types = ['crontab', 'timedelta', 'integer']
5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5272
5272
5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5276
5276
5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5279
5279
5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5282
5282
5283 # task
5283 # task
5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5288
5288
5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5291
5291
5292 @hybrid_property
5292 @hybrid_property
5293 def schedule_type(self):
5293 def schedule_type(self):
5294 return self._schedule_type
5294 return self._schedule_type
5295
5295
5296 @schedule_type.setter
5296 @schedule_type.setter
5297 def schedule_type(self, val):
5297 def schedule_type(self, val):
5298 if val not in self.schedule_types:
5298 if val not in self.schedule_types:
5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5300 val, self.schedule_type))
5300 val, self.schedule_type))
5301
5301
5302 self._schedule_type = val
5302 self._schedule_type = val
5303
5303
5304 @classmethod
5304 @classmethod
5305 def get_uid(cls, obj):
5305 def get_uid(cls, obj):
5306 args = obj.task_args
5306 args = obj.task_args
5307 kwargs = obj.task_kwargs
5307 kwargs = obj.task_kwargs
5308 if isinstance(args, JsonRaw):
5308 if isinstance(args, JsonRaw):
5309 try:
5309 try:
5310 args = json.loads(args)
5310 args = json.loads(args)
5311 except ValueError:
5311 except ValueError:
5312 args = tuple()
5312 args = tuple()
5313
5313
5314 if isinstance(kwargs, JsonRaw):
5314 if isinstance(kwargs, JsonRaw):
5315 try:
5315 try:
5316 kwargs = json.loads(kwargs)
5316 kwargs = json.loads(kwargs)
5317 except ValueError:
5317 except ValueError:
5318 kwargs = dict()
5318 kwargs = dict()
5319
5319
5320 dot_notation = obj.task_dot_notation
5320 dot_notation = obj.task_dot_notation
5321 val = '.'.join(map(safe_str, [
5321 val = '.'.join(map(safe_str, [
5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5323 return hashlib.sha1(val).hexdigest()
5323 return hashlib.sha1(val).hexdigest()
5324
5324
5325 @classmethod
5325 @classmethod
5326 def get_by_schedule_name(cls, schedule_name):
5326 def get_by_schedule_name(cls, schedule_name):
5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5328
5328
5329 @classmethod
5329 @classmethod
5330 def get_by_schedule_id(cls, schedule_id):
5330 def get_by_schedule_id(cls, schedule_id):
5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5332
5332
5333 @property
5333 @property
5334 def task(self):
5334 def task(self):
5335 return self.task_dot_notation
5335 return self.task_dot_notation
5336
5336
5337 @property
5337 @property
5338 def schedule(self):
5338 def schedule(self):
5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5341 return schedule
5341 return schedule
5342
5342
5343 @property
5343 @property
5344 def args(self):
5344 def args(self):
5345 try:
5345 try:
5346 return list(self.task_args or [])
5346 return list(self.task_args or [])
5347 except ValueError:
5347 except ValueError:
5348 return list()
5348 return list()
5349
5349
5350 @property
5350 @property
5351 def kwargs(self):
5351 def kwargs(self):
5352 try:
5352 try:
5353 return dict(self.task_kwargs or {})
5353 return dict(self.task_kwargs or {})
5354 except ValueError:
5354 except ValueError:
5355 return dict()
5355 return dict()
5356
5356
5357 def _as_raw(self, val, indent=None):
5357 def _as_raw(self, val, indent=None):
5358 if hasattr(val, 'de_coerce'):
5358 if hasattr(val, 'de_coerce'):
5359 val = val.de_coerce()
5359 val = val.de_coerce()
5360 if val:
5360 if val:
5361 val = json.dumps(val, indent=indent, sort_keys=True)
5361 val = json.dumps(val, indent=indent, sort_keys=True)
5362
5362
5363 return val
5363 return val
5364
5364
5365 @property
5365 @property
5366 def schedule_definition_raw(self):
5366 def schedule_definition_raw(self):
5367 return self._as_raw(self.schedule_definition)
5367 return self._as_raw(self.schedule_definition)
5368
5368
5369 def args_raw(self, indent=None):
5369 def args_raw(self, indent=None):
5370 return self._as_raw(self.task_args, indent)
5370 return self._as_raw(self.task_args, indent)
5371
5371
5372 def kwargs_raw(self, indent=None):
5372 def kwargs_raw(self, indent=None):
5373 return self._as_raw(self.task_kwargs, indent)
5373 return self._as_raw(self.task_kwargs, indent)
5374
5374
5375 def __repr__(self):
5375 def __repr__(self):
5376 return '<DB:ScheduleEntry({}:{})>'.format(
5376 return '<DB:ScheduleEntry({}:{})>'.format(
5377 self.schedule_entry_id, self.schedule_name)
5377 self.schedule_entry_id, self.schedule_name)
5378
5378
5379
5379
5380 @event.listens_for(ScheduleEntry, 'before_update')
5380 @event.listens_for(ScheduleEntry, 'before_update')
5381 def update_task_uid(mapper, connection, target):
5381 def update_task_uid(mapper, connection, target):
5382 target.task_uid = ScheduleEntry.get_uid(target)
5382 target.task_uid = ScheduleEntry.get_uid(target)
5383
5383
5384
5384
5385 @event.listens_for(ScheduleEntry, 'before_insert')
5385 @event.listens_for(ScheduleEntry, 'before_insert')
5386 def set_task_uid(mapper, connection, target):
5386 def set_task_uid(mapper, connection, target):
5387 target.task_uid = ScheduleEntry.get_uid(target)
5387 target.task_uid = ScheduleEntry.get_uid(target)
5388
5388
5389
5389
5390 class _BaseBranchPerms(BaseModel):
5390 class _BaseBranchPerms(BaseModel):
5391 @classmethod
5391 @classmethod
5392 def compute_hash(cls, value):
5392 def compute_hash(cls, value):
5393 return sha1_safe(value)
5393 return sha1_safe(value)
5394
5394
5395 @hybrid_property
5395 @hybrid_property
5396 def branch_pattern(self):
5396 def branch_pattern(self):
5397 return self._branch_pattern or '*'
5397 return self._branch_pattern or '*'
5398
5398
5399 @hybrid_property
5399 @hybrid_property
5400 def branch_hash(self):
5400 def branch_hash(self):
5401 return self._branch_hash
5401 return self._branch_hash
5402
5402
5403 def _validate_glob(self, value):
5403 def _validate_glob(self, value):
5404 re.compile('^' + glob2re(value) + '$')
5404 re.compile('^' + glob2re(value) + '$')
5405
5405
5406 @branch_pattern.setter
5406 @branch_pattern.setter
5407 def branch_pattern(self, value):
5407 def branch_pattern(self, value):
5408 self._validate_glob(value)
5408 self._validate_glob(value)
5409 self._branch_pattern = value or '*'
5409 self._branch_pattern = value or '*'
5410 # set the Hash when setting the branch pattern
5410 # set the Hash when setting the branch pattern
5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5412
5412
5413 def matches(self, branch):
5413 def matches(self, branch):
5414 """
5414 """
5415 Check if this the branch matches entry
5415 Check if this the branch matches entry
5416
5416
5417 :param branch: branch name for the commit
5417 :param branch: branch name for the commit
5418 """
5418 """
5419
5419
5420 branch = branch or ''
5420 branch = branch or ''
5421
5421
5422 branch_matches = True
5422 branch_matches = True
5423 if branch:
5423 if branch:
5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5425 branch_matches = bool(branch_regex.search(branch))
5425 branch_matches = bool(branch_regex.search(branch))
5426
5426
5427 return branch_matches
5427 return branch_matches
5428
5428
5429
5429
5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5431 __tablename__ = 'user_to_repo_branch_permissions'
5431 __tablename__ = 'user_to_repo_branch_permissions'
5432 __table_args__ = (
5432 __table_args__ = (
5433 base_table_args
5433 base_table_args
5434 )
5434 )
5435
5435
5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5437
5437
5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5439 repo = relationship('Repository', backref='user_branch_perms')
5439 repo = relationship('Repository', backref='user_branch_perms')
5440
5440
5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5442 permission = relationship('Permission')
5442 permission = relationship('Permission')
5443
5443
5444 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5444 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5445 user_repo_to_perm = relationship('UserRepoToPerm')
5445 user_repo_to_perm = relationship('UserRepoToPerm')
5446
5446
5447 rule_order = Column('rule_order', Integer(), nullable=False)
5447 rule_order = Column('rule_order', Integer(), nullable=False)
5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5450
5450
5451 def __unicode__(self):
5451 def __unicode__(self):
5452 return u'<UserBranchPermission(%s => %r)>' % (
5452 return u'<UserBranchPermission(%s => %r)>' % (
5453 self.user_repo_to_perm, self.branch_pattern)
5453 self.user_repo_to_perm, self.branch_pattern)
5454
5454
5455
5455
5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5458 __table_args__ = (
5458 __table_args__ = (
5459 base_table_args
5459 base_table_args
5460 )
5460 )
5461
5461
5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5463
5463
5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5465 repo = relationship('Repository', backref='user_group_branch_perms')
5465 repo = relationship('Repository', backref='user_group_branch_perms')
5466
5466
5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5468 permission = relationship('Permission')
5468 permission = relationship('Permission')
5469
5469
5470 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5470 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5472
5472
5473 rule_order = Column('rule_order', Integer(), nullable=False)
5473 rule_order = Column('rule_order', Integer(), nullable=False)
5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5476
5476
5477 def __unicode__(self):
5477 def __unicode__(self):
5478 return u'<UserBranchPermission(%s => %r)>' % (
5478 return u'<UserBranchPermission(%s => %r)>' % (
5479 self.user_group_repo_to_perm, self.branch_pattern)
5479 self.user_group_repo_to_perm, self.branch_pattern)
5480
5480
5481
5481
5482 class UserBookmark(Base, BaseModel):
5482 class UserBookmark(Base, BaseModel):
5483 __tablename__ = 'user_bookmarks'
5483 __tablename__ = 'user_bookmarks'
5484 __table_args__ = (
5484 __table_args__ = (
5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5487 UniqueConstraint('user_id', 'bookmark_position'),
5487 UniqueConstraint('user_id', 'bookmark_position'),
5488 base_table_args
5488 base_table_args
5489 )
5489 )
5490
5490
5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5493 position = Column("bookmark_position", Integer(), nullable=False)
5493 position = Column("bookmark_position", Integer(), nullable=False)
5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5497
5497
5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5500
5500
5501 user = relationship("User")
5501 user = relationship("User")
5502
5502
5503 repository = relationship("Repository")
5503 repository = relationship("Repository")
5504 repository_group = relationship("RepoGroup")
5504 repository_group = relationship("RepoGroup")
5505
5505
5506 @classmethod
5506 @classmethod
5507 def get_by_position_for_user(cls, position, user_id):
5507 def get_by_position_for_user(cls, position, user_id):
5508 return cls.query() \
5508 return cls.query() \
5509 .filter(UserBookmark.user_id == user_id) \
5509 .filter(UserBookmark.user_id == user_id) \
5510 .filter(UserBookmark.position == position).scalar()
5510 .filter(UserBookmark.position == position).scalar()
5511
5511
5512 @classmethod
5512 @classmethod
5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5514 bookmarks = cls.query() \
5514 bookmarks = cls.query() \
5515 .filter(UserBookmark.user_id == user_id) \
5515 .filter(UserBookmark.user_id == user_id) \
5516 .options(joinedload(UserBookmark.repository)) \
5516 .options(joinedload(UserBookmark.repository)) \
5517 .options(joinedload(UserBookmark.repository_group)) \
5517 .options(joinedload(UserBookmark.repository_group)) \
5518 .order_by(UserBookmark.position.asc())
5518 .order_by(UserBookmark.position.asc())
5519
5519
5520 if cache:
5520 if cache:
5521 bookmarks = bookmarks.options(
5521 bookmarks = bookmarks.options(
5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5523 )
5523 )
5524
5524
5525 return bookmarks.all()
5525 return bookmarks.all()
5526
5526
5527 def __unicode__(self):
5527 def __unicode__(self):
5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5529
5529
5530
5530
5531 class FileStore(Base, BaseModel):
5531 class FileStore(Base, BaseModel):
5532 __tablename__ = 'file_store'
5532 __tablename__ = 'file_store'
5533 __table_args__ = (
5533 __table_args__ = (
5534 base_table_args
5534 base_table_args
5535 )
5535 )
5536
5536
5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5538 file_uid = Column('file_uid', String(1024), nullable=False)
5538 file_uid = Column('file_uid', String(1024), nullable=False)
5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5542
5542
5543 # sha256 hash
5543 # sha256 hash
5544 file_hash = Column('file_hash', String(512), nullable=False)
5544 file_hash = Column('file_hash', String(512), nullable=False)
5545 file_size = Column('file_size', BigInteger(), nullable=False)
5545 file_size = Column('file_size', BigInteger(), nullable=False)
5546
5546
5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5549 accessed_count = Column('accessed_count', Integer(), default=0)
5549 accessed_count = Column('accessed_count', Integer(), default=0)
5550
5550
5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5552
5552
5553 # if repo/repo_group reference is set, check for permissions
5553 # if repo/repo_group reference is set, check for permissions
5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5555
5555
5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5558
5558
5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5561
5561
5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5563
5563
5564 # scope limited to user, which requester have access to
5564 # scope limited to user, which requester have access to
5565 scope_user_id = Column(
5565 scope_user_id = Column(
5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5567 nullable=True, unique=None, default=None)
5567 nullable=True, unique=None, default=None)
5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5569
5569
5570 # scope limited to user group, which requester have access to
5570 # scope limited to user group, which requester have access to
5571 scope_user_group_id = Column(
5571 scope_user_group_id = Column(
5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5573 nullable=True, unique=None, default=None)
5573 nullable=True, unique=None, default=None)
5574 user_group = relationship('UserGroup', lazy='joined')
5574 user_group = relationship('UserGroup', lazy='joined')
5575
5575
5576 # scope limited to repo, which requester have access to
5576 # scope limited to repo, which requester have access to
5577 scope_repo_id = Column(
5577 scope_repo_id = Column(
5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5579 nullable=True, unique=None, default=None)
5579 nullable=True, unique=None, default=None)
5580 repo = relationship('Repository', lazy='joined')
5580 repo = relationship('Repository', lazy='joined')
5581
5581
5582 # scope limited to repo group, which requester have access to
5582 # scope limited to repo group, which requester have access to
5583 scope_repo_group_id = Column(
5583 scope_repo_group_id = Column(
5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5585 nullable=True, unique=None, default=None)
5585 nullable=True, unique=None, default=None)
5586 repo_group = relationship('RepoGroup', lazy='joined')
5586 repo_group = relationship('RepoGroup', lazy='joined')
5587
5587
5588 @classmethod
5588 @classmethod
5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5590 if safe:
5590 if safe:
5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5592 else:
5592 else:
5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5594
5594
5595 @classmethod
5595 @classmethod
5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5597 file_description='', enabled=True, hidden=False, check_acl=True,
5597 file_description='', enabled=True, hidden=False, check_acl=True,
5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5599
5599
5600 store_entry = FileStore()
5600 store_entry = FileStore()
5601 store_entry.file_uid = file_uid
5601 store_entry.file_uid = file_uid
5602 store_entry.file_display_name = file_display_name
5602 store_entry.file_display_name = file_display_name
5603 store_entry.file_org_name = filename
5603 store_entry.file_org_name = filename
5604 store_entry.file_size = file_size
5604 store_entry.file_size = file_size
5605 store_entry.file_hash = file_hash
5605 store_entry.file_hash = file_hash
5606 store_entry.file_description = file_description
5606 store_entry.file_description = file_description
5607
5607
5608 store_entry.check_acl = check_acl
5608 store_entry.check_acl = check_acl
5609 store_entry.enabled = enabled
5609 store_entry.enabled = enabled
5610 store_entry.hidden = hidden
5610 store_entry.hidden = hidden
5611
5611
5612 store_entry.user_id = user_id
5612 store_entry.user_id = user_id
5613 store_entry.scope_user_id = scope_user_id
5613 store_entry.scope_user_id = scope_user_id
5614 store_entry.scope_repo_id = scope_repo_id
5614 store_entry.scope_repo_id = scope_repo_id
5615 store_entry.scope_repo_group_id = scope_repo_group_id
5615 store_entry.scope_repo_group_id = scope_repo_group_id
5616
5616
5617 return store_entry
5617 return store_entry
5618
5618
5619 @classmethod
5619 @classmethod
5620 def store_metadata(cls, file_store_id, args, commit=True):
5620 def store_metadata(cls, file_store_id, args, commit=True):
5621 file_store = FileStore.get(file_store_id)
5621 file_store = FileStore.get(file_store_id)
5622 if file_store is None:
5622 if file_store is None:
5623 return
5623 return
5624
5624
5625 for section, key, value, value_type in args:
5625 for section, key, value, value_type in args:
5626 has_key = FileStoreMetadata().query() \
5626 has_key = FileStoreMetadata().query() \
5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5630 .scalar()
5630 .scalar()
5631 if has_key:
5631 if has_key:
5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5633 .format(key, section)
5633 .format(key, section)
5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5635
5635
5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5637 FileStoreMetadata.valid_value_type(value_type)
5637 FileStoreMetadata.valid_value_type(value_type)
5638
5638
5639 meta_entry = FileStoreMetadata()
5639 meta_entry = FileStoreMetadata()
5640 meta_entry.file_store = file_store
5640 meta_entry.file_store = file_store
5641 meta_entry.file_store_meta_section = section
5641 meta_entry.file_store_meta_section = section
5642 meta_entry.file_store_meta_key = key
5642 meta_entry.file_store_meta_key = key
5643 meta_entry.file_store_meta_value_type = value_type
5643 meta_entry.file_store_meta_value_type = value_type
5644 meta_entry.file_store_meta_value = value
5644 meta_entry.file_store_meta_value = value
5645
5645
5646 Session().add(meta_entry)
5646 Session().add(meta_entry)
5647
5647
5648 try:
5648 try:
5649 if commit:
5649 if commit:
5650 Session().commit()
5650 Session().commit()
5651 except IntegrityError:
5651 except IntegrityError:
5652 Session().rollback()
5652 Session().rollback()
5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5654
5654
5655 @classmethod
5655 @classmethod
5656 def bump_access_counter(cls, file_uid, commit=True):
5656 def bump_access_counter(cls, file_uid, commit=True):
5657 FileStore().query()\
5657 FileStore().query()\
5658 .filter(FileStore.file_uid == file_uid)\
5658 .filter(FileStore.file_uid == file_uid)\
5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5660 FileStore.accessed_on: datetime.datetime.now()})
5660 FileStore.accessed_on: datetime.datetime.now()})
5661 if commit:
5661 if commit:
5662 Session().commit()
5662 Session().commit()
5663
5663
5664 def __json__(self):
5664 def __json__(self):
5665 data = {
5665 data = {
5666 'filename': self.file_display_name,
5666 'filename': self.file_display_name,
5667 'filename_org': self.file_org_name,
5667 'filename_org': self.file_org_name,
5668 'file_uid': self.file_uid,
5668 'file_uid': self.file_uid,
5669 'description': self.file_description,
5669 'description': self.file_description,
5670 'hidden': self.hidden,
5670 'hidden': self.hidden,
5671 'size': self.file_size,
5671 'size': self.file_size,
5672 'created_on': self.created_on,
5672 'created_on': self.created_on,
5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5674 'downloaded_times': self.accessed_count,
5674 'downloaded_times': self.accessed_count,
5675 'sha256': self.file_hash,
5675 'sha256': self.file_hash,
5676 'metadata': self.file_metadata,
5676 'metadata': self.file_metadata,
5677 }
5677 }
5678
5678
5679 return data
5679 return data
5680
5680
5681 def __repr__(self):
5681 def __repr__(self):
5682 return '<FileStore({})>'.format(self.file_store_id)
5682 return '<FileStore({})>'.format(self.file_store_id)
5683
5683
5684
5684
5685 class FileStoreMetadata(Base, BaseModel):
5685 class FileStoreMetadata(Base, BaseModel):
5686 __tablename__ = 'file_store_metadata'
5686 __tablename__ = 'file_store_metadata'
5687 __table_args__ = (
5687 __table_args__ = (
5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5691 base_table_args
5691 base_table_args
5692 )
5692 )
5693 SETTINGS_TYPES = {
5693 SETTINGS_TYPES = {
5694 'str': safe_str,
5694 'str': safe_str,
5695 'int': safe_int,
5695 'int': safe_int,
5696 'unicode': safe_unicode,
5696 'unicode': safe_unicode,
5697 'bool': str2bool,
5697 'bool': str2bool,
5698 'list': functools.partial(aslist, sep=',')
5698 'list': functools.partial(aslist, sep=',')
5699 }
5699 }
5700
5700
5701 file_store_meta_id = Column(
5701 file_store_meta_id = Column(
5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5703 primary_key=True)
5703 primary_key=True)
5704 _file_store_meta_section = Column(
5704 _file_store_meta_section = Column(
5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5706 nullable=True, unique=None, default=None)
5706 nullable=True, unique=None, default=None)
5707 _file_store_meta_section_hash = Column(
5707 _file_store_meta_section_hash = Column(
5708 "file_store_meta_section_hash", String(255),
5708 "file_store_meta_section_hash", String(255),
5709 nullable=True, unique=None, default=None)
5709 nullable=True, unique=None, default=None)
5710 _file_store_meta_key = Column(
5710 _file_store_meta_key = Column(
5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5712 nullable=True, unique=None, default=None)
5712 nullable=True, unique=None, default=None)
5713 _file_store_meta_key_hash = Column(
5713 _file_store_meta_key_hash = Column(
5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5715 _file_store_meta_value = Column(
5715 _file_store_meta_value = Column(
5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5717 nullable=True, unique=None, default=None)
5717 nullable=True, unique=None, default=None)
5718 _file_store_meta_value_type = Column(
5718 _file_store_meta_value_type = Column(
5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5720 default='unicode')
5720 default='unicode')
5721
5721
5722 file_store_id = Column(
5722 file_store_id = Column(
5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5724 nullable=True, unique=None, default=None)
5724 nullable=True, unique=None, default=None)
5725
5725
5726 file_store = relationship('FileStore', lazy='joined')
5726 file_store = relationship('FileStore', lazy='joined')
5727
5727
5728 @classmethod
5728 @classmethod
5729 def valid_value_type(cls, value):
5729 def valid_value_type(cls, value):
5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5731 raise ArtifactMetadataBadValueType(
5731 raise ArtifactMetadataBadValueType(
5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5733
5733
5734 @hybrid_property
5734 @hybrid_property
5735 def file_store_meta_section(self):
5735 def file_store_meta_section(self):
5736 return self._file_store_meta_section
5736 return self._file_store_meta_section
5737
5737
5738 @file_store_meta_section.setter
5738 @file_store_meta_section.setter
5739 def file_store_meta_section(self, value):
5739 def file_store_meta_section(self, value):
5740 self._file_store_meta_section = value
5740 self._file_store_meta_section = value
5741 self._file_store_meta_section_hash = _hash_key(value)
5741 self._file_store_meta_section_hash = _hash_key(value)
5742
5742
5743 @hybrid_property
5743 @hybrid_property
5744 def file_store_meta_key(self):
5744 def file_store_meta_key(self):
5745 return self._file_store_meta_key
5745 return self._file_store_meta_key
5746
5746
5747 @file_store_meta_key.setter
5747 @file_store_meta_key.setter
5748 def file_store_meta_key(self, value):
5748 def file_store_meta_key(self, value):
5749 self._file_store_meta_key = value
5749 self._file_store_meta_key = value
5750 self._file_store_meta_key_hash = _hash_key(value)
5750 self._file_store_meta_key_hash = _hash_key(value)
5751
5751
5752 @hybrid_property
5752 @hybrid_property
5753 def file_store_meta_value(self):
5753 def file_store_meta_value(self):
5754 val = self._file_store_meta_value
5754 val = self._file_store_meta_value
5755
5755
5756 if self._file_store_meta_value_type:
5756 if self._file_store_meta_value_type:
5757 # e.g unicode.encrypted == unicode
5757 # e.g unicode.encrypted == unicode
5758 _type = self._file_store_meta_value_type.split('.')[0]
5758 _type = self._file_store_meta_value_type.split('.')[0]
5759 # decode the encrypted value if it's encrypted field type
5759 # decode the encrypted value if it's encrypted field type
5760 if '.encrypted' in self._file_store_meta_value_type:
5760 if '.encrypted' in self._file_store_meta_value_type:
5761 cipher = EncryptedTextValue()
5761 cipher = EncryptedTextValue()
5762 val = safe_unicode(cipher.process_result_value(val, None))
5762 val = safe_unicode(cipher.process_result_value(val, None))
5763 # do final type conversion
5763 # do final type conversion
5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5765 val = converter(val)
5765 val = converter(val)
5766
5766
5767 return val
5767 return val
5768
5768
5769 @file_store_meta_value.setter
5769 @file_store_meta_value.setter
5770 def file_store_meta_value(self, val):
5770 def file_store_meta_value(self, val):
5771 val = safe_unicode(val)
5771 val = safe_unicode(val)
5772 # encode the encrypted value
5772 # encode the encrypted value
5773 if '.encrypted' in self.file_store_meta_value_type:
5773 if '.encrypted' in self.file_store_meta_value_type:
5774 cipher = EncryptedTextValue()
5774 cipher = EncryptedTextValue()
5775 val = safe_unicode(cipher.process_bind_param(val, None))
5775 val = safe_unicode(cipher.process_bind_param(val, None))
5776 self._file_store_meta_value = val
5776 self._file_store_meta_value = val
5777
5777
5778 @hybrid_property
5778 @hybrid_property
5779 def file_store_meta_value_type(self):
5779 def file_store_meta_value_type(self):
5780 return self._file_store_meta_value_type
5780 return self._file_store_meta_value_type
5781
5781
5782 @file_store_meta_value_type.setter
5782 @file_store_meta_value_type.setter
5783 def file_store_meta_value_type(self, val):
5783 def file_store_meta_value_type(self, val):
5784 # e.g unicode.encrypted
5784 # e.g unicode.encrypted
5785 self.valid_value_type(val)
5785 self.valid_value_type(val)
5786 self._file_store_meta_value_type = val
5786 self._file_store_meta_value_type = val
5787
5787
5788 def __json__(self):
5788 def __json__(self):
5789 data = {
5789 data = {
5790 'artifact': self.file_store.file_uid,
5790 'artifact': self.file_store.file_uid,
5791 'section': self.file_store_meta_section,
5791 'section': self.file_store_meta_section,
5792 'key': self.file_store_meta_key,
5792 'key': self.file_store_meta_key,
5793 'value': self.file_store_meta_value,
5793 'value': self.file_store_meta_value,
5794 }
5794 }
5795
5795
5796 return data
5796 return data
5797
5797
5798 def __repr__(self):
5798 def __repr__(self):
5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5800 self.file_store_meta_key, self.file_store_meta_value)
5800 self.file_store_meta_key, self.file_store_meta_value)
5801
5801
5802
5802
5803 class DbMigrateVersion(Base, BaseModel):
5803 class DbMigrateVersion(Base, BaseModel):
5804 __tablename__ = 'db_migrate_version'
5804 __tablename__ = 'db_migrate_version'
5805 __table_args__ = (
5805 __table_args__ = (
5806 base_table_args,
5806 base_table_args,
5807 )
5807 )
5808
5808
5809 repository_id = Column('repository_id', String(250), primary_key=True)
5809 repository_id = Column('repository_id', String(250), primary_key=True)
5810 repository_path = Column('repository_path', Text)
5810 repository_path = Column('repository_path', Text)
5811 version = Column('version', Integer)
5811 version = Column('version', Integer)
5812
5812
5813 @classmethod
5813 @classmethod
5814 def set_version(cls, version):
5814 def set_version(cls, version):
5815 """
5815 """
5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5817 """
5817 """
5818 ver = DbMigrateVersion.query().first()
5818 ver = DbMigrateVersion.query().first()
5819 ver.version = version
5819 ver.version = version
5820 Session().commit()
5820 Session().commit()
5821
5821
5822
5822
5823 class DbSession(Base, BaseModel):
5823 class DbSession(Base, BaseModel):
5824 __tablename__ = 'db_session'
5824 __tablename__ = 'db_session'
5825 __table_args__ = (
5825 __table_args__ = (
5826 base_table_args,
5826 base_table_args,
5827 )
5827 )
5828
5828
5829 def __repr__(self):
5829 def __repr__(self):
5830 return '<DB:DbSession({})>'.format(self.id)
5830 return '<DB:DbSession({})>'.format(self.id)
5831
5831
5832 id = Column('id', Integer())
5832 id = Column('id', Integer())
5833 namespace = Column('namespace', String(255), primary_key=True)
5833 namespace = Column('namespace', String(255), primary_key=True)
5834 accessed = Column('accessed', DateTime, nullable=False)
5834 accessed = Column('accessed', DateTime, nullable=False)
5835 created = Column('created', DateTime, nullable=False)
5835 created = Column('created', DateTime, nullable=False)
5836 data = Column('data', PickleType, nullable=False)
5836 data = Column('data', PickleType, nullable=False)
@@ -1,2249 +1,2372 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 pull request model for RhodeCode
23 pull request model for RhodeCode
24 """
24 """
25
25
26
26
27 import json
27 import json
28 import logging
28 import logging
29 import os
29 import os
30
30
31 import datetime
31 import datetime
32 import urllib
32 import urllib
33 import collections
33 import collections
34
34
35 from pyramid import compat
35 from pyramid import compat
36 from pyramid.threadlocal import get_current_request
36 from pyramid.threadlocal import get_current_request
37
37
38 from rhodecode.lib.vcs.nodes import FileNode
38 from rhodecode.lib.vcs.nodes import FileNode
39 from rhodecode.translation import lazy_ugettext
39 from rhodecode.translation import lazy_ugettext
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
40 from rhodecode.lib import helpers as h, hooks_utils, diffs
41 from rhodecode.lib import audit_logger
41 from rhodecode.lib import audit_logger
42 from rhodecode.lib.compat import OrderedDict
42 from rhodecode.lib.compat import OrderedDict
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
44 from rhodecode.lib.markup_renderer import (
44 from rhodecode.lib.markup_renderer import (
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
45 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
46 from rhodecode.lib.utils2 import (
46 from rhodecode.lib.utils2 import (
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
47 safe_unicode, safe_str, md5_safe, AttributeDict, safe_int,
48 get_current_rhodecode_user)
48 get_current_rhodecode_user)
49 from rhodecode.lib.vcs.backends.base import (
49 from rhodecode.lib.vcs.backends.base import (
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
50 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason,
51 TargetRefMissing, SourceRefMissing)
51 TargetRefMissing, SourceRefMissing)
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
52 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 CommitDoesNotExistError, EmptyRepositoryError)
54 CommitDoesNotExistError, EmptyRepositoryError)
55 from rhodecode.model import BaseModel
55 from rhodecode.model import BaseModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
56 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.comment import CommentsModel
57 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.db import (
58 from rhodecode.model.db import (
59 or_, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
59 aliased, null, lazyload, and_, or_, func, String, cast, PullRequest, PullRequestReviewers, ChangesetStatus,
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
60 PullRequestVersion, ChangesetComment, Repository, RepoReviewRule, User)
61 from rhodecode.model.meta import Session
61 from rhodecode.model.meta import Session
62 from rhodecode.model.notification import NotificationModel, \
62 from rhodecode.model.notification import NotificationModel, \
63 EmailNotificationModel
63 EmailNotificationModel
64 from rhodecode.model.scm import ScmModel
64 from rhodecode.model.scm import ScmModel
65 from rhodecode.model.settings import VcsSettingsModel
65 from rhodecode.model.settings import VcsSettingsModel
66
66
67
67
68 log = logging.getLogger(__name__)
68 log = logging.getLogger(__name__)
69
69
70
70
71 # Data structure to hold the response data when updating commits during a pull
71 # Data structure to hold the response data when updating commits during a pull
72 # request update.
72 # request update.
73 class UpdateResponse(object):
73 class UpdateResponse(object):
74
74
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
75 def __init__(self, executed, reason, new, old, common_ancestor_id,
76 commit_changes, source_changed, target_changed):
76 commit_changes, source_changed, target_changed):
77
77
78 self.executed = executed
78 self.executed = executed
79 self.reason = reason
79 self.reason = reason
80 self.new = new
80 self.new = new
81 self.old = old
81 self.old = old
82 self.common_ancestor_id = common_ancestor_id
82 self.common_ancestor_id = common_ancestor_id
83 self.changes = commit_changes
83 self.changes = commit_changes
84 self.source_changed = source_changed
84 self.source_changed = source_changed
85 self.target_changed = target_changed
85 self.target_changed = target_changed
86
86
87
87
88 def get_diff_info(
88 def get_diff_info(
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
89 source_repo, source_ref, target_repo, target_ref, get_authors=False,
90 get_commit_authors=True):
90 get_commit_authors=True):
91 """
91 """
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
92 Calculates detailed diff information for usage in preview of creation of a pull-request.
93 This is also used for default reviewers logic
93 This is also used for default reviewers logic
94 """
94 """
95
95
96 source_scm = source_repo.scm_instance()
96 source_scm = source_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
97 target_scm = target_repo.scm_instance()
98
98
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
99 ancestor_id = target_scm.get_common_ancestor(target_ref, source_ref, source_scm)
100 if not ancestor_id:
100 if not ancestor_id:
101 raise ValueError(
101 raise ValueError(
102 'cannot calculate diff info without a common ancestor. '
102 'cannot calculate diff info without a common ancestor. '
103 'Make sure both repositories are related, and have a common forking commit.')
103 'Make sure both repositories are related, and have a common forking commit.')
104
104
105 # case here is that want a simple diff without incoming commits,
105 # case here is that want a simple diff without incoming commits,
106 # previewing what will be merged based only on commits in the source.
106 # previewing what will be merged based only on commits in the source.
107 log.debug('Using ancestor %s as source_ref instead of %s',
107 log.debug('Using ancestor %s as source_ref instead of %s',
108 ancestor_id, source_ref)
108 ancestor_id, source_ref)
109
109
110 # source of changes now is the common ancestor
110 # source of changes now is the common ancestor
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
111 source_commit = source_scm.get_commit(commit_id=ancestor_id)
112 # target commit becomes the source ref as it is the last commit
112 # target commit becomes the source ref as it is the last commit
113 # for diff generation this logic gives proper diff
113 # for diff generation this logic gives proper diff
114 target_commit = source_scm.get_commit(commit_id=source_ref)
114 target_commit = source_scm.get_commit(commit_id=source_ref)
115
115
116 vcs_diff = \
116 vcs_diff = \
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
117 source_scm.get_diff(commit1=source_commit, commit2=target_commit,
118 ignore_whitespace=False, context=3)
118 ignore_whitespace=False, context=3)
119
119
120 diff_processor = diffs.DiffProcessor(
120 diff_processor = diffs.DiffProcessor(
121 vcs_diff, format='newdiff', diff_limit=None,
121 vcs_diff, format='newdiff', diff_limit=None,
122 file_limit=None, show_full_diff=True)
122 file_limit=None, show_full_diff=True)
123
123
124 _parsed = diff_processor.prepare()
124 _parsed = diff_processor.prepare()
125
125
126 all_files = []
126 all_files = []
127 all_files_changes = []
127 all_files_changes = []
128 changed_lines = {}
128 changed_lines = {}
129 stats = [0, 0]
129 stats = [0, 0]
130 for f in _parsed:
130 for f in _parsed:
131 all_files.append(f['filename'])
131 all_files.append(f['filename'])
132 all_files_changes.append({
132 all_files_changes.append({
133 'filename': f['filename'],
133 'filename': f['filename'],
134 'stats': f['stats']
134 'stats': f['stats']
135 })
135 })
136 stats[0] += f['stats']['added']
136 stats[0] += f['stats']['added']
137 stats[1] += f['stats']['deleted']
137 stats[1] += f['stats']['deleted']
138
138
139 changed_lines[f['filename']] = []
139 changed_lines[f['filename']] = []
140 if len(f['chunks']) < 2:
140 if len(f['chunks']) < 2:
141 continue
141 continue
142 # first line is "context" information
142 # first line is "context" information
143 for chunks in f['chunks'][1:]:
143 for chunks in f['chunks'][1:]:
144 for chunk in chunks['lines']:
144 for chunk in chunks['lines']:
145 if chunk['action'] not in ('del', 'mod'):
145 if chunk['action'] not in ('del', 'mod'):
146 continue
146 continue
147 changed_lines[f['filename']].append(chunk['old_lineno'])
147 changed_lines[f['filename']].append(chunk['old_lineno'])
148
148
149 commit_authors = []
149 commit_authors = []
150 user_counts = {}
150 user_counts = {}
151 email_counts = {}
151 email_counts = {}
152 author_counts = {}
152 author_counts = {}
153 _commit_cache = {}
153 _commit_cache = {}
154
154
155 commits = []
155 commits = []
156 if get_commit_authors:
156 if get_commit_authors:
157 log.debug('Obtaining commit authors from set of commits')
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 _compare_data = target_scm.compare(
159 target_ref, source_ref, source_scm, merge=True,
159 target_ref, source_ref, source_scm, merge=True,
160 pre_load=["author", "date", "message"]
160 pre_load=["author", "date", "message"]
161 )
161 )
162
162
163 for commit in _compare_data:
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
166 serialized_commit = dict(
167 author=commit.author,
167 author=commit.author,
168 date=commit.date,
168 date=commit.date,
169 message=commit.message,
169 message=commit.message,
170 commit_id=commit.raw_id,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
171 raw_id=commit.raw_id
172 )
172 )
173 commits.append(serialized_commit)
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
174 user = User.get_from_cs_author(serialized_commit['author'])
175 if user and user not in commit_authors:
175 if user and user not in commit_authors:
176 commit_authors.append(user)
176 commit_authors.append(user)
177
177
178 # lines
178 # lines
179 if get_authors:
179 if get_authors:
180 log.debug('Calculating authors of changed files')
180 log.debug('Calculating authors of changed files')
181 target_commit = source_repo.get_commit(ancestor_id)
181 target_commit = source_repo.get_commit(ancestor_id)
182
182
183 for fname, lines in changed_lines.items():
183 for fname, lines in changed_lines.items():
184
184
185 try:
185 try:
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
187 except Exception:
187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
188 log.exception("Failed to load node with path %s", fname)
189 continue
189 continue
190
190
191 if not isinstance(node, FileNode):
191 if not isinstance(node, FileNode):
192 continue
192 continue
193
193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
195 if node.is_binary:
196 author = node.last_commit.author
196 author = node.last_commit.author
197 email = node.last_commit.author_email
197 email = node.last_commit.author_email
198
198
199 user = User.get_from_cs_author(author)
199 user = User.get_from_cs_author(author)
200 if user:
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
204
205 continue
205 continue
206
206
207 for annotation in node.annotate:
207 for annotation in node.annotate:
208 line_no, commit_id, get_commit_func, line_text = annotation
208 line_no, commit_id, get_commit_func, line_text = annotation
209 if line_no in lines:
209 if line_no in lines:
210 if commit_id not in _commit_cache:
210 if commit_id not in _commit_cache:
211 _commit_cache[commit_id] = get_commit_func()
211 _commit_cache[commit_id] = get_commit_func()
212 commit = _commit_cache[commit_id]
212 commit = _commit_cache[commit_id]
213 author = commit.author
213 author = commit.author
214 email = commit.author_email
214 email = commit.author_email
215 user = User.get_from_cs_author(author)
215 user = User.get_from_cs_author(author)
216 if user:
216 if user:
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
217 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
218 author_counts[author] = author_counts.get(author, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
219 email_counts[email] = email_counts.get(email, 0) + 1
220
220
221 log.debug('Default reviewers processing finished')
221 log.debug('Default reviewers processing finished')
222
222
223 return {
223 return {
224 'commits': commits,
224 'commits': commits,
225 'files': all_files_changes,
225 'files': all_files_changes,
226 'stats': stats,
226 'stats': stats,
227 'ancestor': ancestor_id,
227 'ancestor': ancestor_id,
228 # original authors of modified files
228 # original authors of modified files
229 'original_authors': {
229 'original_authors': {
230 'users': user_counts,
230 'users': user_counts,
231 'authors': author_counts,
231 'authors': author_counts,
232 'emails': email_counts,
232 'emails': email_counts,
233 },
233 },
234 'commit_authors': commit_authors
234 'commit_authors': commit_authors
235 }
235 }
236
236
237
237
238 class PullRequestModel(BaseModel):
238 class PullRequestModel(BaseModel):
239
239
240 cls = PullRequest
240 cls = PullRequest
241
241
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
242 DIFF_CONTEXT = diffs.DEFAULT_CONTEXT
243
243
244 UPDATE_STATUS_MESSAGES = {
244 UPDATE_STATUS_MESSAGES = {
245 UpdateFailureReason.NONE: lazy_ugettext(
245 UpdateFailureReason.NONE: lazy_ugettext(
246 'Pull request update successful.'),
246 'Pull request update successful.'),
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
247 UpdateFailureReason.UNKNOWN: lazy_ugettext(
248 'Pull request update failed because of an unknown error.'),
248 'Pull request update failed because of an unknown error.'),
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
249 UpdateFailureReason.NO_CHANGE: lazy_ugettext(
250 'No update needed because the source and target have not changed.'),
250 'No update needed because the source and target have not changed.'),
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
251 UpdateFailureReason.WRONG_REF_TYPE: lazy_ugettext(
252 'Pull request cannot be updated because the reference type is '
252 'Pull request cannot be updated because the reference type is '
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
253 'not supported for an update. Only Branch, Tag or Bookmark is allowed.'),
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
254 UpdateFailureReason.MISSING_TARGET_REF: lazy_ugettext(
255 'This pull request cannot be updated because the target '
255 'This pull request cannot be updated because the target '
256 'reference is missing.'),
256 'reference is missing.'),
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
257 UpdateFailureReason.MISSING_SOURCE_REF: lazy_ugettext(
258 'This pull request cannot be updated because the source '
258 'This pull request cannot be updated because the source '
259 'reference is missing.'),
259 'reference is missing.'),
260 }
260 }
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
261 REF_TYPES = ['bookmark', 'book', 'tag', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
262 UPDATABLE_REF_TYPES = ['bookmark', 'book', 'branch']
263
263
264 def __get_pull_request(self, pull_request):
264 def __get_pull_request(self, pull_request):
265 return self._get_instance((
265 return self._get_instance((
266 PullRequest, PullRequestVersion), pull_request)
266 PullRequest, PullRequestVersion), pull_request)
267
267
268 def _check_perms(self, perms, pull_request, user, api=False):
268 def _check_perms(self, perms, pull_request, user, api=False):
269 if not api:
269 if not api:
270 return h.HasRepoPermissionAny(*perms)(
270 return h.HasRepoPermissionAny(*perms)(
271 user=user, repo_name=pull_request.target_repo.repo_name)
271 user=user, repo_name=pull_request.target_repo.repo_name)
272 else:
272 else:
273 return h.HasRepoPermissionAnyApi(*perms)(
273 return h.HasRepoPermissionAnyApi(*perms)(
274 user=user, repo_name=pull_request.target_repo.repo_name)
274 user=user, repo_name=pull_request.target_repo.repo_name)
275
275
276 def check_user_read(self, pull_request, user, api=False):
276 def check_user_read(self, pull_request, user, api=False):
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
277 _perms = ('repository.admin', 'repository.write', 'repository.read',)
278 return self._check_perms(_perms, pull_request, user, api)
278 return self._check_perms(_perms, pull_request, user, api)
279
279
280 def check_user_merge(self, pull_request, user, api=False):
280 def check_user_merge(self, pull_request, user, api=False):
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
281 _perms = ('repository.admin', 'repository.write', 'hg.admin',)
282 return self._check_perms(_perms, pull_request, user, api)
282 return self._check_perms(_perms, pull_request, user, api)
283
283
284 def check_user_update(self, pull_request, user, api=False):
284 def check_user_update(self, pull_request, user, api=False):
285 owner = user.user_id == pull_request.user_id
285 owner = user.user_id == pull_request.user_id
286 return self.check_user_merge(pull_request, user, api) or owner
286 return self.check_user_merge(pull_request, user, api) or owner
287
287
288 def check_user_delete(self, pull_request, user):
288 def check_user_delete(self, pull_request, user):
289 owner = user.user_id == pull_request.user_id
289 owner = user.user_id == pull_request.user_id
290 _perms = ('repository.admin',)
290 _perms = ('repository.admin',)
291 return self._check_perms(_perms, pull_request, user) or owner
291 return self._check_perms(_perms, pull_request, user) or owner
292
292
293 def is_user_reviewer(self, pull_request, user):
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
294 return user.user_id in [
295 x.user_id for x in
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
297 if x.user
298 ]
298 ]
299
299
300 def check_user_change_status(self, pull_request, user, api=False):
300 def check_user_change_status(self, pull_request, user, api=False):
301 return self.check_user_update(pull_request, user, api) \
301 return self.check_user_update(pull_request, user, api) \
302 or self.is_user_reviewer(pull_request, user)
302 or self.is_user_reviewer(pull_request, user)
303
303
304 def check_user_comment(self, pull_request, user):
304 def check_user_comment(self, pull_request, user):
305 owner = user.user_id == pull_request.user_id
305 owner = user.user_id == pull_request.user_id
306 return self.check_user_read(pull_request, user) or owner
306 return self.check_user_read(pull_request, user) or owner
307
307
308 def get(self, pull_request):
308 def get(self, pull_request):
309 return self.__get_pull_request(pull_request)
309 return self.__get_pull_request(pull_request)
310
310
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
311 def _prepare_get_all_query(self, repo_name, search_q=None, source=False,
312 statuses=None, opened_by=None, order_by=None,
312 statuses=None, opened_by=None, order_by=None,
313 order_dir='desc', only_created=False):
313 order_dir='desc', only_created=False):
314 repo = None
314 repo = None
315 if repo_name:
315 if repo_name:
316 repo = self._get_repo(repo_name)
316 repo = self._get_repo(repo_name)
317
317
318 q = PullRequest.query()
318 q = PullRequest.query()
319
319
320 if search_q:
320 if search_q:
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
321 like_expression = u'%{}%'.format(safe_unicode(search_q))
322 q = q.join(User)
322 q = q.join(User, User.user_id == PullRequest.user_id)
323 q = q.filter(or_(
323 q = q.filter(or_(
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
324 cast(PullRequest.pull_request_id, String).ilike(like_expression),
325 User.username.ilike(like_expression),
325 User.username.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
326 PullRequest.title.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
327 PullRequest.description.ilike(like_expression),
328 ))
328 ))
329
329
330 # source or target
330 # source or target
331 if repo and source:
331 if repo and source:
332 q = q.filter(PullRequest.source_repo == repo)
332 q = q.filter(PullRequest.source_repo == repo)
333 elif repo:
333 elif repo:
334 q = q.filter(PullRequest.target_repo == repo)
334 q = q.filter(PullRequest.target_repo == repo)
335
335
336 # closed,opened
336 # closed,opened
337 if statuses:
337 if statuses:
338 q = q.filter(PullRequest.status.in_(statuses))
338 q = q.filter(PullRequest.status.in_(statuses))
339
339
340 # opened by filter
340 # opened by filter
341 if opened_by:
341 if opened_by:
342 q = q.filter(PullRequest.user_id.in_(opened_by))
342 q = q.filter(PullRequest.user_id.in_(opened_by))
343
343
344 # only get those that are in "created" state
344 # only get those that are in "created" state
345 if only_created:
345 if only_created:
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
346 q = q.filter(PullRequest.pull_request_state == PullRequest.STATE_CREATED)
347
347
348 if order_by:
348 if order_by:
349 order_map = {
349 order_map = {
350 'name_raw': PullRequest.pull_request_id,
350 'name_raw': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
351 'id': PullRequest.pull_request_id,
352 'title': PullRequest.title,
352 'title': PullRequest.title,
353 'updated_on_raw': PullRequest.updated_on,
353 'updated_on_raw': PullRequest.updated_on,
354 'target_repo': PullRequest.target_repo_id
354 'target_repo': PullRequest.target_repo_id
355 }
355 }
356 if order_dir == 'asc':
356 if order_dir == 'asc':
357 q = q.order_by(order_map[order_by].asc())
357 q = q.order_by(order_map[order_by].asc())
358 else:
358 else:
359 q = q.order_by(order_map[order_by].desc())
359 q = q.order_by(order_map[order_by].desc())
360
360
361 return q
361 return q
362
362
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
363 def count_all(self, repo_name, search_q=None, source=False, statuses=None,
364 opened_by=None):
364 opened_by=None):
365 """
365 """
366 Count the number of pull requests for a specific repository.
366 Count the number of pull requests for a specific repository.
367
367
368 :param repo_name: target or source repo
368 :param repo_name: target or source repo
369 :param search_q: filter by text
369 :param search_q: filter by text
370 :param source: boolean flag to specify if repo_name refers to source
370 :param source: boolean flag to specify if repo_name refers to source
371 :param statuses: list of pull request statuses
371 :param statuses: list of pull request statuses
372 :param opened_by: author user of the pull request
372 :param opened_by: author user of the pull request
373 :returns: int number of pull requests
373 :returns: int number of pull requests
374 """
374 """
375 q = self._prepare_get_all_query(
375 q = self._prepare_get_all_query(
376 repo_name, search_q=search_q, source=source, statuses=statuses,
376 repo_name, search_q=search_q, source=source, statuses=statuses,
377 opened_by=opened_by)
377 opened_by=opened_by)
378
378
379 return q.count()
379 return q.count()
380
380
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
381 def get_all(self, repo_name, search_q=None, source=False, statuses=None,
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
382 opened_by=None, offset=0, length=None, order_by=None, order_dir='desc'):
383 """
383 """
384 Get all pull requests for a specific repository.
384 Get all pull requests for a specific repository.
385
385
386 :param repo_name: target or source repo
386 :param repo_name: target or source repo
387 :param search_q: filter by text
387 :param search_q: filter by text
388 :param source: boolean flag to specify if repo_name refers to source
388 :param source: boolean flag to specify if repo_name refers to source
389 :param statuses: list of pull request statuses
389 :param statuses: list of pull request statuses
390 :param opened_by: author user of the pull request
390 :param opened_by: author user of the pull request
391 :param offset: pagination offset
391 :param offset: pagination offset
392 :param length: length of returned list
392 :param length: length of returned list
393 :param order_by: order of the returned list
393 :param order_by: order of the returned list
394 :param order_dir: 'asc' or 'desc' ordering direction
394 :param order_dir: 'asc' or 'desc' ordering direction
395 :returns: list of pull requests
395 :returns: list of pull requests
396 """
396 """
397 q = self._prepare_get_all_query(
397 q = self._prepare_get_all_query(
398 repo_name, search_q=search_q, source=source, statuses=statuses,
398 repo_name, search_q=search_q, source=source, statuses=statuses,
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
399 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
400
400
401 if length:
401 if length:
402 pull_requests = q.limit(length).offset(offset).all()
402 pull_requests = q.limit(length).offset(offset).all()
403 else:
403 else:
404 pull_requests = q.all()
404 pull_requests = q.all()
405
405
406 return pull_requests
406 return pull_requests
407
407
408 def count_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
408 def count_awaiting_review(self, repo_name, search_q=None, statuses=None):
409 opened_by=None):
410 """
409 """
411 Count the number of pull requests for a specific repository that are
410 Count the number of pull requests for a specific repository that are
412 awaiting review.
411 awaiting review.
413
412
414 :param repo_name: target or source repo
413 :param repo_name: target or source repo
415 :param search_q: filter by text
414 :param search_q: filter by text
416 :param source: boolean flag to specify if repo_name refers to source
417 :param statuses: list of pull request statuses
415 :param statuses: list of pull request statuses
418 :param opened_by: author user of the pull request
419 :returns: int number of pull requests
416 :returns: int number of pull requests
420 """
417 """
421 pull_requests = self.get_awaiting_review(
418 pull_requests = self.get_awaiting_review(
422 repo_name, search_q=search_q, source=source, statuses=statuses, opened_by=opened_by)
419 repo_name, search_q=search_q, statuses=statuses)
423
420
424 return len(pull_requests)
421 return len(pull_requests)
425
422
426 def get_awaiting_review(self, repo_name, search_q=None, source=False, statuses=None,
423 def get_awaiting_review(self, repo_name, search_q=None, statuses=None,
427 opened_by=None, offset=0, length=None,
424 offset=0, length=None, order_by=None, order_dir='desc'):
428 order_by=None, order_dir='desc'):
429 """
425 """
430 Get all pull requests for a specific repository that are awaiting
426 Get all pull requests for a specific repository that are awaiting
431 review.
427 review.
432
428
433 :param repo_name: target or source repo
429 :param repo_name: target or source repo
434 :param search_q: filter by text
430 :param search_q: filter by text
435 :param source: boolean flag to specify if repo_name refers to source
436 :param statuses: list of pull request statuses
431 :param statuses: list of pull request statuses
437 :param opened_by: author user of the pull request
438 :param offset: pagination offset
432 :param offset: pagination offset
439 :param length: length of returned list
433 :param length: length of returned list
440 :param order_by: order of the returned list
434 :param order_by: order of the returned list
441 :param order_dir: 'asc' or 'desc' ordering direction
435 :param order_dir: 'asc' or 'desc' ordering direction
442 :returns: list of pull requests
436 :returns: list of pull requests
443 """
437 """
444 pull_requests = self.get_all(
438 pull_requests = self.get_all(
445 repo_name, search_q=search_q, source=source, statuses=statuses,
439 repo_name, search_q=search_q, statuses=statuses,
446 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
440 order_by=order_by, order_dir=order_dir)
447
441
448 _filtered_pull_requests = []
442 _filtered_pull_requests = []
449 for pr in pull_requests:
443 for pr in pull_requests:
450 status = pr.calculated_review_status()
444 status = pr.calculated_review_status()
451 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
445 if status in [ChangesetStatus.STATUS_NOT_REVIEWED,
452 ChangesetStatus.STATUS_UNDER_REVIEW]:
446 ChangesetStatus.STATUS_UNDER_REVIEW]:
453 _filtered_pull_requests.append(pr)
447 _filtered_pull_requests.append(pr)
454 if length:
448 if length:
455 return _filtered_pull_requests[offset:offset+length]
449 return _filtered_pull_requests[offset:offset+length]
456 else:
450 else:
457 return _filtered_pull_requests
451 return _filtered_pull_requests
458
452
459 def count_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
453 def _prepare_awaiting_my_review_review_query(
460 opened_by=None, user_id=None):
454 self, repo_name, user_id, search_q=None, statuses=None,
455 order_by=None, order_dir='desc'):
456
457 for_review_statuses = [
458 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
459 ]
460
461 pull_request_alias = aliased(PullRequest)
462 status_alias = aliased(ChangesetStatus)
463 reviewers_alias = aliased(PullRequestReviewers)
464 repo_alias = aliased(Repository)
465
466 last_ver_subq = Session()\
467 .query(func.min(ChangesetStatus.version)) \
468 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
469 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
470 .subquery()
471
472 q = Session().query(pull_request_alias) \
473 .options(lazyload(pull_request_alias.author)) \
474 .join(reviewers_alias,
475 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
476 .join(repo_alias,
477 repo_alias.repo_id == pull_request_alias.target_repo_id) \
478 .outerjoin(status_alias,
479 and_(status_alias.user_id == reviewers_alias.user_id,
480 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
481 .filter(or_(status_alias.version == null(),
482 status_alias.version == last_ver_subq)) \
483 .filter(reviewers_alias.user_id == user_id) \
484 .filter(repo_alias.repo_name == repo_name) \
485 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
486 .group_by(pull_request_alias)
487
488 # closed,opened
489 if statuses:
490 q = q.filter(pull_request_alias.status.in_(statuses))
491
492 if search_q:
493 like_expression = u'%{}%'.format(safe_unicode(search_q))
494 q = q.join(User, User.user_id == pull_request_alias.user_id)
495 q = q.filter(or_(
496 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
497 User.username.ilike(like_expression),
498 pull_request_alias.title.ilike(like_expression),
499 pull_request_alias.description.ilike(like_expression),
500 ))
501
502 if order_by:
503 order_map = {
504 'name_raw': pull_request_alias.pull_request_id,
505 'title': pull_request_alias.title,
506 'updated_on_raw': pull_request_alias.updated_on,
507 'target_repo': pull_request_alias.target_repo_id
508 }
509 if order_dir == 'asc':
510 q = q.order_by(order_map[order_by].asc())
511 else:
512 q = q.order_by(order_map[order_by].desc())
513
514 return q
515
516 def count_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None):
461 """
517 """
462 Count the number of pull requests for a specific repository that are
518 Count the number of pull requests for a specific repository that are
463 awaiting review from a specific user.
519 awaiting review from a specific user.
464
520
465 :param repo_name: target or source repo
521 :param repo_name: target or source repo
522 :param user_id: reviewer user of the pull request
466 :param search_q: filter by text
523 :param search_q: filter by text
467 :param source: boolean flag to specify if repo_name refers to source
468 :param statuses: list of pull request statuses
524 :param statuses: list of pull request statuses
469 :param opened_by: author user of the pull request
470 :param user_id: reviewer user of the pull request
471 :returns: int number of pull requests
525 :returns: int number of pull requests
472 """
526 """
473 pull_requests = self.get_awaiting_my_review(
527 q = self._prepare_awaiting_my_review_review_query(
474 repo_name, search_q=search_q, source=source, statuses=statuses,
528 repo_name, user_id, search_q=search_q, statuses=statuses)
475 opened_by=opened_by, user_id=user_id)
529 return q.count()
476
530
477 return len(pull_requests)
531 def get_awaiting_my_review(self, repo_name, user_id, search_q=None, statuses=None,
478
532 offset=0, length=None, order_by=None, order_dir='desc'):
479 def get_awaiting_my_review(self, repo_name, search_q=None, source=False, statuses=None,
480 opened_by=None, user_id=None, offset=0,
481 length=None, order_by=None, order_dir='desc'):
482 """
533 """
483 Get all pull requests for a specific repository that are awaiting
534 Get all pull requests for a specific repository that are awaiting
484 review from a specific user.
535 review from a specific user.
485
536
486 :param repo_name: target or source repo
537 :param repo_name: target or source repo
538 :param user_id: reviewer user of the pull request
487 :param search_q: filter by text
539 :param search_q: filter by text
488 :param source: boolean flag to specify if repo_name refers to source
489 :param statuses: list of pull request statuses
540 :param statuses: list of pull request statuses
490 :param opened_by: author user of the pull request
491 :param user_id: reviewer user of the pull request
492 :param offset: pagination offset
541 :param offset: pagination offset
493 :param length: length of returned list
542 :param length: length of returned list
494 :param order_by: order of the returned list
543 :param order_by: order of the returned list
495 :param order_dir: 'asc' or 'desc' ordering direction
544 :param order_dir: 'asc' or 'desc' ordering direction
496 :returns: list of pull requests
545 :returns: list of pull requests
497 """
546 """
498 pull_requests = self.get_all(
499 repo_name, search_q=search_q, source=source, statuses=statuses,
500 opened_by=opened_by, order_by=order_by, order_dir=order_dir)
501
547
502 _my = PullRequestModel().get_not_reviewed(user_id)
548 q = self._prepare_awaiting_my_review_review_query(
503 my_participation = []
549 repo_name, user_id, search_q=search_q, statuses=statuses,
504 for pr in pull_requests:
550 order_by=order_by, order_dir=order_dir)
505 if pr in _my:
551
506 my_participation.append(pr)
507 _filtered_pull_requests = my_participation
508 if length:
552 if length:
509 return _filtered_pull_requests[offset:offset+length]
553 pull_requests = q.limit(length).offset(offset).all()
510 else:
554 else:
511 return _filtered_pull_requests
555 pull_requests = q.all()
556
557 return pull_requests
512
558
513 def get_not_reviewed(self, user_id):
559 def _prepare_im_participating_query(self, user_id=None, statuses=None, query='',
514 return [
515 x.pull_request for x in PullRequestReviewers.query().filter(
516 PullRequestReviewers.user_id == user_id).all()
517 ]
518
519 def _prepare_participating_query(self, user_id=None, statuses=None, query='',
520 order_by=None, order_dir='desc'):
560 order_by=None, order_dir='desc'):
561 """
562 return a query of pull-requests user is an creator, or he's added as a reviewer
563 """
521 q = PullRequest.query()
564 q = PullRequest.query()
522 if user_id:
565 if user_id:
523 reviewers_subquery = Session().query(
566 reviewers_subquery = Session().query(
524 PullRequestReviewers.pull_request_id).filter(
567 PullRequestReviewers.pull_request_id).filter(
525 PullRequestReviewers.user_id == user_id).subquery()
568 PullRequestReviewers.user_id == user_id).subquery()
526 user_filter = or_(
569 user_filter = or_(
527 PullRequest.user_id == user_id,
570 PullRequest.user_id == user_id,
528 PullRequest.pull_request_id.in_(reviewers_subquery)
571 PullRequest.pull_request_id.in_(reviewers_subquery)
529 )
572 )
530 q = PullRequest.query().filter(user_filter)
573 q = PullRequest.query().filter(user_filter)
531
574
532 # closed,opened
575 # closed,opened
533 if statuses:
576 if statuses:
534 q = q.filter(PullRequest.status.in_(statuses))
577 q = q.filter(PullRequest.status.in_(statuses))
535
578
536 if query:
579 if query:
537 like_expression = u'%{}%'.format(safe_unicode(query))
580 like_expression = u'%{}%'.format(safe_unicode(query))
538 q = q.join(User)
581 q = q.join(User, User.user_id == PullRequest.user_id)
539 q = q.filter(or_(
582 q = q.filter(or_(
540 cast(PullRequest.pull_request_id, String).ilike(like_expression),
583 cast(PullRequest.pull_request_id, String).ilike(like_expression),
541 User.username.ilike(like_expression),
584 User.username.ilike(like_expression),
542 PullRequest.title.ilike(like_expression),
585 PullRequest.title.ilike(like_expression),
543 PullRequest.description.ilike(like_expression),
586 PullRequest.description.ilike(like_expression),
544 ))
587 ))
545 if order_by:
588 if order_by:
546 order_map = {
589 order_map = {
547 'name_raw': PullRequest.pull_request_id,
590 'name_raw': PullRequest.pull_request_id,
548 'title': PullRequest.title,
591 'title': PullRequest.title,
549 'updated_on_raw': PullRequest.updated_on,
592 'updated_on_raw': PullRequest.updated_on,
550 'target_repo': PullRequest.target_repo_id
593 'target_repo': PullRequest.target_repo_id
551 }
594 }
552 if order_dir == 'asc':
595 if order_dir == 'asc':
553 q = q.order_by(order_map[order_by].asc())
596 q = q.order_by(order_map[order_by].asc())
554 else:
597 else:
555 q = q.order_by(order_map[order_by].desc())
598 q = q.order_by(order_map[order_by].desc())
556
599
557 return q
600 return q
558
601
559 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
602 def count_im_participating_in(self, user_id=None, statuses=None, query=''):
560 q = self._prepare_participating_query(user_id, statuses=statuses, query=query)
603 q = self._prepare_im_participating_query(user_id, statuses=statuses, query=query)
561 return q.count()
604 return q.count()
562
605
563 def get_im_participating_in(
606 def get_im_participating_in(
564 self, user_id=None, statuses=None, query='', offset=0,
607 self, user_id=None, statuses=None, query='', offset=0,
565 length=None, order_by=None, order_dir='desc'):
608 length=None, order_by=None, order_dir='desc'):
566 """
609 """
567 Get all Pull requests that i'm participating in, or i have opened
610 Get all Pull requests that i'm participating in as a reviewer, or i have opened
568 """
611 """
569
612
570 q = self._prepare_participating_query(
613 q = self._prepare_im_participating_query(
614 user_id, statuses=statuses, query=query, order_by=order_by,
615 order_dir=order_dir)
616
617 if length:
618 pull_requests = q.limit(length).offset(offset).all()
619 else:
620 pull_requests = q.all()
621
622 return pull_requests
623
624 def _prepare_participating_in_for_review_query(
625 self, user_id, statuses=None, query='', order_by=None, order_dir='desc'):
626
627 for_review_statuses = [
628 ChangesetStatus.STATUS_UNDER_REVIEW, ChangesetStatus.STATUS_NOT_REVIEWED
629 ]
630
631 pull_request_alias = aliased(PullRequest)
632 status_alias = aliased(ChangesetStatus)
633 reviewers_alias = aliased(PullRequestReviewers)
634
635 last_ver_subq = Session()\
636 .query(func.min(ChangesetStatus.version)) \
637 .filter(ChangesetStatus.pull_request_id == reviewers_alias.pull_request_id)\
638 .filter(ChangesetStatus.user_id == reviewers_alias.user_id) \
639 .subquery()
640
641 q = Session().query(pull_request_alias) \
642 .options(lazyload(pull_request_alias.author)) \
643 .join(reviewers_alias,
644 reviewers_alias.pull_request_id == pull_request_alias.pull_request_id) \
645 .outerjoin(status_alias,
646 and_(status_alias.user_id == reviewers_alias.user_id,
647 status_alias.pull_request_id == reviewers_alias.pull_request_id)) \
648 .filter(or_(status_alias.version == null(),
649 status_alias.version == last_ver_subq)) \
650 .filter(reviewers_alias.user_id == user_id) \
651 .filter(or_(status_alias.status == null(), status_alias.status.in_(for_review_statuses))) \
652 .group_by(pull_request_alias)
653
654 # closed,opened
655 if statuses:
656 q = q.filter(pull_request_alias.status.in_(statuses))
657
658 if query:
659 like_expression = u'%{}%'.format(safe_unicode(query))
660 q = q.join(User, User.user_id == pull_request_alias.user_id)
661 q = q.filter(or_(
662 cast(pull_request_alias.pull_request_id, String).ilike(like_expression),
663 User.username.ilike(like_expression),
664 pull_request_alias.title.ilike(like_expression),
665 pull_request_alias.description.ilike(like_expression),
666 ))
667
668 if order_by:
669 order_map = {
670 'name_raw': pull_request_alias.pull_request_id,
671 'title': pull_request_alias.title,
672 'updated_on_raw': pull_request_alias.updated_on,
673 'target_repo': pull_request_alias.target_repo_id
674 }
675 if order_dir == 'asc':
676 q = q.order_by(order_map[order_by].asc())
677 else:
678 q = q.order_by(order_map[order_by].desc())
679
680 return q
681
682 def count_im_participating_in_for_review(self, user_id, statuses=None, query=''):
683 q = self._prepare_participating_in_for_review_query(user_id, statuses=statuses, query=query)
684 return q.count()
685
686 def get_im_participating_in_for_review(
687 self, user_id, statuses=None, query='', offset=0,
688 length=None, order_by=None, order_dir='desc'):
689 """
690 Get all Pull requests that needs user approval or rejection
691 """
692
693 q = self._prepare_participating_in_for_review_query(
571 user_id, statuses=statuses, query=query, order_by=order_by,
694 user_id, statuses=statuses, query=query, order_by=order_by,
572 order_dir=order_dir)
695 order_dir=order_dir)
573
696
574 if length:
697 if length:
575 pull_requests = q.limit(length).offset(offset).all()
698 pull_requests = q.limit(length).offset(offset).all()
576 else:
699 else:
577 pull_requests = q.all()
700 pull_requests = q.all()
578
701
579 return pull_requests
702 return pull_requests
580
703
581 def get_versions(self, pull_request):
704 def get_versions(self, pull_request):
582 """
705 """
583 returns version of pull request sorted by ID descending
706 returns version of pull request sorted by ID descending
584 """
707 """
585 return PullRequestVersion.query()\
708 return PullRequestVersion.query()\
586 .filter(PullRequestVersion.pull_request == pull_request)\
709 .filter(PullRequestVersion.pull_request == pull_request)\
587 .order_by(PullRequestVersion.pull_request_version_id.asc())\
710 .order_by(PullRequestVersion.pull_request_version_id.asc())\
588 .all()
711 .all()
589
712
590 def get_pr_version(self, pull_request_id, version=None):
713 def get_pr_version(self, pull_request_id, version=None):
591 at_version = None
714 at_version = None
592
715
593 if version and version == 'latest':
716 if version and version == 'latest':
594 pull_request_ver = PullRequest.get(pull_request_id)
717 pull_request_ver = PullRequest.get(pull_request_id)
595 pull_request_obj = pull_request_ver
718 pull_request_obj = pull_request_ver
596 _org_pull_request_obj = pull_request_obj
719 _org_pull_request_obj = pull_request_obj
597 at_version = 'latest'
720 at_version = 'latest'
598 elif version:
721 elif version:
599 pull_request_ver = PullRequestVersion.get_or_404(version)
722 pull_request_ver = PullRequestVersion.get_or_404(version)
600 pull_request_obj = pull_request_ver
723 pull_request_obj = pull_request_ver
601 _org_pull_request_obj = pull_request_ver.pull_request
724 _org_pull_request_obj = pull_request_ver.pull_request
602 at_version = pull_request_ver.pull_request_version_id
725 at_version = pull_request_ver.pull_request_version_id
603 else:
726 else:
604 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
727 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
605 pull_request_id)
728 pull_request_id)
606
729
607 pull_request_display_obj = PullRequest.get_pr_display_object(
730 pull_request_display_obj = PullRequest.get_pr_display_object(
608 pull_request_obj, _org_pull_request_obj)
731 pull_request_obj, _org_pull_request_obj)
609
732
610 return _org_pull_request_obj, pull_request_obj, \
733 return _org_pull_request_obj, pull_request_obj, \
611 pull_request_display_obj, at_version
734 pull_request_display_obj, at_version
612
735
613 def pr_commits_versions(self, versions):
736 def pr_commits_versions(self, versions):
614 """
737 """
615 Maps the pull-request commits into all known PR versions. This way we can obtain
738 Maps the pull-request commits into all known PR versions. This way we can obtain
616 each pr version the commit was introduced in.
739 each pr version the commit was introduced in.
617 """
740 """
618 commit_versions = collections.defaultdict(list)
741 commit_versions = collections.defaultdict(list)
619 num_versions = [x.pull_request_version_id for x in versions]
742 num_versions = [x.pull_request_version_id for x in versions]
620 for ver in versions:
743 for ver in versions:
621 for commit_id in ver.revisions:
744 for commit_id in ver.revisions:
622 ver_idx = ChangesetComment.get_index_from_version(
745 ver_idx = ChangesetComment.get_index_from_version(
623 ver.pull_request_version_id, num_versions=num_versions)
746 ver.pull_request_version_id, num_versions=num_versions)
624 commit_versions[commit_id].append(ver_idx)
747 commit_versions[commit_id].append(ver_idx)
625 return commit_versions
748 return commit_versions
626
749
627 def create(self, created_by, source_repo, source_ref, target_repo,
750 def create(self, created_by, source_repo, source_ref, target_repo,
628 target_ref, revisions, reviewers, observers, title, description=None,
751 target_ref, revisions, reviewers, observers, title, description=None,
629 common_ancestor_id=None,
752 common_ancestor_id=None,
630 description_renderer=None,
753 description_renderer=None,
631 reviewer_data=None, translator=None, auth_user=None):
754 reviewer_data=None, translator=None, auth_user=None):
632 translator = translator or get_current_request().translate
755 translator = translator or get_current_request().translate
633
756
634 created_by_user = self._get_user(created_by)
757 created_by_user = self._get_user(created_by)
635 auth_user = auth_user or created_by_user.AuthUser()
758 auth_user = auth_user or created_by_user.AuthUser()
636 source_repo = self._get_repo(source_repo)
759 source_repo = self._get_repo(source_repo)
637 target_repo = self._get_repo(target_repo)
760 target_repo = self._get_repo(target_repo)
638
761
639 pull_request = PullRequest()
762 pull_request = PullRequest()
640 pull_request.source_repo = source_repo
763 pull_request.source_repo = source_repo
641 pull_request.source_ref = source_ref
764 pull_request.source_ref = source_ref
642 pull_request.target_repo = target_repo
765 pull_request.target_repo = target_repo
643 pull_request.target_ref = target_ref
766 pull_request.target_ref = target_ref
644 pull_request.revisions = revisions
767 pull_request.revisions = revisions
645 pull_request.title = title
768 pull_request.title = title
646 pull_request.description = description
769 pull_request.description = description
647 pull_request.description_renderer = description_renderer
770 pull_request.description_renderer = description_renderer
648 pull_request.author = created_by_user
771 pull_request.author = created_by_user
649 pull_request.reviewer_data = reviewer_data
772 pull_request.reviewer_data = reviewer_data
650 pull_request.pull_request_state = pull_request.STATE_CREATING
773 pull_request.pull_request_state = pull_request.STATE_CREATING
651 pull_request.common_ancestor_id = common_ancestor_id
774 pull_request.common_ancestor_id = common_ancestor_id
652
775
653 Session().add(pull_request)
776 Session().add(pull_request)
654 Session().flush()
777 Session().flush()
655
778
656 reviewer_ids = set()
779 reviewer_ids = set()
657 # members / reviewers
780 # members / reviewers
658 for reviewer_object in reviewers:
781 for reviewer_object in reviewers:
659 user_id, reasons, mandatory, role, rules = reviewer_object
782 user_id, reasons, mandatory, role, rules = reviewer_object
660 user = self._get_user(user_id)
783 user = self._get_user(user_id)
661
784
662 # skip duplicates
785 # skip duplicates
663 if user.user_id in reviewer_ids:
786 if user.user_id in reviewer_ids:
664 continue
787 continue
665
788
666 reviewer_ids.add(user.user_id)
789 reviewer_ids.add(user.user_id)
667
790
668 reviewer = PullRequestReviewers()
791 reviewer = PullRequestReviewers()
669 reviewer.user = user
792 reviewer.user = user
670 reviewer.pull_request = pull_request
793 reviewer.pull_request = pull_request
671 reviewer.reasons = reasons
794 reviewer.reasons = reasons
672 reviewer.mandatory = mandatory
795 reviewer.mandatory = mandatory
673 reviewer.role = role
796 reviewer.role = role
674
797
675 # NOTE(marcink): pick only first rule for now
798 # NOTE(marcink): pick only first rule for now
676 rule_id = list(rules)[0] if rules else None
799 rule_id = list(rules)[0] if rules else None
677 rule = RepoReviewRule.get(rule_id) if rule_id else None
800 rule = RepoReviewRule.get(rule_id) if rule_id else None
678 if rule:
801 if rule:
679 review_group = rule.user_group_vote_rule(user_id)
802 review_group = rule.user_group_vote_rule(user_id)
680 # we check if this particular reviewer is member of a voting group
803 # we check if this particular reviewer is member of a voting group
681 if review_group:
804 if review_group:
682 # NOTE(marcink):
805 # NOTE(marcink):
683 # can be that user is member of more but we pick the first same,
806 # can be that user is member of more but we pick the first same,
684 # same as default reviewers algo
807 # same as default reviewers algo
685 review_group = review_group[0]
808 review_group = review_group[0]
686
809
687 rule_data = {
810 rule_data = {
688 'rule_name':
811 'rule_name':
689 rule.review_rule_name,
812 rule.review_rule_name,
690 'rule_user_group_entry_id':
813 'rule_user_group_entry_id':
691 review_group.repo_review_rule_users_group_id,
814 review_group.repo_review_rule_users_group_id,
692 'rule_user_group_name':
815 'rule_user_group_name':
693 review_group.users_group.users_group_name,
816 review_group.users_group.users_group_name,
694 'rule_user_group_members':
817 'rule_user_group_members':
695 [x.user.username for x in review_group.users_group.members],
818 [x.user.username for x in review_group.users_group.members],
696 'rule_user_group_members_id':
819 'rule_user_group_members_id':
697 [x.user.user_id for x in review_group.users_group.members],
820 [x.user.user_id for x in review_group.users_group.members],
698 }
821 }
699 # e.g {'vote_rule': -1, 'mandatory': True}
822 # e.g {'vote_rule': -1, 'mandatory': True}
700 rule_data.update(review_group.rule_data())
823 rule_data.update(review_group.rule_data())
701
824
702 reviewer.rule_data = rule_data
825 reviewer.rule_data = rule_data
703
826
704 Session().add(reviewer)
827 Session().add(reviewer)
705 Session().flush()
828 Session().flush()
706
829
707 for observer_object in observers:
830 for observer_object in observers:
708 user_id, reasons, mandatory, role, rules = observer_object
831 user_id, reasons, mandatory, role, rules = observer_object
709 user = self._get_user(user_id)
832 user = self._get_user(user_id)
710
833
711 # skip duplicates from reviewers
834 # skip duplicates from reviewers
712 if user.user_id in reviewer_ids:
835 if user.user_id in reviewer_ids:
713 continue
836 continue
714
837
715 #reviewer_ids.add(user.user_id)
838 #reviewer_ids.add(user.user_id)
716
839
717 observer = PullRequestReviewers()
840 observer = PullRequestReviewers()
718 observer.user = user
841 observer.user = user
719 observer.pull_request = pull_request
842 observer.pull_request = pull_request
720 observer.reasons = reasons
843 observer.reasons = reasons
721 observer.mandatory = mandatory
844 observer.mandatory = mandatory
722 observer.role = role
845 observer.role = role
723
846
724 # NOTE(marcink): pick only first rule for now
847 # NOTE(marcink): pick only first rule for now
725 rule_id = list(rules)[0] if rules else None
848 rule_id = list(rules)[0] if rules else None
726 rule = RepoReviewRule.get(rule_id) if rule_id else None
849 rule = RepoReviewRule.get(rule_id) if rule_id else None
727 if rule:
850 if rule:
728 # TODO(marcink): do we need this for observers ??
851 # TODO(marcink): do we need this for observers ??
729 pass
852 pass
730
853
731 Session().add(observer)
854 Session().add(observer)
732 Session().flush()
855 Session().flush()
733
856
734 # Set approval status to "Under Review" for all commits which are
857 # Set approval status to "Under Review" for all commits which are
735 # part of this pull request.
858 # part of this pull request.
736 ChangesetStatusModel().set_status(
859 ChangesetStatusModel().set_status(
737 repo=target_repo,
860 repo=target_repo,
738 status=ChangesetStatus.STATUS_UNDER_REVIEW,
861 status=ChangesetStatus.STATUS_UNDER_REVIEW,
739 user=created_by_user,
862 user=created_by_user,
740 pull_request=pull_request
863 pull_request=pull_request
741 )
864 )
742 # we commit early at this point. This has to do with a fact
865 # we commit early at this point. This has to do with a fact
743 # that before queries do some row-locking. And because of that
866 # that before queries do some row-locking. And because of that
744 # we need to commit and finish transaction before below validate call
867 # we need to commit and finish transaction before below validate call
745 # that for large repos could be long resulting in long row locks
868 # that for large repos could be long resulting in long row locks
746 Session().commit()
869 Session().commit()
747
870
748 # prepare workspace, and run initial merge simulation. Set state during that
871 # prepare workspace, and run initial merge simulation. Set state during that
749 # operation
872 # operation
750 pull_request = PullRequest.get(pull_request.pull_request_id)
873 pull_request = PullRequest.get(pull_request.pull_request_id)
751
874
752 # set as merging, for merge simulation, and if finished to created so we mark
875 # set as merging, for merge simulation, and if finished to created so we mark
753 # simulation is working fine
876 # simulation is working fine
754 with pull_request.set_state(PullRequest.STATE_MERGING,
877 with pull_request.set_state(PullRequest.STATE_MERGING,
755 final_state=PullRequest.STATE_CREATED) as state_obj:
878 final_state=PullRequest.STATE_CREATED) as state_obj:
756 MergeCheck.validate(
879 MergeCheck.validate(
757 pull_request, auth_user=auth_user, translator=translator)
880 pull_request, auth_user=auth_user, translator=translator)
758
881
759 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
882 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
760 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
883 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
761
884
762 creation_data = pull_request.get_api_data(with_merge_state=False)
885 creation_data = pull_request.get_api_data(with_merge_state=False)
763 self._log_audit_action(
886 self._log_audit_action(
764 'repo.pull_request.create', {'data': creation_data},
887 'repo.pull_request.create', {'data': creation_data},
765 auth_user, pull_request)
888 auth_user, pull_request)
766
889
767 return pull_request
890 return pull_request
768
891
769 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
892 def trigger_pull_request_hook(self, pull_request, user, action, data=None):
770 pull_request = self.__get_pull_request(pull_request)
893 pull_request = self.__get_pull_request(pull_request)
771 target_scm = pull_request.target_repo.scm_instance()
894 target_scm = pull_request.target_repo.scm_instance()
772 if action == 'create':
895 if action == 'create':
773 trigger_hook = hooks_utils.trigger_create_pull_request_hook
896 trigger_hook = hooks_utils.trigger_create_pull_request_hook
774 elif action == 'merge':
897 elif action == 'merge':
775 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
898 trigger_hook = hooks_utils.trigger_merge_pull_request_hook
776 elif action == 'close':
899 elif action == 'close':
777 trigger_hook = hooks_utils.trigger_close_pull_request_hook
900 trigger_hook = hooks_utils.trigger_close_pull_request_hook
778 elif action == 'review_status_change':
901 elif action == 'review_status_change':
779 trigger_hook = hooks_utils.trigger_review_pull_request_hook
902 trigger_hook = hooks_utils.trigger_review_pull_request_hook
780 elif action == 'update':
903 elif action == 'update':
781 trigger_hook = hooks_utils.trigger_update_pull_request_hook
904 trigger_hook = hooks_utils.trigger_update_pull_request_hook
782 elif action == 'comment':
905 elif action == 'comment':
783 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
906 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
784 elif action == 'comment_edit':
907 elif action == 'comment_edit':
785 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
908 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
786 else:
909 else:
787 return
910 return
788
911
789 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
912 log.debug('Handling pull_request %s trigger_pull_request_hook with action %s and hook: %s',
790 pull_request, action, trigger_hook)
913 pull_request, action, trigger_hook)
791 trigger_hook(
914 trigger_hook(
792 username=user.username,
915 username=user.username,
793 repo_name=pull_request.target_repo.repo_name,
916 repo_name=pull_request.target_repo.repo_name,
794 repo_type=target_scm.alias,
917 repo_type=target_scm.alias,
795 pull_request=pull_request,
918 pull_request=pull_request,
796 data=data)
919 data=data)
797
920
798 def _get_commit_ids(self, pull_request):
921 def _get_commit_ids(self, pull_request):
799 """
922 """
800 Return the commit ids of the merged pull request.
923 Return the commit ids of the merged pull request.
801
924
802 This method is not dealing correctly yet with the lack of autoupdates
925 This method is not dealing correctly yet with the lack of autoupdates
803 nor with the implicit target updates.
926 nor with the implicit target updates.
804 For example: if a commit in the source repo is already in the target it
927 For example: if a commit in the source repo is already in the target it
805 will be reported anyways.
928 will be reported anyways.
806 """
929 """
807 merge_rev = pull_request.merge_rev
930 merge_rev = pull_request.merge_rev
808 if merge_rev is None:
931 if merge_rev is None:
809 raise ValueError('This pull request was not merged yet')
932 raise ValueError('This pull request was not merged yet')
810
933
811 commit_ids = list(pull_request.revisions)
934 commit_ids = list(pull_request.revisions)
812 if merge_rev not in commit_ids:
935 if merge_rev not in commit_ids:
813 commit_ids.append(merge_rev)
936 commit_ids.append(merge_rev)
814
937
815 return commit_ids
938 return commit_ids
816
939
817 def merge_repo(self, pull_request, user, extras):
940 def merge_repo(self, pull_request, user, extras):
818 log.debug("Merging pull request %s", pull_request.pull_request_id)
941 log.debug("Merging pull request %s", pull_request.pull_request_id)
819 extras['user_agent'] = 'internal-merge'
942 extras['user_agent'] = 'internal-merge'
820 merge_state = self._merge_pull_request(pull_request, user, extras)
943 merge_state = self._merge_pull_request(pull_request, user, extras)
821 if merge_state.executed:
944 if merge_state.executed:
822 log.debug("Merge was successful, updating the pull request comments.")
945 log.debug("Merge was successful, updating the pull request comments.")
823 self._comment_and_close_pr(pull_request, user, merge_state)
946 self._comment_and_close_pr(pull_request, user, merge_state)
824
947
825 self._log_audit_action(
948 self._log_audit_action(
826 'repo.pull_request.merge',
949 'repo.pull_request.merge',
827 {'merge_state': merge_state.__dict__},
950 {'merge_state': merge_state.__dict__},
828 user, pull_request)
951 user, pull_request)
829
952
830 else:
953 else:
831 log.warn("Merge failed, not updating the pull request.")
954 log.warn("Merge failed, not updating the pull request.")
832 return merge_state
955 return merge_state
833
956
834 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
957 def _merge_pull_request(self, pull_request, user, extras, merge_msg=None):
835 target_vcs = pull_request.target_repo.scm_instance()
958 target_vcs = pull_request.target_repo.scm_instance()
836 source_vcs = pull_request.source_repo.scm_instance()
959 source_vcs = pull_request.source_repo.scm_instance()
837
960
838 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
961 message = safe_unicode(merge_msg or vcs_settings.MERGE_MESSAGE_TMPL).format(
839 pr_id=pull_request.pull_request_id,
962 pr_id=pull_request.pull_request_id,
840 pr_title=pull_request.title,
963 pr_title=pull_request.title,
841 source_repo=source_vcs.name,
964 source_repo=source_vcs.name,
842 source_ref_name=pull_request.source_ref_parts.name,
965 source_ref_name=pull_request.source_ref_parts.name,
843 target_repo=target_vcs.name,
966 target_repo=target_vcs.name,
844 target_ref_name=pull_request.target_ref_parts.name,
967 target_ref_name=pull_request.target_ref_parts.name,
845 )
968 )
846
969
847 workspace_id = self._workspace_id(pull_request)
970 workspace_id = self._workspace_id(pull_request)
848 repo_id = pull_request.target_repo.repo_id
971 repo_id = pull_request.target_repo.repo_id
849 use_rebase = self._use_rebase_for_merging(pull_request)
972 use_rebase = self._use_rebase_for_merging(pull_request)
850 close_branch = self._close_branch_before_merging(pull_request)
973 close_branch = self._close_branch_before_merging(pull_request)
851 user_name = self._user_name_for_merging(pull_request, user)
974 user_name = self._user_name_for_merging(pull_request, user)
852
975
853 target_ref = self._refresh_reference(
976 target_ref = self._refresh_reference(
854 pull_request.target_ref_parts, target_vcs)
977 pull_request.target_ref_parts, target_vcs)
855
978
856 callback_daemon, extras = prepare_callback_daemon(
979 callback_daemon, extras = prepare_callback_daemon(
857 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
980 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
858 host=vcs_settings.HOOKS_HOST,
981 host=vcs_settings.HOOKS_HOST,
859 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
982 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
860
983
861 with callback_daemon:
984 with callback_daemon:
862 # TODO: johbo: Implement a clean way to run a config_override
985 # TODO: johbo: Implement a clean way to run a config_override
863 # for a single call.
986 # for a single call.
864 target_vcs.config.set(
987 target_vcs.config.set(
865 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
988 'rhodecode', 'RC_SCM_DATA', json.dumps(extras))
866
989
867 merge_state = target_vcs.merge(
990 merge_state = target_vcs.merge(
868 repo_id, workspace_id, target_ref, source_vcs,
991 repo_id, workspace_id, target_ref, source_vcs,
869 pull_request.source_ref_parts,
992 pull_request.source_ref_parts,
870 user_name=user_name, user_email=user.email,
993 user_name=user_name, user_email=user.email,
871 message=message, use_rebase=use_rebase,
994 message=message, use_rebase=use_rebase,
872 close_branch=close_branch)
995 close_branch=close_branch)
873 return merge_state
996 return merge_state
874
997
875 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
998 def _comment_and_close_pr(self, pull_request, user, merge_state, close_msg=None):
876 pull_request.merge_rev = merge_state.merge_ref.commit_id
999 pull_request.merge_rev = merge_state.merge_ref.commit_id
877 pull_request.updated_on = datetime.datetime.now()
1000 pull_request.updated_on = datetime.datetime.now()
878 close_msg = close_msg or 'Pull request merged and closed'
1001 close_msg = close_msg or 'Pull request merged and closed'
879
1002
880 CommentsModel().create(
1003 CommentsModel().create(
881 text=safe_unicode(close_msg),
1004 text=safe_unicode(close_msg),
882 repo=pull_request.target_repo.repo_id,
1005 repo=pull_request.target_repo.repo_id,
883 user=user.user_id,
1006 user=user.user_id,
884 pull_request=pull_request.pull_request_id,
1007 pull_request=pull_request.pull_request_id,
885 f_path=None,
1008 f_path=None,
886 line_no=None,
1009 line_no=None,
887 closing_pr=True
1010 closing_pr=True
888 )
1011 )
889
1012
890 Session().add(pull_request)
1013 Session().add(pull_request)
891 Session().flush()
1014 Session().flush()
892 # TODO: paris: replace invalidation with less radical solution
1015 # TODO: paris: replace invalidation with less radical solution
893 ScmModel().mark_for_invalidation(
1016 ScmModel().mark_for_invalidation(
894 pull_request.target_repo.repo_name)
1017 pull_request.target_repo.repo_name)
895 self.trigger_pull_request_hook(pull_request, user, 'merge')
1018 self.trigger_pull_request_hook(pull_request, user, 'merge')
896
1019
897 def has_valid_update_type(self, pull_request):
1020 def has_valid_update_type(self, pull_request):
898 source_ref_type = pull_request.source_ref_parts.type
1021 source_ref_type = pull_request.source_ref_parts.type
899 return source_ref_type in self.REF_TYPES
1022 return source_ref_type in self.REF_TYPES
900
1023
901 def get_flow_commits(self, pull_request):
1024 def get_flow_commits(self, pull_request):
902
1025
903 # source repo
1026 # source repo
904 source_ref_name = pull_request.source_ref_parts.name
1027 source_ref_name = pull_request.source_ref_parts.name
905 source_ref_type = pull_request.source_ref_parts.type
1028 source_ref_type = pull_request.source_ref_parts.type
906 source_ref_id = pull_request.source_ref_parts.commit_id
1029 source_ref_id = pull_request.source_ref_parts.commit_id
907 source_repo = pull_request.source_repo.scm_instance()
1030 source_repo = pull_request.source_repo.scm_instance()
908
1031
909 try:
1032 try:
910 if source_ref_type in self.REF_TYPES:
1033 if source_ref_type in self.REF_TYPES:
911 source_commit = source_repo.get_commit(
1034 source_commit = source_repo.get_commit(
912 source_ref_name, reference_obj=pull_request.source_ref_parts)
1035 source_ref_name, reference_obj=pull_request.source_ref_parts)
913 else:
1036 else:
914 source_commit = source_repo.get_commit(source_ref_id)
1037 source_commit = source_repo.get_commit(source_ref_id)
915 except CommitDoesNotExistError:
1038 except CommitDoesNotExistError:
916 raise SourceRefMissing()
1039 raise SourceRefMissing()
917
1040
918 # target repo
1041 # target repo
919 target_ref_name = pull_request.target_ref_parts.name
1042 target_ref_name = pull_request.target_ref_parts.name
920 target_ref_type = pull_request.target_ref_parts.type
1043 target_ref_type = pull_request.target_ref_parts.type
921 target_ref_id = pull_request.target_ref_parts.commit_id
1044 target_ref_id = pull_request.target_ref_parts.commit_id
922 target_repo = pull_request.target_repo.scm_instance()
1045 target_repo = pull_request.target_repo.scm_instance()
923
1046
924 try:
1047 try:
925 if target_ref_type in self.REF_TYPES:
1048 if target_ref_type in self.REF_TYPES:
926 target_commit = target_repo.get_commit(
1049 target_commit = target_repo.get_commit(
927 target_ref_name, reference_obj=pull_request.target_ref_parts)
1050 target_ref_name, reference_obj=pull_request.target_ref_parts)
928 else:
1051 else:
929 target_commit = target_repo.get_commit(target_ref_id)
1052 target_commit = target_repo.get_commit(target_ref_id)
930 except CommitDoesNotExistError:
1053 except CommitDoesNotExistError:
931 raise TargetRefMissing()
1054 raise TargetRefMissing()
932
1055
933 return source_commit, target_commit
1056 return source_commit, target_commit
934
1057
935 def update_commits(self, pull_request, updating_user):
1058 def update_commits(self, pull_request, updating_user):
936 """
1059 """
937 Get the updated list of commits for the pull request
1060 Get the updated list of commits for the pull request
938 and return the new pull request version and the list
1061 and return the new pull request version and the list
939 of commits processed by this update action
1062 of commits processed by this update action
940
1063
941 updating_user is the user_object who triggered the update
1064 updating_user is the user_object who triggered the update
942 """
1065 """
943 pull_request = self.__get_pull_request(pull_request)
1066 pull_request = self.__get_pull_request(pull_request)
944 source_ref_type = pull_request.source_ref_parts.type
1067 source_ref_type = pull_request.source_ref_parts.type
945 source_ref_name = pull_request.source_ref_parts.name
1068 source_ref_name = pull_request.source_ref_parts.name
946 source_ref_id = pull_request.source_ref_parts.commit_id
1069 source_ref_id = pull_request.source_ref_parts.commit_id
947
1070
948 target_ref_type = pull_request.target_ref_parts.type
1071 target_ref_type = pull_request.target_ref_parts.type
949 target_ref_name = pull_request.target_ref_parts.name
1072 target_ref_name = pull_request.target_ref_parts.name
950 target_ref_id = pull_request.target_ref_parts.commit_id
1073 target_ref_id = pull_request.target_ref_parts.commit_id
951
1074
952 if not self.has_valid_update_type(pull_request):
1075 if not self.has_valid_update_type(pull_request):
953 log.debug("Skipping update of pull request %s due to ref type: %s",
1076 log.debug("Skipping update of pull request %s due to ref type: %s",
954 pull_request, source_ref_type)
1077 pull_request, source_ref_type)
955 return UpdateResponse(
1078 return UpdateResponse(
956 executed=False,
1079 executed=False,
957 reason=UpdateFailureReason.WRONG_REF_TYPE,
1080 reason=UpdateFailureReason.WRONG_REF_TYPE,
958 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1081 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
959 source_changed=False, target_changed=False)
1082 source_changed=False, target_changed=False)
960
1083
961 try:
1084 try:
962 source_commit, target_commit = self.get_flow_commits(pull_request)
1085 source_commit, target_commit = self.get_flow_commits(pull_request)
963 except SourceRefMissing:
1086 except SourceRefMissing:
964 return UpdateResponse(
1087 return UpdateResponse(
965 executed=False,
1088 executed=False,
966 reason=UpdateFailureReason.MISSING_SOURCE_REF,
1089 reason=UpdateFailureReason.MISSING_SOURCE_REF,
967 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1090 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
968 source_changed=False, target_changed=False)
1091 source_changed=False, target_changed=False)
969 except TargetRefMissing:
1092 except TargetRefMissing:
970 return UpdateResponse(
1093 return UpdateResponse(
971 executed=False,
1094 executed=False,
972 reason=UpdateFailureReason.MISSING_TARGET_REF,
1095 reason=UpdateFailureReason.MISSING_TARGET_REF,
973 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1096 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
974 source_changed=False, target_changed=False)
1097 source_changed=False, target_changed=False)
975
1098
976 source_changed = source_ref_id != source_commit.raw_id
1099 source_changed = source_ref_id != source_commit.raw_id
977 target_changed = target_ref_id != target_commit.raw_id
1100 target_changed = target_ref_id != target_commit.raw_id
978
1101
979 if not (source_changed or target_changed):
1102 if not (source_changed or target_changed):
980 log.debug("Nothing changed in pull request %s", pull_request)
1103 log.debug("Nothing changed in pull request %s", pull_request)
981 return UpdateResponse(
1104 return UpdateResponse(
982 executed=False,
1105 executed=False,
983 reason=UpdateFailureReason.NO_CHANGE,
1106 reason=UpdateFailureReason.NO_CHANGE,
984 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
1107 old=pull_request, new=None, common_ancestor_id=None, commit_changes=None,
985 source_changed=target_changed, target_changed=source_changed)
1108 source_changed=target_changed, target_changed=source_changed)
986
1109
987 change_in_found = 'target repo' if target_changed else 'source repo'
1110 change_in_found = 'target repo' if target_changed else 'source repo'
988 log.debug('Updating pull request because of change in %s detected',
1111 log.debug('Updating pull request because of change in %s detected',
989 change_in_found)
1112 change_in_found)
990
1113
991 # Finally there is a need for an update, in case of source change
1114 # Finally there is a need for an update, in case of source change
992 # we create a new version, else just an update
1115 # we create a new version, else just an update
993 if source_changed:
1116 if source_changed:
994 pull_request_version = self._create_version_from_snapshot(pull_request)
1117 pull_request_version = self._create_version_from_snapshot(pull_request)
995 self._link_comments_to_version(pull_request_version)
1118 self._link_comments_to_version(pull_request_version)
996 else:
1119 else:
997 try:
1120 try:
998 ver = pull_request.versions[-1]
1121 ver = pull_request.versions[-1]
999 except IndexError:
1122 except IndexError:
1000 ver = None
1123 ver = None
1001
1124
1002 pull_request.pull_request_version_id = \
1125 pull_request.pull_request_version_id = \
1003 ver.pull_request_version_id if ver else None
1126 ver.pull_request_version_id if ver else None
1004 pull_request_version = pull_request
1127 pull_request_version = pull_request
1005
1128
1006 source_repo = pull_request.source_repo.scm_instance()
1129 source_repo = pull_request.source_repo.scm_instance()
1007 target_repo = pull_request.target_repo.scm_instance()
1130 target_repo = pull_request.target_repo.scm_instance()
1008
1131
1009 # re-compute commit ids
1132 # re-compute commit ids
1010 old_commit_ids = pull_request.revisions
1133 old_commit_ids = pull_request.revisions
1011 pre_load = ["author", "date", "message", "branch"]
1134 pre_load = ["author", "date", "message", "branch"]
1012 commit_ranges = target_repo.compare(
1135 commit_ranges = target_repo.compare(
1013 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1136 target_commit.raw_id, source_commit.raw_id, source_repo, merge=True,
1014 pre_load=pre_load)
1137 pre_load=pre_load)
1015
1138
1016 target_ref = target_commit.raw_id
1139 target_ref = target_commit.raw_id
1017 source_ref = source_commit.raw_id
1140 source_ref = source_commit.raw_id
1018 ancestor_commit_id = target_repo.get_common_ancestor(
1141 ancestor_commit_id = target_repo.get_common_ancestor(
1019 target_ref, source_ref, source_repo)
1142 target_ref, source_ref, source_repo)
1020
1143
1021 if not ancestor_commit_id:
1144 if not ancestor_commit_id:
1022 raise ValueError(
1145 raise ValueError(
1023 'cannot calculate diff info without a common ancestor. '
1146 'cannot calculate diff info without a common ancestor. '
1024 'Make sure both repositories are related, and have a common forking commit.')
1147 'Make sure both repositories are related, and have a common forking commit.')
1025
1148
1026 pull_request.common_ancestor_id = ancestor_commit_id
1149 pull_request.common_ancestor_id = ancestor_commit_id
1027
1150
1028 pull_request.source_ref = '%s:%s:%s' % (
1151 pull_request.source_ref = '%s:%s:%s' % (
1029 source_ref_type, source_ref_name, source_commit.raw_id)
1152 source_ref_type, source_ref_name, source_commit.raw_id)
1030 pull_request.target_ref = '%s:%s:%s' % (
1153 pull_request.target_ref = '%s:%s:%s' % (
1031 target_ref_type, target_ref_name, ancestor_commit_id)
1154 target_ref_type, target_ref_name, ancestor_commit_id)
1032
1155
1033 pull_request.revisions = [
1156 pull_request.revisions = [
1034 commit.raw_id for commit in reversed(commit_ranges)]
1157 commit.raw_id for commit in reversed(commit_ranges)]
1035 pull_request.updated_on = datetime.datetime.now()
1158 pull_request.updated_on = datetime.datetime.now()
1036 Session().add(pull_request)
1159 Session().add(pull_request)
1037 new_commit_ids = pull_request.revisions
1160 new_commit_ids = pull_request.revisions
1038
1161
1039 old_diff_data, new_diff_data = self._generate_update_diffs(
1162 old_diff_data, new_diff_data = self._generate_update_diffs(
1040 pull_request, pull_request_version)
1163 pull_request, pull_request_version)
1041
1164
1042 # calculate commit and file changes
1165 # calculate commit and file changes
1043 commit_changes = self._calculate_commit_id_changes(
1166 commit_changes = self._calculate_commit_id_changes(
1044 old_commit_ids, new_commit_ids)
1167 old_commit_ids, new_commit_ids)
1045 file_changes = self._calculate_file_changes(
1168 file_changes = self._calculate_file_changes(
1046 old_diff_data, new_diff_data)
1169 old_diff_data, new_diff_data)
1047
1170
1048 # set comments as outdated if DIFFS changed
1171 # set comments as outdated if DIFFS changed
1049 CommentsModel().outdate_comments(
1172 CommentsModel().outdate_comments(
1050 pull_request, old_diff_data=old_diff_data,
1173 pull_request, old_diff_data=old_diff_data,
1051 new_diff_data=new_diff_data)
1174 new_diff_data=new_diff_data)
1052
1175
1053 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1176 valid_commit_changes = (commit_changes.added or commit_changes.removed)
1054 file_node_changes = (
1177 file_node_changes = (
1055 file_changes.added or file_changes.modified or file_changes.removed)
1178 file_changes.added or file_changes.modified or file_changes.removed)
1056 pr_has_changes = valid_commit_changes or file_node_changes
1179 pr_has_changes = valid_commit_changes or file_node_changes
1057
1180
1058 # Add an automatic comment to the pull request, in case
1181 # Add an automatic comment to the pull request, in case
1059 # anything has changed
1182 # anything has changed
1060 if pr_has_changes:
1183 if pr_has_changes:
1061 update_comment = CommentsModel().create(
1184 update_comment = CommentsModel().create(
1062 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1185 text=self._render_update_message(ancestor_commit_id, commit_changes, file_changes),
1063 repo=pull_request.target_repo,
1186 repo=pull_request.target_repo,
1064 user=pull_request.author,
1187 user=pull_request.author,
1065 pull_request=pull_request,
1188 pull_request=pull_request,
1066 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1189 send_email=False, renderer=DEFAULT_COMMENTS_RENDERER)
1067
1190
1068 # Update status to "Under Review" for added commits
1191 # Update status to "Under Review" for added commits
1069 for commit_id in commit_changes.added:
1192 for commit_id in commit_changes.added:
1070 ChangesetStatusModel().set_status(
1193 ChangesetStatusModel().set_status(
1071 repo=pull_request.source_repo,
1194 repo=pull_request.source_repo,
1072 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1195 status=ChangesetStatus.STATUS_UNDER_REVIEW,
1073 comment=update_comment,
1196 comment=update_comment,
1074 user=pull_request.author,
1197 user=pull_request.author,
1075 pull_request=pull_request,
1198 pull_request=pull_request,
1076 revision=commit_id)
1199 revision=commit_id)
1077
1200
1078 # send update email to users
1201 # send update email to users
1079 try:
1202 try:
1080 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1203 self.notify_users(pull_request=pull_request, updating_user=updating_user,
1081 ancestor_commit_id=ancestor_commit_id,
1204 ancestor_commit_id=ancestor_commit_id,
1082 commit_changes=commit_changes,
1205 commit_changes=commit_changes,
1083 file_changes=file_changes)
1206 file_changes=file_changes)
1084 except Exception:
1207 except Exception:
1085 log.exception('Failed to send email notification to users')
1208 log.exception('Failed to send email notification to users')
1086
1209
1087 log.debug(
1210 log.debug(
1088 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1211 'Updated pull request %s, added_ids: %s, common_ids: %s, '
1089 'removed_ids: %s', pull_request.pull_request_id,
1212 'removed_ids: %s', pull_request.pull_request_id,
1090 commit_changes.added, commit_changes.common, commit_changes.removed)
1213 commit_changes.added, commit_changes.common, commit_changes.removed)
1091 log.debug(
1214 log.debug(
1092 'Updated pull request with the following file changes: %s',
1215 'Updated pull request with the following file changes: %s',
1093 file_changes)
1216 file_changes)
1094
1217
1095 log.info(
1218 log.info(
1096 "Updated pull request %s from commit %s to commit %s, "
1219 "Updated pull request %s from commit %s to commit %s, "
1097 "stored new version %s of this pull request.",
1220 "stored new version %s of this pull request.",
1098 pull_request.pull_request_id, source_ref_id,
1221 pull_request.pull_request_id, source_ref_id,
1099 pull_request.source_ref_parts.commit_id,
1222 pull_request.source_ref_parts.commit_id,
1100 pull_request_version.pull_request_version_id)
1223 pull_request_version.pull_request_version_id)
1101 Session().commit()
1224 Session().commit()
1102 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1225 self.trigger_pull_request_hook(pull_request, pull_request.author, 'update')
1103
1226
1104 return UpdateResponse(
1227 return UpdateResponse(
1105 executed=True, reason=UpdateFailureReason.NONE,
1228 executed=True, reason=UpdateFailureReason.NONE,
1106 old=pull_request, new=pull_request_version,
1229 old=pull_request, new=pull_request_version,
1107 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1230 common_ancestor_id=ancestor_commit_id, commit_changes=commit_changes,
1108 source_changed=source_changed, target_changed=target_changed)
1231 source_changed=source_changed, target_changed=target_changed)
1109
1232
1110 def _create_version_from_snapshot(self, pull_request):
1233 def _create_version_from_snapshot(self, pull_request):
1111 version = PullRequestVersion()
1234 version = PullRequestVersion()
1112 version.title = pull_request.title
1235 version.title = pull_request.title
1113 version.description = pull_request.description
1236 version.description = pull_request.description
1114 version.status = pull_request.status
1237 version.status = pull_request.status
1115 version.pull_request_state = pull_request.pull_request_state
1238 version.pull_request_state = pull_request.pull_request_state
1116 version.created_on = datetime.datetime.now()
1239 version.created_on = datetime.datetime.now()
1117 version.updated_on = pull_request.updated_on
1240 version.updated_on = pull_request.updated_on
1118 version.user_id = pull_request.user_id
1241 version.user_id = pull_request.user_id
1119 version.source_repo = pull_request.source_repo
1242 version.source_repo = pull_request.source_repo
1120 version.source_ref = pull_request.source_ref
1243 version.source_ref = pull_request.source_ref
1121 version.target_repo = pull_request.target_repo
1244 version.target_repo = pull_request.target_repo
1122 version.target_ref = pull_request.target_ref
1245 version.target_ref = pull_request.target_ref
1123
1246
1124 version._last_merge_source_rev = pull_request._last_merge_source_rev
1247 version._last_merge_source_rev = pull_request._last_merge_source_rev
1125 version._last_merge_target_rev = pull_request._last_merge_target_rev
1248 version._last_merge_target_rev = pull_request._last_merge_target_rev
1126 version.last_merge_status = pull_request.last_merge_status
1249 version.last_merge_status = pull_request.last_merge_status
1127 version.last_merge_metadata = pull_request.last_merge_metadata
1250 version.last_merge_metadata = pull_request.last_merge_metadata
1128 version.shadow_merge_ref = pull_request.shadow_merge_ref
1251 version.shadow_merge_ref = pull_request.shadow_merge_ref
1129 version.merge_rev = pull_request.merge_rev
1252 version.merge_rev = pull_request.merge_rev
1130 version.reviewer_data = pull_request.reviewer_data
1253 version.reviewer_data = pull_request.reviewer_data
1131
1254
1132 version.revisions = pull_request.revisions
1255 version.revisions = pull_request.revisions
1133 version.common_ancestor_id = pull_request.common_ancestor_id
1256 version.common_ancestor_id = pull_request.common_ancestor_id
1134 version.pull_request = pull_request
1257 version.pull_request = pull_request
1135 Session().add(version)
1258 Session().add(version)
1136 Session().flush()
1259 Session().flush()
1137
1260
1138 return version
1261 return version
1139
1262
1140 def _generate_update_diffs(self, pull_request, pull_request_version):
1263 def _generate_update_diffs(self, pull_request, pull_request_version):
1141
1264
1142 diff_context = (
1265 diff_context = (
1143 self.DIFF_CONTEXT +
1266 self.DIFF_CONTEXT +
1144 CommentsModel.needed_extra_diff_context())
1267 CommentsModel.needed_extra_diff_context())
1145 hide_whitespace_changes = False
1268 hide_whitespace_changes = False
1146 source_repo = pull_request_version.source_repo
1269 source_repo = pull_request_version.source_repo
1147 source_ref_id = pull_request_version.source_ref_parts.commit_id
1270 source_ref_id = pull_request_version.source_ref_parts.commit_id
1148 target_ref_id = pull_request_version.target_ref_parts.commit_id
1271 target_ref_id = pull_request_version.target_ref_parts.commit_id
1149 old_diff = self._get_diff_from_pr_or_version(
1272 old_diff = self._get_diff_from_pr_or_version(
1150 source_repo, source_ref_id, target_ref_id,
1273 source_repo, source_ref_id, target_ref_id,
1151 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1274 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1152
1275
1153 source_repo = pull_request.source_repo
1276 source_repo = pull_request.source_repo
1154 source_ref_id = pull_request.source_ref_parts.commit_id
1277 source_ref_id = pull_request.source_ref_parts.commit_id
1155 target_ref_id = pull_request.target_ref_parts.commit_id
1278 target_ref_id = pull_request.target_ref_parts.commit_id
1156
1279
1157 new_diff = self._get_diff_from_pr_or_version(
1280 new_diff = self._get_diff_from_pr_or_version(
1158 source_repo, source_ref_id, target_ref_id,
1281 source_repo, source_ref_id, target_ref_id,
1159 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1282 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1160
1283
1161 old_diff_data = diffs.DiffProcessor(old_diff)
1284 old_diff_data = diffs.DiffProcessor(old_diff)
1162 old_diff_data.prepare()
1285 old_diff_data.prepare()
1163 new_diff_data = diffs.DiffProcessor(new_diff)
1286 new_diff_data = diffs.DiffProcessor(new_diff)
1164 new_diff_data.prepare()
1287 new_diff_data.prepare()
1165
1288
1166 return old_diff_data, new_diff_data
1289 return old_diff_data, new_diff_data
1167
1290
1168 def _link_comments_to_version(self, pull_request_version):
1291 def _link_comments_to_version(self, pull_request_version):
1169 """
1292 """
1170 Link all unlinked comments of this pull request to the given version.
1293 Link all unlinked comments of this pull request to the given version.
1171
1294
1172 :param pull_request_version: The `PullRequestVersion` to which
1295 :param pull_request_version: The `PullRequestVersion` to which
1173 the comments shall be linked.
1296 the comments shall be linked.
1174
1297
1175 """
1298 """
1176 pull_request = pull_request_version.pull_request
1299 pull_request = pull_request_version.pull_request
1177 comments = ChangesetComment.query()\
1300 comments = ChangesetComment.query()\
1178 .filter(
1301 .filter(
1179 # TODO: johbo: Should we query for the repo at all here?
1302 # TODO: johbo: Should we query for the repo at all here?
1180 # Pending decision on how comments of PRs are to be related
1303 # Pending decision on how comments of PRs are to be related
1181 # to either the source repo, the target repo or no repo at all.
1304 # to either the source repo, the target repo or no repo at all.
1182 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1305 ChangesetComment.repo_id == pull_request.target_repo.repo_id,
1183 ChangesetComment.pull_request == pull_request,
1306 ChangesetComment.pull_request == pull_request,
1184 ChangesetComment.pull_request_version == None)\
1307 ChangesetComment.pull_request_version == None)\
1185 .order_by(ChangesetComment.comment_id.asc())
1308 .order_by(ChangesetComment.comment_id.asc())
1186
1309
1187 # TODO: johbo: Find out why this breaks if it is done in a bulk
1310 # TODO: johbo: Find out why this breaks if it is done in a bulk
1188 # operation.
1311 # operation.
1189 for comment in comments:
1312 for comment in comments:
1190 comment.pull_request_version_id = (
1313 comment.pull_request_version_id = (
1191 pull_request_version.pull_request_version_id)
1314 pull_request_version.pull_request_version_id)
1192 Session().add(comment)
1315 Session().add(comment)
1193
1316
1194 def _calculate_commit_id_changes(self, old_ids, new_ids):
1317 def _calculate_commit_id_changes(self, old_ids, new_ids):
1195 added = [x for x in new_ids if x not in old_ids]
1318 added = [x for x in new_ids if x not in old_ids]
1196 common = [x for x in new_ids if x in old_ids]
1319 common = [x for x in new_ids if x in old_ids]
1197 removed = [x for x in old_ids if x not in new_ids]
1320 removed = [x for x in old_ids if x not in new_ids]
1198 total = new_ids
1321 total = new_ids
1199 return ChangeTuple(added, common, removed, total)
1322 return ChangeTuple(added, common, removed, total)
1200
1323
1201 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1324 def _calculate_file_changes(self, old_diff_data, new_diff_data):
1202
1325
1203 old_files = OrderedDict()
1326 old_files = OrderedDict()
1204 for diff_data in old_diff_data.parsed_diff:
1327 for diff_data in old_diff_data.parsed_diff:
1205 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1328 old_files[diff_data['filename']] = md5_safe(diff_data['raw_diff'])
1206
1329
1207 added_files = []
1330 added_files = []
1208 modified_files = []
1331 modified_files = []
1209 removed_files = []
1332 removed_files = []
1210 for diff_data in new_diff_data.parsed_diff:
1333 for diff_data in new_diff_data.parsed_diff:
1211 new_filename = diff_data['filename']
1334 new_filename = diff_data['filename']
1212 new_hash = md5_safe(diff_data['raw_diff'])
1335 new_hash = md5_safe(diff_data['raw_diff'])
1213
1336
1214 old_hash = old_files.get(new_filename)
1337 old_hash = old_files.get(new_filename)
1215 if not old_hash:
1338 if not old_hash:
1216 # file is not present in old diff, we have to figure out from parsed diff
1339 # file is not present in old diff, we have to figure out from parsed diff
1217 # operation ADD/REMOVE
1340 # operation ADD/REMOVE
1218 operations_dict = diff_data['stats']['ops']
1341 operations_dict = diff_data['stats']['ops']
1219 if diffs.DEL_FILENODE in operations_dict:
1342 if diffs.DEL_FILENODE in operations_dict:
1220 removed_files.append(new_filename)
1343 removed_files.append(new_filename)
1221 else:
1344 else:
1222 added_files.append(new_filename)
1345 added_files.append(new_filename)
1223 else:
1346 else:
1224 if new_hash != old_hash:
1347 if new_hash != old_hash:
1225 modified_files.append(new_filename)
1348 modified_files.append(new_filename)
1226 # now remove a file from old, since we have seen it already
1349 # now remove a file from old, since we have seen it already
1227 del old_files[new_filename]
1350 del old_files[new_filename]
1228
1351
1229 # removed files is when there are present in old, but not in NEW,
1352 # removed files is when there are present in old, but not in NEW,
1230 # since we remove old files that are present in new diff, left-overs
1353 # since we remove old files that are present in new diff, left-overs
1231 # if any should be the removed files
1354 # if any should be the removed files
1232 removed_files.extend(old_files.keys())
1355 removed_files.extend(old_files.keys())
1233
1356
1234 return FileChangeTuple(added_files, modified_files, removed_files)
1357 return FileChangeTuple(added_files, modified_files, removed_files)
1235
1358
1236 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1359 def _render_update_message(self, ancestor_commit_id, changes, file_changes):
1237 """
1360 """
1238 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1361 render the message using DEFAULT_COMMENTS_RENDERER (RST renderer),
1239 so it's always looking the same disregarding on which default
1362 so it's always looking the same disregarding on which default
1240 renderer system is using.
1363 renderer system is using.
1241
1364
1242 :param ancestor_commit_id: ancestor raw_id
1365 :param ancestor_commit_id: ancestor raw_id
1243 :param changes: changes named tuple
1366 :param changes: changes named tuple
1244 :param file_changes: file changes named tuple
1367 :param file_changes: file changes named tuple
1245
1368
1246 """
1369 """
1247 new_status = ChangesetStatus.get_status_lbl(
1370 new_status = ChangesetStatus.get_status_lbl(
1248 ChangesetStatus.STATUS_UNDER_REVIEW)
1371 ChangesetStatus.STATUS_UNDER_REVIEW)
1249
1372
1250 changed_files = (
1373 changed_files = (
1251 file_changes.added + file_changes.modified + file_changes.removed)
1374 file_changes.added + file_changes.modified + file_changes.removed)
1252
1375
1253 params = {
1376 params = {
1254 'under_review_label': new_status,
1377 'under_review_label': new_status,
1255 'added_commits': changes.added,
1378 'added_commits': changes.added,
1256 'removed_commits': changes.removed,
1379 'removed_commits': changes.removed,
1257 'changed_files': changed_files,
1380 'changed_files': changed_files,
1258 'added_files': file_changes.added,
1381 'added_files': file_changes.added,
1259 'modified_files': file_changes.modified,
1382 'modified_files': file_changes.modified,
1260 'removed_files': file_changes.removed,
1383 'removed_files': file_changes.removed,
1261 'ancestor_commit_id': ancestor_commit_id
1384 'ancestor_commit_id': ancestor_commit_id
1262 }
1385 }
1263 renderer = RstTemplateRenderer()
1386 renderer = RstTemplateRenderer()
1264 return renderer.render('pull_request_update.mako', **params)
1387 return renderer.render('pull_request_update.mako', **params)
1265
1388
1266 def edit(self, pull_request, title, description, description_renderer, user):
1389 def edit(self, pull_request, title, description, description_renderer, user):
1267 pull_request = self.__get_pull_request(pull_request)
1390 pull_request = self.__get_pull_request(pull_request)
1268 old_data = pull_request.get_api_data(with_merge_state=False)
1391 old_data = pull_request.get_api_data(with_merge_state=False)
1269 if pull_request.is_closed():
1392 if pull_request.is_closed():
1270 raise ValueError('This pull request is closed')
1393 raise ValueError('This pull request is closed')
1271 if title:
1394 if title:
1272 pull_request.title = title
1395 pull_request.title = title
1273 pull_request.description = description
1396 pull_request.description = description
1274 pull_request.updated_on = datetime.datetime.now()
1397 pull_request.updated_on = datetime.datetime.now()
1275 pull_request.description_renderer = description_renderer
1398 pull_request.description_renderer = description_renderer
1276 Session().add(pull_request)
1399 Session().add(pull_request)
1277 self._log_audit_action(
1400 self._log_audit_action(
1278 'repo.pull_request.edit', {'old_data': old_data},
1401 'repo.pull_request.edit', {'old_data': old_data},
1279 user, pull_request)
1402 user, pull_request)
1280
1403
1281 def update_reviewers(self, pull_request, reviewer_data, user):
1404 def update_reviewers(self, pull_request, reviewer_data, user):
1282 """
1405 """
1283 Update the reviewers in the pull request
1406 Update the reviewers in the pull request
1284
1407
1285 :param pull_request: the pr to update
1408 :param pull_request: the pr to update
1286 :param reviewer_data: list of tuples
1409 :param reviewer_data: list of tuples
1287 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1410 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1288 :param user: current use who triggers this action
1411 :param user: current use who triggers this action
1289 """
1412 """
1290
1413
1291 pull_request = self.__get_pull_request(pull_request)
1414 pull_request = self.__get_pull_request(pull_request)
1292 if pull_request.is_closed():
1415 if pull_request.is_closed():
1293 raise ValueError('This pull request is closed')
1416 raise ValueError('This pull request is closed')
1294
1417
1295 reviewers = {}
1418 reviewers = {}
1296 for user_id, reasons, mandatory, role, rules in reviewer_data:
1419 for user_id, reasons, mandatory, role, rules in reviewer_data:
1297 if isinstance(user_id, (int, compat.string_types)):
1420 if isinstance(user_id, (int, compat.string_types)):
1298 user_id = self._get_user(user_id).user_id
1421 user_id = self._get_user(user_id).user_id
1299 reviewers[user_id] = {
1422 reviewers[user_id] = {
1300 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1423 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1301
1424
1302 reviewers_ids = set(reviewers.keys())
1425 reviewers_ids = set(reviewers.keys())
1303 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1426 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1304 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1427 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1305
1428
1306 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1429 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1307
1430
1308 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1431 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1309 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1432 ids_to_remove = current_reviewers_ids.difference(reviewers_ids)
1310
1433
1311 log.debug("Adding %s reviewers", ids_to_add)
1434 log.debug("Adding %s reviewers", ids_to_add)
1312 log.debug("Removing %s reviewers", ids_to_remove)
1435 log.debug("Removing %s reviewers", ids_to_remove)
1313 changed = False
1436 changed = False
1314 added_audit_reviewers = []
1437 added_audit_reviewers = []
1315 removed_audit_reviewers = []
1438 removed_audit_reviewers = []
1316
1439
1317 for uid in ids_to_add:
1440 for uid in ids_to_add:
1318 changed = True
1441 changed = True
1319 _usr = self._get_user(uid)
1442 _usr = self._get_user(uid)
1320 reviewer = PullRequestReviewers()
1443 reviewer = PullRequestReviewers()
1321 reviewer.user = _usr
1444 reviewer.user = _usr
1322 reviewer.pull_request = pull_request
1445 reviewer.pull_request = pull_request
1323 reviewer.reasons = reviewers[uid]['reasons']
1446 reviewer.reasons = reviewers[uid]['reasons']
1324 # NOTE(marcink): mandatory shouldn't be changed now
1447 # NOTE(marcink): mandatory shouldn't be changed now
1325 # reviewer.mandatory = reviewers[uid]['reasons']
1448 # reviewer.mandatory = reviewers[uid]['reasons']
1326 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1449 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1327 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1450 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1328 Session().add(reviewer)
1451 Session().add(reviewer)
1329 added_audit_reviewers.append(reviewer.get_dict())
1452 added_audit_reviewers.append(reviewer.get_dict())
1330
1453
1331 for uid in ids_to_remove:
1454 for uid in ids_to_remove:
1332 changed = True
1455 changed = True
1333 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1456 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1334 # This is an edge case that handles previous state of having the same reviewer twice.
1457 # This is an edge case that handles previous state of having the same reviewer twice.
1335 # this CAN happen due to the lack of DB checks
1458 # this CAN happen due to the lack of DB checks
1336 reviewers = PullRequestReviewers.query()\
1459 reviewers = PullRequestReviewers.query()\
1337 .filter(PullRequestReviewers.user_id == uid,
1460 .filter(PullRequestReviewers.user_id == uid,
1338 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1461 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1339 PullRequestReviewers.pull_request == pull_request)\
1462 PullRequestReviewers.pull_request == pull_request)\
1340 .all()
1463 .all()
1341
1464
1342 for obj in reviewers:
1465 for obj in reviewers:
1343 added_audit_reviewers.append(obj.get_dict())
1466 added_audit_reviewers.append(obj.get_dict())
1344 Session().delete(obj)
1467 Session().delete(obj)
1345
1468
1346 if changed:
1469 if changed:
1347 Session().expire_all()
1470 Session().expire_all()
1348 pull_request.updated_on = datetime.datetime.now()
1471 pull_request.updated_on = datetime.datetime.now()
1349 Session().add(pull_request)
1472 Session().add(pull_request)
1350
1473
1351 # finally store audit logs
1474 # finally store audit logs
1352 for user_data in added_audit_reviewers:
1475 for user_data in added_audit_reviewers:
1353 self._log_audit_action(
1476 self._log_audit_action(
1354 'repo.pull_request.reviewer.add', {'data': user_data},
1477 'repo.pull_request.reviewer.add', {'data': user_data},
1355 user, pull_request)
1478 user, pull_request)
1356 for user_data in removed_audit_reviewers:
1479 for user_data in removed_audit_reviewers:
1357 self._log_audit_action(
1480 self._log_audit_action(
1358 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1481 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1359 user, pull_request)
1482 user, pull_request)
1360
1483
1361 self.notify_reviewers(pull_request, ids_to_add, user)
1484 self.notify_reviewers(pull_request, ids_to_add, user)
1362 return ids_to_add, ids_to_remove
1485 return ids_to_add, ids_to_remove
1363
1486
1364 def update_observers(self, pull_request, observer_data, user):
1487 def update_observers(self, pull_request, observer_data, user):
1365 """
1488 """
1366 Update the observers in the pull request
1489 Update the observers in the pull request
1367
1490
1368 :param pull_request: the pr to update
1491 :param pull_request: the pr to update
1369 :param observer_data: list of tuples
1492 :param observer_data: list of tuples
1370 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1493 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1371 :param user: current use who triggers this action
1494 :param user: current use who triggers this action
1372 """
1495 """
1373 pull_request = self.__get_pull_request(pull_request)
1496 pull_request = self.__get_pull_request(pull_request)
1374 if pull_request.is_closed():
1497 if pull_request.is_closed():
1375 raise ValueError('This pull request is closed')
1498 raise ValueError('This pull request is closed')
1376
1499
1377 observers = {}
1500 observers = {}
1378 for user_id, reasons, mandatory, role, rules in observer_data:
1501 for user_id, reasons, mandatory, role, rules in observer_data:
1379 if isinstance(user_id, (int, compat.string_types)):
1502 if isinstance(user_id, (int, compat.string_types)):
1380 user_id = self._get_user(user_id).user_id
1503 user_id = self._get_user(user_id).user_id
1381 observers[user_id] = {
1504 observers[user_id] = {
1382 'reasons': reasons, 'observers': mandatory, 'role': role}
1505 'reasons': reasons, 'observers': mandatory, 'role': role}
1383
1506
1384 observers_ids = set(observers.keys())
1507 observers_ids = set(observers.keys())
1385 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1508 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1386 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1509 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1387
1510
1388 current_observers_ids = set([x.user.user_id for x in current_observers])
1511 current_observers_ids = set([x.user.user_id for x in current_observers])
1389
1512
1390 ids_to_add = observers_ids.difference(current_observers_ids)
1513 ids_to_add = observers_ids.difference(current_observers_ids)
1391 ids_to_remove = current_observers_ids.difference(observers_ids)
1514 ids_to_remove = current_observers_ids.difference(observers_ids)
1392
1515
1393 log.debug("Adding %s observer", ids_to_add)
1516 log.debug("Adding %s observer", ids_to_add)
1394 log.debug("Removing %s observer", ids_to_remove)
1517 log.debug("Removing %s observer", ids_to_remove)
1395 changed = False
1518 changed = False
1396 added_audit_observers = []
1519 added_audit_observers = []
1397 removed_audit_observers = []
1520 removed_audit_observers = []
1398
1521
1399 for uid in ids_to_add:
1522 for uid in ids_to_add:
1400 changed = True
1523 changed = True
1401 _usr = self._get_user(uid)
1524 _usr = self._get_user(uid)
1402 observer = PullRequestReviewers()
1525 observer = PullRequestReviewers()
1403 observer.user = _usr
1526 observer.user = _usr
1404 observer.pull_request = pull_request
1527 observer.pull_request = pull_request
1405 observer.reasons = observers[uid]['reasons']
1528 observer.reasons = observers[uid]['reasons']
1406 # NOTE(marcink): mandatory shouldn't be changed now
1529 # NOTE(marcink): mandatory shouldn't be changed now
1407 # observer.mandatory = observer[uid]['reasons']
1530 # observer.mandatory = observer[uid]['reasons']
1408
1531
1409 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1532 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1410 observer.role = PullRequestReviewers.ROLE_OBSERVER
1533 observer.role = PullRequestReviewers.ROLE_OBSERVER
1411 Session().add(observer)
1534 Session().add(observer)
1412 added_audit_observers.append(observer.get_dict())
1535 added_audit_observers.append(observer.get_dict())
1413
1536
1414 for uid in ids_to_remove:
1537 for uid in ids_to_remove:
1415 changed = True
1538 changed = True
1416 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1539 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1417 # This is an edge case that handles previous state of having the same reviewer twice.
1540 # This is an edge case that handles previous state of having the same reviewer twice.
1418 # this CAN happen due to the lack of DB checks
1541 # this CAN happen due to the lack of DB checks
1419 observers = PullRequestReviewers.query()\
1542 observers = PullRequestReviewers.query()\
1420 .filter(PullRequestReviewers.user_id == uid,
1543 .filter(PullRequestReviewers.user_id == uid,
1421 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1544 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1422 PullRequestReviewers.pull_request == pull_request)\
1545 PullRequestReviewers.pull_request == pull_request)\
1423 .all()
1546 .all()
1424
1547
1425 for obj in observers:
1548 for obj in observers:
1426 added_audit_observers.append(obj.get_dict())
1549 added_audit_observers.append(obj.get_dict())
1427 Session().delete(obj)
1550 Session().delete(obj)
1428
1551
1429 if changed:
1552 if changed:
1430 Session().expire_all()
1553 Session().expire_all()
1431 pull_request.updated_on = datetime.datetime.now()
1554 pull_request.updated_on = datetime.datetime.now()
1432 Session().add(pull_request)
1555 Session().add(pull_request)
1433
1556
1434 # finally store audit logs
1557 # finally store audit logs
1435 for user_data in added_audit_observers:
1558 for user_data in added_audit_observers:
1436 self._log_audit_action(
1559 self._log_audit_action(
1437 'repo.pull_request.observer.add', {'data': user_data},
1560 'repo.pull_request.observer.add', {'data': user_data},
1438 user, pull_request)
1561 user, pull_request)
1439 for user_data in removed_audit_observers:
1562 for user_data in removed_audit_observers:
1440 self._log_audit_action(
1563 self._log_audit_action(
1441 'repo.pull_request.observer.delete', {'old_data': user_data},
1564 'repo.pull_request.observer.delete', {'old_data': user_data},
1442 user, pull_request)
1565 user, pull_request)
1443
1566
1444 self.notify_observers(pull_request, ids_to_add, user)
1567 self.notify_observers(pull_request, ids_to_add, user)
1445 return ids_to_add, ids_to_remove
1568 return ids_to_add, ids_to_remove
1446
1569
1447 def get_url(self, pull_request, request=None, permalink=False):
1570 def get_url(self, pull_request, request=None, permalink=False):
1448 if not request:
1571 if not request:
1449 request = get_current_request()
1572 request = get_current_request()
1450
1573
1451 if permalink:
1574 if permalink:
1452 return request.route_url(
1575 return request.route_url(
1453 'pull_requests_global',
1576 'pull_requests_global',
1454 pull_request_id=pull_request.pull_request_id,)
1577 pull_request_id=pull_request.pull_request_id,)
1455 else:
1578 else:
1456 return request.route_url('pullrequest_show',
1579 return request.route_url('pullrequest_show',
1457 repo_name=safe_str(pull_request.target_repo.repo_name),
1580 repo_name=safe_str(pull_request.target_repo.repo_name),
1458 pull_request_id=pull_request.pull_request_id,)
1581 pull_request_id=pull_request.pull_request_id,)
1459
1582
1460 def get_shadow_clone_url(self, pull_request, request=None):
1583 def get_shadow_clone_url(self, pull_request, request=None):
1461 """
1584 """
1462 Returns qualified url pointing to the shadow repository. If this pull
1585 Returns qualified url pointing to the shadow repository. If this pull
1463 request is closed there is no shadow repository and ``None`` will be
1586 request is closed there is no shadow repository and ``None`` will be
1464 returned.
1587 returned.
1465 """
1588 """
1466 if pull_request.is_closed():
1589 if pull_request.is_closed():
1467 return None
1590 return None
1468 else:
1591 else:
1469 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1592 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1470 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1593 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1471
1594
1472 def _notify_reviewers(self, pull_request, user_ids, role, user):
1595 def _notify_reviewers(self, pull_request, user_ids, role, user):
1473 # notification to reviewers/observers
1596 # notification to reviewers/observers
1474 if not user_ids:
1597 if not user_ids:
1475 return
1598 return
1476
1599
1477 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1600 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1478
1601
1479 pull_request_obj = pull_request
1602 pull_request_obj = pull_request
1480 # get the current participants of this pull request
1603 # get the current participants of this pull request
1481 recipients = user_ids
1604 recipients = user_ids
1482 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1605 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1483
1606
1484 pr_source_repo = pull_request_obj.source_repo
1607 pr_source_repo = pull_request_obj.source_repo
1485 pr_target_repo = pull_request_obj.target_repo
1608 pr_target_repo = pull_request_obj.target_repo
1486
1609
1487 pr_url = h.route_url('pullrequest_show',
1610 pr_url = h.route_url('pullrequest_show',
1488 repo_name=pr_target_repo.repo_name,
1611 repo_name=pr_target_repo.repo_name,
1489 pull_request_id=pull_request_obj.pull_request_id,)
1612 pull_request_id=pull_request_obj.pull_request_id,)
1490
1613
1491 # set some variables for email notification
1614 # set some variables for email notification
1492 pr_target_repo_url = h.route_url(
1615 pr_target_repo_url = h.route_url(
1493 'repo_summary', repo_name=pr_target_repo.repo_name)
1616 'repo_summary', repo_name=pr_target_repo.repo_name)
1494
1617
1495 pr_source_repo_url = h.route_url(
1618 pr_source_repo_url = h.route_url(
1496 'repo_summary', repo_name=pr_source_repo.repo_name)
1619 'repo_summary', repo_name=pr_source_repo.repo_name)
1497
1620
1498 # pull request specifics
1621 # pull request specifics
1499 pull_request_commits = [
1622 pull_request_commits = [
1500 (x.raw_id, x.message)
1623 (x.raw_id, x.message)
1501 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1624 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1502
1625
1503 current_rhodecode_user = user
1626 current_rhodecode_user = user
1504 kwargs = {
1627 kwargs = {
1505 'user': current_rhodecode_user,
1628 'user': current_rhodecode_user,
1506 'pull_request_author': pull_request.author,
1629 'pull_request_author': pull_request.author,
1507 'pull_request': pull_request_obj,
1630 'pull_request': pull_request_obj,
1508 'pull_request_commits': pull_request_commits,
1631 'pull_request_commits': pull_request_commits,
1509
1632
1510 'pull_request_target_repo': pr_target_repo,
1633 'pull_request_target_repo': pr_target_repo,
1511 'pull_request_target_repo_url': pr_target_repo_url,
1634 'pull_request_target_repo_url': pr_target_repo_url,
1512
1635
1513 'pull_request_source_repo': pr_source_repo,
1636 'pull_request_source_repo': pr_source_repo,
1514 'pull_request_source_repo_url': pr_source_repo_url,
1637 'pull_request_source_repo_url': pr_source_repo_url,
1515
1638
1516 'pull_request_url': pr_url,
1639 'pull_request_url': pr_url,
1517 'thread_ids': [pr_url],
1640 'thread_ids': [pr_url],
1518 'user_role': role
1641 'user_role': role
1519 }
1642 }
1520
1643
1521 # create notification objects, and emails
1644 # create notification objects, and emails
1522 NotificationModel().create(
1645 NotificationModel().create(
1523 created_by=current_rhodecode_user,
1646 created_by=current_rhodecode_user,
1524 notification_subject='', # Filled in based on the notification_type
1647 notification_subject='', # Filled in based on the notification_type
1525 notification_body='', # Filled in based on the notification_type
1648 notification_body='', # Filled in based on the notification_type
1526 notification_type=notification_type,
1649 notification_type=notification_type,
1527 recipients=recipients,
1650 recipients=recipients,
1528 email_kwargs=kwargs,
1651 email_kwargs=kwargs,
1529 )
1652 )
1530
1653
1531 def notify_reviewers(self, pull_request, reviewers_ids, user):
1654 def notify_reviewers(self, pull_request, reviewers_ids, user):
1532 return self._notify_reviewers(pull_request, reviewers_ids,
1655 return self._notify_reviewers(pull_request, reviewers_ids,
1533 PullRequestReviewers.ROLE_REVIEWER, user)
1656 PullRequestReviewers.ROLE_REVIEWER, user)
1534
1657
1535 def notify_observers(self, pull_request, observers_ids, user):
1658 def notify_observers(self, pull_request, observers_ids, user):
1536 return self._notify_reviewers(pull_request, observers_ids,
1659 return self._notify_reviewers(pull_request, observers_ids,
1537 PullRequestReviewers.ROLE_OBSERVER, user)
1660 PullRequestReviewers.ROLE_OBSERVER, user)
1538
1661
1539 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1662 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1540 commit_changes, file_changes):
1663 commit_changes, file_changes):
1541
1664
1542 updating_user_id = updating_user.user_id
1665 updating_user_id = updating_user.user_id
1543 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1666 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1544 # NOTE(marcink): send notification to all other users except to
1667 # NOTE(marcink): send notification to all other users except to
1545 # person who updated the PR
1668 # person who updated the PR
1546 recipients = reviewers.difference(set([updating_user_id]))
1669 recipients = reviewers.difference(set([updating_user_id]))
1547
1670
1548 log.debug('Notify following recipients about pull-request update %s', recipients)
1671 log.debug('Notify following recipients about pull-request update %s', recipients)
1549
1672
1550 pull_request_obj = pull_request
1673 pull_request_obj = pull_request
1551
1674
1552 # send email about the update
1675 # send email about the update
1553 changed_files = (
1676 changed_files = (
1554 file_changes.added + file_changes.modified + file_changes.removed)
1677 file_changes.added + file_changes.modified + file_changes.removed)
1555
1678
1556 pr_source_repo = pull_request_obj.source_repo
1679 pr_source_repo = pull_request_obj.source_repo
1557 pr_target_repo = pull_request_obj.target_repo
1680 pr_target_repo = pull_request_obj.target_repo
1558
1681
1559 pr_url = h.route_url('pullrequest_show',
1682 pr_url = h.route_url('pullrequest_show',
1560 repo_name=pr_target_repo.repo_name,
1683 repo_name=pr_target_repo.repo_name,
1561 pull_request_id=pull_request_obj.pull_request_id,)
1684 pull_request_id=pull_request_obj.pull_request_id,)
1562
1685
1563 # set some variables for email notification
1686 # set some variables for email notification
1564 pr_target_repo_url = h.route_url(
1687 pr_target_repo_url = h.route_url(
1565 'repo_summary', repo_name=pr_target_repo.repo_name)
1688 'repo_summary', repo_name=pr_target_repo.repo_name)
1566
1689
1567 pr_source_repo_url = h.route_url(
1690 pr_source_repo_url = h.route_url(
1568 'repo_summary', repo_name=pr_source_repo.repo_name)
1691 'repo_summary', repo_name=pr_source_repo.repo_name)
1569
1692
1570 email_kwargs = {
1693 email_kwargs = {
1571 'date': datetime.datetime.now(),
1694 'date': datetime.datetime.now(),
1572 'updating_user': updating_user,
1695 'updating_user': updating_user,
1573
1696
1574 'pull_request': pull_request_obj,
1697 'pull_request': pull_request_obj,
1575
1698
1576 'pull_request_target_repo': pr_target_repo,
1699 'pull_request_target_repo': pr_target_repo,
1577 'pull_request_target_repo_url': pr_target_repo_url,
1700 'pull_request_target_repo_url': pr_target_repo_url,
1578
1701
1579 'pull_request_source_repo': pr_source_repo,
1702 'pull_request_source_repo': pr_source_repo,
1580 'pull_request_source_repo_url': pr_source_repo_url,
1703 'pull_request_source_repo_url': pr_source_repo_url,
1581
1704
1582 'pull_request_url': pr_url,
1705 'pull_request_url': pr_url,
1583
1706
1584 'ancestor_commit_id': ancestor_commit_id,
1707 'ancestor_commit_id': ancestor_commit_id,
1585 'added_commits': commit_changes.added,
1708 'added_commits': commit_changes.added,
1586 'removed_commits': commit_changes.removed,
1709 'removed_commits': commit_changes.removed,
1587 'changed_files': changed_files,
1710 'changed_files': changed_files,
1588 'added_files': file_changes.added,
1711 'added_files': file_changes.added,
1589 'modified_files': file_changes.modified,
1712 'modified_files': file_changes.modified,
1590 'removed_files': file_changes.removed,
1713 'removed_files': file_changes.removed,
1591 'thread_ids': [pr_url],
1714 'thread_ids': [pr_url],
1592 }
1715 }
1593
1716
1594 # create notification objects, and emails
1717 # create notification objects, and emails
1595 NotificationModel().create(
1718 NotificationModel().create(
1596 created_by=updating_user,
1719 created_by=updating_user,
1597 notification_subject='', # Filled in based on the notification_type
1720 notification_subject='', # Filled in based on the notification_type
1598 notification_body='', # Filled in based on the notification_type
1721 notification_body='', # Filled in based on the notification_type
1599 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1722 notification_type=EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE,
1600 recipients=recipients,
1723 recipients=recipients,
1601 email_kwargs=email_kwargs,
1724 email_kwargs=email_kwargs,
1602 )
1725 )
1603
1726
1604 def delete(self, pull_request, user=None):
1727 def delete(self, pull_request, user=None):
1605 if not user:
1728 if not user:
1606 user = getattr(get_current_rhodecode_user(), 'username', None)
1729 user = getattr(get_current_rhodecode_user(), 'username', None)
1607
1730
1608 pull_request = self.__get_pull_request(pull_request)
1731 pull_request = self.__get_pull_request(pull_request)
1609 old_data = pull_request.get_api_data(with_merge_state=False)
1732 old_data = pull_request.get_api_data(with_merge_state=False)
1610 self._cleanup_merge_workspace(pull_request)
1733 self._cleanup_merge_workspace(pull_request)
1611 self._log_audit_action(
1734 self._log_audit_action(
1612 'repo.pull_request.delete', {'old_data': old_data},
1735 'repo.pull_request.delete', {'old_data': old_data},
1613 user, pull_request)
1736 user, pull_request)
1614 Session().delete(pull_request)
1737 Session().delete(pull_request)
1615
1738
1616 def close_pull_request(self, pull_request, user):
1739 def close_pull_request(self, pull_request, user):
1617 pull_request = self.__get_pull_request(pull_request)
1740 pull_request = self.__get_pull_request(pull_request)
1618 self._cleanup_merge_workspace(pull_request)
1741 self._cleanup_merge_workspace(pull_request)
1619 pull_request.status = PullRequest.STATUS_CLOSED
1742 pull_request.status = PullRequest.STATUS_CLOSED
1620 pull_request.updated_on = datetime.datetime.now()
1743 pull_request.updated_on = datetime.datetime.now()
1621 Session().add(pull_request)
1744 Session().add(pull_request)
1622 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1745 self.trigger_pull_request_hook(pull_request, pull_request.author, 'close')
1623
1746
1624 pr_data = pull_request.get_api_data(with_merge_state=False)
1747 pr_data = pull_request.get_api_data(with_merge_state=False)
1625 self._log_audit_action(
1748 self._log_audit_action(
1626 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1749 'repo.pull_request.close', {'data': pr_data}, user, pull_request)
1627
1750
1628 def close_pull_request_with_comment(
1751 def close_pull_request_with_comment(
1629 self, pull_request, user, repo, message=None, auth_user=None):
1752 self, pull_request, user, repo, message=None, auth_user=None):
1630
1753
1631 pull_request_review_status = pull_request.calculated_review_status()
1754 pull_request_review_status = pull_request.calculated_review_status()
1632
1755
1633 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1756 if pull_request_review_status == ChangesetStatus.STATUS_APPROVED:
1634 # approved only if we have voting consent
1757 # approved only if we have voting consent
1635 status = ChangesetStatus.STATUS_APPROVED
1758 status = ChangesetStatus.STATUS_APPROVED
1636 else:
1759 else:
1637 status = ChangesetStatus.STATUS_REJECTED
1760 status = ChangesetStatus.STATUS_REJECTED
1638 status_lbl = ChangesetStatus.get_status_lbl(status)
1761 status_lbl = ChangesetStatus.get_status_lbl(status)
1639
1762
1640 default_message = (
1763 default_message = (
1641 'Closing with status change {transition_icon} {status}.'
1764 'Closing with status change {transition_icon} {status}.'
1642 ).format(transition_icon='>', status=status_lbl)
1765 ).format(transition_icon='>', status=status_lbl)
1643 text = message or default_message
1766 text = message or default_message
1644
1767
1645 # create a comment, and link it to new status
1768 # create a comment, and link it to new status
1646 comment = CommentsModel().create(
1769 comment = CommentsModel().create(
1647 text=text,
1770 text=text,
1648 repo=repo.repo_id,
1771 repo=repo.repo_id,
1649 user=user.user_id,
1772 user=user.user_id,
1650 pull_request=pull_request.pull_request_id,
1773 pull_request=pull_request.pull_request_id,
1651 status_change=status_lbl,
1774 status_change=status_lbl,
1652 status_change_type=status,
1775 status_change_type=status,
1653 closing_pr=True,
1776 closing_pr=True,
1654 auth_user=auth_user,
1777 auth_user=auth_user,
1655 )
1778 )
1656
1779
1657 # calculate old status before we change it
1780 # calculate old status before we change it
1658 old_calculated_status = pull_request.calculated_review_status()
1781 old_calculated_status = pull_request.calculated_review_status()
1659 ChangesetStatusModel().set_status(
1782 ChangesetStatusModel().set_status(
1660 repo.repo_id,
1783 repo.repo_id,
1661 status,
1784 status,
1662 user.user_id,
1785 user.user_id,
1663 comment=comment,
1786 comment=comment,
1664 pull_request=pull_request.pull_request_id
1787 pull_request=pull_request.pull_request_id
1665 )
1788 )
1666
1789
1667 Session().flush()
1790 Session().flush()
1668
1791
1669 self.trigger_pull_request_hook(pull_request, user, 'comment',
1792 self.trigger_pull_request_hook(pull_request, user, 'comment',
1670 data={'comment': comment})
1793 data={'comment': comment})
1671
1794
1672 # we now calculate the status of pull request again, and based on that
1795 # we now calculate the status of pull request again, and based on that
1673 # calculation trigger status change. This might happen in cases
1796 # calculation trigger status change. This might happen in cases
1674 # that non-reviewer admin closes a pr, which means his vote doesn't
1797 # that non-reviewer admin closes a pr, which means his vote doesn't
1675 # change the status, while if he's a reviewer this might change it.
1798 # change the status, while if he's a reviewer this might change it.
1676 calculated_status = pull_request.calculated_review_status()
1799 calculated_status = pull_request.calculated_review_status()
1677 if old_calculated_status != calculated_status:
1800 if old_calculated_status != calculated_status:
1678 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1801 self.trigger_pull_request_hook(pull_request, user, 'review_status_change',
1679 data={'status': calculated_status})
1802 data={'status': calculated_status})
1680
1803
1681 # finally close the PR
1804 # finally close the PR
1682 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1805 PullRequestModel().close_pull_request(pull_request.pull_request_id, user)
1683
1806
1684 return comment, status
1807 return comment, status
1685
1808
1686 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1809 def merge_status(self, pull_request, translator=None, force_shadow_repo_refresh=False):
1687 _ = translator or get_current_request().translate
1810 _ = translator or get_current_request().translate
1688
1811
1689 if not self._is_merge_enabled(pull_request):
1812 if not self._is_merge_enabled(pull_request):
1690 return None, False, _('Server-side pull request merging is disabled.')
1813 return None, False, _('Server-side pull request merging is disabled.')
1691
1814
1692 if pull_request.is_closed():
1815 if pull_request.is_closed():
1693 return None, False, _('This pull request is closed.')
1816 return None, False, _('This pull request is closed.')
1694
1817
1695 merge_possible, msg = self._check_repo_requirements(
1818 merge_possible, msg = self._check_repo_requirements(
1696 target=pull_request.target_repo, source=pull_request.source_repo,
1819 target=pull_request.target_repo, source=pull_request.source_repo,
1697 translator=_)
1820 translator=_)
1698 if not merge_possible:
1821 if not merge_possible:
1699 return None, merge_possible, msg
1822 return None, merge_possible, msg
1700
1823
1701 try:
1824 try:
1702 merge_response = self._try_merge(
1825 merge_response = self._try_merge(
1703 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1826 pull_request, force_shadow_repo_refresh=force_shadow_repo_refresh)
1704 log.debug("Merge response: %s", merge_response)
1827 log.debug("Merge response: %s", merge_response)
1705 return merge_response, merge_response.possible, merge_response.merge_status_message
1828 return merge_response, merge_response.possible, merge_response.merge_status_message
1706 except NotImplementedError:
1829 except NotImplementedError:
1707 return None, False, _('Pull request merging is not supported.')
1830 return None, False, _('Pull request merging is not supported.')
1708
1831
1709 def _check_repo_requirements(self, target, source, translator):
1832 def _check_repo_requirements(self, target, source, translator):
1710 """
1833 """
1711 Check if `target` and `source` have compatible requirements.
1834 Check if `target` and `source` have compatible requirements.
1712
1835
1713 Currently this is just checking for largefiles.
1836 Currently this is just checking for largefiles.
1714 """
1837 """
1715 _ = translator
1838 _ = translator
1716 target_has_largefiles = self._has_largefiles(target)
1839 target_has_largefiles = self._has_largefiles(target)
1717 source_has_largefiles = self._has_largefiles(source)
1840 source_has_largefiles = self._has_largefiles(source)
1718 merge_possible = True
1841 merge_possible = True
1719 message = u''
1842 message = u''
1720
1843
1721 if target_has_largefiles != source_has_largefiles:
1844 if target_has_largefiles != source_has_largefiles:
1722 merge_possible = False
1845 merge_possible = False
1723 if source_has_largefiles:
1846 if source_has_largefiles:
1724 message = _(
1847 message = _(
1725 'Target repository large files support is disabled.')
1848 'Target repository large files support is disabled.')
1726 else:
1849 else:
1727 message = _(
1850 message = _(
1728 'Source repository large files support is disabled.')
1851 'Source repository large files support is disabled.')
1729
1852
1730 return merge_possible, message
1853 return merge_possible, message
1731
1854
1732 def _has_largefiles(self, repo):
1855 def _has_largefiles(self, repo):
1733 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1856 largefiles_ui = VcsSettingsModel(repo=repo).get_ui_settings(
1734 'extensions', 'largefiles')
1857 'extensions', 'largefiles')
1735 return largefiles_ui and largefiles_ui[0].active
1858 return largefiles_ui and largefiles_ui[0].active
1736
1859
1737 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1860 def _try_merge(self, pull_request, force_shadow_repo_refresh=False):
1738 """
1861 """
1739 Try to merge the pull request and return the merge status.
1862 Try to merge the pull request and return the merge status.
1740 """
1863 """
1741 log.debug(
1864 log.debug(
1742 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1865 "Trying out if the pull request %s can be merged. Force_refresh=%s",
1743 pull_request.pull_request_id, force_shadow_repo_refresh)
1866 pull_request.pull_request_id, force_shadow_repo_refresh)
1744 target_vcs = pull_request.target_repo.scm_instance()
1867 target_vcs = pull_request.target_repo.scm_instance()
1745 # Refresh the target reference.
1868 # Refresh the target reference.
1746 try:
1869 try:
1747 target_ref = self._refresh_reference(
1870 target_ref = self._refresh_reference(
1748 pull_request.target_ref_parts, target_vcs)
1871 pull_request.target_ref_parts, target_vcs)
1749 except CommitDoesNotExistError:
1872 except CommitDoesNotExistError:
1750 merge_state = MergeResponse(
1873 merge_state = MergeResponse(
1751 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1874 False, False, None, MergeFailureReason.MISSING_TARGET_REF,
1752 metadata={'target_ref': pull_request.target_ref_parts})
1875 metadata={'target_ref': pull_request.target_ref_parts})
1753 return merge_state
1876 return merge_state
1754
1877
1755 target_locked = pull_request.target_repo.locked
1878 target_locked = pull_request.target_repo.locked
1756 if target_locked and target_locked[0]:
1879 if target_locked and target_locked[0]:
1757 locked_by = 'user:{}'.format(target_locked[0])
1880 locked_by = 'user:{}'.format(target_locked[0])
1758 log.debug("The target repository is locked by %s.", locked_by)
1881 log.debug("The target repository is locked by %s.", locked_by)
1759 merge_state = MergeResponse(
1882 merge_state = MergeResponse(
1760 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1883 False, False, None, MergeFailureReason.TARGET_IS_LOCKED,
1761 metadata={'locked_by': locked_by})
1884 metadata={'locked_by': locked_by})
1762 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1885 elif force_shadow_repo_refresh or self._needs_merge_state_refresh(
1763 pull_request, target_ref):
1886 pull_request, target_ref):
1764 log.debug("Refreshing the merge status of the repository.")
1887 log.debug("Refreshing the merge status of the repository.")
1765 merge_state = self._refresh_merge_state(
1888 merge_state = self._refresh_merge_state(
1766 pull_request, target_vcs, target_ref)
1889 pull_request, target_vcs, target_ref)
1767 else:
1890 else:
1768 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1891 possible = pull_request.last_merge_status == MergeFailureReason.NONE
1769 metadata = {
1892 metadata = {
1770 'unresolved_files': '',
1893 'unresolved_files': '',
1771 'target_ref': pull_request.target_ref_parts,
1894 'target_ref': pull_request.target_ref_parts,
1772 'source_ref': pull_request.source_ref_parts,
1895 'source_ref': pull_request.source_ref_parts,
1773 }
1896 }
1774 if pull_request.last_merge_metadata:
1897 if pull_request.last_merge_metadata:
1775 metadata.update(pull_request.last_merge_metadata_parsed)
1898 metadata.update(pull_request.last_merge_metadata_parsed)
1776
1899
1777 if not possible and target_ref.type == 'branch':
1900 if not possible and target_ref.type == 'branch':
1778 # NOTE(marcink): case for mercurial multiple heads on branch
1901 # NOTE(marcink): case for mercurial multiple heads on branch
1779 heads = target_vcs._heads(target_ref.name)
1902 heads = target_vcs._heads(target_ref.name)
1780 if len(heads) != 1:
1903 if len(heads) != 1:
1781 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1904 heads = '\n,'.join(target_vcs._heads(target_ref.name))
1782 metadata.update({
1905 metadata.update({
1783 'heads': heads
1906 'heads': heads
1784 })
1907 })
1785
1908
1786 merge_state = MergeResponse(
1909 merge_state = MergeResponse(
1787 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1910 possible, False, None, pull_request.last_merge_status, metadata=metadata)
1788
1911
1789 return merge_state
1912 return merge_state
1790
1913
1791 def _refresh_reference(self, reference, vcs_repository):
1914 def _refresh_reference(self, reference, vcs_repository):
1792 if reference.type in self.UPDATABLE_REF_TYPES:
1915 if reference.type in self.UPDATABLE_REF_TYPES:
1793 name_or_id = reference.name
1916 name_or_id = reference.name
1794 else:
1917 else:
1795 name_or_id = reference.commit_id
1918 name_or_id = reference.commit_id
1796
1919
1797 refreshed_commit = vcs_repository.get_commit(name_or_id)
1920 refreshed_commit = vcs_repository.get_commit(name_or_id)
1798 refreshed_reference = Reference(
1921 refreshed_reference = Reference(
1799 reference.type, reference.name, refreshed_commit.raw_id)
1922 reference.type, reference.name, refreshed_commit.raw_id)
1800 return refreshed_reference
1923 return refreshed_reference
1801
1924
1802 def _needs_merge_state_refresh(self, pull_request, target_reference):
1925 def _needs_merge_state_refresh(self, pull_request, target_reference):
1803 return not(
1926 return not(
1804 pull_request.revisions and
1927 pull_request.revisions and
1805 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1928 pull_request.revisions[0] == pull_request._last_merge_source_rev and
1806 target_reference.commit_id == pull_request._last_merge_target_rev)
1929 target_reference.commit_id == pull_request._last_merge_target_rev)
1807
1930
1808 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1931 def _refresh_merge_state(self, pull_request, target_vcs, target_reference):
1809 workspace_id = self._workspace_id(pull_request)
1932 workspace_id = self._workspace_id(pull_request)
1810 source_vcs = pull_request.source_repo.scm_instance()
1933 source_vcs = pull_request.source_repo.scm_instance()
1811 repo_id = pull_request.target_repo.repo_id
1934 repo_id = pull_request.target_repo.repo_id
1812 use_rebase = self._use_rebase_for_merging(pull_request)
1935 use_rebase = self._use_rebase_for_merging(pull_request)
1813 close_branch = self._close_branch_before_merging(pull_request)
1936 close_branch = self._close_branch_before_merging(pull_request)
1814 merge_state = target_vcs.merge(
1937 merge_state = target_vcs.merge(
1815 repo_id, workspace_id,
1938 repo_id, workspace_id,
1816 target_reference, source_vcs, pull_request.source_ref_parts,
1939 target_reference, source_vcs, pull_request.source_ref_parts,
1817 dry_run=True, use_rebase=use_rebase,
1940 dry_run=True, use_rebase=use_rebase,
1818 close_branch=close_branch)
1941 close_branch=close_branch)
1819
1942
1820 # Do not store the response if there was an unknown error.
1943 # Do not store the response if there was an unknown error.
1821 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1944 if merge_state.failure_reason != MergeFailureReason.UNKNOWN:
1822 pull_request._last_merge_source_rev = \
1945 pull_request._last_merge_source_rev = \
1823 pull_request.source_ref_parts.commit_id
1946 pull_request.source_ref_parts.commit_id
1824 pull_request._last_merge_target_rev = target_reference.commit_id
1947 pull_request._last_merge_target_rev = target_reference.commit_id
1825 pull_request.last_merge_status = merge_state.failure_reason
1948 pull_request.last_merge_status = merge_state.failure_reason
1826 pull_request.last_merge_metadata = merge_state.metadata
1949 pull_request.last_merge_metadata = merge_state.metadata
1827
1950
1828 pull_request.shadow_merge_ref = merge_state.merge_ref
1951 pull_request.shadow_merge_ref = merge_state.merge_ref
1829 Session().add(pull_request)
1952 Session().add(pull_request)
1830 Session().commit()
1953 Session().commit()
1831
1954
1832 return merge_state
1955 return merge_state
1833
1956
1834 def _workspace_id(self, pull_request):
1957 def _workspace_id(self, pull_request):
1835 workspace_id = 'pr-%s' % pull_request.pull_request_id
1958 workspace_id = 'pr-%s' % pull_request.pull_request_id
1836 return workspace_id
1959 return workspace_id
1837
1960
1838 def generate_repo_data(self, repo, commit_id=None, branch=None,
1961 def generate_repo_data(self, repo, commit_id=None, branch=None,
1839 bookmark=None, translator=None):
1962 bookmark=None, translator=None):
1840 from rhodecode.model.repo import RepoModel
1963 from rhodecode.model.repo import RepoModel
1841
1964
1842 all_refs, selected_ref = \
1965 all_refs, selected_ref = \
1843 self._get_repo_pullrequest_sources(
1966 self._get_repo_pullrequest_sources(
1844 repo.scm_instance(), commit_id=commit_id,
1967 repo.scm_instance(), commit_id=commit_id,
1845 branch=branch, bookmark=bookmark, translator=translator)
1968 branch=branch, bookmark=bookmark, translator=translator)
1846
1969
1847 refs_select2 = []
1970 refs_select2 = []
1848 for element in all_refs:
1971 for element in all_refs:
1849 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1972 children = [{'id': x[0], 'text': x[1]} for x in element[0]]
1850 refs_select2.append({'text': element[1], 'children': children})
1973 refs_select2.append({'text': element[1], 'children': children})
1851
1974
1852 return {
1975 return {
1853 'user': {
1976 'user': {
1854 'user_id': repo.user.user_id,
1977 'user_id': repo.user.user_id,
1855 'username': repo.user.username,
1978 'username': repo.user.username,
1856 'firstname': repo.user.first_name,
1979 'firstname': repo.user.first_name,
1857 'lastname': repo.user.last_name,
1980 'lastname': repo.user.last_name,
1858 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1981 'gravatar_link': h.gravatar_url(repo.user.email, 14),
1859 },
1982 },
1860 'name': repo.repo_name,
1983 'name': repo.repo_name,
1861 'link': RepoModel().get_url(repo),
1984 'link': RepoModel().get_url(repo),
1862 'description': h.chop_at_smart(repo.description_safe, '\n'),
1985 'description': h.chop_at_smart(repo.description_safe, '\n'),
1863 'refs': {
1986 'refs': {
1864 'all_refs': all_refs,
1987 'all_refs': all_refs,
1865 'selected_ref': selected_ref,
1988 'selected_ref': selected_ref,
1866 'select2_refs': refs_select2
1989 'select2_refs': refs_select2
1867 }
1990 }
1868 }
1991 }
1869
1992
1870 def generate_pullrequest_title(self, source, source_ref, target):
1993 def generate_pullrequest_title(self, source, source_ref, target):
1871 return u'{source}#{at_ref} to {target}'.format(
1994 return u'{source}#{at_ref} to {target}'.format(
1872 source=source,
1995 source=source,
1873 at_ref=source_ref,
1996 at_ref=source_ref,
1874 target=target,
1997 target=target,
1875 )
1998 )
1876
1999
1877 def _cleanup_merge_workspace(self, pull_request):
2000 def _cleanup_merge_workspace(self, pull_request):
1878 # Merging related cleanup
2001 # Merging related cleanup
1879 repo_id = pull_request.target_repo.repo_id
2002 repo_id = pull_request.target_repo.repo_id
1880 target_scm = pull_request.target_repo.scm_instance()
2003 target_scm = pull_request.target_repo.scm_instance()
1881 workspace_id = self._workspace_id(pull_request)
2004 workspace_id = self._workspace_id(pull_request)
1882
2005
1883 try:
2006 try:
1884 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
2007 target_scm.cleanup_merge_workspace(repo_id, workspace_id)
1885 except NotImplementedError:
2008 except NotImplementedError:
1886 pass
2009 pass
1887
2010
1888 def _get_repo_pullrequest_sources(
2011 def _get_repo_pullrequest_sources(
1889 self, repo, commit_id=None, branch=None, bookmark=None,
2012 self, repo, commit_id=None, branch=None, bookmark=None,
1890 translator=None):
2013 translator=None):
1891 """
2014 """
1892 Return a structure with repo's interesting commits, suitable for
2015 Return a structure with repo's interesting commits, suitable for
1893 the selectors in pullrequest controller
2016 the selectors in pullrequest controller
1894
2017
1895 :param commit_id: a commit that must be in the list somehow
2018 :param commit_id: a commit that must be in the list somehow
1896 and selected by default
2019 and selected by default
1897 :param branch: a branch that must be in the list and selected
2020 :param branch: a branch that must be in the list and selected
1898 by default - even if closed
2021 by default - even if closed
1899 :param bookmark: a bookmark that must be in the list and selected
2022 :param bookmark: a bookmark that must be in the list and selected
1900 """
2023 """
1901 _ = translator or get_current_request().translate
2024 _ = translator or get_current_request().translate
1902
2025
1903 commit_id = safe_str(commit_id) if commit_id else None
2026 commit_id = safe_str(commit_id) if commit_id else None
1904 branch = safe_unicode(branch) if branch else None
2027 branch = safe_unicode(branch) if branch else None
1905 bookmark = safe_unicode(bookmark) if bookmark else None
2028 bookmark = safe_unicode(bookmark) if bookmark else None
1906
2029
1907 selected = None
2030 selected = None
1908
2031
1909 # order matters: first source that has commit_id in it will be selected
2032 # order matters: first source that has commit_id in it will be selected
1910 sources = []
2033 sources = []
1911 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
2034 sources.append(('book', repo.bookmarks.items(), _('Bookmarks'), bookmark))
1912 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
2035 sources.append(('branch', repo.branches.items(), _('Branches'), branch))
1913
2036
1914 if commit_id:
2037 if commit_id:
1915 ref_commit = (h.short_id(commit_id), commit_id)
2038 ref_commit = (h.short_id(commit_id), commit_id)
1916 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
2039 sources.append(('rev', [ref_commit], _('Commit IDs'), commit_id))
1917
2040
1918 sources.append(
2041 sources.append(
1919 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
2042 ('branch', repo.branches_closed.items(), _('Closed Branches'), branch),
1920 )
2043 )
1921
2044
1922 groups = []
2045 groups = []
1923
2046
1924 for group_key, ref_list, group_name, match in sources:
2047 for group_key, ref_list, group_name, match in sources:
1925 group_refs = []
2048 group_refs = []
1926 for ref_name, ref_id in ref_list:
2049 for ref_name, ref_id in ref_list:
1927 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
2050 ref_key = u'{}:{}:{}'.format(group_key, ref_name, ref_id)
1928 group_refs.append((ref_key, ref_name))
2051 group_refs.append((ref_key, ref_name))
1929
2052
1930 if not selected:
2053 if not selected:
1931 if set([commit_id, match]) & set([ref_id, ref_name]):
2054 if set([commit_id, match]) & set([ref_id, ref_name]):
1932 selected = ref_key
2055 selected = ref_key
1933
2056
1934 if group_refs:
2057 if group_refs:
1935 groups.append((group_refs, group_name))
2058 groups.append((group_refs, group_name))
1936
2059
1937 if not selected:
2060 if not selected:
1938 ref = commit_id or branch or bookmark
2061 ref = commit_id or branch or bookmark
1939 if ref:
2062 if ref:
1940 raise CommitDoesNotExistError(
2063 raise CommitDoesNotExistError(
1941 u'No commit refs could be found matching: {}'.format(ref))
2064 u'No commit refs could be found matching: {}'.format(ref))
1942 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
2065 elif repo.DEFAULT_BRANCH_NAME in repo.branches:
1943 selected = u'branch:{}:{}'.format(
2066 selected = u'branch:{}:{}'.format(
1944 safe_unicode(repo.DEFAULT_BRANCH_NAME),
2067 safe_unicode(repo.DEFAULT_BRANCH_NAME),
1945 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
2068 safe_unicode(repo.branches[repo.DEFAULT_BRANCH_NAME])
1946 )
2069 )
1947 elif repo.commit_ids:
2070 elif repo.commit_ids:
1948 # make the user select in this case
2071 # make the user select in this case
1949 selected = None
2072 selected = None
1950 else:
2073 else:
1951 raise EmptyRepositoryError()
2074 raise EmptyRepositoryError()
1952 return groups, selected
2075 return groups, selected
1953
2076
1954 def get_diff(self, source_repo, source_ref_id, target_ref_id,
2077 def get_diff(self, source_repo, source_ref_id, target_ref_id,
1955 hide_whitespace_changes, diff_context):
2078 hide_whitespace_changes, diff_context):
1956
2079
1957 return self._get_diff_from_pr_or_version(
2080 return self._get_diff_from_pr_or_version(
1958 source_repo, source_ref_id, target_ref_id,
2081 source_repo, source_ref_id, target_ref_id,
1959 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
2082 hide_whitespace_changes=hide_whitespace_changes, diff_context=diff_context)
1960
2083
1961 def _get_diff_from_pr_or_version(
2084 def _get_diff_from_pr_or_version(
1962 self, source_repo, source_ref_id, target_ref_id,
2085 self, source_repo, source_ref_id, target_ref_id,
1963 hide_whitespace_changes, diff_context):
2086 hide_whitespace_changes, diff_context):
1964
2087
1965 target_commit = source_repo.get_commit(
2088 target_commit = source_repo.get_commit(
1966 commit_id=safe_str(target_ref_id))
2089 commit_id=safe_str(target_ref_id))
1967 source_commit = source_repo.get_commit(
2090 source_commit = source_repo.get_commit(
1968 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
2091 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
1969 if isinstance(source_repo, Repository):
2092 if isinstance(source_repo, Repository):
1970 vcs_repo = source_repo.scm_instance()
2093 vcs_repo = source_repo.scm_instance()
1971 else:
2094 else:
1972 vcs_repo = source_repo
2095 vcs_repo = source_repo
1973
2096
1974 # TODO: johbo: In the context of an update, we cannot reach
2097 # TODO: johbo: In the context of an update, we cannot reach
1975 # the old commit anymore with our normal mechanisms. It needs
2098 # the old commit anymore with our normal mechanisms. It needs
1976 # some sort of special support in the vcs layer to avoid this
2099 # some sort of special support in the vcs layer to avoid this
1977 # workaround.
2100 # workaround.
1978 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
2101 if (source_commit.raw_id == vcs_repo.EMPTY_COMMIT_ID and
1979 vcs_repo.alias == 'git'):
2102 vcs_repo.alias == 'git'):
1980 source_commit.raw_id = safe_str(source_ref_id)
2103 source_commit.raw_id = safe_str(source_ref_id)
1981
2104
1982 log.debug('calculating diff between '
2105 log.debug('calculating diff between '
1983 'source_ref:%s and target_ref:%s for repo `%s`',
2106 'source_ref:%s and target_ref:%s for repo `%s`',
1984 target_ref_id, source_ref_id,
2107 target_ref_id, source_ref_id,
1985 safe_unicode(vcs_repo.path))
2108 safe_unicode(vcs_repo.path))
1986
2109
1987 vcs_diff = vcs_repo.get_diff(
2110 vcs_diff = vcs_repo.get_diff(
1988 commit1=target_commit, commit2=source_commit,
2111 commit1=target_commit, commit2=source_commit,
1989 ignore_whitespace=hide_whitespace_changes, context=diff_context)
2112 ignore_whitespace=hide_whitespace_changes, context=diff_context)
1990 return vcs_diff
2113 return vcs_diff
1991
2114
1992 def _is_merge_enabled(self, pull_request):
2115 def _is_merge_enabled(self, pull_request):
1993 return self._get_general_setting(
2116 return self._get_general_setting(
1994 pull_request, 'rhodecode_pr_merge_enabled')
2117 pull_request, 'rhodecode_pr_merge_enabled')
1995
2118
1996 def _use_rebase_for_merging(self, pull_request):
2119 def _use_rebase_for_merging(self, pull_request):
1997 repo_type = pull_request.target_repo.repo_type
2120 repo_type = pull_request.target_repo.repo_type
1998 if repo_type == 'hg':
2121 if repo_type == 'hg':
1999 return self._get_general_setting(
2122 return self._get_general_setting(
2000 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2123 pull_request, 'rhodecode_hg_use_rebase_for_merging')
2001 elif repo_type == 'git':
2124 elif repo_type == 'git':
2002 return self._get_general_setting(
2125 return self._get_general_setting(
2003 pull_request, 'rhodecode_git_use_rebase_for_merging')
2126 pull_request, 'rhodecode_git_use_rebase_for_merging')
2004
2127
2005 return False
2128 return False
2006
2129
2007 def _user_name_for_merging(self, pull_request, user):
2130 def _user_name_for_merging(self, pull_request, user):
2008 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2131 env_user_name_attr = os.environ.get('RC_MERGE_USER_NAME_ATTR', '')
2009 if env_user_name_attr and hasattr(user, env_user_name_attr):
2132 if env_user_name_attr and hasattr(user, env_user_name_attr):
2010 user_name_attr = env_user_name_attr
2133 user_name_attr = env_user_name_attr
2011 else:
2134 else:
2012 user_name_attr = 'short_contact'
2135 user_name_attr = 'short_contact'
2013
2136
2014 user_name = getattr(user, user_name_attr)
2137 user_name = getattr(user, user_name_attr)
2015 return user_name
2138 return user_name
2016
2139
2017 def _close_branch_before_merging(self, pull_request):
2140 def _close_branch_before_merging(self, pull_request):
2018 repo_type = pull_request.target_repo.repo_type
2141 repo_type = pull_request.target_repo.repo_type
2019 if repo_type == 'hg':
2142 if repo_type == 'hg':
2020 return self._get_general_setting(
2143 return self._get_general_setting(
2021 pull_request, 'rhodecode_hg_close_branch_before_merging')
2144 pull_request, 'rhodecode_hg_close_branch_before_merging')
2022 elif repo_type == 'git':
2145 elif repo_type == 'git':
2023 return self._get_general_setting(
2146 return self._get_general_setting(
2024 pull_request, 'rhodecode_git_close_branch_before_merging')
2147 pull_request, 'rhodecode_git_close_branch_before_merging')
2025
2148
2026 return False
2149 return False
2027
2150
2028 def _get_general_setting(self, pull_request, settings_key, default=False):
2151 def _get_general_setting(self, pull_request, settings_key, default=False):
2029 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2152 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
2030 settings = settings_model.get_general_settings()
2153 settings = settings_model.get_general_settings()
2031 return settings.get(settings_key, default)
2154 return settings.get(settings_key, default)
2032
2155
2033 def _log_audit_action(self, action, action_data, user, pull_request):
2156 def _log_audit_action(self, action, action_data, user, pull_request):
2034 audit_logger.store(
2157 audit_logger.store(
2035 action=action,
2158 action=action,
2036 action_data=action_data,
2159 action_data=action_data,
2037 user=user,
2160 user=user,
2038 repo=pull_request.target_repo)
2161 repo=pull_request.target_repo)
2039
2162
2040 def get_reviewer_functions(self):
2163 def get_reviewer_functions(self):
2041 """
2164 """
2042 Fetches functions for validation and fetching default reviewers.
2165 Fetches functions for validation and fetching default reviewers.
2043 If available we use the EE package, else we fallback to CE
2166 If available we use the EE package, else we fallback to CE
2044 package functions
2167 package functions
2045 """
2168 """
2046 try:
2169 try:
2047 from rc_reviewers.utils import get_default_reviewers_data
2170 from rc_reviewers.utils import get_default_reviewers_data
2048 from rc_reviewers.utils import validate_default_reviewers
2171 from rc_reviewers.utils import validate_default_reviewers
2049 from rc_reviewers.utils import validate_observers
2172 from rc_reviewers.utils import validate_observers
2050 except ImportError:
2173 except ImportError:
2051 from rhodecode.apps.repository.utils import get_default_reviewers_data
2174 from rhodecode.apps.repository.utils import get_default_reviewers_data
2052 from rhodecode.apps.repository.utils import validate_default_reviewers
2175 from rhodecode.apps.repository.utils import validate_default_reviewers
2053 from rhodecode.apps.repository.utils import validate_observers
2176 from rhodecode.apps.repository.utils import validate_observers
2054
2177
2055 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2178 return get_default_reviewers_data, validate_default_reviewers, validate_observers
2056
2179
2057
2180
2058 class MergeCheck(object):
2181 class MergeCheck(object):
2059 """
2182 """
2060 Perform Merge Checks and returns a check object which stores information
2183 Perform Merge Checks and returns a check object which stores information
2061 about merge errors, and merge conditions
2184 about merge errors, and merge conditions
2062 """
2185 """
2063 TODO_CHECK = 'todo'
2186 TODO_CHECK = 'todo'
2064 PERM_CHECK = 'perm'
2187 PERM_CHECK = 'perm'
2065 REVIEW_CHECK = 'review'
2188 REVIEW_CHECK = 'review'
2066 MERGE_CHECK = 'merge'
2189 MERGE_CHECK = 'merge'
2067 WIP_CHECK = 'wip'
2190 WIP_CHECK = 'wip'
2068
2191
2069 def __init__(self):
2192 def __init__(self):
2070 self.review_status = None
2193 self.review_status = None
2071 self.merge_possible = None
2194 self.merge_possible = None
2072 self.merge_msg = ''
2195 self.merge_msg = ''
2073 self.merge_response = None
2196 self.merge_response = None
2074 self.failed = None
2197 self.failed = None
2075 self.errors = []
2198 self.errors = []
2076 self.error_details = OrderedDict()
2199 self.error_details = OrderedDict()
2077 self.source_commit = AttributeDict()
2200 self.source_commit = AttributeDict()
2078 self.target_commit = AttributeDict()
2201 self.target_commit = AttributeDict()
2079 self.reviewers_count = 0
2202 self.reviewers_count = 0
2080 self.observers_count = 0
2203 self.observers_count = 0
2081
2204
2082 def __repr__(self):
2205 def __repr__(self):
2083 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2206 return '<MergeCheck(possible:{}, failed:{}, errors:{})>'.format(
2084 self.merge_possible, self.failed, self.errors)
2207 self.merge_possible, self.failed, self.errors)
2085
2208
2086 def push_error(self, error_type, message, error_key, details):
2209 def push_error(self, error_type, message, error_key, details):
2087 self.failed = True
2210 self.failed = True
2088 self.errors.append([error_type, message])
2211 self.errors.append([error_type, message])
2089 self.error_details[error_key] = dict(
2212 self.error_details[error_key] = dict(
2090 details=details,
2213 details=details,
2091 error_type=error_type,
2214 error_type=error_type,
2092 message=message
2215 message=message
2093 )
2216 )
2094
2217
2095 @classmethod
2218 @classmethod
2096 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2219 def validate(cls, pull_request, auth_user, translator, fail_early=False,
2097 force_shadow_repo_refresh=False):
2220 force_shadow_repo_refresh=False):
2098 _ = translator
2221 _ = translator
2099 merge_check = cls()
2222 merge_check = cls()
2100
2223
2101 # title has WIP:
2224 # title has WIP:
2102 if pull_request.work_in_progress:
2225 if pull_request.work_in_progress:
2103 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2226 log.debug("MergeCheck: cannot merge, title has wip: marker.")
2104
2227
2105 msg = _('WIP marker in title prevents from accidental merge.')
2228 msg = _('WIP marker in title prevents from accidental merge.')
2106 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2229 merge_check.push_error('error', msg, cls.WIP_CHECK, pull_request.title)
2107 if fail_early:
2230 if fail_early:
2108 return merge_check
2231 return merge_check
2109
2232
2110 # permissions to merge
2233 # permissions to merge
2111 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2234 user_allowed_to_merge = PullRequestModel().check_user_merge(pull_request, auth_user)
2112 if not user_allowed_to_merge:
2235 if not user_allowed_to_merge:
2113 log.debug("MergeCheck: cannot merge, approval is pending.")
2236 log.debug("MergeCheck: cannot merge, approval is pending.")
2114
2237
2115 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2238 msg = _('User `{}` not allowed to perform merge.').format(auth_user.username)
2116 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2239 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2117 if fail_early:
2240 if fail_early:
2118 return merge_check
2241 return merge_check
2119
2242
2120 # permission to merge into the target branch
2243 # permission to merge into the target branch
2121 target_commit_id = pull_request.target_ref_parts.commit_id
2244 target_commit_id = pull_request.target_ref_parts.commit_id
2122 if pull_request.target_ref_parts.type == 'branch':
2245 if pull_request.target_ref_parts.type == 'branch':
2123 branch_name = pull_request.target_ref_parts.name
2246 branch_name = pull_request.target_ref_parts.name
2124 else:
2247 else:
2125 # for mercurial we can always figure out the branch from the commit
2248 # for mercurial we can always figure out the branch from the commit
2126 # in case of bookmark
2249 # in case of bookmark
2127 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2250 target_commit = pull_request.target_repo.get_commit(target_commit_id)
2128 branch_name = target_commit.branch
2251 branch_name = target_commit.branch
2129
2252
2130 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2253 rule, branch_perm = auth_user.get_rule_and_branch_permission(
2131 pull_request.target_repo.repo_name, branch_name)
2254 pull_request.target_repo.repo_name, branch_name)
2132 if branch_perm and branch_perm == 'branch.none':
2255 if branch_perm and branch_perm == 'branch.none':
2133 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2256 msg = _('Target branch `{}` changes rejected by rule {}.').format(
2134 branch_name, rule)
2257 branch_name, rule)
2135 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2258 merge_check.push_error('error', msg, cls.PERM_CHECK, auth_user.username)
2136 if fail_early:
2259 if fail_early:
2137 return merge_check
2260 return merge_check
2138
2261
2139 # review status, must be always present
2262 # review status, must be always present
2140 review_status = pull_request.calculated_review_status()
2263 review_status = pull_request.calculated_review_status()
2141 merge_check.review_status = review_status
2264 merge_check.review_status = review_status
2142 merge_check.reviewers_count = pull_request.reviewers_count
2265 merge_check.reviewers_count = pull_request.reviewers_count
2143 merge_check.observers_count = pull_request.observers_count
2266 merge_check.observers_count = pull_request.observers_count
2144
2267
2145 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2268 status_approved = review_status == ChangesetStatus.STATUS_APPROVED
2146 if not status_approved and merge_check.reviewers_count:
2269 if not status_approved and merge_check.reviewers_count:
2147 log.debug("MergeCheck: cannot merge, approval is pending.")
2270 log.debug("MergeCheck: cannot merge, approval is pending.")
2148 msg = _('Pull request reviewer approval is pending.')
2271 msg = _('Pull request reviewer approval is pending.')
2149
2272
2150 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2273 merge_check.push_error('warning', msg, cls.REVIEW_CHECK, review_status)
2151
2274
2152 if fail_early:
2275 if fail_early:
2153 return merge_check
2276 return merge_check
2154
2277
2155 # left over TODOs
2278 # left over TODOs
2156 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2279 todos = CommentsModel().get_pull_request_unresolved_todos(pull_request)
2157 if todos:
2280 if todos:
2158 log.debug("MergeCheck: cannot merge, {} "
2281 log.debug("MergeCheck: cannot merge, {} "
2159 "unresolved TODOs left.".format(len(todos)))
2282 "unresolved TODOs left.".format(len(todos)))
2160
2283
2161 if len(todos) == 1:
2284 if len(todos) == 1:
2162 msg = _('Cannot merge, {} TODO still not resolved.').format(
2285 msg = _('Cannot merge, {} TODO still not resolved.').format(
2163 len(todos))
2286 len(todos))
2164 else:
2287 else:
2165 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2288 msg = _('Cannot merge, {} TODOs still not resolved.').format(
2166 len(todos))
2289 len(todos))
2167
2290
2168 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2291 merge_check.push_error('warning', msg, cls.TODO_CHECK, todos)
2169
2292
2170 if fail_early:
2293 if fail_early:
2171 return merge_check
2294 return merge_check
2172
2295
2173 # merge possible, here is the filesystem simulation + shadow repo
2296 # merge possible, here is the filesystem simulation + shadow repo
2174 merge_response, merge_status, msg = PullRequestModel().merge_status(
2297 merge_response, merge_status, msg = PullRequestModel().merge_status(
2175 pull_request, translator=translator,
2298 pull_request, translator=translator,
2176 force_shadow_repo_refresh=force_shadow_repo_refresh)
2299 force_shadow_repo_refresh=force_shadow_repo_refresh)
2177
2300
2178 merge_check.merge_possible = merge_status
2301 merge_check.merge_possible = merge_status
2179 merge_check.merge_msg = msg
2302 merge_check.merge_msg = msg
2180 merge_check.merge_response = merge_response
2303 merge_check.merge_response = merge_response
2181
2304
2182 source_ref_id = pull_request.source_ref_parts.commit_id
2305 source_ref_id = pull_request.source_ref_parts.commit_id
2183 target_ref_id = pull_request.target_ref_parts.commit_id
2306 target_ref_id = pull_request.target_ref_parts.commit_id
2184
2307
2185 try:
2308 try:
2186 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2309 source_commit, target_commit = PullRequestModel().get_flow_commits(pull_request)
2187 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2310 merge_check.source_commit.changed = source_ref_id != source_commit.raw_id
2188 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2311 merge_check.source_commit.ref_spec = pull_request.source_ref_parts
2189 merge_check.source_commit.current_raw_id = source_commit.raw_id
2312 merge_check.source_commit.current_raw_id = source_commit.raw_id
2190 merge_check.source_commit.previous_raw_id = source_ref_id
2313 merge_check.source_commit.previous_raw_id = source_ref_id
2191
2314
2192 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2315 merge_check.target_commit.changed = target_ref_id != target_commit.raw_id
2193 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2316 merge_check.target_commit.ref_spec = pull_request.target_ref_parts
2194 merge_check.target_commit.current_raw_id = target_commit.raw_id
2317 merge_check.target_commit.current_raw_id = target_commit.raw_id
2195 merge_check.target_commit.previous_raw_id = target_ref_id
2318 merge_check.target_commit.previous_raw_id = target_ref_id
2196 except (SourceRefMissing, TargetRefMissing):
2319 except (SourceRefMissing, TargetRefMissing):
2197 pass
2320 pass
2198
2321
2199 if not merge_status:
2322 if not merge_status:
2200 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2323 log.debug("MergeCheck: cannot merge, pull request merge not possible.")
2201 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2324 merge_check.push_error('warning', msg, cls.MERGE_CHECK, None)
2202
2325
2203 if fail_early:
2326 if fail_early:
2204 return merge_check
2327 return merge_check
2205
2328
2206 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2329 log.debug('MergeCheck: is failed: %s', merge_check.failed)
2207 return merge_check
2330 return merge_check
2208
2331
2209 @classmethod
2332 @classmethod
2210 def get_merge_conditions(cls, pull_request, translator):
2333 def get_merge_conditions(cls, pull_request, translator):
2211 _ = translator
2334 _ = translator
2212 merge_details = {}
2335 merge_details = {}
2213
2336
2214 model = PullRequestModel()
2337 model = PullRequestModel()
2215 use_rebase = model._use_rebase_for_merging(pull_request)
2338 use_rebase = model._use_rebase_for_merging(pull_request)
2216
2339
2217 if use_rebase:
2340 if use_rebase:
2218 merge_details['merge_strategy'] = dict(
2341 merge_details['merge_strategy'] = dict(
2219 details={},
2342 details={},
2220 message=_('Merge strategy: rebase')
2343 message=_('Merge strategy: rebase')
2221 )
2344 )
2222 else:
2345 else:
2223 merge_details['merge_strategy'] = dict(
2346 merge_details['merge_strategy'] = dict(
2224 details={},
2347 details={},
2225 message=_('Merge strategy: explicit merge commit')
2348 message=_('Merge strategy: explicit merge commit')
2226 )
2349 )
2227
2350
2228 close_branch = model._close_branch_before_merging(pull_request)
2351 close_branch = model._close_branch_before_merging(pull_request)
2229 if close_branch:
2352 if close_branch:
2230 repo_type = pull_request.target_repo.repo_type
2353 repo_type = pull_request.target_repo.repo_type
2231 close_msg = ''
2354 close_msg = ''
2232 if repo_type == 'hg':
2355 if repo_type == 'hg':
2233 close_msg = _('Source branch will be closed before the merge.')
2356 close_msg = _('Source branch will be closed before the merge.')
2234 elif repo_type == 'git':
2357 elif repo_type == 'git':
2235 close_msg = _('Source branch will be deleted after the merge.')
2358 close_msg = _('Source branch will be deleted after the merge.')
2236
2359
2237 merge_details['close_branch'] = dict(
2360 merge_details['close_branch'] = dict(
2238 details={},
2361 details={},
2239 message=close_msg
2362 message=close_msg
2240 )
2363 )
2241
2364
2242 return merge_details
2365 return merge_details
2243
2366
2244
2367
2245 ChangeTuple = collections.namedtuple(
2368 ChangeTuple = collections.namedtuple(
2246 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2369 'ChangeTuple', ['added', 'common', 'removed', 'total'])
2247
2370
2248 FileChangeTuple = collections.namedtuple(
2371 FileChangeTuple = collections.namedtuple(
2249 'FileChangeTuple', ['added', 'modified', 'removed'])
2372 'FileChangeTuple', ['added', 'modified', 'removed'])
@@ -1,630 +1,630 b''
1
1
2
2
3 //BUTTONS
3 //BUTTONS
4 button,
4 button,
5 .btn,
5 .btn,
6 input[type="button"] {
6 input[type="button"] {
7 -webkit-appearance: none;
7 -webkit-appearance: none;
8 display: inline-block;
8 display: inline-block;
9 margin: 0 @padding/3 0 0;
9 margin: 0 @padding/3 0 0;
10 padding: @button-padding;
10 padding: @button-padding;
11 text-align: center;
11 text-align: center;
12 font-size: @basefontsize;
12 font-size: @basefontsize;
13 line-height: 1em;
13 line-height: 1em;
14 font-family: @text-light;
14 font-family: @text-light;
15 text-decoration: none;
15 text-decoration: none;
16 text-shadow: none;
16 text-shadow: none;
17 color: @grey2;
17 color: @grey2;
18 background-color: white;
18 background-color: white;
19 background-image: none;
19 background-image: none;
20 border: none;
20 border: none;
21 .border ( @border-thickness-buttons, @grey5 );
21 .border ( @border-thickness-buttons, @grey5 );
22 .border-radius (@border-radius);
22 .border-radius (@border-radius);
23 cursor: pointer;
23 cursor: pointer;
24 white-space: nowrap;
24 white-space: nowrap;
25 -webkit-transition: background .3s,color .3s;
25 -webkit-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
26 -moz-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
27 -o-transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
28 transition: background .3s,color .3s;
29 box-shadow: @button-shadow;
29 box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
30 -webkit-box-shadow: @button-shadow;
31
31
32
32
33
33
34 a {
34 a {
35 display: block;
35 display: block;
36 margin: 0;
36 margin: 0;
37 padding: 0;
37 padding: 0;
38 color: inherit;
38 color: inherit;
39 text-decoration: none;
39 text-decoration: none;
40
40
41 &:hover {
41 &:hover {
42 text-decoration: none;
42 text-decoration: none;
43 }
43 }
44 }
44 }
45
45
46 &:focus,
46 &:focus,
47 &:active {
47 &:active {
48 outline:none;
48 outline:none;
49 }
49 }
50
50
51 &:hover {
51 &:hover {
52 color: @rcdarkblue;
52 color: @rcdarkblue;
53 background-color: @grey6;
53 background-color: @grey6;
54
54
55 }
55 }
56
56
57 &.btn-active {
57 &.btn-active {
58 color: @rcdarkblue;
58 color: @rcdarkblue;
59 background-color: @grey6;
59 background-color: @grey6;
60 }
60 }
61
61
62 .icon-remove {
62 .icon-remove {
63 display: none;
63 display: none;
64 }
64 }
65
65
66 //disabled buttons
66 //disabled buttons
67 //last; overrides any other styles
67 //last; overrides any other styles
68 &:disabled {
68 &:disabled {
69 opacity: .7;
69 opacity: .7;
70 cursor: auto;
70 cursor: auto;
71 background-color: white;
71 background-color: white;
72 color: @grey4;
72 color: @grey4;
73 text-shadow: none;
73 text-shadow: none;
74 }
74 }
75
75
76 &.no-margin {
76 &.no-margin {
77 margin: 0 0 0 0;
77 margin: 0 0 0 0;
78 }
78 }
79
79
80
80
81
81
82 }
82 }
83
83
84
84
85 .btn-default {
85 .btn-default {
86 border: @border-thickness solid @grey5;
86 border: @border-thickness solid @grey5;
87 background-image: none;
87 background-image: none;
88 color: @grey2;
88 color: @grey2;
89
89
90 a {
90 a {
91 color: @grey2;
91 color: @grey2;
92 }
92 }
93
93
94 &:hover,
94 &:hover,
95 &.active {
95 &.active {
96 color: @rcdarkblue;
96 color: @rcdarkblue;
97 background-color: @white;
97 background-color: @white;
98 .border ( @border-thickness, @grey4 );
98 .border ( @border-thickness, @grey4 );
99
99
100 a {
100 a {
101 color: @grey2;
101 color: @grey2;
102 }
102 }
103 }
103 }
104 &:disabled {
104 &:disabled {
105 .border ( @border-thickness-buttons, @grey5 );
105 .border ( @border-thickness-buttons, @grey5 );
106 background-color: transparent;
106 background-color: transparent;
107 }
107 }
108 &.btn-active {
108 &.btn-active {
109 color: @rcdarkblue;
109 color: @rcdarkblue;
110 background-color: @white;
110 background-color: @white;
111 .border ( @border-thickness, @rcdarkblue );
111 .border ( @border-thickness, @rcdarkblue );
112 }
112 }
113 }
113 }
114
114
115 .btn-primary,
115 .btn-primary,
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
116 .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */
117 .btn-success {
117 .btn-success {
118 .border ( @border-thickness, @rcblue );
118 .border ( @border-thickness, @rcblue );
119 background-color: @rcblue;
119 background-color: @rcblue;
120 color: white;
120 color: white;
121
121
122 a {
122 a {
123 color: white;
123 color: white;
124 }
124 }
125
125
126 &:hover,
126 &:hover,
127 &.active {
127 &.active {
128 .border ( @border-thickness, @rcdarkblue );
128 .border ( @border-thickness, @rcdarkblue );
129 color: white;
129 color: white;
130 background-color: @rcdarkblue;
130 background-color: @rcdarkblue;
131
131
132 a {
132 a {
133 color: white;
133 color: white;
134 }
134 }
135 }
135 }
136 &:disabled {
136 &:disabled {
137 background-color: @rcblue;
137 background-color: @rcblue;
138 }
138 }
139 }
139 }
140
140
141 .btn-secondary {
141 .btn-secondary {
142 &:extend(.btn-default);
142 &:extend(.btn-default);
143
143
144 background-color: white;
144 background-color: white;
145
145
146 &:focus {
146 &:focus {
147 outline: 0;
147 outline: 0;
148 }
148 }
149
149
150 &:hover {
150 &:hover {
151 &:extend(.btn-default:hover);
151 &:extend(.btn-default:hover);
152 }
152 }
153
153
154 &.btn-link {
154 &.btn-link {
155 &:extend(.btn-link);
155 &:extend(.btn-link);
156 color: @rcblue;
156 color: @rcblue;
157 }
157 }
158
158
159 &:disabled {
159 &:disabled {
160 color: @rcblue;
160 color: @rcblue;
161 background-color: white;
161 background-color: white;
162 }
162 }
163 }
163 }
164
164
165 .btn-danger,
165 .btn-danger,
166 .revoke_perm,
166 .revoke_perm,
167 .btn-x,
167 .btn-x,
168 .form .action_button.btn-x {
168 .form .action_button.btn-x {
169 .border ( @border-thickness, @alert2 );
169 .border ( @border-thickness, @alert2 );
170 background-color: white;
170 background-color: white;
171 color: @alert2;
171 color: @alert2;
172
172
173 a {
173 a {
174 color: @alert2;
174 color: @alert2;
175 }
175 }
176
176
177 &:hover,
177 &:hover,
178 &.active {
178 &.active {
179 .border ( @border-thickness, @alert2 );
179 .border ( @border-thickness, @alert2 );
180 color: white;
180 color: white;
181 background-color: @alert2;
181 background-color: @alert2;
182
182
183 a {
183 a {
184 color: white;
184 color: white;
185 }
185 }
186 }
186 }
187
187
188 i {
188 i {
189 display:none;
189 display:none;
190 }
190 }
191
191
192 &:disabled {
192 &:disabled {
193 background-color: white;
193 background-color: white;
194 color: @alert2;
194 color: @alert2;
195 }
195 }
196 }
196 }
197
197
198 .btn-warning {
198 .btn-warning {
199 .border ( @border-thickness, @alert3 );
199 .border ( @border-thickness, @alert3 );
200 background-color: white;
200 background-color: white;
201 color: @alert3;
201 color: @alert3;
202
202
203 a {
203 a {
204 color: @alert3;
204 color: @alert3;
205 }
205 }
206
206
207 &:hover,
207 &:hover,
208 &.active {
208 &.active {
209 .border ( @border-thickness, @alert3 );
209 .border ( @border-thickness, @alert3 );
210 color: white;
210 color: white;
211 background-color: @alert3;
211 background-color: @alert3;
212
212
213 a {
213 a {
214 color: white;
214 color: white;
215 }
215 }
216 }
216 }
217
217
218 i {
218 i {
219 display:none;
219 display:none;
220 }
220 }
221
221
222 &:disabled {
222 &:disabled {
223 background-color: white;
223 background-color: white;
224 color: @alert3;
224 color: @alert3;
225 }
225 }
226 }
226 }
227
227
228
228
229 .btn-approved-status {
229 .btn-approved-status {
230 .border ( @border-thickness, @alert1 );
230 .border ( @border-thickness, @alert1 );
231 background-color: white;
231 background-color: white;
232 color: @alert1;
232 color: @alert1;
233
233
234 }
234 }
235
235
236 .btn-rejected-status {
236 .btn-rejected-status {
237 .border ( @border-thickness, @alert2 );
237 .border ( @border-thickness, @alert2 );
238 background-color: white;
238 background-color: white;
239 color: @alert2;
239 color: @alert2;
240 }
240 }
241
241
242 .btn-sm,
242 .btn-sm,
243 .btn-mini,
243 .btn-mini,
244 .field-sm .btn {
244 .field-sm .btn {
245 padding: @padding/3;
245 padding: @padding/3;
246 }
246 }
247
247
248 .btn-xs {
248 .btn-xs {
249 padding: @padding/4;
249 padding: @padding/4;
250 }
250 }
251
251
252 .btn-lg {
252 .btn-lg {
253 padding: @padding * 1.2;
253 padding: @padding * 1.2;
254 }
254 }
255
255
256 .btn-group {
256 .btn-group {
257 display: inline-block;
257 display: inline-block;
258 .btn {
258 .btn {
259 float: left;
259 float: left;
260 margin: 0 0 0 0;
260 margin: 0 0 0 0;
261 // first item
261 // first item
262 &:first-of-type:not(:last-of-type) {
262 &:first-of-type:not(:last-of-type) {
263 border-radius: @border-radius 0 0 @border-radius;
263 border-radius: @border-radius 0 0 @border-radius;
264
264
265 }
265 }
266 // 2nd, if only 2 elements are there
266 // 2nd, if only 2 elements are there
267 &:nth-of-type(2) {
267 &:nth-of-type(2) {
268 border-left-width: 0;
268 border-left-width: 0;
269 }
269 }
270 // middle elements
270 // middle elements
271 &:not(:first-of-type):not(:last-of-type) {
271 &:not(:first-of-type):not(:last-of-type) {
272 border-radius: 0;
272 border-radius: 0;
273 border-left-width: 0;
273 border-left-width: 0;
274 border-right-width: 0;
274 border-right-width: 0;
275 }
275 }
276 // last item
276 // last item
277 &:last-of-type:not(:first-of-type) {
277 &:last-of-type:not(:first-of-type) {
278 border-radius: 0 @border-radius @border-radius 0;
278 border-radius: 0 @border-radius @border-radius 0;
279 }
279 }
280
280
281 &:only-child {
281 &:only-child {
282 border-radius: @border-radius;
282 border-radius: @border-radius;
283 }
283 }
284 }
284 }
285
285
286 }
286 }
287
287
288
288
289 .btn-group-actions {
289 .btn-group-actions {
290 position: relative;
290 position: relative;
291 z-index: 50;
291 z-index: 50;
292
292
293 &:not(.open) .btn-action-switcher-container {
293 &:not(.open) .btn-action-switcher-container {
294 display: none;
294 display: none;
295 }
295 }
296
296
297 .btn-more-option {
297 .btn-more-option {
298 margin-left: -1px;
298 margin-left: -1px;
299 padding-left: 2px;
299 padding-left: 2px;
300 padding-right: 2px;
300 padding-right: 2px;
301 }
301 }
302 }
302 }
303
303
304
304
305 .btn-action-switcher-container {
305 .btn-action-switcher-container {
306 position: absolute;
306 position: absolute;
307 top: 100%;
307 top: 100%;
308
308
309 &.left-align {
309 &.left-align {
310 left: 0;
310 left: 0;
311 }
311 }
312 &.right-align {
312 &.right-align {
313 right: 0;
313 right: 0;
314 }
314 }
315
315
316 }
316 }
317
317
318 .btn-action-switcher {
318 .btn-action-switcher {
319 display: block;
319 display: block;
320 position: relative;
320 position: relative;
321 z-index: 300;
321 z-index: 300;
322 max-width: 600px;
322 max-width: 600px;
323 margin-top: 4px;
323 margin-top: 4px;
324 margin-bottom: 24px;
324 margin-bottom: 24px;
325 font-size: 14px;
325 font-size: 14px;
326 font-weight: 400;
326 font-weight: 400;
327 padding: 8px 0;
327 padding: 8px 0;
328 background-color: #fff;
328 background-color: #fff;
329 border: 1px solid @grey4;
329 border: 1px solid @grey4;
330 border-radius: 3px;
330 border-radius: 3px;
331 box-shadow: @dropdown-shadow;
331 box-shadow: @dropdown-shadow;
332 overflow: auto;
332 overflow: auto;
333
333
334 li {
334 li {
335 display: block;
335 display: block;
336 text-align: left;
336 text-align: left;
337 list-style: none;
337 list-style: none;
338 padding: 5px 10px;
338 padding: 5px 10px;
339 }
339 }
340
340
341 li .action-help-block {
341 li .action-help-block {
342 font-size: 10px;
342 font-size: 10px;
343 line-height: normal;
343 line-height: normal;
344 color: @grey4;
344 color: @grey4;
345 }
345 }
346
346
347 }
347 }
348
348
349 .btn-link {
349 .btn-link {
350 background: transparent;
350 background: transparent;
351 border: none;
351 border: none;
352 padding: 0;
352 padding: 0;
353 color: @rcblue;
353 color: @rcblue;
354
354
355 &:hover {
355 &:hover {
356 background: transparent;
356 background: transparent;
357 border: none;
357 border: none;
358 color: @rcdarkblue;
358 color: @rcdarkblue;
359 }
359 }
360
360
361 //disabled buttons
361 //disabled buttons
362 //last; overrides any other styles
362 //last; overrides any other styles
363 &:disabled {
363 &:disabled {
364 opacity: .7;
364 opacity: .7;
365 cursor: auto;
365 cursor: auto;
366 background-color: white;
366 background-color: white;
367 color: @grey4;
367 color: @grey4;
368 text-shadow: none;
368 text-shadow: none;
369 }
369 }
370
370
371 // TODO: johbo: Check if we can avoid this, indicates that the structure
371 // TODO: johbo: Check if we can avoid this, indicates that the structure
372 // is not yet good.
372 // is not yet good.
373 // lisa: The button CSS reflects the button HTML; both need a cleanup.
373 // lisa: The button CSS reflects the button HTML; both need a cleanup.
374 &.btn-danger {
374 &.btn-danger {
375 color: @alert2;
375 color: @alert2;
376
376
377 &:hover {
377 &:hover {
378 color: darken(@alert2, 30%);
378 color: darken(@alert2, 30%);
379 }
379 }
380
380
381 &:disabled {
381 &:disabled {
382 color: @alert2;
382 color: @alert2;
383 }
383 }
384 }
384 }
385 }
385 }
386
386
387 .btn-social {
387 .btn-social {
388 &:extend(.btn-default);
388 &:extend(.btn-default);
389 margin: 5px 5px 5px 0px;
389 margin: 5px 5px 5px 0px;
390 min-width: 160px;
390 min-width: 160px;
391 }
391 }
392
392
393 // TODO: johbo: check these exceptions
393 // TODO: johbo: check these exceptions
394
394
395 .links {
395 .links {
396
396
397 .btn + .btn {
397 .btn + .btn {
398 margin-top: @padding;
398 margin-top: @padding;
399 }
399 }
400 }
400 }
401
401
402
402
403 .action_button {
403 .action_button {
404 display:inline;
404 display:inline;
405 margin: 0;
405 margin: 0;
406 padding: 0 1em 0 0;
406 padding: 0 1em 0 0;
407 font-size: inherit;
407 font-size: inherit;
408 color: @rcblue;
408 color: @rcblue;
409 border: none;
409 border: none;
410 border-radius: 0;
410 border-radius: 0;
411 background-color: transparent;
411 background-color: transparent;
412
412
413 &.last-item {
413 &.last-item {
414 border: none;
414 border: none;
415 padding: 0 0 0 0;
415 padding: 0 0 0 0;
416 }
416 }
417
417
418 &:last-child {
418 &:last-child {
419 border: none;
419 border: none;
420 padding: 0 0 0 0;
420 padding: 0 0 0 0;
421 }
421 }
422
422
423 &:hover {
423 &:hover {
424 color: @rcdarkblue;
424 color: @rcdarkblue;
425 background-color: transparent;
425 background-color: transparent;
426 border: none;
426 border: none;
427 }
427 }
428 .noselect
428 .noselect
429 }
429 }
430
430
431 .grid_delete {
431 .grid_delete {
432 .action_button {
432 .action_button {
433 border: none;
433 border: none;
434 }
434 }
435 }
435 }
436
436
437 input[type="submit"].btn-draft {
437 input[type="submit"].btn-draft {
438 .border ( @border-thickness, @rcblue );
438 .border ( @border-thickness, @rcblue );
439 background-color: white;
439 background-color: white;
440 color: @rcblue;
440 color: @rcblue;
441
441
442 a {
442 a {
443 color: @rcblue;
443 color: @rcblue;
444 }
444 }
445
445
446 &:hover,
446 &:hover,
447 &.active {
447 &.active {
448 .border ( @border-thickness, @rcdarkblue );
448 .border ( @border-thickness, @rcdarkblue );
449 background-color: white;
449 background-color: white;
450 color: @rcdarkblue;
450 color: @rcdarkblue;
451
451
452 a {
452 a {
453 color: @rcdarkblue;
453 color: @rcdarkblue;
454 }
454 }
455 }
455 }
456
456
457 &:disabled {
457 &:disabled {
458 background-color: white;
458 background-color: white;
459 color: @rcblue;
459 color: @rcblue;
460 }
460 }
461 }
461 }
462
462
463 input[type="submit"].btn-warning {
463 input[type="submit"].btn-warning {
464 &:extend(.btn-warning);
464 &:extend(.btn-warning);
465
465
466 &:focus {
466 &:focus {
467 outline: 0;
467 outline: 0;
468 }
468 }
469
469
470 &:hover {
470 &:hover {
471 &:extend(.btn-warning:hover);
471 &:extend(.btn-warning:hover);
472 }
472 }
473
473
474 &.btn-link {
474 &.btn-link {
475 &:extend(.btn-link);
475 &:extend(.btn-link);
476 color: @alert3;
476 color: @alert3;
477
477
478 &:disabled {
478 &:disabled {
479 color: @alert3;
479 color: @alert3;
480 background-color: transparent;
480 background-color: transparent;
481 }
481 }
482 }
482 }
483
483
484 &:disabled {
484 &:disabled {
485 .border ( @border-thickness-buttons, @alert3 );
485 .border ( @border-thickness-buttons, @alert3 );
486 background-color: white;
486 background-color: white;
487 color: @alert3;
487 color: @alert3;
488 opacity: 0.5;
488 opacity: 0.5;
489 }
489 }
490 }
490 }
491
491
492
492
493
493
494 // TODO: johbo: Form button tweaks, check if we can use the classes instead
494 // TODO: johbo: Form button tweaks, check if we can use the classes instead
495 input[type="submit"] {
495 input[type="submit"] {
496 &:extend(.btn-primary);
496 &:extend(.btn-primary);
497
497
498 &:focus {
498 &:focus {
499 outline: 0;
499 outline: 0;
500 }
500 }
501
501
502 &:hover {
502 &:hover {
503 &:extend(.btn-primary:hover);
503 &:extend(.btn-primary:hover);
504 }
504 }
505
505
506 &.btn-link {
506 &.btn-link {
507 &:extend(.btn-link);
507 &:extend(.btn-link);
508 color: @rcblue;
508 color: @rcblue;
509
509
510 &:disabled {
510 &:disabled {
511 color: @rcblue;
511 color: @rcblue;
512 background-color: transparent;
512 background-color: transparent;
513 }
513 }
514 }
514 }
515
515
516 &:disabled {
516 &:disabled {
517 .border ( @border-thickness-buttons, @rcblue );
517 .border ( @border-thickness-buttons, @rcblue );
518 background-color: @rcblue;
518 background-color: @rcblue;
519 color: white;
519 color: white;
520 opacity: 0.5;
520 opacity: 0.5;
521 }
521 }
522 }
522 }
523
523
524 input[type="reset"] {
524 input[type="reset"] {
525 &:extend(.btn-default);
525 &:extend(.btn-default);
526
526
527 // TODO: johbo: Check if this tweak can be avoided.
527 // TODO: johbo: Check if this tweak can be avoided.
528 background: transparent;
528 background: transparent;
529
529
530 &:focus {
530 &:focus {
531 outline: 0;
531 outline: 0;
532 }
532 }
533
533
534 &:hover {
534 &:hover {
535 &:extend(.btn-default:hover);
535 &:extend(.btn-default:hover);
536 }
536 }
537
537
538 &.btn-link {
538 &.btn-link {
539 &:extend(.btn-link);
539 &:extend(.btn-link);
540 color: @rcblue;
540 color: @rcblue;
541
541
542 &:disabled {
542 &:disabled {
543 border: none;
543 border: none;
544 }
544 }
545 }
545 }
546
546
547 &:disabled {
547 &:disabled {
548 .border ( @border-thickness-buttons, @rcblue );
548 .border ( @border-thickness-buttons, @rcblue );
549 background-color: white;
549 background-color: white;
550 color: @rcblue;
550 color: @rcblue;
551 }
551 }
552 }
552 }
553
553
554 input[type="submit"],
554 input[type="submit"],
555 input[type="reset"] {
555 input[type="reset"] {
556 &.btn-danger {
556 &.btn-danger {
557 &:extend(.btn-danger);
557 &:extend(.btn-danger);
558
558
559 &:focus {
559 &:focus {
560 outline: 0;
560 outline: 0;
561 }
561 }
562
562
563 &:hover {
563 &:hover {
564 &:extend(.btn-danger:hover);
564 &:extend(.btn-danger:hover);
565 }
565 }
566
566
567 &.btn-link {
567 &.btn-link {
568 &:extend(.btn-link);
568 &:extend(.btn-link);
569 color: @alert2;
569 color: @alert2;
570
570
571 &:hover {
571 &:hover {
572 color: darken(@alert2,30%);
572 color: darken(@alert2,30%);
573 }
573 }
574 }
574 }
575
575
576 &:disabled {
576 &:disabled {
577 color: @alert2;
577 color: @alert2;
578 background-color: white;
578 background-color: white;
579 }
579 }
580 }
580 }
581 &.btn-danger-action {
581 &.btn-danger-action {
582 .border ( @border-thickness, @alert2 );
582 .border ( @border-thickness, @alert2 );
583 background-color: @alert2;
583 background-color: @alert2;
584 color: white;
584 color: white;
585
585
586 a {
586 a {
587 color: white;
587 color: white;
588 }
588 }
589
589
590 &:hover {
590 &:hover {
591 background-color: darken(@alert2,20%);
591 background-color: darken(@alert2,20%);
592 }
592 }
593
593
594 &.active {
594 &.active {
595 .border ( @border-thickness, @alert2 );
595 .border ( @border-thickness, @alert2 );
596 color: white;
596 color: white;
597 background-color: @alert2;
597 background-color: @alert2;
598
598
599 a {
599 a {
600 color: white;
600 color: white;
601 }
601 }
602 }
602 }
603
603
604 &:disabled {
604 &:disabled {
605 background-color: white;
605 background-color: white;
606 color: @alert2;
606 color: @alert2;
607 }
607 }
608 }
608 }
609 }
609 }
610
610
611
611
612 .button-links {
612 .button-links {
613 float: left;
613 float: left;
614 display: inline;
614 display: inline;
615 margin: 0;
615 margin: 0;
616 padding-left: 0;
616 padding-left: 0;
617 list-style: none;
617 list-style: none;
618 text-align: right;
618 text-align: right;
619
619
620 li {
620 li {
621
621 list-style: none;
622
622 text-align: right;
623 display: inline-block;
623 }
624 }
624
625
625 li.active {
626 a.active {
626 background-color: @grey6;
627 border: 2px solid @rcblue;
627 .border ( @border-thickness, @grey4 );
628 }
628 }
629
629
630 }
630 }
@@ -1,85 +1,85 b''
1 // See panels-bootstrap.less
1 // See panels-bootstrap.less
2 // These provide overrides for custom styling of Bootstrap panels
2 // These provide overrides for custom styling of Bootstrap panels
3
3
4 .panel {
4 .panel {
5 &:extend(.clearfix);
5 &:extend(.clearfix);
6
6
7 width: 100%;
7 width: 100%;
8 margin: 0 0 25px 0;
8 margin: 0 0 25px 0;
9 .border-radius(@border-radius);
9 .border-radius(@border-radius);
10 .box-shadow(none);
10 .box-shadow(none);
11
11
12 .permalink {
12 .permalink {
13 visibility: hidden;
13 visibility: hidden;
14 }
14 }
15
15
16 &:hover .permalink {
16 &:hover .permalink {
17 visibility: visible;
17 visibility: visible;
18 color: @rcblue;
18 color: @rcblue;
19 }
19 }
20
20
21 .panel-heading {
21 .panel-heading {
22 position: relative;
22 position: relative;
23 min-height: 1em;
23 min-height: 1em;
24 padding: @padding @panel-padding;
24 padding: @padding @panel-padding;
25 border-bottom: none;
25 border-bottom: none;
26
26
27 .panel-title,
27 .panel-title,
28 h3.panel-title {
28 h3.panel-title {
29 float: left;
29 float: left;
30 padding: 0 @padding 0 0;
30 padding: 0 @padding 0 0;
31 line-height: 1;
31 line-height: 1;
32 font-size: @panel-title;
32 font-size: @panel-title;
33 color: @grey1;
33 color: @grey1;
34 }
34 }
35
35
36 .panel-edit {
36 .panel-edit {
37 float: right;
37 float: right;
38 line-height: 1;
38 line-height: 1;
39 font-size: @panel-title;
39 font-size: @panel-title;
40 }
40 }
41 }
41 }
42
42
43 .panel-body {
43 .panel-body {
44 padding: @panel-padding;
44 padding: @panel-padding;
45
45
46 &.panel-body-min-height {
46 &.panel-body-min-height {
47 min-height: 150px
47 min-height: 200px
48 }
48 }
49 }
49 }
50
50
51 .panel-footer {
51 .panel-footer {
52 background-color: white;
52 background-color: white;
53 padding: .65em @panel-padding .5em;
53 padding: .65em @panel-padding .5em;
54 font-size: @panel-footer;
54 font-size: @panel-footer;
55 color: @text-muted;
55 color: @text-muted;
56 }
56 }
57
57
58 .q_filter_box {
58 .q_filter_box {
59 min-width: 40%;
59 min-width: 40%;
60 }
60 }
61
61
62 // special cases
62 // special cases
63 &.user-profile {
63 &.user-profile {
64 float: left;
64 float: left;
65
65
66 .panel-body {
66 .panel-body {
67 &:extend(.clearfix);
67 &:extend(.clearfix);
68 }
68 }
69 }
69 }
70 }
70 }
71
71
72 .main-content h3.panel-title {
72 .main-content h3.panel-title {
73 font-size: @panel-title;
73 font-size: @panel-title;
74 color: @grey1;
74 color: @grey1;
75 }
75 }
76
76
77 .panel-body-title-text {
77 .panel-body-title-text {
78 margin: 0 0 20px 0;
78 margin: 0 0 20px 0;
79 }
79 }
80
80
81 // play nice with the current form and field css
81 // play nice with the current form and field css
82 .field.panel-default,
82 .field.panel-default,
83 .form.panel-default {
83 .form.panel-default {
84 width: auto;
84 width: auto;
85 } No newline at end of file
85 }
@@ -1,151 +1,164 b''
1 <%namespace name="base" file="/base/base.mako"/>
1 <%namespace name="base" file="/base/base.mako"/>
2
2
3 <div class="panel panel-default">
3 <div class="panel panel-default">
4 <div class="panel-body">
4 <div class="panel-heading">
5 <div style="height: 35px">
5 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
6 <%
6 </div>
7 selected_filter = 'all'
8 if c.closed:
9 selected_filter = 'all_closed'
10 %>
11
7
8 <div class="panel-body panel-body-min-height">
9 <div class="title">
12 <ul class="button-links">
10 <ul class="button-links">
13 <li class="btn ${h.is_active('all', selected_filter)}"><a href="${h.route_path('my_account_pullrequests')}">${_('All')}</a></li>
11 <li><a class="btn ${h.is_active('all', c.selected_filter)}"
14 <li class="btn ${h.is_active('all_closed', selected_filter)}"><a href="${h.route_path('my_account_pullrequests', _query={'pr_show_closed':1})}">${_('All + Closed')}</a></li>
12 href="${h.route_path('my_account_pullrequests', _query={})}">
13 ${_('Open')}
14 </a>
15 </li>
16 <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}"
17 href="${h.route_path('my_account_pullrequests', _query={'closed':1})}">
18 ${_('All + Closed')}
19 </a>
20 </li>
21 <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}"
22 href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}">
23
24 ${_('Awaiting my review')}
25 </a>
26 </li>
15 </ul>
27 </ul>
16
28
17 <div class="grid-quick-filter">
29 <div class="grid-quick-filter">
18 <ul class="grid-filter-box">
30 <ul class="grid-filter-box">
19 <li class="grid-filter-box-icon">
31 <li class="grid-filter-box-icon">
20 <i class="icon-search"></i>
32 <i class="icon-search"></i>
21 </li>
33 </li>
22 <li class="grid-filter-box-input">
34 <li class="grid-filter-box-input">
23 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
35 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter"
36 placeholder="${_('quick filter...')}" value=""/>
24 </li>
37 </li>
25 </ul>
38 </ul>
26 </div>
39 </div>
27 </div>
40 </div>
28 </div>
29 </div>
30
41
31 <div class="panel panel-default">
32 <div class="panel-heading">
33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 </div>
35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
42 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 </div>
43 </div>
38 </div>
44 </div>
39
45
40 <script type="text/javascript">
46 <script type="text/javascript">
41 $(document).ready(function () {
47 $(document).ready(function () {
42
48
43 var $pullRequestListTable = $('#pull_request_list_table');
49 var $pullRequestListTable = $('#pull_request_list_table');
44
50
45 // participating object list
51 // participating object list
46 $pullRequestListTable.DataTable({
52 $pullRequestListTable.DataTable({
47 processing: true,
53 processing: true,
48 serverSide: true,
54 serverSide: true,
49 stateSave: true,
55 stateSave: true,
50 stateDuration: -1,
56 stateDuration: -1,
51 ajax: {
57 ajax: {
52 "url": "${h.route_path('my_account_pullrequests_data')}",
58 "url": "${h.route_path('my_account_pullrequests_data')}",
53 "data": function (d) {
59 "data": function (d) {
54 d.closed = "${c.closed}";
60 d.closed = "${c.closed}";
61 d.awaiting_my_review = "${c.awaiting_my_review}";
55 },
62 },
56 "dataSrc": function (json) {
63 "dataSrc": function (json) {
57 return json.data;
64 return json.data;
58 }
65 }
59 },
66 },
60
67
61 dom: 'rtp',
68 dom: 'rtp',
62 pageLength: ${c.visual.dashboard_items},
69 pageLength: ${c.visual.dashboard_items},
63 order: [[1, "desc"]],
70 order: [[2, "desc"]],
64 columns: [
71 columns: [
65 {
72 {
66 data: {
73 data: {
67 "_": "status",
74 "_": "status",
68 "sort": "status"
75 "sort": "status"
69 }, title: "", className: "td-status", orderable: false
76 }, title: "PR", className: "td-status", orderable: false
77 },
78 {
79 data: {
80 "_": "my_status",
81 "sort": "status"
82 }, title: "You", className: "td-status", orderable: false
70 },
83 },
71 {
84 {
72 data: {
85 data: {
73 "_": "name",
86 "_": "name",
74 "sort": "name_raw"
87 "sort": "name_raw"
75 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
88 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
76 },
89 },
77 {
90 {
78 data: {
91 data: {
79 "_": "title",
92 "_": "title",
80 "sort": "title"
93 "sort": "title"
81 }, title: "${_('Title')}", className: "td-description"
94 }, title: "${_('Title')}", className: "td-description"
82 },
95 },
83 {
96 {
84 data: {
97 data: {
85 "_": "author",
98 "_": "author",
86 "sort": "author_raw"
99 "sort": "author_raw"
87 }, title: "${_('Author')}", className: "td-user", orderable: false
100 }, title: "${_('Author')}", className: "td-user", orderable: false
88 },
101 },
89 {
102 {
90 data: {
103 data: {
91 "_": "comments",
104 "_": "comments",
92 "sort": "comments_raw"
105 "sort": "comments_raw"
93 }, title: "", className: "td-comments", orderable: false
106 }, title: "", className: "td-comments", orderable: false
94 },
107 },
95 {
108 {
96 data: {
109 data: {
97 "_": "updated_on",
110 "_": "updated_on",
98 "sort": "updated_on_raw"
111 "sort": "updated_on_raw"
99 }, title: "${_('Last Update')}", className: "td-time"
112 }, title: "${_('Last Update')}", className: "td-time"
100 },
113 },
101 {
114 {
102 data: {
115 data: {
103 "_": "target_repo",
116 "_": "target_repo",
104 "sort": "target_repo"
117 "sort": "target_repo"
105 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
118 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
106 },
119 },
107 ],
120 ],
108 language: {
121 language: {
109 paginate: DEFAULT_GRID_PAGINATION,
122 paginate: DEFAULT_GRID_PAGINATION,
110 sProcessing: _gettext('loading...'),
123 sProcessing: _gettext('loading...'),
111 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
124 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
112 },
125 },
113 "drawCallback": function (settings, json) {
126 "drawCallback": function (settings, json) {
114 timeagoActivate();
127 timeagoActivate();
115 tooltipActivate();
128 tooltipActivate();
116 },
129 },
117 "createdRow": function (row, data, index) {
130 "createdRow": function (row, data, index) {
118 if (data['closed']) {
131 if (data['closed']) {
119 $(row).addClass('closed');
132 $(row).addClass('closed');
120 }
133 }
121 if (data['owned']) {
134 if (data['owned']) {
122 $(row).addClass('owned');
135 $(row).addClass('owned');
123 }
136 }
124 },
137 },
125 "stateSaveParams": function (settings, data) {
138 "stateSaveParams": function (settings, data) {
126 data.search.search = ""; // Don't save search
139 data.search.search = ""; // Don't save search
127 data.start = 0; // don't save pagination
140 data.start = 0; // don't save pagination
128 }
141 }
129 });
142 });
130 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
143 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
131 $pullRequestListTable.css('opacity', 1);
144 $pullRequestListTable.css('opacity', 1);
132 });
145 });
133
146
134 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
147 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
135 $pullRequestListTable.css('opacity', 0.3);
148 $pullRequestListTable.css('opacity', 0.3);
136 });
149 });
137
150
138
151
139 // filter
152 // filter
140 $('#q_filter').on('keyup',
153 $('#q_filter').on('keyup',
141 $.debounce(250, function () {
154 $.debounce(250, function () {
142 $pullRequestListTable.DataTable().search(
155 $pullRequestListTable.DataTable().search(
143 $('#q_filter').val()
156 $('#q_filter').val()
144 ).draw();
157 ).draw();
145 })
158 })
146 );
159 );
147
160
148 });
161 });
149
162
150
163
151 </script>
164 </script>
@@ -1,146 +1,177 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Pull Requests').format(c.repo_name)}
4 ${_('{} Pull Requests').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()"></%def>
10 <%def name="breadcrumbs_links()"></%def>
11
11
12 <%def name="menu_bar_nav()">
12 <%def name="menu_bar_nav()">
13 ${self.menu_items(active='repositories')}
13 ${self.menu_items(active='repositories')}
14 </%def>
14 </%def>
15
15
16
16
17 <%def name="menu_bar_subnav()">
17 <%def name="menu_bar_subnav()">
18 ${self.repo_menu(active='showpullrequest')}
18 ${self.repo_menu(active='showpullrequest')}
19 </%def>
19 </%def>
20
20
21
21
22 <%def name="main()">
22 <%def name="main()">
23
23
24 <div class="box">
24 <div class="box">
25 <div class="title">
25 <div class="title">
26
26 <ul class="button-links">
27 <ul class="button-links">
27 <li class="btn ${h.is_active('open', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0})}">${_('Opened')}</a></li>
28 <li><a class="btn ${h.is_active('open', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'open':1})}">${_('Open')}</a></li>
28 <li class="btn ${h.is_active('my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Opened by me')}</a></li>
29 <li><a class="btn ${h.is_active('my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'my':1})}">${_('Created by me')}</a></li>
29 <li class="btn ${h.is_active('awaiting', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li><a class="btn ${h.is_active('awaiting', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_review':1})}">${_('Awaiting review')}</a></li>
30 <li class="btn ${h.is_active('awaiting_my', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li><a class="btn ${h.is_active('awaiting_my', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'awaiting_my_review':1})}">${_('Awaiting my review')}</a></li>
31 <li class="btn ${h.is_active('closed', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li><a class="btn ${h.is_active('closed', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':0,'closed':1})}">${_('Closed')}</a></li>
32 <li class="btn ${h.is_active('source', c.active)}"><a href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 <li><a class="btn ${h.is_active('source', c.active)}" href="${h.route_path('pullrequest_show_all',repo_name=c.repo_name, _query={'source':1})}">${_('From this repo')}</a></li>
33 </ul>
34 </ul>
34
35
35 <ul class="links">
36 <ul class="links">
36 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 % if c.rhodecode_user.username != h.DEFAULT_USER:
37 <li>
38 <li>
38 <span>
39 <span>
39 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
40 ${_('Open new Pull Request')}
41 ${_('Open new Pull Request')}
41 </a>
42 </a>
42 </span>
43 </span>
43 </li>
44 </li>
44 % endif
45 % endif
45
46
46 <li>
47 <li>
47 <div class="grid-quick-filter">
48 <div class="grid-quick-filter">
48 <ul class="grid-filter-box">
49 <ul class="grid-filter-box">
49 <li class="grid-filter-box-icon">
50 <li class="grid-filter-box-icon">
50 <i class="icon-search"></i>
51 <i class="icon-search"></i>
51 </li>
52 </li>
52 <li class="grid-filter-box-input">
53 <li class="grid-filter-box-input">
53 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
54 </li>
55 </li>
55 </ul>
56 </ul>
56 </div>
57 </div>
57 </li>
58 </li>
58
59
59 </ul>
60 </ul>
60
61
61 </div>
62 </div>
62
63
63 <div class="main-content-full-width">
64 <div class="main-content-full-width">
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 </div>
66 </div>
66
67
67 </div>
68 </div>
68
69
69 <script type="text/javascript">
70 <script type="text/javascript">
70 $(document).ready(function() {
71 $(document).ready(function() {
71 var $pullRequestListTable = $('#pull_request_list_table');
72 var $pullRequestListTable = $('#pull_request_list_table');
72
73
73 // object list
74 // object list
74 $pullRequestListTable.DataTable({
75 $pullRequestListTable.DataTable({
75 processing: true,
76 processing: true,
76 serverSide: true,
77 serverSide: true,
77 stateSave: true,
78 stateSave: true,
78 stateDuration: -1,
79 stateDuration: -1,
79 ajax: {
80 ajax: {
80 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
81 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
81 "data": function (d) {
82 "data": function (d) {
82 d.source = "${c.source}";
83 d.source = "${c.source}";
83 d.closed = "${c.closed}";
84 d.closed = "${c.closed}";
84 d.my = "${c.my}";
85 d.my = "${c.my}";
85 d.awaiting_review = "${c.awaiting_review}";
86 d.awaiting_review = "${c.awaiting_review}";
86 d.awaiting_my_review = "${c.awaiting_my_review}";
87 d.awaiting_my_review = "${c.awaiting_my_review}";
87 }
88 }
88 },
89 },
89 dom: 'rtp',
90 dom: 'rtp',
90 pageLength: ${c.visual.dashboard_items},
91 pageLength: ${c.visual.dashboard_items},
91 order: [[ 1, "desc" ]],
92 order: [[ 2, "desc" ]],
92 columns: [
93 columns: [
93 { data: {"_": "status",
94 {
94 "sort": "status"}, title: "", className: "td-status", orderable: false},
95 data: {
95 { data: {"_": "name",
96 "_": "status",
96 "sort": "name_raw"}, title: "${_('Id')}", className: "td-componentname", "type": "num" },
97 "sort": "status"
97 { data: {"_": "title",
98 }, title: "PR", className: "td-status", orderable: false
98 "sort": "title"}, title: "${_('Title')}", className: "td-description" },
99 },
99 { data: {"_": "author",
100 {
100 "sort": "author_raw"}, title: "${_('Author')}", className: "td-user", orderable: false },
101 data: {
101 { data: {"_": "comments",
102 "_": "my_status",
102 "sort": "comments_raw"}, title: "", className: "td-comments", orderable: false},
103 "sort": "status"
103 { data: {"_": "updated_on",
104 }, title: "You", className: "td-status", orderable: false
104 "sort": "updated_on_raw"}, title: "${_('Last Update')}", className: "td-time" }
105 },
106 {
107 data: {
108 "_": "name",
109 "sort": "name_raw"
110 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
111 },
112 {
113 data: {
114 "_": "title",
115 "sort": "title"
116 }, title: "${_('Title')}", className: "td-description"
117 },
118 {
119 data: {
120 "_": "author",
121 "sort": "author_raw"
122 }, title: "${_('Author')}", className: "td-user", orderable: false
123 },
124 {
125 data: {
126 "_": "comments",
127 "sort": "comments_raw"
128 }, title: "", className: "td-comments", orderable: false
129 },
130 {
131 data: {
132 "_": "updated_on",
133 "sort": "updated_on_raw"
134 }, title: "${_('Last Update')}", className: "td-time"
135 }
105 ],
136 ],
106 language: {
137 language: {
107 paginate: DEFAULT_GRID_PAGINATION,
138 paginate: DEFAULT_GRID_PAGINATION,
108 sProcessing: _gettext('loading...'),
139 sProcessing: _gettext('loading...'),
109 emptyTable: _gettext("No pull requests available yet.")
140 emptyTable: _gettext("No pull requests available yet.")
110 },
141 },
111 "drawCallback": function( settings, json ) {
142 "drawCallback": function( settings, json ) {
112 timeagoActivate();
143 timeagoActivate();
113 tooltipActivate();
144 tooltipActivate();
114 },
145 },
115 "createdRow": function ( row, data, index ) {
146 "createdRow": function ( row, data, index ) {
116 if (data['closed']) {
147 if (data['closed']) {
117 $(row).addClass('closed');
148 $(row).addClass('closed');
118 }
149 }
119 },
150 },
120 "stateSaveParams": function (settings, data) {
151 "stateSaveParams": function (settings, data) {
121 data.search.search = ""; // Don't save search
152 data.search.search = ""; // Don't save search
122 data.start = 0; // don't save pagination
153 data.start = 0; // don't save pagination
123 }
154 }
124 });
155 });
125
156
126 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
157 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
127 $pullRequestListTable.css('opacity', 1);
158 $pullRequestListTable.css('opacity', 1);
128 });
159 });
129
160
130 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
161 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
131 $pullRequestListTable.css('opacity', 0.3);
162 $pullRequestListTable.css('opacity', 0.3);
132 });
163 });
133
164
134 // filter
165 // filter
135 $('#q_filter').on('keyup',
166 $('#q_filter').on('keyup',
136 $.debounce(250, function() {
167 $.debounce(250, function() {
137 $pullRequestListTable.DataTable().search(
168 $pullRequestListTable.DataTable().search(
138 $('#q_filter').val()
169 $('#q_filter').val()
139 ).draw();
170 ).draw();
140 })
171 })
141 );
172 );
142
173
143 });
174 });
144
175
145 </script>
176 </script>
146 </%def>
177 </%def>
General Comments 1
Under Review
author

Auto status change to "Under Review"

You need to be logged in to leave comments. Login now