##// END OF EJS Templates
pull-requests: added commit flow into pr listing tables
super-admin -
r5170:a77dd674 default
parent child Browse files
Show More
@@ -1,293 +1,297 b''
1 1 # deps, generated via pipdeptree --exclude setuptools,wheel,pipdeptree,pip -f | tr '[:upper:]' '[:lower:]'
2 2
3 3 alembic==1.11.3
4 4 mako==1.2.4
5 5 markupsafe==2.1.2
6 6 sqlalchemy==1.4.49
7 7 greenlet==2.0.2
8 8 typing_extensions==4.7.1
9 9 async-timeout==4.0.2
10 10 babel==2.12.1
11 11 celery==5.3.1
12 12 billiard==4.1.0
13 13 click==8.1.3
14 14 click-didyoumean==0.3.0
15 15 click==8.1.3
16 16 click-plugins==1.1.1
17 17 click==8.1.3
18 18 click-repl==0.2.0
19 19 click==8.1.3
20 20 prompt-toolkit==3.0.38
21 21 wcwidth==0.2.6
22 22 six==1.16.0
23 23 kombu==5.3.1
24 24 amqp==5.1.1
25 25 vine==5.0.0
26 26 vine==5.0.0
27 27 python-dateutil==2.8.2
28 28 six==1.16.0
29 29 tzdata==2023.3
30 30 vine==5.0.0
31 31 channelstream==0.7.1
32 32 gevent==23.7.0
33 33 greenlet==2.0.2
34 34 zope.event==5.0.0
35 35 zope.interface==6.0.0
36 36 itsdangerous==1.1.0
37 37 marshmallow==2.18.0
38 38 pyramid==2.0.2
39 39 hupper==1.12
40 40 plaster==1.1.2
41 41 plaster-pastedeploy==1.0.1
42 42 pastedeploy==3.0.1
43 43 plaster==1.1.2
44 44 translationstring==1.4
45 45 venusian==3.0.0
46 46 webob==1.8.7
47 47 zope.deprecation==5.0.0
48 48 zope.interface==6.0.0
49 49 pyramid-apispec==0.3.3
50 50 apispec==1.3.3
51 51 pyramid-jinja2==2.10
52 52 jinja2==3.1.2
53 53 markupsafe==2.1.2
54 54 markupsafe==2.1.2
55 55 pyramid==2.0.2
56 56 hupper==1.12
57 57 plaster==1.1.2
58 58 plaster-pastedeploy==1.0.1
59 59 pastedeploy==3.0.1
60 60 plaster==1.1.2
61 61 translationstring==1.4
62 62 venusian==3.0.0
63 63 webob==1.8.7
64 64 zope.deprecation==5.0.0
65 65 zope.interface==6.0.0
66 66 zope.deprecation==5.0.0
67 67 python-dateutil==2.8.2
68 68 six==1.16.0
69 69 requests==2.28.2
70 70 certifi==2022.12.7
71 71 charset-normalizer==3.1.0
72 72 idna==3.4
73 73 urllib3==1.26.14
74 74 ws4py==0.5.1
75 75 deform==2.0.15
76 76 chameleon==3.10.2
77 77 colander==2.0
78 78 iso8601==1.1.0
79 79 translationstring==1.4
80 80 iso8601==1.1.0
81 81 peppercorn==0.6
82 82 translationstring==1.4
83 83 zope.deprecation==5.0.0
84 84 diskcache==5.6.1
85 85 docutils==0.19
86 86 dogpile.cache==1.2.2
87 87 decorator==5.1.1
88 88 stevedore==5.0.0
89 89 pbr==5.11.1
90 90 formencode==2.0.1
91 91 six==1.16.0
92 92 gunicorn==21.2.0
93 93 packaging==23.1
94 gevent==23.7.0
95 greenlet==2.0.2
96 zope.event==5.0.0
97 zope.interface==6.0.0
94 98 infrae.cache==1.0.1
95 99 beaker==1.12.1
96 100 repoze.lru==0.7
97 101 ipython==8.14.0
98 102 backcall==0.2.0
99 103 decorator==5.1.1
100 104 jedi==0.19.0
101 105 parso==0.8.3
102 106 matplotlib-inline==0.1.6
103 107 traitlets==5.9.0
104 108 pexpect==4.8.0
105 109 ptyprocess==0.7.0
106 110 pickleshare==0.7.5
107 111 prompt-toolkit==3.0.38
108 112 wcwidth==0.2.6
109 113 pygments==2.15.1
110 114 stack-data==0.6.2
111 115 asttokens==2.2.1
112 116 six==1.16.0
113 117 executing==1.2.0
114 118 pure-eval==0.2.2
115 119 traitlets==5.9.0
116 120 markdown==3.4.3
117 121 msgpack==1.0.5
118 122 mysqlclient==2.1.1
119 123 nbconvert==7.7.3
120 124 beautifulsoup4==4.11.2
121 125 soupsieve==2.4
122 126 bleach==6.0.0
123 127 six==1.16.0
124 128 webencodings==0.5.1
125 129 defusedxml==0.7.1
126 130 jinja2==3.1.2
127 131 markupsafe==2.1.2
128 132 jupyter_core==5.3.1
129 133 platformdirs==3.10.0
130 134 traitlets==5.9.0
131 135 jupyterlab-pygments==0.2.2
132 136 markupsafe==2.1.2
133 137 mistune==2.0.5
134 138 nbclient==0.8.0
135 139 jupyter_client==8.3.0
136 140 jupyter_core==5.3.1
137 141 platformdirs==3.10.0
138 142 traitlets==5.9.0
139 143 python-dateutil==2.8.2
140 144 six==1.16.0
141 145 pyzmq==25.0.0
142 146 tornado==6.2
143 147 traitlets==5.9.0
144 148 jupyter_core==5.3.1
145 149 platformdirs==3.10.0
146 150 traitlets==5.9.0
147 151 nbformat==5.9.2
148 152 fastjsonschema==2.18.0
149 153 jsonschema==4.18.6
150 154 attrs==22.2.0
151 155 pyrsistent==0.19.3
152 156 jupyter_core==5.3.1
153 157 platformdirs==3.10.0
154 158 traitlets==5.9.0
155 159 traitlets==5.9.0
156 160 traitlets==5.9.0
157 161 nbformat==5.9.2
158 162 fastjsonschema==2.18.0
159 163 jsonschema==4.18.6
160 164 attrs==22.2.0
161 165 pyrsistent==0.19.3
162 166 jupyter_core==5.3.1
163 167 platformdirs==3.10.0
164 168 traitlets==5.9.0
165 169 traitlets==5.9.0
166 170 packaging==23.1
167 171 pandocfilters==1.5.0
168 172 pygments==2.15.1
169 173 tinycss2==1.2.1
170 174 webencodings==0.5.1
171 175 traitlets==5.9.0
172 176 orjson==3.9.5
173 177 pastescript==3.3.0
174 178 paste==3.5.3
175 179 six==1.16.0
176 180 pastedeploy==3.0.1
177 181 six==1.16.0
178 182 premailer==3.10.0
179 183 cachetools==5.3.1
180 184 cssselect==1.2.0
181 185 cssutils==2.6.0
182 186 lxml==4.9.3
183 187 requests==2.28.2
184 188 certifi==2022.12.7
185 189 charset-normalizer==3.1.0
186 190 idna==3.4
187 191 urllib3==1.26.14
188 192 psutil==5.9.5
189 193 psycopg2==2.9.7
190 194 py-bcrypt==0.4
191 195 pycmarkgfm==1.2.0
192 196 cffi==1.15.1
193 197 pycparser==2.21
194 198 pycryptodome==3.17
195 199 pycurl==7.45.2
196 200 pymysql==1.0.3
197 201 pyotp==2.8.0
198 202 pyparsing==3.1.0
199 203 pyramid-debugtoolbar==4.10
200 204 pygments==2.15.1
201 205 pyramid==2.0.2
202 206 hupper==1.12
203 207 plaster==1.1.2
204 208 plaster-pastedeploy==1.0.1
205 209 pastedeploy==3.0.1
206 210 plaster==1.1.2
207 211 translationstring==1.4
208 212 venusian==3.0.0
209 213 webob==1.8.7
210 214 zope.deprecation==5.0.0
211 215 zope.interface==6.0.0
212 216 pyramid-mako==1.1.0
213 217 mako==1.2.4
214 218 markupsafe==2.1.2
215 219 pyramid==2.0.2
216 220 hupper==1.12
217 221 plaster==1.1.2
218 222 plaster-pastedeploy==1.0.1
219 223 pastedeploy==3.0.1
220 224 plaster==1.1.2
221 225 translationstring==1.4
222 226 venusian==3.0.0
223 227 webob==1.8.7
224 228 zope.deprecation==5.0.0
225 229 zope.interface==6.0.0
226 230 pyramid-mailer==0.15.1
227 231 pyramid==2.0.2
228 232 hupper==1.12
229 233 plaster==1.1.2
230 234 plaster-pastedeploy==1.0.1
231 235 pastedeploy==3.0.1
232 236 plaster==1.1.2
233 237 translationstring==1.4
234 238 venusian==3.0.0
235 239 webob==1.8.7
236 240 zope.deprecation==5.0.0
237 241 zope.interface==6.0.0
238 242 repoze.sendmail==4.4.1
239 243 transaction==3.1.0
240 244 zope.interface==6.0.0
241 245 zope.interface==6.0.0
242 246 transaction==3.1.0
243 247 zope.interface==6.0.0
244 248 python-ldap==3.4.3
245 249 pyasn1==0.4.8
246 250 pyasn1-modules==0.2.8
247 251 pyasn1==0.4.8
248 252 python-memcached==1.59
249 253 six==1.16.0
250 254 python-pam==2.0.2
251 255 python3-saml==1.15.0
252 256 isodate==0.6.1
253 257 six==1.16.0
254 258 lxml==4.9.3
255 259 xmlsec==1.3.13
256 260 lxml==4.9.3
257 261 pyyaml==6.0.1
258 262 redis==5.0.0
259 263 regex==2022.10.31
260 264 routes==2.5.1
261 265 repoze.lru==0.7
262 266 six==1.16.0
263 267 simplejson==3.19.1
264 268 sshpubkeys==3.3.1
265 269 cryptography==40.0.2
266 270 cffi==1.15.1
267 271 pycparser==2.21
268 272 ecdsa==0.18.0
269 273 six==1.16.0
270 274 sqlalchemy==1.4.49
271 275 greenlet==2.0.2
272 276 typing_extensions==4.7.1
273 277 supervisor==4.2.5
274 278 tzlocal==4.3
275 279 pytz-deprecation-shim==0.1.0.post0
276 280 tzdata==2023.3
277 281 unidecode==1.3.6
278 282 urlobject==2.4.3
279 283 waitress==2.1.2
280 284 weberror==0.13.1
281 285 paste==3.5.3
282 286 six==1.16.0
283 287 pygments==2.15.1
284 288 tempita==0.5.2
285 289 webob==1.8.7
286 290 webhelpers2==2.0
287 291 markupsafe==2.1.2
288 292 six==1.16.0
289 293 whoosh==2.7.4
290 294 zope.cachedescriptors==5.0.0
291 295
292 296 ## uncomment to add the debug libraries
293 297 #-r requirements_debug.txt
@@ -1,782 +1,783 b''
1 1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import datetime
21 21 import string
22 22
23 23 import formencode
24 24 import formencode.htmlfill
25 25 import peppercorn
26 26 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
27 27
28 28 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 29 from rhodecode import forms
30 30 from rhodecode.lib import helpers as h
31 31 from rhodecode.lib import audit_logger
32 32 from rhodecode.lib import ext_json
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, NotAnonymous, CSRFRequired,
35 35 HasRepoPermissionAny, HasRepoGroupPermissionAny, AuthUser)
36 36 from rhodecode.lib.channelstream import (
37 37 channelstream_request, ChannelstreamException)
38 38 from rhodecode.lib.hash_utils import md5_safe
39 39 from rhodecode.lib.utils2 import safe_int, md5, str2bool
40 40 from rhodecode.model.auth_token import AuthTokenModel
41 41 from rhodecode.model.comment import CommentsModel
42 42 from rhodecode.model.db import (
43 43 IntegrityError, or_, in_filter_generator,
44 44 Repository, UserEmailMap, UserApiKeys, UserFollowing,
45 45 PullRequest, UserBookmark, RepoGroup, ChangesetStatus)
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.pull_request import PullRequestModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.user_group import UserGroupModel
50 50 from rhodecode.model.validation_schema.schemas import user_schema
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountView(BaseAppView, DataGridAppView):
56 56 ALLOW_SCOPED_TOKENS = False
57 57 """
58 58 This view has alternative version inside EE, if modified please take a look
59 59 in there as well.
60 60 """
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context()
64 64 c.user = c.auth_user.get_instance()
65 65 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
66 66 return c
67 67
68 68 @LoginRequired()
69 69 @NotAnonymous()
70 70 def my_account_profile(self):
71 71 c = self.load_default_context()
72 72 c.active = 'profile'
73 73 c.extern_type = c.user.extern_type
74 74 return self._get_template_context(c)
75 75
76 76 @LoginRequired()
77 77 @NotAnonymous()
78 78 def my_account_edit(self):
79 79 c = self.load_default_context()
80 80 c.active = 'profile_edit'
81 81 c.extern_type = c.user.extern_type
82 82 c.extern_name = c.user.extern_name
83 83
84 84 schema = user_schema.UserProfileSchema().bind(
85 85 username=c.user.username, user_emails=c.user.emails)
86 86 appstruct = {
87 87 'username': c.user.username,
88 88 'email': c.user.email,
89 89 'firstname': c.user.firstname,
90 90 'lastname': c.user.lastname,
91 91 'description': c.user.description,
92 92 }
93 93 c.form = forms.RcForm(
94 94 schema, appstruct=appstruct,
95 95 action=h.route_path('my_account_update'),
96 96 buttons=(forms.buttons.save, forms.buttons.reset))
97 97
98 98 return self._get_template_context(c)
99 99
100 100 @LoginRequired()
101 101 @NotAnonymous()
102 102 @CSRFRequired()
103 103 def my_account_update(self):
104 104 _ = self.request.translate
105 105 c = self.load_default_context()
106 106 c.active = 'profile_edit'
107 107 c.perm_user = c.auth_user
108 108 c.extern_type = c.user.extern_type
109 109 c.extern_name = c.user.extern_name
110 110
111 111 schema = user_schema.UserProfileSchema().bind(
112 112 username=c.user.username, user_emails=c.user.emails)
113 113 form = forms.RcForm(
114 114 schema, buttons=(forms.buttons.save, forms.buttons.reset))
115 115
116 116 controls = list(self.request.POST.items())
117 117 try:
118 118 valid_data = form.validate(controls)
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 if c.extern_type != "rhodecode":
122 122 # forbid updating username for external accounts
123 123 skip_attrs.append('username')
124 124 old_email = c.user.email
125 125 UserModel().update_user(
126 126 self._rhodecode_user.user_id, skip_attrs=skip_attrs,
127 127 **valid_data)
128 128 if old_email != valid_data['email']:
129 129 old = UserEmailMap.query() \
130 130 .filter(UserEmailMap.user == c.user)\
131 131 .filter(UserEmailMap.email == valid_data['email'])\
132 132 .first()
133 133 old.email = old_email
134 134 h.flash(_('Your account was updated successfully'), category='success')
135 135 Session().commit()
136 136 except forms.ValidationFailure as e:
137 137 c.form = e
138 138 return self._get_template_context(c)
139 139 except Exception:
140 140 log.exception("Exception updating user")
141 141 h.flash(_('Error occurred during update of user'),
142 142 category='error')
143 143 raise HTTPFound(h.route_path('my_account_profile'))
144 144
145 145 @LoginRequired()
146 146 @NotAnonymous()
147 147 def my_account_password(self):
148 148 c = self.load_default_context()
149 149 c.active = 'password'
150 150 c.extern_type = c.user.extern_type
151 151
152 152 schema = user_schema.ChangePasswordSchema().bind(
153 153 username=c.user.username)
154 154
155 155 form = forms.Form(
156 156 schema,
157 157 action=h.route_path('my_account_password_update'),
158 158 buttons=(forms.buttons.save, forms.buttons.reset))
159 159
160 160 c.form = form
161 161 return self._get_template_context(c)
162 162
163 163 @LoginRequired()
164 164 @NotAnonymous()
165 165 @CSRFRequired()
166 166 def my_account_password_update(self):
167 167 _ = self.request.translate
168 168 c = self.load_default_context()
169 169 c.active = 'password'
170 170 c.extern_type = c.user.extern_type
171 171
172 172 schema = user_schema.ChangePasswordSchema().bind(
173 173 username=c.user.username)
174 174
175 175 form = forms.Form(
176 176 schema, buttons=(forms.buttons.save, forms.buttons.reset))
177 177
178 178 if c.extern_type != 'rhodecode':
179 179 raise HTTPFound(self.request.route_path('my_account_password'))
180 180
181 181 controls = list(self.request.POST.items())
182 182 try:
183 183 valid_data = form.validate(controls)
184 184 UserModel().update_user(c.user.user_id, **valid_data)
185 185 c.user.update_userdata(force_password_change=False)
186 186 Session().commit()
187 187 except forms.ValidationFailure as e:
188 188 c.form = e
189 189 return self._get_template_context(c)
190 190
191 191 except Exception:
192 192 log.exception("Exception updating password")
193 193 h.flash(_('Error occurred during update of user password'),
194 194 category='error')
195 195 else:
196 196 instance = c.auth_user.get_instance()
197 197 self.session.setdefault('rhodecode_user', {}).update(
198 198 {'password': md5_safe(instance.password)})
199 199 self.session.save()
200 200 h.flash(_("Successfully updated password"), category='success')
201 201
202 202 raise HTTPFound(self.request.route_path('my_account_password'))
203 203
204 204 @LoginRequired()
205 205 @NotAnonymous()
206 206 def my_account_auth_tokens(self):
207 207 _ = self.request.translate
208 208
209 209 c = self.load_default_context()
210 210 c.active = 'auth_tokens'
211 211 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
212 212 c.role_values = [
213 213 (x, AuthTokenModel.cls._get_role_name(x))
214 214 for x in AuthTokenModel.cls.ROLES]
215 215 c.role_options = [(c.role_values, _("Role"))]
216 216 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
217 217 c.user.user_id, show_expired=True)
218 218 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
219 219 return self._get_template_context(c)
220 220
221 221 @LoginRequired()
222 222 @NotAnonymous()
223 223 @CSRFRequired()
224 224 def my_account_auth_tokens_view(self):
225 225 _ = self.request.translate
226 226 c = self.load_default_context()
227 227
228 228 auth_token_id = self.request.POST.get('auth_token_id')
229 229
230 230 if auth_token_id:
231 231 token = UserApiKeys.get_or_404(auth_token_id)
232 232 if token.user.user_id != c.user.user_id:
233 233 raise HTTPNotFound()
234 234
235 235 return {
236 236 'auth_token': token.api_key
237 237 }
238 238
239 239 def maybe_attach_token_scope(self, token):
240 240 # implemented in EE edition
241 241 pass
242 242
243 243 @LoginRequired()
244 244 @NotAnonymous()
245 245 @CSRFRequired()
246 246 def my_account_auth_tokens_add(self):
247 247 _ = self.request.translate
248 248 c = self.load_default_context()
249 249
250 250 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
251 251 description = self.request.POST.get('description')
252 252 role = self.request.POST.get('role')
253 253
254 254 token = UserModel().add_auth_token(
255 255 user=c.user.user_id,
256 256 lifetime_minutes=lifetime, role=role, description=description,
257 257 scope_callback=self.maybe_attach_token_scope)
258 258 token_data = token.get_api_data()
259 259
260 260 audit_logger.store_web(
261 261 'user.edit.token.add', action_data={
262 262 'data': {'token': token_data, 'user': 'self'}},
263 263 user=self._rhodecode_user, )
264 264 Session().commit()
265 265
266 266 h.flash(_("Auth token successfully created"), category='success')
267 267 return HTTPFound(h.route_path('my_account_auth_tokens'))
268 268
269 269 @LoginRequired()
270 270 @NotAnonymous()
271 271 @CSRFRequired()
272 272 def my_account_auth_tokens_delete(self):
273 273 _ = self.request.translate
274 274 c = self.load_default_context()
275 275
276 276 del_auth_token = self.request.POST.get('del_auth_token')
277 277
278 278 if del_auth_token:
279 279 token = UserApiKeys.get_or_404(del_auth_token)
280 280 token_data = token.get_api_data()
281 281
282 282 AuthTokenModel().delete(del_auth_token, c.user.user_id)
283 283 audit_logger.store_web(
284 284 'user.edit.token.delete', action_data={
285 285 'data': {'token': token_data, 'user': 'self'}},
286 286 user=self._rhodecode_user,)
287 287 Session().commit()
288 288 h.flash(_("Auth token successfully deleted"), category='success')
289 289
290 290 return HTTPFound(h.route_path('my_account_auth_tokens'))
291 291
292 292 @LoginRequired()
293 293 @NotAnonymous()
294 294 def my_account_emails(self):
295 295 _ = self.request.translate
296 296
297 297 c = self.load_default_context()
298 298 c.active = 'emails'
299 299
300 300 c.user_email_map = UserEmailMap.query()\
301 301 .filter(UserEmailMap.user == c.user).all()
302 302
303 303 schema = user_schema.AddEmailSchema().bind(
304 304 username=c.user.username, user_emails=c.user.emails)
305 305
306 306 form = forms.RcForm(schema,
307 307 action=h.route_path('my_account_emails_add'),
308 308 buttons=(forms.buttons.save, forms.buttons.reset))
309 309
310 310 c.form = form
311 311 return self._get_template_context(c)
312 312
313 313 @LoginRequired()
314 314 @NotAnonymous()
315 315 @CSRFRequired()
316 316 def my_account_emails_add(self):
317 317 _ = self.request.translate
318 318 c = self.load_default_context()
319 319 c.active = 'emails'
320 320
321 321 schema = user_schema.AddEmailSchema().bind(
322 322 username=c.user.username, user_emails=c.user.emails)
323 323
324 324 form = forms.RcForm(
325 325 schema, action=h.route_path('my_account_emails_add'),
326 326 buttons=(forms.buttons.save, forms.buttons.reset))
327 327
328 328 controls = list(self.request.POST.items())
329 329 try:
330 330 valid_data = form.validate(controls)
331 331 UserModel().add_extra_email(c.user.user_id, valid_data['email'])
332 332 audit_logger.store_web(
333 333 'user.edit.email.add', action_data={
334 334 'data': {'email': valid_data['email'], 'user': 'self'}},
335 335 user=self._rhodecode_user,)
336 336 Session().commit()
337 337 except formencode.Invalid as error:
338 338 h.flash(h.escape(error.error_dict['email']), category='error')
339 339 except forms.ValidationFailure as e:
340 340 c.user_email_map = UserEmailMap.query() \
341 341 .filter(UserEmailMap.user == c.user).all()
342 342 c.form = e
343 343 return self._get_template_context(c)
344 344 except Exception:
345 345 log.exception("Exception adding email")
346 346 h.flash(_('Error occurred during adding email'),
347 347 category='error')
348 348 else:
349 349 h.flash(_("Successfully added email"), category='success')
350 350
351 351 raise HTTPFound(self.request.route_path('my_account_emails'))
352 352
353 353 @LoginRequired()
354 354 @NotAnonymous()
355 355 @CSRFRequired()
356 356 def my_account_emails_delete(self):
357 357 _ = self.request.translate
358 358 c = self.load_default_context()
359 359
360 360 del_email_id = self.request.POST.get('del_email_id')
361 361 if del_email_id:
362 362 email = UserEmailMap.get_or_404(del_email_id).email
363 363 UserModel().delete_extra_email(c.user.user_id, del_email_id)
364 364 audit_logger.store_web(
365 365 'user.edit.email.delete', action_data={
366 366 'data': {'email': email, 'user': 'self'}},
367 367 user=self._rhodecode_user,)
368 368 Session().commit()
369 369 h.flash(_("Email successfully deleted"),
370 370 category='success')
371 371 return HTTPFound(h.route_path('my_account_emails'))
372 372
373 373 @LoginRequired()
374 374 @NotAnonymous()
375 375 @CSRFRequired()
376 376 def my_account_notifications_test_channelstream(self):
377 377 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
378 378 self._rhodecode_user.username, datetime.datetime.now())
379 379 payload = {
380 380 # 'channel': 'broadcast',
381 381 'type': 'message',
382 382 'timestamp': datetime.datetime.utcnow(),
383 383 'user': 'system',
384 384 'pm_users': [self._rhodecode_user.username],
385 385 'message': {
386 386 'message': message,
387 387 'level': 'info',
388 388 'topic': '/notifications'
389 389 }
390 390 }
391 391
392 392 registry = self.request.registry
393 393 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
394 394 channelstream_config = rhodecode_plugins.get('channelstream', {})
395 395
396 396 try:
397 397 channelstream_request(channelstream_config, [payload], '/message')
398 398 except ChannelstreamException as e:
399 399 log.exception('Failed to send channelstream data')
400 400 return {"response": f'ERROR: {e.__class__.__name__}'}
401 401 return {"response": 'Channelstream data sent. '
402 402 'You should see a new live message now.'}
403 403
404 404 def _load_my_repos_data(self, watched=False):
405 405
406 406 allowed_ids = [-1] + self._rhodecode_user.repo_acl_ids_from_stack(AuthUser.repo_read_perms)
407 407
408 408 if watched:
409 409 # repos user watch
410 410 repo_list = Session().query(
411 411 Repository
412 412 ) \
413 413 .join(
414 414 (UserFollowing, UserFollowing.follows_repo_id == Repository.repo_id)
415 415 ) \
416 416 .filter(
417 417 UserFollowing.user_id == self._rhodecode_user.user_id
418 418 ) \
419 419 .filter(or_(
420 420 # generate multiple IN to fix limitation problems
421 421 *in_filter_generator(Repository.repo_id, allowed_ids))
422 422 ) \
423 423 .order_by(Repository.repo_name) \
424 424 .all()
425 425
426 426 else:
427 427 # repos user is owner of
428 428 repo_list = Session().query(
429 429 Repository
430 430 ) \
431 431 .filter(
432 432 Repository.user_id == self._rhodecode_user.user_id
433 433 ) \
434 434 .filter(or_(
435 435 # generate multiple IN to fix limitation problems
436 436 *in_filter_generator(Repository.repo_id, allowed_ids))
437 437 ) \
438 438 .order_by(Repository.repo_name) \
439 439 .all()
440 440
441 441 _render = self.request.get_partial_renderer(
442 442 'rhodecode:templates/data_table/_dt_elements.mako')
443 443
444 444 def repo_lnk(name, rtype, rstate, private, archived, fork_of):
445 445 return _render('repo_name', name, rtype, rstate, private, archived, fork_of,
446 446 short_name=False, admin=False)
447 447
448 448 repos_data = []
449 449 for repo in repo_list:
450 450 row = {
451 451 "name": repo_lnk(repo.repo_name, repo.repo_type, repo.repo_state,
452 452 repo.private, repo.archived, repo.fork),
453 453 "name_raw": repo.repo_name.lower(),
454 454 }
455 455
456 456 repos_data.append(row)
457 457
458 458 # json used to render the grid
459 459 return ext_json.str_json(repos_data)
460 460
461 461 @LoginRequired()
462 462 @NotAnonymous()
463 463 def my_account_repos(self):
464 464 c = self.load_default_context()
465 465 c.active = 'repos'
466 466
467 467 # json used to render the grid
468 468 c.data = self._load_my_repos_data()
469 469 return self._get_template_context(c)
470 470
471 471 @LoginRequired()
472 472 @NotAnonymous()
473 473 def my_account_watched(self):
474 474 c = self.load_default_context()
475 475 c.active = 'watched'
476 476
477 477 # json used to render the grid
478 478 c.data = self._load_my_repos_data(watched=True)
479 479 return self._get_template_context(c)
480 480
481 481 @LoginRequired()
482 482 @NotAnonymous()
483 483 def my_account_bookmarks(self):
484 484 c = self.load_default_context()
485 485 c.active = 'bookmarks'
486 486 c.bookmark_items = UserBookmark.get_bookmarks_for_user(
487 487 self._rhodecode_db_user.user_id, cache=False)
488 488 return self._get_template_context(c)
489 489
490 490 def _process_bookmark_entry(self, entry, user_id):
491 491 position = safe_int(entry.get('position'))
492 492 cur_position = safe_int(entry.get('cur_position'))
493 493 if position is None:
494 494 return
495 495
496 496 # check if this is an existing entry
497 497 is_new = False
498 498 db_entry = UserBookmark().get_by_position_for_user(cur_position, user_id)
499 499
500 500 if db_entry and str2bool(entry.get('remove')):
501 501 log.debug('Marked bookmark %s for deletion', db_entry)
502 502 Session().delete(db_entry)
503 503 return
504 504
505 505 if not db_entry:
506 506 # new
507 507 db_entry = UserBookmark()
508 508 is_new = True
509 509
510 510 should_save = False
511 511 default_redirect_url = ''
512 512
513 513 # save repo
514 514 if entry.get('bookmark_repo') and safe_int(entry.get('bookmark_repo')):
515 515 repo = Repository.get(entry['bookmark_repo'])
516 516 perm_check = HasRepoPermissionAny(
517 517 'repository.read', 'repository.write', 'repository.admin')
518 518 if repo and perm_check(repo_name=repo.repo_name):
519 519 db_entry.repository = repo
520 520 should_save = True
521 521 default_redirect_url = '${repo_url}'
522 522 # save repo group
523 523 elif entry.get('bookmark_repo_group') and safe_int(entry.get('bookmark_repo_group')):
524 524 repo_group = RepoGroup.get(entry['bookmark_repo_group'])
525 525 perm_check = HasRepoGroupPermissionAny(
526 526 'group.read', 'group.write', 'group.admin')
527 527
528 528 if repo_group and perm_check(group_name=repo_group.group_name):
529 529 db_entry.repository_group = repo_group
530 530 should_save = True
531 531 default_redirect_url = '${repo_group_url}'
532 532 # save generic info
533 533 elif entry.get('title') and entry.get('redirect_url'):
534 534 should_save = True
535 535
536 536 if should_save:
537 537 # mark user and position
538 538 db_entry.user_id = user_id
539 539 db_entry.position = position
540 540 db_entry.title = entry.get('title')
541 541 db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url
542 542 log.debug('Saving bookmark %s, new:%s', db_entry, is_new)
543 543
544 544 Session().add(db_entry)
545 545
546 546 @LoginRequired()
547 547 @NotAnonymous()
548 548 @CSRFRequired()
549 549 def my_account_bookmarks_update(self):
550 550 _ = self.request.translate
551 551 c = self.load_default_context()
552 552 c.active = 'bookmarks'
553 553
554 554 controls = peppercorn.parse(self.request.POST.items())
555 555 user_id = c.user.user_id
556 556
557 557 # validate positions
558 558 positions = {}
559 559 for entry in controls.get('bookmarks', []):
560 560 position = safe_int(entry['position'])
561 561 if position is None:
562 562 continue
563 563
564 564 if position in positions:
565 565 h.flash(_("Position {} is defined twice. "
566 566 "Please correct this error.").format(position), category='error')
567 567 return HTTPFound(h.route_path('my_account_bookmarks'))
568 568
569 569 entry['position'] = position
570 570 entry['cur_position'] = safe_int(entry.get('cur_position'))
571 571 positions[position] = entry
572 572
573 573 try:
574 574 for entry in positions.values():
575 575 self._process_bookmark_entry(entry, user_id)
576 576
577 577 Session().commit()
578 578 h.flash(_("Update Bookmarks"), category='success')
579 579 except IntegrityError:
580 580 h.flash(_("Failed to update bookmarks. "
581 581 "Make sure an unique position is used."), category='error')
582 582
583 583 return HTTPFound(h.route_path('my_account_bookmarks'))
584 584
585 585 @LoginRequired()
586 586 @NotAnonymous()
587 587 def my_account_goto_bookmark(self):
588 588
589 589 bookmark_id = self.request.matchdict['bookmark_id']
590 590 user_bookmark = UserBookmark().query()\
591 591 .filter(UserBookmark.user_id == self.request.user.user_id) \
592 592 .filter(UserBookmark.position == bookmark_id).scalar()
593 593
594 594 redirect_url = h.route_path('my_account_bookmarks')
595 595 if not user_bookmark:
596 596 raise HTTPFound(redirect_url)
597 597
598 598 # repository set
599 599 if user_bookmark.repository:
600 600 repo_name = user_bookmark.repository.repo_name
601 601 base_redirect_url = h.route_path(
602 602 'repo_summary', repo_name=repo_name)
603 603 if user_bookmark.redirect_url and \
604 604 '${repo_url}' in user_bookmark.redirect_url:
605 605 redirect_url = string.Template(user_bookmark.redirect_url)\
606 606 .safe_substitute({'repo_url': base_redirect_url})
607 607 else:
608 608 redirect_url = base_redirect_url
609 609 # repository group set
610 610 elif user_bookmark.repository_group:
611 611 repo_group_name = user_bookmark.repository_group.group_name
612 612 base_redirect_url = h.route_path(
613 613 'repo_group_home', repo_group_name=repo_group_name)
614 614 if user_bookmark.redirect_url and \
615 615 '${repo_group_url}' in user_bookmark.redirect_url:
616 616 redirect_url = string.Template(user_bookmark.redirect_url)\
617 617 .safe_substitute({'repo_group_url': base_redirect_url})
618 618 else:
619 619 redirect_url = base_redirect_url
620 620 # custom URL set
621 621 elif user_bookmark.redirect_url:
622 622 server_url = h.route_url('home').rstrip('/')
623 623 redirect_url = string.Template(user_bookmark.redirect_url) \
624 624 .safe_substitute({'server_url': server_url})
625 625
626 626 log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url)
627 627 raise HTTPFound(redirect_url)
628 628
629 629 @LoginRequired()
630 630 @NotAnonymous()
631 631 def my_account_perms(self):
632 632 c = self.load_default_context()
633 633 c.active = 'perms'
634 634
635 635 c.perm_user = c.auth_user
636 636 return self._get_template_context(c)
637 637
638 638 @LoginRequired()
639 639 @NotAnonymous()
640 640 def my_notifications(self):
641 641 c = self.load_default_context()
642 642 c.active = 'notifications'
643 643
644 644 return self._get_template_context(c)
645 645
646 646 @LoginRequired()
647 647 @NotAnonymous()
648 648 @CSRFRequired()
649 649 def my_notifications_toggle_visibility(self):
650 650 user = self._rhodecode_db_user
651 651 new_status = not user.user_data.get('notification_status', True)
652 652 user.update_userdata(notification_status=new_status)
653 653 Session().commit()
654 654 return user.user_data['notification_status']
655 655
656 656 def _get_pull_requests_list(self, statuses, filter_type=None):
657 657 draw, start, limit = self._extract_chunk(self.request)
658 658 search_q, order_by, order_dir = self._extract_ordering(self.request)
659 659
660 660 _render = self.request.get_partial_renderer(
661 661 'rhodecode:templates/data_table/_dt_elements.mako')
662 662
663 663 if filter_type == 'awaiting_my_review':
664 664 pull_requests = PullRequestModel().get_im_participating_in_for_review(
665 665 user_id=self._rhodecode_user.user_id,
666 666 statuses=statuses, query=search_q,
667 667 offset=start, length=limit, order_by=order_by,
668 668 order_dir=order_dir)
669 669
670 670 pull_requests_total_count = PullRequestModel().count_im_participating_in_for_review(
671 671 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
672 672 else:
673 673 pull_requests = PullRequestModel().get_im_participating_in(
674 674 user_id=self._rhodecode_user.user_id,
675 675 statuses=statuses, query=search_q,
676 676 offset=start, length=limit, order_by=order_by,
677 677 order_dir=order_dir)
678 678
679 679 pull_requests_total_count = PullRequestModel().count_im_participating_in(
680 680 user_id=self._rhodecode_user.user_id, statuses=statuses, query=search_q)
681 681
682 682 data = []
683 683 comments_model = CommentsModel()
684 684 for pr in pull_requests:
685 685 repo_id = pr.target_repo_id
686 686 comments_count = comments_model.get_all_comments(
687 687 repo_id, pull_request=pr, include_drafts=False, count_only=True)
688 688 owned = pr.user_id == self._rhodecode_user.user_id
689 689
690 690 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
691 691 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
692 692 if review_statuses and review_statuses[4]:
693 693 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
694 694 my_review_status = statuses[0][1].status
695 695
696 696 data.append({
697 697 'target_repo': _render('pullrequest_target_repo',
698 698 pr.target_repo.repo_name),
699 699 'name': _render('pullrequest_name',
700 700 pr.pull_request_id, pr.pull_request_state,
701 701 pr.work_in_progress, pr.target_repo.repo_name,
702 702 short=True),
703 703 'name_raw': pr.pull_request_id,
704 704 'status': _render('pullrequest_status',
705 705 pr.calculated_review_status()),
706 706 'my_status': _render('pullrequest_status',
707 707 my_review_status),
708 708 'title': _render('pullrequest_title', pr.title, pr.description),
709 'pr_flow': _render('pullrequest_commit_flow', pr),
709 710 'description': h.escape(pr.description),
710 711 'updated_on': _render('pullrequest_updated_on',
711 712 h.datetime_to_time(pr.updated_on),
712 713 pr.versions_count),
713 714 'updated_on_raw': h.datetime_to_time(pr.updated_on),
714 715 'created_on': _render('pullrequest_updated_on',
715 716 h.datetime_to_time(pr.created_on)),
716 717 'created_on_raw': h.datetime_to_time(pr.created_on),
717 718 'state': pr.pull_request_state,
718 719 'author': _render('pullrequest_author',
719 720 pr.author.full_contact, ),
720 721 'author_raw': pr.author.full_name,
721 722 'comments': _render('pullrequest_comments', comments_count),
722 723 'comments_raw': comments_count,
723 724 'closed': pr.is_closed(),
724 725 'owned': owned
725 726 })
726 727
727 728 # json used to render the grid
728 729 data = ({
729 730 'draw': draw,
730 731 'data': data,
731 732 'recordsTotal': pull_requests_total_count,
732 733 'recordsFiltered': pull_requests_total_count,
733 734 })
734 735 return data
735 736
736 737 @LoginRequired()
737 738 @NotAnonymous()
738 739 def my_account_pullrequests(self):
739 740 c = self.load_default_context()
740 741 c.active = 'pullrequests'
741 742 req_get = self.request.GET
742 743
743 744 c.closed = str2bool(req_get.get('closed'))
744 745 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
745 746
746 747 c.selected_filter = 'all'
747 748 if c.closed:
748 749 c.selected_filter = 'all_closed'
749 750 if c.awaiting_my_review:
750 751 c.selected_filter = 'awaiting_my_review'
751 752
752 753 return self._get_template_context(c)
753 754
754 755 @LoginRequired()
755 756 @NotAnonymous()
756 757 def my_account_pullrequests_data(self):
757 758 self.load_default_context()
758 759 req_get = self.request.GET
759 760
760 761 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
761 762 closed = str2bool(req_get.get('closed'))
762 763
763 764 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
764 765 if closed:
765 766 statuses += [PullRequest.STATUS_CLOSED]
766 767
767 768 filter_type = \
768 769 'awaiting_my_review' if awaiting_my_review \
769 770 else None
770 771
771 772 data = self._get_pull_requests_list(statuses=statuses, filter_type=filter_type)
772 773 return data
773 774
774 775 @LoginRequired()
775 776 @NotAnonymous()
776 777 def my_account_user_group_membership(self):
777 778 c = self.load_default_context()
778 779 c.active = 'user_group_membership'
779 780 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
780 781 for group in self._rhodecode_db_user.group_member]
781 782 c.user_groups = ext_json.str_json(groups)
782 783 return self._get_template_context(c)
@@ -1,1874 +1,1875 b''
1 1 # Copyright (C) 2011-2023 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 import logging
20 20 import collections
21 21
22 22 import formencode
23 23 import formencode.htmlfill
24 24 import peppercorn
25 25 from pyramid.httpexceptions import (
26 26 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
27 27
28 28 from pyramid.renderers import render
29 29
30 30 from rhodecode.apps._base import RepoAppView, DataGridAppView
31 31
32 32 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
33 33 from rhodecode.lib.base import vcs_operation_context
34 34 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
35 35 from rhodecode.lib.exceptions import CommentVersionMismatch
36 36 from rhodecode.lib import ext_json
37 37 from rhodecode.lib.auth import (
38 38 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
39 39 NotAnonymous, CSRFRequired)
40 40 from rhodecode.lib.utils2 import str2bool, safe_str, safe_int, aslist, retry
41 41 from rhodecode.lib.vcs.backends.base import (
42 42 EmptyCommit, UpdateFailureReason, unicode_to_reference)
43 43 from rhodecode.lib.vcs.exceptions import (
44 44 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 45 from rhodecode.model.changeset_status import ChangesetStatusModel
46 46 from rhodecode.model.comment import CommentsModel
47 47 from rhodecode.model.db import (
48 48 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
49 49 PullRequestReviewers)
50 50 from rhodecode.model.forms import PullRequestForm
51 51 from rhodecode.model.meta import Session
52 52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 53 from rhodecode.model.scm import ScmModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59 59
60 60 def load_default_context(self):
61 61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 64 # backward compat., we use for OLD PRs a plain renderer
65 65 c.renderer = 'plain'
66 66 return c
67 67
68 68 def _get_pull_requests_list(
69 69 self, repo_name, source, filter_type, opened_by, statuses):
70 70
71 71 draw, start, limit = self._extract_chunk(self.request)
72 72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 73 _render = self.request.get_partial_renderer(
74 74 'rhodecode:templates/data_table/_dt_elements.mako')
75 75
76 76 # pagination
77 77
78 78 if filter_type == 'awaiting_review':
79 79 pull_requests = PullRequestModel().get_awaiting_review(
80 80 repo_name,
81 81 search_q=search_q, statuses=statuses,
82 82 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
83 83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 84 repo_name,
85 85 search_q=search_q, statuses=statuses)
86 86 elif filter_type == 'awaiting_my_review':
87 87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 88 repo_name, self._rhodecode_user.user_id,
89 89 search_q=search_q, statuses=statuses,
90 90 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
91 91 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 92 repo_name, self._rhodecode_user.user_id,
93 93 search_q=search_q, statuses=statuses)
94 94 else:
95 95 pull_requests = PullRequestModel().get_all(
96 96 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 97 statuses=statuses, offset=start, length=limit,
98 98 order_by=order_by, order_dir=order_dir)
99 99 pull_requests_total_count = PullRequestModel().count_all(
100 100 repo_name, search_q=search_q, source=source, statuses=statuses,
101 101 opened_by=opened_by)
102 102
103 103 data = []
104 104 comments_model = CommentsModel()
105 105 for pr in pull_requests:
106 106 comments_count = comments_model.get_all_comments(
107 107 self.db_repo.repo_id, pull_request=pr,
108 108 include_drafts=False, count_only=True)
109 109
110 110 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
111 111 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
112 112 if review_statuses and review_statuses[4]:
113 113 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
114 114 my_review_status = statuses[0][1].status
115 115
116 116 data.append({
117 117 'name': _render('pullrequest_name',
118 118 pr.pull_request_id, pr.pull_request_state,
119 119 pr.work_in_progress, pr.target_repo.repo_name,
120 120 short=True),
121 121 'name_raw': pr.pull_request_id,
122 122 'status': _render('pullrequest_status',
123 123 pr.calculated_review_status()),
124 124 'my_status': _render('pullrequest_status',
125 125 my_review_status),
126 126 'title': _render('pullrequest_title', pr.title, pr.description),
127 'pr_flow': _render('pullrequest_commit_flow', pr),
127 128 'description': h.escape(pr.description),
128 129 'updated_on': _render('pullrequest_updated_on',
129 130 h.datetime_to_time(pr.updated_on),
130 131 pr.versions_count),
131 132 'updated_on_raw': h.datetime_to_time(pr.updated_on),
132 133 'created_on': _render('pullrequest_updated_on',
133 134 h.datetime_to_time(pr.created_on)),
134 135 'created_on_raw': h.datetime_to_time(pr.created_on),
135 136 'state': pr.pull_request_state,
136 137 'author': _render('pullrequest_author',
137 138 pr.author.full_contact, ),
138 139 'author_raw': pr.author.full_name,
139 140 'comments': _render('pullrequest_comments', comments_count),
140 141 'comments_raw': comments_count,
141 142 'closed': pr.is_closed(),
142 143 })
143 144
144 145 data = ({
145 146 'draw': draw,
146 147 'data': data,
147 148 'recordsTotal': pull_requests_total_count,
148 149 'recordsFiltered': pull_requests_total_count,
149 150 })
150 151 return data
151 152
152 153 @LoginRequired()
153 154 @HasRepoPermissionAnyDecorator(
154 155 'repository.read', 'repository.write', 'repository.admin')
155 156 def pull_request_list(self):
156 157 c = self.load_default_context()
157 158
158 159 req_get = self.request.GET
159 160 c.source = str2bool(req_get.get('source'))
160 161 c.closed = str2bool(req_get.get('closed'))
161 162 c.my = str2bool(req_get.get('my'))
162 163 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
163 164 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
164 165
165 166 c.active = 'open'
166 167 if c.my:
167 168 c.active = 'my'
168 169 if c.closed:
169 170 c.active = 'closed'
170 171 if c.awaiting_review and not c.source:
171 172 c.active = 'awaiting'
172 173 if c.source and not c.awaiting_review:
173 174 c.active = 'source'
174 175 if c.awaiting_my_review:
175 176 c.active = 'awaiting_my'
176 177
177 178 return self._get_template_context(c)
178 179
179 180 @LoginRequired()
180 181 @HasRepoPermissionAnyDecorator(
181 182 'repository.read', 'repository.write', 'repository.admin')
182 183 def pull_request_list_data(self):
183 184 self.load_default_context()
184 185
185 186 # additional filters
186 187 req_get = self.request.GET
187 188 source = str2bool(req_get.get('source'))
188 189 closed = str2bool(req_get.get('closed'))
189 190 my = str2bool(req_get.get('my'))
190 191 awaiting_review = str2bool(req_get.get('awaiting_review'))
191 192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
192 193
193 194 filter_type = 'awaiting_review' if awaiting_review \
194 195 else 'awaiting_my_review' if awaiting_my_review \
195 196 else None
196 197
197 198 opened_by = None
198 199 if my:
199 200 opened_by = [self._rhodecode_user.user_id]
200 201
201 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 203 if closed:
203 204 statuses = [PullRequest.STATUS_CLOSED]
204 205
205 206 data = self._get_pull_requests_list(
206 207 repo_name=self.db_repo_name, source=source,
207 208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
208 209
209 210 return data
210 211
211 212 def _is_diff_cache_enabled(self, target_repo):
212 213 caching_enabled = self._get_general_setting(
213 214 target_repo, 'rhodecode_diff_cache')
214 215 log.debug('Diff caching enabled: %s', caching_enabled)
215 216 return caching_enabled
216 217
217 218 def _get_diffset(self, source_repo_name, source_repo,
218 219 ancestor_commit,
219 220 source_ref_id, target_ref_id,
220 221 target_commit, source_commit, diff_limit, file_limit,
221 222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
222 223
223 224 target_commit_final = target_commit
224 225 source_commit_final = source_commit
225 226
226 227 if use_ancestor:
227 228 # we might want to not use it for versions
228 229 target_ref_id = ancestor_commit.raw_id
229 230 target_commit_final = ancestor_commit
230 231
231 232 vcs_diff = PullRequestModel().get_diff(
232 233 source_repo, source_ref_id, target_ref_id,
233 234 hide_whitespace_changes, diff_context)
234 235
235 236 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff', diff_limit=diff_limit,
236 237 file_limit=file_limit, show_full_diff=fulldiff)
237 238
238 239 _parsed = diff_processor.prepare()
239 240
240 241 diffset = codeblocks.DiffSet(
241 242 repo_name=self.db_repo_name,
242 243 source_repo_name=source_repo_name,
243 244 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
244 245 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
245 246 )
246 247 diffset = self.path_filter.render_patchset_filtered(
247 248 diffset, _parsed, target_ref_id, source_ref_id)
248 249
249 250 return diffset
250 251
251 252 def _get_range_diffset(self, source_scm, source_repo,
252 253 commit1, commit2, diff_limit, file_limit,
253 254 fulldiff, hide_whitespace_changes, diff_context):
254 255 vcs_diff = source_scm.get_diff(
255 256 commit1, commit2,
256 257 ignore_whitespace=hide_whitespace_changes,
257 258 context=diff_context)
258 259
259 260 diff_processor = diffs.DiffProcessor(vcs_diff, diff_format='newdiff',
260 261 diff_limit=diff_limit,
261 262 file_limit=file_limit, show_full_diff=fulldiff)
262 263
263 264 _parsed = diff_processor.prepare()
264 265
265 266 diffset = codeblocks.DiffSet(
266 267 repo_name=source_repo.repo_name,
267 268 source_node_getter=codeblocks.diffset_node_getter(commit1),
268 269 target_node_getter=codeblocks.diffset_node_getter(commit2))
269 270
270 271 diffset = self.path_filter.render_patchset_filtered(
271 272 diffset, _parsed, commit1.raw_id, commit2.raw_id)
272 273
273 274 return diffset
274 275
275 276 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
276 277 comments_model = CommentsModel()
277 278
278 279 # GENERAL COMMENTS with versions #
279 280 q = comments_model._all_general_comments_of_pull_request(pull_request)
280 281 q = q.order_by(ChangesetComment.comment_id.asc())
281 282 if not include_drafts:
282 283 q = q.filter(ChangesetComment.draft == false())
283 284 general_comments = q
284 285
285 286 # pick comments we want to render at current version
286 287 c.comment_versions = comments_model.aggregate_comments(
287 288 general_comments, versions, c.at_version_num)
288 289
289 290 # INLINE COMMENTS with versions #
290 291 q = comments_model._all_inline_comments_of_pull_request(pull_request)
291 292 q = q.order_by(ChangesetComment.comment_id.asc())
292 293 if not include_drafts:
293 294 q = q.filter(ChangesetComment.draft == false())
294 295 inline_comments = q
295 296
296 297 c.inline_versions = comments_model.aggregate_comments(
297 298 inline_comments, versions, c.at_version_num, inline=True)
298 299
299 300 # Comments inline+general
300 301 if c.at_version:
301 302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
302 303 c.comments = c.comment_versions[c.at_version_num]['display']
303 304 else:
304 305 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
305 306 c.comments = c.comment_versions[c.at_version_num]['until']
306 307
307 308 return general_comments, inline_comments
308 309
309 310 @LoginRequired()
310 311 @HasRepoPermissionAnyDecorator(
311 312 'repository.read', 'repository.write', 'repository.admin')
312 313 def pull_request_show(self):
313 314 _ = self.request.translate
314 315 c = self.load_default_context()
315 316
316 317 pull_request = PullRequest.get_or_404(
317 318 self.request.matchdict['pull_request_id'])
318 319 pull_request_id = pull_request.pull_request_id
319 320
320 321 c.state_progressing = pull_request.is_state_changing()
321 322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
322 323
323 324 _new_state = {
324 325 'created': PullRequest.STATE_CREATED,
325 326 }.get(self.request.GET.get('force_state'))
326 327 can_force_state = c.is_super_admin or HasRepoPermissionAny('repository.admin')(c.repo_name)
327 328
328 329 if can_force_state and _new_state:
329 330 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
330 331 h.flash(
331 332 _('Pull Request state was force changed to `{}`').format(_new_state),
332 333 category='success')
333 334 Session().commit()
334 335
335 336 raise HTTPFound(h.route_path(
336 337 'pullrequest_show', repo_name=self.db_repo_name,
337 338 pull_request_id=pull_request_id))
338 339
339 340 version = self.request.GET.get('version')
340 341 from_version = self.request.GET.get('from_version') or version
341 342 merge_checks = self.request.GET.get('merge_checks')
342 343 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
343 344 force_refresh = str2bool(self.request.GET.get('force_refresh'))
344 345 c.range_diff_on = self.request.GET.get('range-diff') == "1"
345 346
346 347 # fetch global flags of ignore ws or context lines
347 348 diff_context = diffs.get_diff_context(self.request)
348 349 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
349 350
350 351 (pull_request_latest,
351 352 pull_request_at_ver,
352 353 pull_request_display_obj,
353 354 at_version) = PullRequestModel().get_pr_version(
354 355 pull_request_id, version=version)
355 356
356 357 pr_closed = pull_request_latest.is_closed()
357 358
358 359 if pr_closed and (version or from_version):
359 360 # not allow to browse versions for closed PR
360 361 raise HTTPFound(h.route_path(
361 362 'pullrequest_show', repo_name=self.db_repo_name,
362 363 pull_request_id=pull_request_id))
363 364
364 365 versions = pull_request_display_obj.versions()
365 366
366 367 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
367 368
368 369 # used to store per-commit range diffs
369 370 c.changes = collections.OrderedDict()
370 371
371 372 c.at_version = at_version
372 373 c.at_version_num = (at_version
373 374 if at_version and at_version != PullRequest.LATEST_VER
374 375 else None)
375 376
376 377 c.at_version_index = ChangesetComment.get_index_from_version(
377 378 c.at_version_num, versions)
378 379
379 380 (prev_pull_request_latest,
380 381 prev_pull_request_at_ver,
381 382 prev_pull_request_display_obj,
382 383 prev_at_version) = PullRequestModel().get_pr_version(
383 384 pull_request_id, version=from_version)
384 385
385 386 c.from_version = prev_at_version
386 387 c.from_version_num = (prev_at_version
387 388 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
388 389 else None)
389 390 c.from_version_index = ChangesetComment.get_index_from_version(
390 391 c.from_version_num, versions)
391 392
392 393 # define if we're in COMPARE mode or VIEW at version mode
393 394 compare = at_version != prev_at_version
394 395
395 396 # pull_requests repo_name we opened it against
396 397 # ie. target_repo must match
397 398 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
398 399 log.warning('Mismatch between the current repo: %s, and target %s',
399 400 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
400 401 raise HTTPNotFound()
401 402
402 403 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
403 404
404 405 c.pull_request = pull_request_display_obj
405 406 c.renderer = pull_request_at_ver.description_renderer or c.renderer
406 407 c.pull_request_latest = pull_request_latest
407 408
408 409 # inject latest version
409 410 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
410 411 c.versions = versions + [latest_ver]
411 412
412 413 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
413 414 c.allowed_to_change_status = False
414 415 c.allowed_to_update = False
415 416 c.allowed_to_merge = False
416 417 c.allowed_to_delete = False
417 418 c.allowed_to_comment = False
418 419 c.allowed_to_close = False
419 420 else:
420 421 can_change_status = PullRequestModel().check_user_change_status(
421 422 pull_request_at_ver, self._rhodecode_user)
422 423 c.allowed_to_change_status = can_change_status and not pr_closed
423 424
424 425 c.allowed_to_update = PullRequestModel().check_user_update(
425 426 pull_request_latest, self._rhodecode_user) and not pr_closed
426 427 c.allowed_to_merge = PullRequestModel().check_user_merge(
427 428 pull_request_latest, self._rhodecode_user) and not pr_closed
428 429 c.allowed_to_delete = PullRequestModel().check_user_delete(
429 430 pull_request_latest, self._rhodecode_user) and not pr_closed
430 431 c.allowed_to_comment = not pr_closed
431 432 c.allowed_to_close = c.allowed_to_merge and not pr_closed
432 433
433 434 c.forbid_adding_reviewers = False
434 435
435 436 if pull_request_latest.reviewer_data and \
436 437 'rules' in pull_request_latest.reviewer_data:
437 438 rules = pull_request_latest.reviewer_data['rules'] or {}
438 439 try:
439 440 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
440 441 except Exception:
441 442 pass
442 443
443 444 # check merge capabilities
444 445 _merge_check = MergeCheck.validate(
445 446 pull_request_latest, auth_user=self._rhodecode_user,
446 447 translator=self.request.translate,
447 448 force_shadow_repo_refresh=force_refresh)
448 449
449 450 c.pr_merge_errors = _merge_check.error_details
450 451 c.pr_merge_possible = not _merge_check.failed
451 452 c.pr_merge_message = _merge_check.merge_msg
452 453 c.pr_merge_source_commit = _merge_check.source_commit
453 454 c.pr_merge_target_commit = _merge_check.target_commit
454 455
455 456 c.pr_merge_info = MergeCheck.get_merge_conditions(
456 457 pull_request_latest, translator=self.request.translate)
457 458
458 459 c.pull_request_review_status = _merge_check.review_status
459 460 if merge_checks:
460 461 self.request.override_renderer = \
461 462 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
462 463 return self._get_template_context(c)
463 464
464 465 c.reviewers_count = pull_request.reviewers_count
465 466 c.observers_count = pull_request.observers_count
466 467
467 468 # reviewers and statuses
468 469 c.pull_request_default_reviewers_data_json = ext_json.str_json(pull_request.reviewer_data)
469 470 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
470 471 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
471 472
472 473 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
473 474 member_reviewer = h.reviewer_as_json(
474 475 member, reasons=reasons, mandatory=mandatory,
475 476 role=review_obj.role,
476 477 user_group=review_obj.rule_user_group_data()
477 478 )
478 479
479 480 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
480 481 member_reviewer['review_status'] = current_review_status
481 482 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
482 483 member_reviewer['allowed_to_update'] = c.allowed_to_update
483 484 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
484 485
485 486 c.pull_request_set_reviewers_data_json = ext_json.str_json(c.pull_request_set_reviewers_data_json)
486 487
487 488 for observer_obj, member in pull_request_at_ver.observers():
488 489 member_observer = h.reviewer_as_json(
489 490 member, reasons=[], mandatory=False,
490 491 role=observer_obj.role,
491 492 user_group=observer_obj.rule_user_group_data()
492 493 )
493 494 member_observer['allowed_to_update'] = c.allowed_to_update
494 495 c.pull_request_set_observers_data_json['observers'].append(member_observer)
495 496
496 497 c.pull_request_set_observers_data_json = ext_json.str_json(c.pull_request_set_observers_data_json)
497 498
498 499 general_comments, inline_comments = \
499 500 self.register_comments_vars(c, pull_request_latest, versions)
500 501
501 502 # TODOs
502 503 c.unresolved_comments = CommentsModel() \
503 504 .get_pull_request_unresolved_todos(pull_request_latest)
504 505 c.resolved_comments = CommentsModel() \
505 506 .get_pull_request_resolved_todos(pull_request_latest)
506 507
507 508 # Drafts
508 509 c.draft_comments = CommentsModel().get_pull_request_drafts(
509 510 self._rhodecode_db_user.user_id,
510 511 pull_request_latest)
511 512
512 513 # if we use version, then do not show later comments
513 514 # than current version
514 515 display_inline_comments = collections.defaultdict(
515 516 lambda: collections.defaultdict(list))
516 517 for co in inline_comments:
517 518 if c.at_version_num:
518 519 # pick comments that are at least UPTO given version, so we
519 520 # don't render comments for higher version
520 521 should_render = co.pull_request_version_id and \
521 522 co.pull_request_version_id <= c.at_version_num
522 523 else:
523 524 # showing all, for 'latest'
524 525 should_render = True
525 526
526 527 if should_render:
527 528 display_inline_comments[co.f_path][co.line_no].append(co)
528 529
529 530 # load diff data into template context, if we use compare mode then
530 531 # diff is calculated based on changes between versions of PR
531 532
532 533 source_repo = pull_request_at_ver.source_repo
533 534 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
534 535
535 536 target_repo = pull_request_at_ver.target_repo
536 537 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
537 538
538 539 if compare:
539 540 # in compare switch the diff base to latest commit from prev version
540 541 target_ref_id = prev_pull_request_display_obj.revisions[0]
541 542
542 543 # despite opening commits for bookmarks/branches/tags, we always
543 544 # convert this to rev to prevent changes after bookmark or branch change
544 545 c.source_ref_type = 'rev'
545 546 c.source_ref = source_ref_id
546 547
547 548 c.target_ref_type = 'rev'
548 549 c.target_ref = target_ref_id
549 550
550 551 c.source_repo = source_repo
551 552 c.target_repo = target_repo
552 553
553 554 c.commit_ranges = []
554 555 source_commit = EmptyCommit()
555 556 target_commit = EmptyCommit()
556 557 c.missing_requirements = False
557 558
558 559 source_scm = source_repo.scm_instance()
559 560 target_scm = target_repo.scm_instance()
560 561
561 562 shadow_scm = None
562 563 try:
563 564 shadow_scm = pull_request_latest.get_shadow_repo()
564 565 except Exception:
565 566 log.debug('Failed to get shadow repo', exc_info=True)
566 567 # try first the existing source_repo, and then shadow
567 568 # repo if we can obtain one
568 569 commits_source_repo = source_scm
569 570 if shadow_scm:
570 571 commits_source_repo = shadow_scm
571 572
572 573 c.commits_source_repo = commits_source_repo
573 574 c.ancestor = None # set it to None, to hide it from PR view
574 575
575 576 # empty version means latest, so we keep this to prevent
576 577 # double caching
577 578 version_normalized = version or PullRequest.LATEST_VER
578 579 from_version_normalized = from_version or PullRequest.LATEST_VER
579 580
580 581 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
581 582 cache_file_path = diff_cache_exist(
582 583 cache_path, 'pull_request', pull_request_id, version_normalized,
583 584 from_version_normalized, source_ref_id, target_ref_id,
584 585 hide_whitespace_changes, diff_context, c.fulldiff)
585 586
586 587 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
587 588 force_recache = self.get_recache_flag()
588 589
589 590 cached_diff = None
590 591 if caching_enabled:
591 592 cached_diff = load_cached_diff(cache_file_path)
592 593
593 594 has_proper_commit_cache = (
594 595 cached_diff and cached_diff.get('commits')
595 596 and len(cached_diff.get('commits', [])) == 5
596 597 and cached_diff.get('commits')[0]
597 598 and cached_diff.get('commits')[3])
598 599
599 600 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
600 601 diff_commit_cache = \
601 602 (ancestor_commit, commit_cache, missing_requirements,
602 603 source_commit, target_commit) = cached_diff['commits']
603 604 else:
604 605 # NOTE(marcink): we reach potentially unreachable errors when a PR has
605 606 # merge errors resulting in potentially hidden commits in the shadow repo.
606 607 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
607 608 and _merge_check.merge_response
608 609 maybe_unreachable = maybe_unreachable \
609 610 and _merge_check.merge_response.metadata.get('unresolved_files')
610 611 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
611 612 diff_commit_cache = \
612 613 (ancestor_commit, commit_cache, missing_requirements,
613 614 source_commit, target_commit) = self.get_commits(
614 615 commits_source_repo,
615 616 pull_request_at_ver,
616 617 source_commit,
617 618 source_ref_id,
618 619 source_scm,
619 620 target_commit,
620 621 target_ref_id,
621 622 target_scm,
622 623 maybe_unreachable=maybe_unreachable)
623 624
624 625 # register our commit range
625 626 for comm in commit_cache.values():
626 627 c.commit_ranges.append(comm)
627 628
628 629 c.missing_requirements = missing_requirements
629 630 c.ancestor_commit = ancestor_commit
630 631 c.statuses = source_repo.statuses(
631 632 [x.raw_id for x in c.commit_ranges])
632 633
633 634 # auto collapse if we have more than limit
634 635 collapse_limit = diffs.DiffProcessor._collapse_commits_over
635 636 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
636 637 c.compare_mode = compare
637 638
638 639 # diff_limit is the old behavior, will cut off the whole diff
639 640 # if the limit is applied otherwise will just hide the
640 641 # big files from the front-end
641 642 diff_limit = c.visual.cut_off_limit_diff
642 643 file_limit = c.visual.cut_off_limit_file
643 644
644 645 c.missing_commits = False
645 646 if (c.missing_requirements
646 647 or isinstance(source_commit, EmptyCommit)
647 648 or source_commit == target_commit):
648 649
649 650 c.missing_commits = True
650 651 else:
651 652 c.inline_comments = display_inline_comments
652 653
653 654 use_ancestor = True
654 655 if from_version_normalized != version_normalized:
655 656 use_ancestor = False
656 657
657 658 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
658 659 if not force_recache and has_proper_diff_cache:
659 660 c.diffset = cached_diff['diff']
660 661 else:
661 662 try:
662 663 c.diffset = self._get_diffset(
663 664 c.source_repo.repo_name, commits_source_repo,
664 665 c.ancestor_commit,
665 666 source_ref_id, target_ref_id,
666 667 target_commit, source_commit,
667 668 diff_limit, file_limit, c.fulldiff,
668 669 hide_whitespace_changes, diff_context,
669 670 use_ancestor=use_ancestor
670 671 )
671 672
672 673 # save cached diff
673 674 if caching_enabled:
674 675 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
675 676 except CommitDoesNotExistError:
676 677 log.exception('Failed to generate diffset')
677 678 c.missing_commits = True
678 679
679 680 if not c.missing_commits:
680 681
681 682 c.limited_diff = c.diffset.limited_diff
682 683
683 684 # calculate removed files that are bound to comments
684 685 comment_deleted_files = [
685 686 fname for fname in display_inline_comments
686 687 if fname not in c.diffset.file_stats]
687 688
688 689 c.deleted_files_comments = collections.defaultdict(dict)
689 690 for fname, per_line_comments in display_inline_comments.items():
690 691 if fname in comment_deleted_files:
691 692 c.deleted_files_comments[fname]['stats'] = 0
692 693 c.deleted_files_comments[fname]['comments'] = list()
693 694 for lno, comments in per_line_comments.items():
694 695 c.deleted_files_comments[fname]['comments'].extend(comments)
695 696
696 697 # maybe calculate the range diff
697 698 if c.range_diff_on:
698 699 # TODO(marcink): set whitespace/context
699 700 context_lcl = 3
700 701 ign_whitespace_lcl = False
701 702
702 703 for commit in c.commit_ranges:
703 704 commit2 = commit
704 705 commit1 = commit.first_parent
705 706
706 707 range_diff_cache_file_path = diff_cache_exist(
707 708 cache_path, 'diff', commit.raw_id,
708 709 ign_whitespace_lcl, context_lcl, c.fulldiff)
709 710
710 711 cached_diff = None
711 712 if caching_enabled:
712 713 cached_diff = load_cached_diff(range_diff_cache_file_path)
713 714
714 715 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
715 716 if not force_recache and has_proper_diff_cache:
716 717 diffset = cached_diff['diff']
717 718 else:
718 719 diffset = self._get_range_diffset(
719 720 commits_source_repo, source_repo,
720 721 commit1, commit2, diff_limit, file_limit,
721 722 c.fulldiff, ign_whitespace_lcl, context_lcl
722 723 )
723 724
724 725 # save cached diff
725 726 if caching_enabled:
726 727 cache_diff(range_diff_cache_file_path, diffset, None)
727 728
728 729 c.changes[commit.raw_id] = diffset
729 730
730 731 # this is a hack to properly display links, when creating PR, the
731 732 # compare view and others uses different notation, and
732 733 # compare_commits.mako renders links based on the target_repo.
733 734 # We need to swap that here to generate it properly on the html side
734 735 c.target_repo = c.source_repo
735 736
736 737 c.commit_statuses = ChangesetStatus.STATUSES
737 738
738 739 c.show_version_changes = not pr_closed
739 740 if c.show_version_changes:
740 741 cur_obj = pull_request_at_ver
741 742 prev_obj = prev_pull_request_at_ver
742 743
743 744 old_commit_ids = prev_obj.revisions
744 745 new_commit_ids = cur_obj.revisions
745 746 commit_changes = PullRequestModel()._calculate_commit_id_changes(
746 747 old_commit_ids, new_commit_ids)
747 748 c.commit_changes_summary = commit_changes
748 749
749 750 # calculate the diff for commits between versions
750 751 c.commit_changes = []
751 752
752 753 def mark(cs, fw):
753 754 return list(h.itertools.zip_longest([], cs, fillvalue=fw))
754 755
755 756 for c_type, raw_id in mark(commit_changes.added, 'a') \
756 757 + mark(commit_changes.removed, 'r') \
757 758 + mark(commit_changes.common, 'c'):
758 759
759 760 if raw_id in commit_cache:
760 761 commit = commit_cache[raw_id]
761 762 else:
762 763 try:
763 764 commit = commits_source_repo.get_commit(raw_id)
764 765 except CommitDoesNotExistError:
765 766 # in case we fail extracting still use "dummy" commit
766 767 # for display in commit diff
767 768 commit = h.AttributeDict(
768 769 {'raw_id': raw_id,
769 770 'message': 'EMPTY or MISSING COMMIT'})
770 771 c.commit_changes.append([c_type, commit])
771 772
772 773 # current user review statuses for each version
773 774 c.review_versions = {}
774 775 is_reviewer = PullRequestModel().is_user_reviewer(
775 776 pull_request, self._rhodecode_user)
776 777 if is_reviewer:
777 778 for co in general_comments:
778 779 if co.author.user_id == self._rhodecode_user.user_id:
779 780 status = co.status_change
780 781 if status:
781 782 _ver_pr = status[0].comment.pull_request_version_id
782 783 c.review_versions[_ver_pr] = status[0]
783 784
784 785 return self._get_template_context(c)
785 786
786 787 def get_commits(
787 788 self, commits_source_repo, pull_request_at_ver, source_commit,
788 789 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
789 790 maybe_unreachable=False):
790 791
791 792 commit_cache = collections.OrderedDict()
792 793 missing_requirements = False
793 794
794 795 try:
795 796 pre_load = ["author", "date", "message", "branch", "parents"]
796 797
797 798 pull_request_commits = pull_request_at_ver.revisions
798 799 log.debug('Loading %s commits from %s',
799 800 len(pull_request_commits), commits_source_repo)
800 801
801 802 for rev in pull_request_commits:
802 803 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
803 804 maybe_unreachable=maybe_unreachable)
804 805 commit_cache[comm.raw_id] = comm
805 806
806 807 # Order here matters, we first need to get target, and then
807 808 # the source
808 809 target_commit = commits_source_repo.get_commit(
809 810 commit_id=safe_str(target_ref_id))
810 811
811 812 source_commit = commits_source_repo.get_commit(
812 813 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
813 814 except CommitDoesNotExistError:
814 815 log.warning('Failed to get commit from `{}` repo'.format(
815 816 commits_source_repo), exc_info=True)
816 817 except RepositoryRequirementError:
817 818 log.warning('Failed to get all required data from repo', exc_info=True)
818 819 missing_requirements = True
819 820
820 821 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
821 822
822 823 try:
823 824 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
824 825 except Exception:
825 826 ancestor_commit = None
826 827
827 828 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
828 829
829 830 def assure_not_empty_repo(self):
830 831 _ = self.request.translate
831 832
832 833 try:
833 834 self.db_repo.scm_instance().get_commit()
834 835 except EmptyRepositoryError:
835 836 h.flash(h.literal(_('There are no commits yet')),
836 837 category='warning')
837 838 raise HTTPFound(
838 839 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
839 840
840 841 @LoginRequired()
841 842 @NotAnonymous()
842 843 @HasRepoPermissionAnyDecorator(
843 844 'repository.read', 'repository.write', 'repository.admin')
844 845 def pull_request_new(self):
845 846 _ = self.request.translate
846 847 c = self.load_default_context()
847 848
848 849 self.assure_not_empty_repo()
849 850 source_repo = self.db_repo
850 851
851 852 commit_id = self.request.GET.get('commit')
852 853 branch_ref = self.request.GET.get('branch')
853 854 bookmark_ref = self.request.GET.get('bookmark')
854 855
855 856 try:
856 857 source_repo_data = PullRequestModel().generate_repo_data(
857 858 source_repo, commit_id=commit_id,
858 859 branch=branch_ref, bookmark=bookmark_ref,
859 860 translator=self.request.translate)
860 861 except CommitDoesNotExistError as e:
861 862 log.exception(e)
862 863 h.flash(_('Commit does not exist'), 'error')
863 864 raise HTTPFound(
864 865 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
865 866
866 867 default_target_repo = source_repo
867 868
868 869 if source_repo.parent and c.has_origin_repo_read_perm:
869 870 parent_vcs_obj = source_repo.parent.scm_instance()
870 871 if parent_vcs_obj and not parent_vcs_obj.is_empty():
871 872 # change default if we have a parent repo
872 873 default_target_repo = source_repo.parent
873 874
874 875 target_repo_data = PullRequestModel().generate_repo_data(
875 876 default_target_repo, translator=self.request.translate)
876 877
877 878 selected_source_ref = source_repo_data['refs']['selected_ref']
878 879 title_source_ref = ''
879 880 if selected_source_ref:
880 881 title_source_ref = selected_source_ref.split(':', 2)[1]
881 882 c.default_title = PullRequestModel().generate_pullrequest_title(
882 883 source=source_repo.repo_name,
883 884 source_ref=title_source_ref,
884 885 target=default_target_repo.repo_name
885 886 )
886 887
887 888 c.default_repo_data = {
888 889 'source_repo_name': source_repo.repo_name,
889 890 'source_refs_json': ext_json.str_json(source_repo_data),
890 891 'target_repo_name': default_target_repo.repo_name,
891 892 'target_refs_json': ext_json.str_json(target_repo_data),
892 893 }
893 894 c.default_source_ref = selected_source_ref
894 895
895 896 return self._get_template_context(c)
896 897
897 898 @LoginRequired()
898 899 @NotAnonymous()
899 900 @HasRepoPermissionAnyDecorator(
900 901 'repository.read', 'repository.write', 'repository.admin')
901 902 def pull_request_repo_refs(self):
902 903 self.load_default_context()
903 904 target_repo_name = self.request.matchdict['target_repo_name']
904 905 repo = Repository.get_by_repo_name(target_repo_name)
905 906 if not repo:
906 907 raise HTTPNotFound()
907 908
908 909 target_perm = HasRepoPermissionAny(
909 910 'repository.read', 'repository.write', 'repository.admin')(
910 911 target_repo_name)
911 912 if not target_perm:
912 913 raise HTTPNotFound()
913 914
914 915 return PullRequestModel().generate_repo_data(
915 916 repo, translator=self.request.translate)
916 917
917 918 @LoginRequired()
918 919 @NotAnonymous()
919 920 @HasRepoPermissionAnyDecorator(
920 921 'repository.read', 'repository.write', 'repository.admin')
921 922 def pullrequest_repo_targets(self):
922 923 _ = self.request.translate
923 924 filter_query = self.request.GET.get('query')
924 925
925 926 # get the parents
926 927 parent_target_repos = []
927 928 if self.db_repo.parent:
928 929 parents_query = Repository.query() \
929 930 .order_by(func.length(Repository.repo_name)) \
930 931 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
931 932
932 933 if filter_query:
933 934 ilike_expression = f'%{safe_str(filter_query)}%'
934 935 parents_query = parents_query.filter(
935 936 Repository.repo_name.ilike(ilike_expression))
936 937 parents = parents_query.limit(20).all()
937 938
938 939 for parent in parents:
939 940 parent_vcs_obj = parent.scm_instance()
940 941 if parent_vcs_obj and not parent_vcs_obj.is_empty():
941 942 parent_target_repos.append(parent)
942 943
943 944 # get other forks, and repo itself
944 945 query = Repository.query() \
945 946 .order_by(func.length(Repository.repo_name)) \
946 947 .filter(
947 948 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
948 949 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
949 950 ) \
950 951 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
951 952
952 953 if filter_query:
953 954 ilike_expression = f'%{safe_str(filter_query)}%'
954 955 query = query.filter(Repository.repo_name.ilike(ilike_expression))
955 956
956 957 limit = max(20 - len(parent_target_repos), 5) # not less then 5
957 958 target_repos = query.limit(limit).all()
958 959
959 960 all_target_repos = target_repos + parent_target_repos
960 961
961 962 repos = []
962 963 # This checks permissions to the repositories
963 964 for obj in ScmModel().get_repos(all_target_repos):
964 965 repos.append({
965 966 'id': obj['name'],
966 967 'text': obj['name'],
967 968 'type': 'repo',
968 969 'repo_id': obj['dbrepo']['repo_id'],
969 970 'repo_type': obj['dbrepo']['repo_type'],
970 971 'private': obj['dbrepo']['private'],
971 972
972 973 })
973 974
974 975 data = {
975 976 'more': False,
976 977 'results': [{
977 978 'text': _('Repositories'),
978 979 'children': repos
979 980 }] if repos else []
980 981 }
981 982 return data
982 983
983 984 @classmethod
984 985 def get_comment_ids(cls, post_data):
985 986 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
986 987
987 988 @LoginRequired()
988 989 @NotAnonymous()
989 990 @HasRepoPermissionAnyDecorator(
990 991 'repository.read', 'repository.write', 'repository.admin')
991 992 def pullrequest_comments(self):
992 993 self.load_default_context()
993 994
994 995 pull_request = PullRequest.get_or_404(
995 996 self.request.matchdict['pull_request_id'])
996 997 pull_request_id = pull_request.pull_request_id
997 998 version = self.request.GET.get('version')
998 999
999 1000 _render = self.request.get_partial_renderer(
1000 1001 'rhodecode:templates/base/sidebar.mako')
1001 1002 c = _render.get_call_context()
1002 1003
1003 1004 (pull_request_latest,
1004 1005 pull_request_at_ver,
1005 1006 pull_request_display_obj,
1006 1007 at_version) = PullRequestModel().get_pr_version(
1007 1008 pull_request_id, version=version)
1008 1009 versions = pull_request_display_obj.versions()
1009 1010 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1010 1011 c.versions = versions + [latest_ver]
1011 1012
1012 1013 c.at_version = at_version
1013 1014 c.at_version_num = (at_version
1014 1015 if at_version and at_version != PullRequest.LATEST_VER
1015 1016 else None)
1016 1017
1017 1018 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1018 1019 all_comments = c.inline_comments_flat + c.comments
1019 1020
1020 1021 existing_ids = self.get_comment_ids(self.request.POST)
1021 1022 return _render('comments_table', all_comments, len(all_comments),
1022 1023 existing_ids=existing_ids)
1023 1024
1024 1025 @LoginRequired()
1025 1026 @NotAnonymous()
1026 1027 @HasRepoPermissionAnyDecorator(
1027 1028 'repository.read', 'repository.write', 'repository.admin')
1028 1029 def pullrequest_todos(self):
1029 1030 self.load_default_context()
1030 1031
1031 1032 pull_request = PullRequest.get_or_404(
1032 1033 self.request.matchdict['pull_request_id'])
1033 1034 pull_request_id = pull_request.pull_request_id
1034 1035 version = self.request.GET.get('version')
1035 1036
1036 1037 _render = self.request.get_partial_renderer(
1037 1038 'rhodecode:templates/base/sidebar.mako')
1038 1039 c = _render.get_call_context()
1039 1040 (pull_request_latest,
1040 1041 pull_request_at_ver,
1041 1042 pull_request_display_obj,
1042 1043 at_version) = PullRequestModel().get_pr_version(
1043 1044 pull_request_id, version=version)
1044 1045 versions = pull_request_display_obj.versions()
1045 1046 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1046 1047 c.versions = versions + [latest_ver]
1047 1048
1048 1049 c.at_version = at_version
1049 1050 c.at_version_num = (at_version
1050 1051 if at_version and at_version != PullRequest.LATEST_VER
1051 1052 else None)
1052 1053
1053 1054 c.unresolved_comments = CommentsModel() \
1054 1055 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1055 1056 c.resolved_comments = CommentsModel() \
1056 1057 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1057 1058
1058 1059 all_comments = c.unresolved_comments + c.resolved_comments
1059 1060 existing_ids = self.get_comment_ids(self.request.POST)
1060 1061 return _render('comments_table', all_comments, len(c.unresolved_comments),
1061 1062 todo_comments=True, existing_ids=existing_ids)
1062 1063
1063 1064 @LoginRequired()
1064 1065 @NotAnonymous()
1065 1066 @HasRepoPermissionAnyDecorator(
1066 1067 'repository.read', 'repository.write', 'repository.admin')
1067 1068 def pullrequest_drafts(self):
1068 1069 self.load_default_context()
1069 1070
1070 1071 pull_request = PullRequest.get_or_404(
1071 1072 self.request.matchdict['pull_request_id'])
1072 1073 pull_request_id = pull_request.pull_request_id
1073 1074 version = self.request.GET.get('version')
1074 1075
1075 1076 _render = self.request.get_partial_renderer(
1076 1077 'rhodecode:templates/base/sidebar.mako')
1077 1078 c = _render.get_call_context()
1078 1079
1079 1080 (pull_request_latest,
1080 1081 pull_request_at_ver,
1081 1082 pull_request_display_obj,
1082 1083 at_version) = PullRequestModel().get_pr_version(
1083 1084 pull_request_id, version=version)
1084 1085 versions = pull_request_display_obj.versions()
1085 1086 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1086 1087 c.versions = versions + [latest_ver]
1087 1088
1088 1089 c.at_version = at_version
1089 1090 c.at_version_num = (at_version
1090 1091 if at_version and at_version != PullRequest.LATEST_VER
1091 1092 else None)
1092 1093
1093 1094 c.draft_comments = CommentsModel() \
1094 1095 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1095 1096
1096 1097 all_comments = c.draft_comments
1097 1098
1098 1099 existing_ids = self.get_comment_ids(self.request.POST)
1099 1100 return _render('comments_table', all_comments, len(all_comments),
1100 1101 existing_ids=existing_ids, draft_comments=True)
1101 1102
1102 1103 @LoginRequired()
1103 1104 @NotAnonymous()
1104 1105 @HasRepoPermissionAnyDecorator(
1105 1106 'repository.read', 'repository.write', 'repository.admin')
1106 1107 @CSRFRequired()
1107 1108 def pull_request_create(self):
1108 1109 _ = self.request.translate
1109 1110 self.assure_not_empty_repo()
1110 1111 self.load_default_context()
1111 1112
1112 1113 controls = peppercorn.parse(self.request.POST.items())
1113 1114
1114 1115 try:
1115 1116 form = PullRequestForm(
1116 1117 self.request.translate, self.db_repo.repo_id)()
1117 1118 _form = form.to_python(controls)
1118 1119 except formencode.Invalid as errors:
1119 1120 if errors.error_dict.get('revisions'):
1120 1121 msg = 'Revisions: {}'.format(errors.error_dict['revisions'])
1121 1122 elif errors.error_dict.get('pullrequest_title'):
1122 1123 msg = errors.error_dict.get('pullrequest_title')
1123 1124 else:
1124 1125 msg = _('Error creating pull request: {}').format(errors)
1125 1126 log.exception(msg)
1126 1127 h.flash(msg, 'error')
1127 1128
1128 1129 # would rather just go back to form ...
1129 1130 raise HTTPFound(
1130 1131 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1131 1132
1132 1133 source_repo = _form['source_repo']
1133 1134 source_ref = _form['source_ref']
1134 1135 target_repo = _form['target_repo']
1135 1136 target_ref = _form['target_ref']
1136 1137 commit_ids = _form['revisions'][::-1]
1137 1138 common_ancestor_id = _form['common_ancestor']
1138 1139
1139 1140 # find the ancestor for this pr
1140 1141 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1141 1142 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1142 1143
1143 1144 if not (source_db_repo or target_db_repo):
1144 1145 h.flash(_('source_repo or target repo not found'), category='error')
1145 1146 raise HTTPFound(
1146 1147 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1147 1148
1148 1149 # re-check permissions again here
1149 1150 # source_repo we must have read permissions
1150 1151
1151 1152 source_perm = HasRepoPermissionAny(
1152 1153 'repository.read', 'repository.write', 'repository.admin')(
1153 1154 source_db_repo.repo_name)
1154 1155 if not source_perm:
1155 1156 msg = _('Not Enough permissions to source repo `{}`.'.format(
1156 1157 source_db_repo.repo_name))
1157 1158 h.flash(msg, category='error')
1158 1159 # copy the args back to redirect
1159 1160 org_query = self.request.GET.mixed()
1160 1161 raise HTTPFound(
1161 1162 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1162 1163 _query=org_query))
1163 1164
1164 1165 # target repo we must have read permissions, and also later on
1165 1166 # we want to check branch permissions here
1166 1167 target_perm = HasRepoPermissionAny(
1167 1168 'repository.read', 'repository.write', 'repository.admin')(
1168 1169 target_db_repo.repo_name)
1169 1170 if not target_perm:
1170 1171 msg = _('Not Enough permissions to target repo `{}`.'.format(
1171 1172 target_db_repo.repo_name))
1172 1173 h.flash(msg, category='error')
1173 1174 # copy the args back to redirect
1174 1175 org_query = self.request.GET.mixed()
1175 1176 raise HTTPFound(
1176 1177 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1177 1178 _query=org_query))
1178 1179
1179 1180 source_scm = source_db_repo.scm_instance()
1180 1181 target_scm = target_db_repo.scm_instance()
1181 1182
1182 1183 source_ref_obj = unicode_to_reference(source_ref)
1183 1184 target_ref_obj = unicode_to_reference(target_ref)
1184 1185
1185 1186 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1186 1187 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1187 1188
1188 1189 ancestor = source_scm.get_common_ancestor(
1189 1190 source_commit.raw_id, target_commit.raw_id, target_scm)
1190 1191
1191 1192 # recalculate target ref based on ancestor
1192 1193 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1193 1194
1194 1195 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1195 1196 PullRequestModel().get_reviewer_functions()
1196 1197
1197 1198 # recalculate reviewers logic, to make sure we can validate this
1198 1199 reviewer_rules = get_default_reviewers_data(
1199 1200 self._rhodecode_db_user,
1200 1201 source_db_repo,
1201 1202 source_ref_obj,
1202 1203 target_db_repo,
1203 1204 target_ref_obj,
1204 1205 include_diff_info=False)
1205 1206
1206 1207 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1207 1208 observers = validate_observers(_form['observer_members'], reviewer_rules)
1208 1209
1209 1210 pullrequest_title = _form['pullrequest_title']
1210 1211 title_source_ref = source_ref_obj.name
1211 1212 if not pullrequest_title:
1212 1213 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1213 1214 source=source_repo,
1214 1215 source_ref=title_source_ref,
1215 1216 target=target_repo
1216 1217 )
1217 1218
1218 1219 description = _form['pullrequest_desc']
1219 1220 description_renderer = _form['description_renderer']
1220 1221
1221 1222 try:
1222 1223 pull_request = PullRequestModel().create(
1223 1224 created_by=self._rhodecode_user.user_id,
1224 1225 source_repo=source_repo,
1225 1226 source_ref=source_ref,
1226 1227 target_repo=target_repo,
1227 1228 target_ref=target_ref,
1228 1229 revisions=commit_ids,
1229 1230 common_ancestor_id=common_ancestor_id,
1230 1231 reviewers=reviewers,
1231 1232 observers=observers,
1232 1233 title=pullrequest_title,
1233 1234 description=description,
1234 1235 description_renderer=description_renderer,
1235 1236 reviewer_data=reviewer_rules,
1236 1237 auth_user=self._rhodecode_user
1237 1238 )
1238 1239 Session().commit()
1239 1240
1240 1241 h.flash(_('Successfully opened new pull request'),
1241 1242 category='success')
1242 1243 except Exception:
1243 1244 msg = _('Error occurred during creation of this pull request.')
1244 1245 log.exception(msg)
1245 1246 h.flash(msg, category='error')
1246 1247
1247 1248 # copy the args back to redirect
1248 1249 org_query = self.request.GET.mixed()
1249 1250 raise HTTPFound(
1250 1251 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1251 1252 _query=org_query))
1252 1253
1253 1254 raise HTTPFound(
1254 1255 h.route_path('pullrequest_show', repo_name=target_repo,
1255 1256 pull_request_id=pull_request.pull_request_id))
1256 1257
1257 1258 @LoginRequired()
1258 1259 @NotAnonymous()
1259 1260 @HasRepoPermissionAnyDecorator(
1260 1261 'repository.read', 'repository.write', 'repository.admin')
1261 1262 @CSRFRequired()
1262 1263 def pull_request_update(self):
1263 1264 pull_request = PullRequest.get_or_404(
1264 1265 self.request.matchdict['pull_request_id'])
1265 1266 _ = self.request.translate
1266 1267
1267 1268 c = self.load_default_context()
1268 1269 redirect_url = None
1269 1270 # we do this check as first, because we want to know ASAP in the flow that
1270 1271 # pr is updating currently
1271 1272 is_state_changing = pull_request.is_state_changing()
1272 1273
1273 1274 if pull_request.is_closed():
1274 1275 log.debug('update: forbidden because pull request is closed')
1275 1276 msg = _('Cannot update closed pull requests.')
1276 1277 h.flash(msg, category='error')
1277 1278 return {'response': True,
1278 1279 'redirect_url': redirect_url}
1279 1280
1280 1281 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281 1282
1282 1283 # only owner or admin can update it
1283 1284 allowed_to_update = PullRequestModel().check_user_update(
1284 1285 pull_request, self._rhodecode_user)
1285 1286
1286 1287 if allowed_to_update:
1287 1288 controls = peppercorn.parse(self.request.POST.items())
1288 1289 force_refresh = str2bool(self.request.POST.get('force_refresh', 'false'))
1289 1290 do_update_commits = str2bool(self.request.POST.get('update_commits', 'false'))
1290 1291
1291 1292 if 'review_members' in controls:
1292 1293 self._update_reviewers(
1293 1294 c,
1294 1295 pull_request, controls['review_members'],
1295 1296 pull_request.reviewer_data,
1296 1297 PullRequestReviewers.ROLE_REVIEWER)
1297 1298 elif 'observer_members' in controls:
1298 1299 self._update_reviewers(
1299 1300 c,
1300 1301 pull_request, controls['observer_members'],
1301 1302 pull_request.reviewer_data,
1302 1303 PullRequestReviewers.ROLE_OBSERVER)
1303 1304 elif do_update_commits:
1304 1305 if is_state_changing:
1305 1306 log.debug('commits update: forbidden because pull request is in state %s',
1306 1307 pull_request.pull_request_state)
1307 1308 msg = _('Cannot update pull requests commits in state other than `{}`. '
1308 1309 'Current state is: `{}`').format(
1309 1310 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1310 1311 h.flash(msg, category='error')
1311 1312 return {'response': True,
1312 1313 'redirect_url': redirect_url}
1313 1314
1314 1315 self._update_commits(c, pull_request)
1315 1316 if force_refresh:
1316 1317 redirect_url = h.route_path(
1317 1318 'pullrequest_show', repo_name=self.db_repo_name,
1318 1319 pull_request_id=pull_request.pull_request_id,
1319 1320 _query={"force_refresh": 1})
1320 1321 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1321 1322 self._edit_pull_request(pull_request)
1322 1323 else:
1323 1324 log.error('Unhandled update data.')
1324 1325 raise HTTPBadRequest()
1325 1326
1326 1327 return {'response': True,
1327 1328 'redirect_url': redirect_url}
1328 1329 raise HTTPForbidden()
1329 1330
1330 1331 def _edit_pull_request(self, pull_request):
1331 1332 """
1332 1333 Edit title and description
1333 1334 """
1334 1335 _ = self.request.translate
1335 1336
1336 1337 try:
1337 1338 PullRequestModel().edit(
1338 1339 pull_request,
1339 1340 self.request.POST.get('title'),
1340 1341 self.request.POST.get('description'),
1341 1342 self.request.POST.get('description_renderer'),
1342 1343 self._rhodecode_user)
1343 1344 except ValueError:
1344 1345 msg = _('Cannot update closed pull requests.')
1345 1346 h.flash(msg, category='error')
1346 1347 return
1347 1348 else:
1348 1349 Session().commit()
1349 1350
1350 1351 msg = _('Pull request title & description updated.')
1351 1352 h.flash(msg, category='success')
1352 1353 return
1353 1354
1354 1355 def _update_commits(self, c, pull_request):
1355 1356 _ = self.request.translate
1356 1357 log.debug('pull-request: running update commits actions')
1357 1358
1358 1359 @retry(exception=Exception, n_tries=3, delay=2)
1359 1360 def commits_update():
1360 1361 return PullRequestModel().update_commits(
1361 1362 pull_request, self._rhodecode_db_user)
1362 1363
1363 1364 with pull_request.set_state(PullRequest.STATE_UPDATING):
1364 1365 resp = commits_update() # retry x3
1365 1366
1366 1367 if resp.executed:
1367 1368
1368 1369 if resp.target_changed and resp.source_changed:
1369 1370 changed = 'target and source repositories'
1370 1371 elif resp.target_changed and not resp.source_changed:
1371 1372 changed = 'target repository'
1372 1373 elif not resp.target_changed and resp.source_changed:
1373 1374 changed = 'source repository'
1374 1375 else:
1375 1376 changed = 'nothing'
1376 1377
1377 1378 msg = _('Pull request updated to "{source_commit_id}" with '
1378 1379 '{count_added} added, {count_removed} removed commits. '
1379 1380 'Source of changes: {change_source}.')
1380 1381 msg = msg.format(
1381 1382 source_commit_id=pull_request.source_ref_parts.commit_id,
1382 1383 count_added=len(resp.changes.added),
1383 1384 count_removed=len(resp.changes.removed),
1384 1385 change_source=changed)
1385 1386 h.flash(msg, category='success')
1386 1387 channelstream.pr_update_channelstream_push(
1387 1388 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1388 1389 else:
1389 1390 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1390 1391 warning_reasons = [
1391 1392 UpdateFailureReason.NO_CHANGE,
1392 1393 UpdateFailureReason.WRONG_REF_TYPE,
1393 1394 ]
1394 1395 category = 'warning' if resp.reason in warning_reasons else 'error'
1395 1396 h.flash(msg, category=category)
1396 1397
1397 1398 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1398 1399 _ = self.request.translate
1399 1400
1400 1401 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1401 1402 PullRequestModel().get_reviewer_functions()
1402 1403
1403 1404 if role == PullRequestReviewers.ROLE_REVIEWER:
1404 1405 try:
1405 1406 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1406 1407 except ValueError as e:
1407 1408 log.error(f'Reviewers Validation: {e}')
1408 1409 h.flash(e, category='error')
1409 1410 return
1410 1411
1411 1412 old_calculated_status = pull_request.calculated_review_status()
1412 1413 PullRequestModel().update_reviewers(
1413 1414 pull_request, reviewers, self._rhodecode_db_user)
1414 1415
1415 1416 Session().commit()
1416 1417
1417 1418 msg = _('Pull request reviewers updated.')
1418 1419 h.flash(msg, category='success')
1419 1420 channelstream.pr_update_channelstream_push(
1420 1421 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1421 1422
1422 1423 # trigger status changed if change in reviewers changes the status
1423 1424 calculated_status = pull_request.calculated_review_status()
1424 1425 if old_calculated_status != calculated_status:
1425 1426 PullRequestModel().trigger_pull_request_hook(
1426 1427 pull_request, self._rhodecode_user, 'review_status_change',
1427 1428 data={'status': calculated_status})
1428 1429
1429 1430 elif role == PullRequestReviewers.ROLE_OBSERVER:
1430 1431 try:
1431 1432 observers = validate_observers(review_members, reviewer_rules)
1432 1433 except ValueError as e:
1433 1434 log.error(f'Observers Validation: {e}')
1434 1435 h.flash(e, category='error')
1435 1436 return
1436 1437
1437 1438 PullRequestModel().update_observers(
1438 1439 pull_request, observers, self._rhodecode_db_user)
1439 1440
1440 1441 Session().commit()
1441 1442 msg = _('Pull request observers updated.')
1442 1443 h.flash(msg, category='success')
1443 1444 channelstream.pr_update_channelstream_push(
1444 1445 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1445 1446
1446 1447 @LoginRequired()
1447 1448 @NotAnonymous()
1448 1449 @HasRepoPermissionAnyDecorator(
1449 1450 'repository.read', 'repository.write', 'repository.admin')
1450 1451 @CSRFRequired()
1451 1452 def pull_request_merge(self):
1452 1453 """
1453 1454 Merge will perform a server-side merge of the specified
1454 1455 pull request, if the pull request is approved and mergeable.
1455 1456 After successful merging, the pull request is automatically
1456 1457 closed, with a relevant comment.
1457 1458 """
1458 1459 pull_request = PullRequest.get_or_404(
1459 1460 self.request.matchdict['pull_request_id'])
1460 1461 _ = self.request.translate
1461 1462
1462 1463 if pull_request.is_state_changing():
1463 1464 log.debug('show: forbidden because pull request is in state %s',
1464 1465 pull_request.pull_request_state)
1465 1466 msg = _('Cannot merge pull requests in state other than `{}`. '
1466 1467 'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1467 1468 pull_request.pull_request_state)
1468 1469 h.flash(msg, category='error')
1469 1470 raise HTTPFound(
1470 1471 h.route_path('pullrequest_show',
1471 1472 repo_name=pull_request.target_repo.repo_name,
1472 1473 pull_request_id=pull_request.pull_request_id))
1473 1474
1474 1475 self.load_default_context()
1475 1476
1476 1477 with pull_request.set_state(PullRequest.STATE_UPDATING):
1477 1478 check = MergeCheck.validate(
1478 1479 pull_request, auth_user=self._rhodecode_user,
1479 1480 translator=self.request.translate)
1480 1481 merge_possible = not check.failed
1481 1482
1482 1483 for err_type, error_msg in check.errors:
1483 1484 h.flash(error_msg, category=err_type)
1484 1485
1485 1486 if merge_possible:
1486 1487 log.debug("Pre-conditions checked, trying to merge.")
1487 1488 extras = vcs_operation_context(
1488 1489 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1489 1490 username=self._rhodecode_db_user.username, action='push',
1490 1491 scm=pull_request.target_repo.repo_type)
1491 1492 with pull_request.set_state(PullRequest.STATE_UPDATING):
1492 1493 self._merge_pull_request(
1493 1494 pull_request, self._rhodecode_db_user, extras)
1494 1495 else:
1495 1496 log.debug("Pre-conditions failed, NOT merging.")
1496 1497
1497 1498 raise HTTPFound(
1498 1499 h.route_path('pullrequest_show',
1499 1500 repo_name=pull_request.target_repo.repo_name,
1500 1501 pull_request_id=pull_request.pull_request_id))
1501 1502
1502 1503 def _merge_pull_request(self, pull_request, user, extras):
1503 1504 _ = self.request.translate
1504 1505 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1505 1506
1506 1507 if merge_resp.executed:
1507 1508 log.debug("The merge was successful, closing the pull request.")
1508 1509 PullRequestModel().close_pull_request(
1509 1510 pull_request.pull_request_id, user)
1510 1511 Session().commit()
1511 1512 msg = _('Pull request was successfully merged and closed.')
1512 1513 h.flash(msg, category='success')
1513 1514 else:
1514 1515 log.debug(
1515 1516 "The merge was not successful. Merge response: %s", merge_resp)
1516 1517 msg = merge_resp.merge_status_message
1517 1518 h.flash(msg, category='error')
1518 1519
1519 1520 @LoginRequired()
1520 1521 @NotAnonymous()
1521 1522 @HasRepoPermissionAnyDecorator(
1522 1523 'repository.read', 'repository.write', 'repository.admin')
1523 1524 @CSRFRequired()
1524 1525 def pull_request_delete(self):
1525 1526 _ = self.request.translate
1526 1527
1527 1528 pull_request = PullRequest.get_or_404(
1528 1529 self.request.matchdict['pull_request_id'])
1529 1530 self.load_default_context()
1530 1531
1531 1532 pr_closed = pull_request.is_closed()
1532 1533 allowed_to_delete = PullRequestModel().check_user_delete(
1533 1534 pull_request, self._rhodecode_user) and not pr_closed
1534 1535
1535 1536 # only owner can delete it !
1536 1537 if allowed_to_delete:
1537 1538 PullRequestModel().delete(pull_request, self._rhodecode_user)
1538 1539 Session().commit()
1539 1540 h.flash(_('Successfully deleted pull request'),
1540 1541 category='success')
1541 1542 raise HTTPFound(h.route_path('pullrequest_show_all',
1542 1543 repo_name=self.db_repo_name))
1543 1544
1544 1545 log.warning('user %s tried to delete pull request without access',
1545 1546 self._rhodecode_user)
1546 1547 raise HTTPNotFound()
1547 1548
1548 1549 def _pull_request_comments_create(self, pull_request, comments):
1549 1550 _ = self.request.translate
1550 1551 data = {}
1551 1552 if not comments:
1552 1553 return
1553 1554 pull_request_id = pull_request.pull_request_id
1554 1555
1555 1556 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1556 1557
1557 1558 for entry in comments:
1558 1559 c = self.load_default_context()
1559 1560 comment_type = entry['comment_type']
1560 1561 text = entry['text']
1561 1562 status = entry['status']
1562 1563 is_draft = str2bool(entry['is_draft'])
1563 1564 resolves_comment_id = entry['resolves_comment_id']
1564 1565 close_pull_request = entry['close_pull_request']
1565 1566 f_path = entry['f_path']
1566 1567 line_no = entry['line']
1567 1568 target_elem_id = f'file-{h.safeid(h.safe_str(f_path))}'
1568 1569
1569 1570 # the logic here should work like following, if we submit close
1570 1571 # pr comment, use `close_pull_request_with_comment` function
1571 1572 # else handle regular comment logic
1572 1573
1573 1574 if close_pull_request:
1574 1575 # only owner or admin or person with write permissions
1575 1576 allowed_to_close = PullRequestModel().check_user_update(
1576 1577 pull_request, self._rhodecode_user)
1577 1578 if not allowed_to_close:
1578 1579 log.debug('comment: forbidden because not allowed to close '
1579 1580 'pull request %s', pull_request_id)
1580 1581 raise HTTPForbidden()
1581 1582
1582 1583 # This also triggers `review_status_change`
1583 1584 comment, status = PullRequestModel().close_pull_request_with_comment(
1584 1585 pull_request, self._rhodecode_user, self.db_repo, message=text,
1585 1586 auth_user=self._rhodecode_user)
1586 1587 Session().flush()
1587 1588 is_inline = comment.is_inline
1588 1589
1589 1590 PullRequestModel().trigger_pull_request_hook(
1590 1591 pull_request, self._rhodecode_user, 'comment',
1591 1592 data={'comment': comment})
1592 1593
1593 1594 else:
1594 1595 # regular comment case, could be inline, or one with status.
1595 1596 # for that one we check also permissions
1596 1597 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1597 1598 allowed_to_change_status = PullRequestModel().check_user_change_status(
1598 1599 pull_request, self._rhodecode_user) and not is_draft
1599 1600
1600 1601 if status and allowed_to_change_status:
1601 1602 message = (_('Status change %(transition_icon)s %(status)s')
1602 1603 % {'transition_icon': '>',
1603 1604 'status': ChangesetStatus.get_status_lbl(status)})
1604 1605 text = text or message
1605 1606
1606 1607 comment = CommentsModel().create(
1607 1608 text=text,
1608 1609 repo=self.db_repo.repo_id,
1609 1610 user=self._rhodecode_user.user_id,
1610 1611 pull_request=pull_request,
1611 1612 f_path=f_path,
1612 1613 line_no=line_no,
1613 1614 status_change=(ChangesetStatus.get_status_lbl(status)
1614 1615 if status and allowed_to_change_status else None),
1615 1616 status_change_type=(status
1616 1617 if status and allowed_to_change_status else None),
1617 1618 comment_type=comment_type,
1618 1619 is_draft=is_draft,
1619 1620 resolves_comment_id=resolves_comment_id,
1620 1621 auth_user=self._rhodecode_user,
1621 1622 send_email=not is_draft, # skip notification for draft comments
1622 1623 )
1623 1624 is_inline = comment.is_inline
1624 1625
1625 1626 if allowed_to_change_status:
1626 1627 # calculate old status before we change it
1627 1628 old_calculated_status = pull_request.calculated_review_status()
1628 1629
1629 1630 # get status if set !
1630 1631 if status:
1631 1632 ChangesetStatusModel().set_status(
1632 1633 self.db_repo.repo_id,
1633 1634 status,
1634 1635 self._rhodecode_user.user_id,
1635 1636 comment,
1636 1637 pull_request=pull_request
1637 1638 )
1638 1639
1639 1640 Session().flush()
1640 1641 # this is somehow required to get access to some relationship
1641 1642 # loaded on comment
1642 1643 Session().refresh(comment)
1643 1644
1644 1645 # skip notifications for drafts
1645 1646 if not is_draft:
1646 1647 PullRequestModel().trigger_pull_request_hook(
1647 1648 pull_request, self._rhodecode_user, 'comment',
1648 1649 data={'comment': comment})
1649 1650
1650 1651 # we now calculate the status of pull request, and based on that
1651 1652 # calculation we set the commits status
1652 1653 calculated_status = pull_request.calculated_review_status()
1653 1654 if old_calculated_status != calculated_status:
1654 1655 PullRequestModel().trigger_pull_request_hook(
1655 1656 pull_request, self._rhodecode_user, 'review_status_change',
1656 1657 data={'status': calculated_status})
1657 1658
1658 1659 comment_id = comment.comment_id
1659 1660 data[comment_id] = {
1660 1661 'target_id': target_elem_id
1661 1662 }
1662 1663 Session().flush()
1663 1664
1664 1665 c.co = comment
1665 1666 c.at_version_num = None
1666 1667 c.is_new = True
1667 1668 rendered_comment = render(
1668 1669 'rhodecode:templates/changeset/changeset_comment_block.mako',
1669 1670 self._get_template_context(c), self.request)
1670 1671
1671 1672 data[comment_id].update(comment.get_dict())
1672 1673 data[comment_id].update({'rendered_text': rendered_comment})
1673 1674
1674 1675 Session().commit()
1675 1676
1676 1677 # skip channelstream for draft comments
1677 1678 if not all_drafts:
1678 1679 comment_broadcast_channel = channelstream.comment_channel(
1679 1680 self.db_repo_name, pull_request_obj=pull_request)
1680 1681
1681 1682 comment_data = data
1682 1683 posted_comment_type = 'inline' if is_inline else 'general'
1683 1684 if len(data) == 1:
1684 1685 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1685 1686 else:
1686 1687 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1687 1688
1688 1689 channelstream.comment_channelstream_push(
1689 1690 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1690 1691 comment_data=comment_data)
1691 1692
1692 1693 return data
1693 1694
1694 1695 @LoginRequired()
1695 1696 @NotAnonymous()
1696 1697 @HasRepoPermissionAnyDecorator(
1697 1698 'repository.read', 'repository.write', 'repository.admin')
1698 1699 @CSRFRequired()
1699 1700 def pull_request_comment_create(self):
1700 1701 _ = self.request.translate
1701 1702
1702 1703 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1703 1704
1704 1705 if pull_request.is_closed():
1705 1706 log.debug('comment: forbidden because pull request is closed')
1706 1707 raise HTTPForbidden()
1707 1708
1708 1709 allowed_to_comment = PullRequestModel().check_user_comment(
1709 1710 pull_request, self._rhodecode_user)
1710 1711 if not allowed_to_comment:
1711 1712 log.debug('comment: forbidden because pull request is from forbidden repo')
1712 1713 raise HTTPForbidden()
1713 1714
1714 1715 comment_data = {
1715 1716 'comment_type': self.request.POST.get('comment_type'),
1716 1717 'text': self.request.POST.get('text'),
1717 1718 'status': self.request.POST.get('changeset_status', None),
1718 1719 'is_draft': self.request.POST.get('draft'),
1719 1720 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1720 1721 'close_pull_request': self.request.POST.get('close_pull_request'),
1721 1722 'f_path': self.request.POST.get('f_path'),
1722 1723 'line': self.request.POST.get('line'),
1723 1724 }
1724 1725 data = self._pull_request_comments_create(pull_request, [comment_data])
1725 1726
1726 1727 return data
1727 1728
1728 1729 @LoginRequired()
1729 1730 @NotAnonymous()
1730 1731 @HasRepoPermissionAnyDecorator(
1731 1732 'repository.read', 'repository.write', 'repository.admin')
1732 1733 @CSRFRequired()
1733 1734 def pull_request_comment_delete(self):
1734 1735 pull_request = PullRequest.get_or_404(
1735 1736 self.request.matchdict['pull_request_id'])
1736 1737
1737 1738 comment = ChangesetComment.get_or_404(
1738 1739 self.request.matchdict['comment_id'])
1739 1740 comment_id = comment.comment_id
1740 1741
1741 1742 if comment.immutable:
1742 1743 # don't allow deleting comments that are immutable
1743 1744 raise HTTPForbidden()
1744 1745
1745 1746 if pull_request.is_closed():
1746 1747 log.debug('comment: forbidden because pull request is closed')
1747 1748 raise HTTPForbidden()
1748 1749
1749 1750 if not comment:
1750 1751 log.debug('Comment with id:%s not found, skipping', comment_id)
1751 1752 # comment already deleted in another call probably
1752 1753 return True
1753 1754
1754 1755 if comment.pull_request.is_closed():
1755 1756 # don't allow deleting comments on closed pull request
1756 1757 raise HTTPForbidden()
1757 1758
1758 1759 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1759 1760 super_admin = h.HasPermissionAny('hg.admin')()
1760 1761 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1761 1762 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1762 1763 comment_repo_admin = is_repo_admin and is_repo_comment
1763 1764
1764 1765 if comment.draft and not comment_owner:
1765 1766 # We never allow to delete draft comments for other than owners
1766 1767 raise HTTPNotFound()
1767 1768
1768 1769 if super_admin or comment_owner or comment_repo_admin:
1769 1770 old_calculated_status = comment.pull_request.calculated_review_status()
1770 1771 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1771 1772 Session().commit()
1772 1773 calculated_status = comment.pull_request.calculated_review_status()
1773 1774 if old_calculated_status != calculated_status:
1774 1775 PullRequestModel().trigger_pull_request_hook(
1775 1776 comment.pull_request, self._rhodecode_user, 'review_status_change',
1776 1777 data={'status': calculated_status})
1777 1778 return True
1778 1779 else:
1779 1780 log.warning('No permissions for user %s to delete comment_id: %s',
1780 1781 self._rhodecode_db_user, comment_id)
1781 1782 raise HTTPNotFound()
1782 1783
1783 1784 @LoginRequired()
1784 1785 @NotAnonymous()
1785 1786 @HasRepoPermissionAnyDecorator(
1786 1787 'repository.read', 'repository.write', 'repository.admin')
1787 1788 @CSRFRequired()
1788 1789 def pull_request_comment_edit(self):
1789 1790 self.load_default_context()
1790 1791
1791 1792 pull_request = PullRequest.get_or_404(
1792 1793 self.request.matchdict['pull_request_id']
1793 1794 )
1794 1795 comment = ChangesetComment.get_or_404(
1795 1796 self.request.matchdict['comment_id']
1796 1797 )
1797 1798 comment_id = comment.comment_id
1798 1799
1799 1800 if comment.immutable:
1800 1801 # don't allow deleting comments that are immutable
1801 1802 raise HTTPForbidden()
1802 1803
1803 1804 if pull_request.is_closed():
1804 1805 log.debug('comment: forbidden because pull request is closed')
1805 1806 raise HTTPForbidden()
1806 1807
1807 1808 if comment.pull_request.is_closed():
1808 1809 # don't allow deleting comments on closed pull request
1809 1810 raise HTTPForbidden()
1810 1811
1811 1812 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1812 1813 super_admin = h.HasPermissionAny('hg.admin')()
1813 1814 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1814 1815 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1815 1816 comment_repo_admin = is_repo_admin and is_repo_comment
1816 1817
1817 1818 if super_admin or comment_owner or comment_repo_admin:
1818 1819 text = self.request.POST.get('text')
1819 1820 version = self.request.POST.get('version')
1820 1821 if text == comment.text:
1821 1822 log.warning(
1822 1823 'Comment(PR): '
1823 1824 'Trying to create new version '
1824 1825 'with the same comment body {}'.format(
1825 1826 comment_id,
1826 1827 )
1827 1828 )
1828 1829 raise HTTPNotFound()
1829 1830
1830 1831 if version.isdigit():
1831 1832 version = int(version)
1832 1833 else:
1833 1834 log.warning(
1834 1835 'Comment(PR): Wrong version type {} {} '
1835 1836 'for comment {}'.format(
1836 1837 version,
1837 1838 type(version),
1838 1839 comment_id,
1839 1840 )
1840 1841 )
1841 1842 raise HTTPNotFound()
1842 1843
1843 1844 try:
1844 1845 comment_history = CommentsModel().edit(
1845 1846 comment_id=comment_id,
1846 1847 text=text,
1847 1848 auth_user=self._rhodecode_user,
1848 1849 version=version,
1849 1850 )
1850 1851 except CommentVersionMismatch:
1851 1852 raise HTTPConflict()
1852 1853
1853 1854 if not comment_history:
1854 1855 raise HTTPNotFound()
1855 1856
1856 1857 Session().commit()
1857 1858 if not comment.draft:
1858 1859 PullRequestModel().trigger_pull_request_hook(
1859 1860 pull_request, self._rhodecode_user, 'comment_edit',
1860 1861 data={'comment': comment})
1861 1862
1862 1863 return {
1863 1864 'comment_history_id': comment_history.comment_history_id,
1864 1865 'comment_id': comment.comment_id,
1865 1866 'comment_version': comment_history.version,
1866 1867 'comment_author_username': comment_history.author.username,
1867 1868 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16, request=self.request),
1868 1869 'comment_created_on': h.age_component(comment_history.created_on,
1869 1870 time_is_local=True),
1870 1871 }
1871 1872 else:
1872 1873 log.warning('No permissions for user %s to edit comment_id: %s',
1873 1874 self._rhodecode_db_user, comment_id)
1874 1875 raise HTTPNotFound()
@@ -1,164 +1,170 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading">
5 5 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
6 6 </div>
7 7
8 8 <div class="panel-body panel-body-min-height">
9 9 <div class="title">
10 10 <ul class="button-links">
11 11 <li><a class="btn ${h.is_active('all', c.selected_filter)}"
12 12 href="${h.route_path('my_account_pullrequests', _query={})}">
13 13 ${_('Open')}
14 14 </a>
15 15 </li>
16 16 <li><a class="btn ${h.is_active('all_closed', c.selected_filter)}"
17 17 href="${h.route_path('my_account_pullrequests', _query={'closed':1})}">
18 18 ${_('All + Closed')}
19 19 </a>
20 20 </li>
21 21 <li><a class="btn ${h.is_active('awaiting_my_review', c.selected_filter)}"
22 22 href="${h.route_path('my_account_pullrequests', _query={'awaiting_my_review':1})}">
23 23
24 24 ${_('Awaiting my review')}
25 25 </a>
26 26 </li>
27 27 </ul>
28 28
29 29 <div class="grid-quick-filter">
30 30 <ul class="grid-filter-box">
31 31 <li class="grid-filter-box-icon">
32 32 <i class="icon-search"></i>
33 33 </li>
34 34 <li class="grid-filter-box-input">
35 35 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter"
36 36 placeholder="${_('quick filter...')}" value=""/>
37 37 </li>
38 38 </ul>
39 39 </div>
40 40 </div>
41 41
42 42 <table id="pull_request_list_table" class="rctable table-bordered"></table>
43 43 </div>
44 44 </div>
45 45
46 46 <script type="text/javascript">
47 47 $(document).ready(function () {
48 48
49 49 var $pullRequestListTable = $('#pull_request_list_table');
50 50
51 51 // participating object list
52 52 $pullRequestListTable.DataTable({
53 53 processing: true,
54 54 serverSide: true,
55 55 stateSave: true,
56 56 stateDuration: -1,
57 57 ajax: {
58 58 "url": "${h.route_path('my_account_pullrequests_data')}",
59 59 "data": function (d) {
60 60 d.closed = "${c.closed}";
61 61 d.awaiting_my_review = "${c.awaiting_my_review}";
62 62 },
63 63 "dataSrc": function (json) {
64 64 return json.data;
65 65 }
66 66 },
67 67
68 68 dom: 'rtp',
69 69 pageLength: ${c.visual.dashboard_items},
70 70 order: [[2, "desc"]],
71 71 columns: [
72 72 {
73 73 data: {
74 74 "_": "status",
75 75 "sort": "status"
76 76 }, title: "PR", className: "td-status", orderable: false
77 77 },
78 78 {
79 79 data: {
80 80 "_": "my_status",
81 81 "sort": "status"
82 82 }, title: "You", className: "td-status", orderable: false
83 83 },
84 84 {
85 85 data: {
86 86 "_": "name",
87 87 "sort": "name_raw"
88 88 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
89 89 },
90 90 {
91 91 data: {
92 92 "_": "title",
93 93 "sort": "title"
94 94 }, title: "${_('Title')}", className: "td-description"
95 95 },
96 96 {
97 97 data: {
98 "_": "pr_flow",
99 "sort": "pr_flow"
100 }, title: "${_('Flow')}", className: "td-componentname"
101 },
102 {
103 data: {
98 104 "_": "author",
99 105 "sort": "author_raw"
100 106 }, title: "${_('Author')}", className: "td-user", orderable: false
101 107 },
102 108 {
103 109 data: {
104 110 "_": "comments",
105 111 "sort": "comments_raw"
106 112 }, title: "", className: "td-comments", orderable: false
107 113 },
108 114 {
109 115 data: {
110 116 "_": "updated_on",
111 117 "sort": "updated_on_raw"
112 118 }, title: "${_('Last Update')}", className: "td-time"
113 119 },
114 120 {
115 121 data: {
116 122 "_": "target_repo",
117 123 "sort": "target_repo"
118 124 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
119 125 },
120 126 ],
121 127 language: {
122 128 paginate: DEFAULT_GRID_PAGINATION,
123 129 sProcessing: _gettext('loading...'),
124 130 emptyTable: _gettext("There are currently no open pull requests requiring your participation.")
125 131 },
126 132 "drawCallback": function (settings, json) {
127 133 timeagoActivate();
128 134 tooltipActivate();
129 135 },
130 136 "createdRow": function (row, data, index) {
131 137 if (data['closed']) {
132 138 $(row).addClass('closed');
133 139 }
134 140 if (data['owned']) {
135 141 $(row).addClass('owned');
136 142 }
137 143 },
138 144 "stateSaveParams": function (settings, data) {
139 145 data.search.search = ""; // Don't save search
140 146 data.start = 0; // don't save pagination
141 147 }
142 148 });
143 149 $pullRequestListTable.on('xhr.dt', function (e, settings, json, xhr) {
144 150 $pullRequestListTable.css('opacity', 1);
145 151 });
146 152
147 153 $pullRequestListTable.on('preXhr.dt', function (e, settings, data) {
148 154 $pullRequestListTable.css('opacity', 0.3);
149 155 });
150 156
151 157
152 158 // filter
153 159 $('#q_filter').on('keyup',
154 160 $.debounce(250, function () {
155 161 $pullRequestListTable.DataTable().search(
156 162 $('#q_filter').val()
157 163 ).draw();
158 164 })
159 165 );
160 166
161 167 });
162 168
163 169
164 170 </script>
@@ -1,499 +1,518 b''
1 1 ## DATA TABLE RE USABLE ELEMENTS
2 2 ## usage:
3 3 ## <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="base" file="/base/base.mako"/>
5 5
6 6 <%def name="metatags_help()">
7 7 <table>
8 8 <%
9 9 example_tags = [
10 10 ('state','[stable]'),
11 11 ('state','[stale]'),
12 12 ('state','[featured]'),
13 13 ('state','[dev]'),
14 14 ('state','[dead]'),
15 15 ('state','[deprecated]'),
16 16
17 17 ('label','[personal]'),
18 18 ('generic','[v2.0.0]'),
19 19
20 20 ('lang','[lang =&gt; JavaScript]'),
21 21 ('license','[license =&gt; LicenseName]'),
22 22
23 23 ('ref','[requires =&gt; RepoName]'),
24 24 ('ref','[recommends =&gt; GroupName]'),
25 25 ('ref','[conflicts =&gt; SomeName]'),
26 26 ('ref','[base =&gt; SomeName]'),
27 27 ('url','[url =&gt; [linkName](https://rhodecode.com)]'),
28 28 ('see','[see =&gt; http://rhodecode.com]'),
29 29 ]
30 30 %>
31 31 % for tag_type, tag in example_tags:
32 32 <tr>
33 33 <td>${tag|n}</td>
34 34 <td>${h.style_metatag(tag_type, tag)|n}</td>
35 35 </tr>
36 36 % endfor
37 37 </table>
38 38 </%def>
39 39
40 40 <%def name="render_description(description, stylify_metatags)">
41 41 <%
42 42 tags = []
43 43 if stylify_metatags:
44 44 tags, description = h.extract_metatags(description)
45 45 %>
46 46 % for tag_type, tag in tags:
47 47 ${h.style_metatag(tag_type, tag)|n,trim}
48 48 % endfor
49 49 <code style="white-space: pre-wrap">${description}</code>
50 50 </%def>
51 51
52 52 ## REPOSITORY RENDERERS
53 53 <%def name="quick_menu(repo_name)">
54 54 <i class="icon-more"></i>
55 55 <div class="menu_items_container hidden">
56 56 <ul class="menu_items">
57 57 <li>
58 58 <a title="${_('Summary')}" href="${h.route_path('repo_summary',repo_name=repo_name)}">
59 59 <span>${_('Summary')}</span>
60 60 </a>
61 61 </li>
62 62 <li>
63 63 <a title="${_('Commits')}" href="${h.route_path('repo_commits',repo_name=repo_name)}">
64 64 <span>${_('Commits')}</span>
65 65 </a>
66 66 </li>
67 67 <li>
68 68 <a title="${_('Files')}" href="${h.route_path('repo_files:default_commit',repo_name=repo_name)}">
69 69 <span>${_('Files')}</span>
70 70 </a>
71 71 </li>
72 72 <li>
73 73 <a title="${_('Fork')}" href="${h.route_path('repo_fork_new',repo_name=repo_name)}">
74 74 <span>${_('Fork')}</span>
75 75 </a>
76 76 </li>
77 77 </ul>
78 78 </div>
79 79 </%def>
80 80
81 81 <%def name="repo_name(name,rtype,rstate,private,archived,fork_repo_name,short_name=False,admin=False)">
82 82 <%
83 83 def get_name(name,short_name=short_name):
84 84 if short_name:
85 85 return name.split('/')[-1]
86 86 else:
87 87 return name
88 88 %>
89 89 <div class="${'repo_state_pending' if rstate == 'repo_state_pending' else ''} truncate">
90 90 ##NAME
91 91 <a href="${h.route_path('edit_repo',repo_name=name) if admin else h.route_path('repo_summary',repo_name=name)}">
92 92
93 93 ##TYPE OF REPO
94 94 %if h.is_hg(rtype):
95 95 <span title="${_('Mercurial repository')}"><i class="icon-hg" style="font-size: 14px;"></i></span>
96 96 %elif h.is_git(rtype):
97 97 <span title="${_('Git repository')}"><i class="icon-git" style="font-size: 14px"></i></span>
98 98 %elif h.is_svn(rtype):
99 99 <span title="${_('Subversion repository')}"><i class="icon-svn" style="font-size: 14px"></i></span>
100 100 %endif
101 101
102 102 ##PRIVATE/PUBLIC
103 103 %if private is True and c.visual.show_private_icon:
104 104 <i class="icon-lock" title="${_('Private repository')}"></i>
105 105 %elif private is False and c.visual.show_public_icon:
106 106 <i class="icon-unlock-alt" title="${_('Public repository')}"></i>
107 107 %else:
108 108 <span></span>
109 109 %endif
110 110 ${get_name(name)}
111 111 </a>
112 112 %if fork_repo_name:
113 113 <a href="${h.route_path('repo_summary',repo_name=fork_repo_name)}"><i class="icon-code-fork"></i></a>
114 114 %endif
115 115 %if rstate == 'repo_state_pending':
116 116 <span class="creation_in_progress tooltip" title="${_('This repository is being created in a background task')}">
117 117 (${_('creating...')})
118 118 </span>
119 119 %endif
120 120
121 121 </div>
122 122 </%def>
123 123
124 124 <%def name="repo_desc(description, stylify_metatags)">
125 125 <%
126 126 tags, description = h.extract_metatags(description)
127 127 %>
128 128
129 129 <div class="truncate-wrap">
130 130 % if stylify_metatags:
131 131 % for tag_type, tag in tags:
132 132 ${h.style_metatag(tag_type, tag)|n}
133 133 % endfor
134 134 % endif
135 135 ${description}
136 136 </div>
137 137
138 138 </%def>
139 139
140 140 <%def name="last_change(last_change)">
141 141 ${h.age_component(last_change, time_is_local=True)}
142 142 </%def>
143 143
144 144 <%def name="revision(repo_name, rev, commit_id, author, last_msg, commit_date)">
145 145 <div>
146 146 %if rev >= 0:
147 147 <code><a class="tooltip-hovercard" data-hovercard-alt=${h.tooltip(last_msg)} data-hovercard-url="${h.route_path('hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)}" href="${h.route_path('repo_commit',repo_name=repo_name,commit_id=commit_id)}">${'r{}:{}'.format(rev,h.short_id(commit_id))}</a></code>
148 148 %else:
149 149 ${_('No commits yet')}
150 150 %endif
151 151 </div>
152 152 </%def>
153 153
154 154 <%def name="rss(name)">
155 155 %if c.rhodecode_user.username != h.DEFAULT_USER:
156 156 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
157 157 %else:
158 158 <a title="${h.tooltip(_('Subscribe to %s rss feed')% name)}" href="${h.route_path('rss_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
159 159 %endif
160 160 </%def>
161 161
162 162 <%def name="atom(name)">
163 163 %if c.rhodecode_user.username != h.DEFAULT_USER:
164 164 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name, _query=dict(auth_token=c.rhodecode_user.feed_token))}"><i class="icon-rss-sign"></i></a>
165 165 %else:
166 166 <a title="${h.tooltip(_('Subscribe to %s atom feed')% name)}" href="${h.route_path('atom_feed_home', repo_name=name)}"><i class="icon-rss-sign"></i></a>
167 167 %endif
168 168 </%def>
169 169
170 170 <%def name="repo_actions(repo_name, super_user=True)">
171 171 <div>
172 172 <div class="grid_edit">
173 173 <a href="${h.route_path('edit_repo',repo_name=repo_name)}" title="${_('Edit')}">
174 174 Edit
175 175 </a>
176 176 </div>
177 177 <div class="grid_delete">
178 178 ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), request=request)}
179 179 <input class="btn btn-link btn-danger" id="remove_${repo_name}" name="remove_${repo_name}"
180 180 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository'), _gettext('Delete'), '${repo_name}')"
181 181 type="submit" value="Delete"
182 182 >
183 183 ${h.end_form()}
184 184 </div>
185 185 </div>
186 186 </%def>
187 187
188 188 <%def name="repo_state(repo_state)">
189 189 <div>
190 190 %if repo_state == 'repo_state_pending':
191 191 <div class="tag tag4">${_('Creating')}</div>
192 192 %elif repo_state == 'repo_state_created':
193 193 <div class="tag tag1">${_('Created')}</div>
194 194 %else:
195 195 <div class="tag alert2" title="${h.tooltip(repo_state)}">invalid</div>
196 196 %endif
197 197 </div>
198 198 </%def>
199 199
200 200
201 201 ## REPO GROUP RENDERERS
202 202 <%def name="quick_repo_group_menu(repo_group_name)">
203 203 <i class="icon-more"></i>
204 204 <div class="menu_items_container hidden">
205 205 <ul class="menu_items">
206 206 <li>
207 207 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">${_('Summary')}</a>
208 208 </li>
209 209
210 210 </ul>
211 211 </div>
212 212 </%def>
213 213
214 214 <%def name="repo_group_name(repo_group_name, children_groups=None)">
215 215 <div>
216 216 <a href="${h.route_path('repo_group_home', repo_group_name=repo_group_name)}">
217 217 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
218 218 %if children_groups:
219 219 ${h.literal(' &raquo; '.join(children_groups))}
220 220 %else:
221 221 ${repo_group_name}
222 222 %endif
223 223 </a>
224 224 </div>
225 225 </%def>
226 226
227 227 <%def name="repo_group_desc(description, personal, stylify_metatags)">
228 228
229 229 <%
230 230 if stylify_metatags:
231 231 tags, description = h.extract_metatags(description)
232 232 %>
233 233
234 234 <div class="truncate-wrap">
235 235 % if personal:
236 236 <div class="metatag" tag="personal">${_('personal')}</div>
237 237 % endif
238 238
239 239 % if stylify_metatags:
240 240 % for tag_type, tag in tags:
241 241 ${h.style_metatag(tag_type, tag)|n}
242 242 % endfor
243 243 % endif
244 244 ${description}
245 245 </div>
246 246
247 247 </%def>
248 248
249 249 <%def name="repo_group_actions(repo_group_id, repo_group_name, gr_count)">
250 250 <div class="grid_edit">
251 251 <a href="${h.route_path('edit_repo_group',repo_group_name=repo_group_name)}" title="${_('Edit')}">Edit</a>
252 252 </div>
253 253 <div class="grid_delete">
254 254 ${h.secure_form(h.route_path('edit_repo_group_advanced_delete', repo_group_name=repo_group_name), request=request)}
255 255 <input class="btn btn-link btn-danger" id="remove_${repo_group_name}" name="remove_${repo_group_name}"
256 256 onclick="submitConfirm(event, this, _gettext('Confirm to delete this repository group'), _gettext('Delete'), '${_ungettext('`{}` with {} repository','`{}` with {} repositories',gr_count).format(repo_group_name, gr_count)}')"
257 257 type="submit" value="Delete"
258 258 >
259 259 ${h.end_form()}
260 260 </div>
261 261 </%def>
262 262
263 263
264 264 <%def name="user_actions(user_id, username)">
265 265 <div class="grid_edit">
266 266 <a href="${h.route_path('user_edit',user_id=user_id)}" title="${_('Edit')}">
267 267 ${_('Edit')}
268 268 </a>
269 269 </div>
270 270 <div class="grid_delete">
271 271 ${h.secure_form(h.route_path('user_delete', user_id=user_id), request=request)}
272 272 <input class="btn btn-link btn-danger" id="remove_user_${user_id}" name="remove_user_${user_id}"
273 273 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Delete'), '${username}')"
274 274 type="submit" value="Delete"
275 275 >
276 276 ${h.end_form()}
277 277 </div>
278 278 </%def>
279 279
280 280 <%def name="user_group_actions(user_group_id, user_group_name)">
281 281 <div class="grid_edit">
282 282 <a href="${h.route_path('edit_user_group', user_group_id=user_group_id)}" title="${_('Edit')}">Edit</a>
283 283 </div>
284 284 <div class="grid_delete">
285 285 ${h.secure_form(h.route_path('user_groups_delete', user_group_id=user_group_id), request=request)}
286 286 <input class="btn btn-link btn-danger" id="remove_group_${user_group_id}" name="remove_group_${user_group_id}"
287 287 onclick="submitConfirm(event, this, _gettext('Confirm to delete this user group'), _gettext('Delete'), '${user_group_name}')"
288 288 type="submit" value="Delete"
289 289 >
290 290 ${h.end_form()}
291 291 </div>
292 292 </%def>
293 293
294 294
295 295 <%def name="user_name(user_id, username)">
296 296 ${h.link_to(h.person(username, 'username_or_name_or_email'), h.route_path('user_edit', user_id=user_id))}
297 297 </%def>
298 298
299 299 <%def name="user_profile(username)">
300 300 ${base.gravatar_with_user(username, 16, tooltip=True)}
301 301 </%def>
302 302
303 303 <%def name="user_group_name(user_group_name)">
304 304 <div>
305 305 <i class="icon-user-group" title="${_('User group')}"></i>
306 306 ${h.link_to_group(user_group_name)}
307 307 </div>
308 308 </%def>
309 309
310 310
311 311 ## GISTS
312 312
313 313 <%def name="gist_gravatar(full_contact)">
314 314 <div class="gist_gravatar">
315 315 ${base.gravatar(full_contact, 30)}
316 316 </div>
317 317 </%def>
318 318
319 319 <%def name="gist_access_id(gist_access_id, full_contact)">
320 320 <div>
321 321 <code>
322 322 <a href="${h.route_path('gist_show', gist_id=gist_access_id)}">${gist_access_id}</a>
323 323 </code>
324 324 </div>
325 325 </%def>
326 326
327 327 <%def name="gist_author(full_contact, created_on, expires)">
328 328 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
329 329 </%def>
330 330
331 331
332 332 <%def name="gist_created(created_on)">
333 333 <div class="created">
334 334 ${h.age_component(created_on, time_is_local=True)}
335 335 </div>
336 336 </%def>
337 337
338 338 <%def name="gist_expires(expires)">
339 339 <div class="created">
340 340 %if expires == -1:
341 341 ${_('never')}
342 342 %else:
343 343 ${h.age_component(h.time_to_utcdatetime(expires))}
344 344 %endif
345 345 </div>
346 346 </%def>
347 347
348 348 <%def name="gist_type(gist_type)">
349 349 %if gist_type == 'public':
350 350 <span class="tag tag-gist-public disabled">${_('Public Gist')}</span>
351 351 %else:
352 352 <span class="tag tag-gist-private disabled">${_('Private Gist')}</span>
353 353 %endif
354 354 </%def>
355 355
356 356 <%def name="gist_description(gist_description)">
357 357 ${gist_description}
358 358 </%def>
359 359
360 360
361 361 ## PULL REQUESTS GRID RENDERERS
362 362
363 363 <%def name="pullrequest_target_repo(repo_name)">
364 364 <div class="truncate">
365 365 ${h.link_to(repo_name,h.route_path('repo_summary',repo_name=repo_name))}
366 366 </div>
367 367 </%def>
368 368
369 369 <%def name="pullrequest_status(status)">
370 370 <i class="icon-circle review-status-${status}"></i>
371 371 </%def>
372 372
373 373 <%def name="pullrequest_title(title, description)">
374 374 ${title}
375 375 </%def>
376 376
377 <%def name="pullrequest_commit_flow(pull_request)">
378 <div class="pr-commit-flow">
379 <%!
380 def pr_ref_type_to_icon(ref_type):
381 return dict(
382 branch='branch',
383 book='bookmark',
384 rev='history',
385 ).get(ref_type, 'branch')
386
387 %>
388 ## Source
389 <code class="pr-source-info"><i class="icon-${pr_ref_type_to_icon(pull_request.source_ref_parts.type)}"></i>${pull_request.source_ref_parts.name}</code>
390 &rarr;
391 ## Target
392 <code class="pr-target-info"><i class="icon-${pr_ref_type_to_icon(pull_request.target_ref_parts.type)}"></i>${pull_request.target_ref_parts.name}</code>
393 </div>
394 </%def>
395
377 396 <%def name="pullrequest_comments(comments_nr)">
378 397 <i class="icon-comment"></i> ${comments_nr}
379 398 </%def>
380 399
381 400 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
382 401 <code>
383 402 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
384 403 % if short:
385 404 !${pull_request_id}
386 405 % else:
387 406 ${_('Pull request !{}').format(pull_request_id)}
388 407 % endif
389 408 </a>
390 409 </code>
391 410 % if state not in ['created']:
392 411 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
393 412 % endif
394 413
395 414 % if is_wip:
396 415 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
397 416 % endif
398 417 </%def>
399 418
400 419 <%def name="pullrequest_updated_on(updated_on, pr_version=None)">
401 420 % if pr_version:
402 421 <code>v${pr_version}</code>
403 422 % endif
404 423 ${h.age_component(h.time_to_utcdatetime(updated_on))}
405 424 </%def>
406 425
407 426 <%def name="pullrequest_author(full_contact)">
408 427 ${base.gravatar_with_user(full_contact, 16, tooltip=True)}
409 428 </%def>
410 429
411 430
412 431 ## ARTIFACT RENDERERS
413 432 <%def name="repo_artifact_name(repo_name, file_uid, artifact_display_name)">
414 433 <a href="${h.route_path('repo_artifacts_get', repo_name=repo_name, uid=file_uid)}">
415 434 ${artifact_display_name or '_EMPTY_NAME_'}
416 435 </a>
417 436 </%def>
418 437
419 438 <%def name="repo_artifact_admin_name(file_uid, artifact_display_name)">
420 439 <a href="${h.route_path('admin_artifacts_show_info', uid=file_uid)}">
421 440 ${(artifact_display_name or '_EMPTY_NAME_')}
422 441 </a>
423 442 </%def>
424 443
425 444 <%def name="repo_artifact_uid(repo_name, file_uid)">
426 445 <code>${h.shorter(file_uid, size=24, prefix=True)}</code>
427 446 </%def>
428 447
429 448 <%def name="repo_artifact_sha256(artifact_sha256)">
430 449 <div class="code">${h.shorter(artifact_sha256, 12)}</div>
431 450 </%def>
432 451
433 452 <%def name="repo_artifact_actions(repo_name, file_store_id, file_uid)">
434 453 ## <div class="grid_edit">
435 454 ## <a href="#Edit" title="${_('Edit')}">${_('Edit')}</a>
436 455 ## </div>
437 456 <div class="grid_edit">
438 457 <a href="${h.route_path('repo_artifacts_info', repo_name=repo_name, uid=file_store_id)}" title="${_('Info')}">${_('Info')}</a>
439 458 </div>
440 459 % if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
441 460 <div class="grid_delete">
442 461 ${h.secure_form(h.route_path('repo_artifacts_delete', repo_name=repo_name, uid=file_store_id), request=request)}
443 462 <input class="btn btn-link btn-danger" id="remove_artifact_${file_store_id}" name="remove_artifact_${file_store_id}"
444 463 onclick="submitConfirm(event, this, _gettext('Confirm to delete this artifact'), _gettext('Delete'), '${file_uid}')"
445 464 type="submit" value="${_('Delete')}"
446 465 >
447 466 ${h.end_form()}
448 467 </div>
449 468 % endif
450 469 </%def>
451 470
452 471
453 472 <%def name="markup_form(form_id, form_text='', help_text=None)">
454 473
455 474 <div class="markup-form">
456 475 <div class="markup-form-area">
457 476 <div class="markup-form-area-header">
458 477 <ul class="nav-links clearfix">
459 478 <li class="active">
460 479 <a href="#edit-text" tabindex="-1" id="edit-btn_${form_id}">${_('Write')}</a>
461 480 </li>
462 481 <li class="">
463 482 <a href="#preview-text" tabindex="-1" id="preview-btn_${form_id}">${_('Preview')}</a>
464 483 </li>
465 484 </ul>
466 485 </div>
467 486
468 487 <div class="markup-form-area-write" style="display: block;">
469 488 <div id="edit-container_${form_id}" style="margin-top: -1px">
470 489 <textarea id="${form_id}" name="${form_id}" class="comment-block-ta ac-input">${form_text if form_text else ''}</textarea>
471 490 </div>
472 491 <div id="preview-container_${form_id}" class="clearfix" style="display: none;">
473 492 <div id="preview-box_${form_id}" class="preview-box"></div>
474 493 </div>
475 494 </div>
476 495
477 496 <div class="markup-form-area-footer">
478 497 <div class="toolbar">
479 498 <div class="toolbar-text">
480 499 ${(_('Parsed using %s syntax') % (
481 500 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
482 501 )
483 502 )|n}
484 503 </div>
485 504 </div>
486 505 </div>
487 506 </div>
488 507
489 508 <div class="markup-form-footer">
490 509 % if help_text:
491 510 <span class="help-block">${help_text}</span>
492 511 % endif
493 512 </div>
494 513 </div>
495 514 <script type="text/javascript">
496 515 new MarkupForm('${form_id}');
497 516 </script>
498 517
499 518 </%def>
@@ -1,177 +1,183 b''
1 1 <%inherit file="/base/base.mako"/>
2 2
3 3 <%def name="title()">
4 4 ${_('{} Pull Requests').format(c.repo_name)}
5 5 %if c.rhodecode_name:
6 6 &middot; ${h.branding(c.rhodecode_name)}
7 7 %endif
8 8 </%def>
9 9
10 10 <%def name="breadcrumbs_links()"></%def>
11 11
12 12 <%def name="menu_bar_nav()">
13 13 ${self.menu_items(active='repositories')}
14 14 </%def>
15 15
16 16
17 17 <%def name="menu_bar_subnav()">
18 18 ${self.repo_menu(active='showpullrequest')}
19 19 </%def>
20 20
21 21
22 22 <%def name="main()">
23 23
24 24 <div class="box">
25 25 <div class="title">
26 26
27 27 <ul class="button-links">
28 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>
29 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>
30 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>
31 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>
32 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>
33 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>
34 34 </ul>
35 35
36 36 <ul class="links">
37 37 % if c.rhodecode_user.username != h.DEFAULT_USER:
38 38 <li>
39 39 <span>
40 40 <a id="open_new_pull_request" class="btn btn-small btn-success" href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">
41 41 ${_('Open new Pull Request')}
42 42 </a>
43 43 </span>
44 44 </li>
45 45 % endif
46 46
47 47 <li>
48 48 <div class="grid-quick-filter">
49 49 <ul class="grid-filter-box">
50 50 <li class="grid-filter-box-icon">
51 51 <i class="icon-search"></i>
52 52 </li>
53 53 <li class="grid-filter-box-input">
54 54 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
55 55 </li>
56 56 </ul>
57 57 </div>
58 58 </li>
59 59
60 60 </ul>
61 61
62 62 </div>
63 63
64 64 <div class="main-content-full-width">
65 65 <table id="pull_request_list_table" class="rctable table-bordered"></table>
66 66 </div>
67 67
68 68 </div>
69 69
70 70 <script type="text/javascript">
71 71 $(document).ready(function() {
72 72 var $pullRequestListTable = $('#pull_request_list_table');
73 73
74 74 // object list
75 75 $pullRequestListTable.DataTable({
76 76 processing: true,
77 77 serverSide: true,
78 78 stateSave: true,
79 79 stateDuration: -1,
80 80 ajax: {
81 81 "url": "${h.route_path('pullrequest_show_all_data', repo_name=c.repo_name)}",
82 82 "data": function (d) {
83 83 d.source = "${c.source}";
84 84 d.closed = "${c.closed}";
85 85 d.my = "${c.my}";
86 86 d.awaiting_review = "${c.awaiting_review}";
87 87 d.awaiting_my_review = "${c.awaiting_my_review}";
88 88 }
89 89 },
90 90 dom: 'rtp',
91 91 pageLength: ${c.visual.dashboard_items},
92 92 order: [[ 2, "desc" ]],
93 93 columns: [
94 94 {
95 95 data: {
96 96 "_": "status",
97 97 "sort": "status"
98 98 }, title: "PR", className: "td-status", orderable: false
99 99 },
100 100 {
101 101 data: {
102 102 "_": "my_status",
103 103 "sort": "status"
104 104 }, title: "You", className: "td-status", orderable: false
105 105 },
106 106 {
107 107 data: {
108 108 "_": "name",
109 109 "sort": "name_raw"
110 110 }, title: "${_('Id')}", className: "td-componentname", "type": "num"
111 111 },
112 112 {
113 113 data: {
114 114 "_": "title",
115 115 "sort": "title"
116 116 }, title: "${_('Title')}", className: "td-description"
117 117 },
118 118 {
119 119 data: {
120 "_": "pr_flow",
121 "sort": "pr_flow"
122 }, title: "${_('Flow')}", className: "td-componentname"
123 },
124 {
125 data: {
120 126 "_": "author",
121 127 "sort": "author_raw"
122 128 }, title: "${_('Author')}", className: "td-user", orderable: false
123 129 },
124 130 {
125 131 data: {
126 132 "_": "comments",
127 133 "sort": "comments_raw"
128 134 }, title: "", className: "td-comments", orderable: false
129 135 },
130 136 {
131 137 data: {
132 138 "_": "updated_on",
133 139 "sort": "updated_on_raw"
134 140 }, title: "${_('Last Update')}", className: "td-time"
135 141 }
136 142 ],
137 143 language: {
138 144 paginate: DEFAULT_GRID_PAGINATION,
139 145 sProcessing: _gettext('loading...'),
140 146 emptyTable: _gettext("No pull requests available yet.")
141 147 },
142 148 "drawCallback": function( settings, json ) {
143 149 timeagoActivate();
144 150 tooltipActivate();
145 151 },
146 152 "createdRow": function ( row, data, index ) {
147 153 if (data['closed']) {
148 154 $(row).addClass('closed');
149 155 }
150 156 },
151 157 "stateSaveParams": function (settings, data) {
152 158 data.search.search = ""; // Don't save search
153 159 data.start = 0; // don't save pagination
154 160 }
155 161 });
156 162
157 163 $pullRequestListTable.on('xhr.dt', function(e, settings, json, xhr){
158 164 $pullRequestListTable.css('opacity', 1);
159 165 });
160 166
161 167 $pullRequestListTable.on('preXhr.dt', function(e, settings, data){
162 168 $pullRequestListTable.css('opacity', 0.3);
163 169 });
164 170
165 171 // filter
166 172 $('#q_filter').on('keyup',
167 173 $.debounce(250, function() {
168 174 $pullRequestListTable.DataTable().search(
169 175 $('#q_filter').val()
170 176 ).draw();
171 177 })
172 178 );
173 179
174 180 });
175 181
176 182 </script>
177 183 </%def>
General Comments 0
You need to be logged in to leave comments. Login now