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