Show More
The requested changes are too big and content was truncated. Show full diff
@@ -0,0 +1,53 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | import logging | |
|
4 | from sqlalchemy import * | |
|
5 | ||
|
6 | from alembic.migration import MigrationContext | |
|
7 | from alembic.operations import Operations | |
|
8 | ||
|
9 | from rhodecode.lib.dbmigrate.versions import _reset_base | |
|
10 | from rhodecode.model import meta, init_model_encryption | |
|
11 | ||
|
12 | ||
|
13 | log = logging.getLogger(__name__) | |
|
14 | ||
|
15 | ||
|
16 | def upgrade(migrate_engine): | |
|
17 | """ | |
|
18 | Upgrade operations go here. | |
|
19 | Don't create your own engine; bind migrate_engine to your metadata | |
|
20 | """ | |
|
21 | _reset_base(migrate_engine) | |
|
22 | from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db | |
|
23 | ||
|
24 | init_model_encryption(db) | |
|
25 | ||
|
26 | context = MigrationContext.configure(migrate_engine.connect()) | |
|
27 | op = Operations(context) | |
|
28 | ||
|
29 | table = db.ChangesetComment.__table__ | |
|
30 | with op.batch_alter_table(table.name) as batch_op: | |
|
31 | new_column = Column('draft', Boolean(), nullable=True) | |
|
32 | batch_op.add_column(new_column) | |
|
33 | ||
|
34 | _set_default_as_non_draft(op, meta.Session) | |
|
35 | ||
|
36 | ||
|
37 | def downgrade(migrate_engine): | |
|
38 | meta = MetaData() | |
|
39 | meta.bind = migrate_engine | |
|
40 | ||
|
41 | ||
|
42 | def fixups(models, _SESSION): | |
|
43 | pass | |
|
44 | ||
|
45 | ||
|
46 | def _set_default_as_non_draft(op, session): | |
|
47 | params = {'draft': False} | |
|
48 | query = text( | |
|
49 | 'UPDATE changeset_comments SET draft = :draft' | |
|
50 | ).bindparams(**params) | |
|
51 | op.execute(query) | |
|
52 | session().commit() | |
|
53 |
@@ -1,60 +1,60 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2020 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import os |
|
22 | 22 | from collections import OrderedDict |
|
23 | 23 | |
|
24 | 24 | import sys |
|
25 | 25 | import platform |
|
26 | 26 | |
|
27 | 27 | VERSION = tuple(open(os.path.join( |
|
28 | 28 | os.path.dirname(__file__), 'VERSION')).read().split('.')) |
|
29 | 29 | |
|
30 | 30 | BACKENDS = OrderedDict() |
|
31 | 31 | |
|
32 | 32 | BACKENDS['hg'] = 'Mercurial repository' |
|
33 | 33 | BACKENDS['git'] = 'Git repository' |
|
34 | 34 | BACKENDS['svn'] = 'Subversion repository' |
|
35 | 35 | |
|
36 | 36 | |
|
37 | 37 | CELERY_ENABLED = False |
|
38 | 38 | CELERY_EAGER = False |
|
39 | 39 | |
|
40 | 40 | # link to config for pyramid |
|
41 | 41 | CONFIG = {} |
|
42 | 42 | |
|
43 | 43 | # Populated with the settings dictionary from application init in |
|
44 | 44 | # rhodecode.conf.environment.load_pyramid_environment |
|
45 | 45 | PYRAMID_SETTINGS = {} |
|
46 | 46 | |
|
47 | 47 | # Linked module for extensions |
|
48 | 48 | EXTENSIONS = {} |
|
49 | 49 | |
|
50 | 50 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
51 |
__dbversion__ = 11 |
|
|
51 | __dbversion__ = 111 # defines current db version for migrations | |
|
52 | 52 | __platform__ = platform.system() |
|
53 | 53 | __license__ = 'AGPLv3, and Commercial License' |
|
54 | 54 | __author__ = 'RhodeCode GmbH' |
|
55 | 55 | __url__ = 'https://code.rhodecode.com' |
|
56 | 56 | |
|
57 | 57 | is_windows = __platform__ in ['Windows'] |
|
58 | 58 | is_unix = not is_windows |
|
59 | 59 | is_test = False |
|
60 | 60 | disable_error_handler = False |
@@ -1,1816 +1,1826 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2020 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | import collections |
|
23 | 23 | |
|
24 | 24 | import formencode |
|
25 | 25 | import formencode.htmlfill |
|
26 | 26 | import peppercorn |
|
27 | 27 | from pyramid.httpexceptions import ( |
|
28 | 28 | HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict) |
|
29 | 29 | from pyramid.view import view_config |
|
30 | 30 | from pyramid.renderers import render |
|
31 | 31 | |
|
32 | 32 | from rhodecode.apps._base import RepoAppView, DataGridAppView |
|
33 | 33 | |
|
34 | 34 | from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream |
|
35 | 35 | from rhodecode.lib.base import vcs_operation_context |
|
36 | 36 | from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist |
|
37 | 37 | from rhodecode.lib.exceptions import CommentVersionMismatch |
|
38 | 38 | from rhodecode.lib.ext_json import json |
|
39 | 39 | from rhodecode.lib.auth import ( |
|
40 | 40 | LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator, |
|
41 | 41 | NotAnonymous, CSRFRequired) |
|
42 | 42 | from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist |
|
43 | 43 | from rhodecode.lib.vcs.backends.base import ( |
|
44 | 44 | EmptyCommit, UpdateFailureReason, unicode_to_reference) |
|
45 | 45 | from rhodecode.lib.vcs.exceptions import ( |
|
46 | 46 | CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError) |
|
47 | 47 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
48 | 48 | from rhodecode.model.comment import CommentsModel |
|
49 | 49 | from rhodecode.model.db import ( |
|
50 | func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository, | |
|
50 | func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository, | |
|
51 | 51 | PullRequestReviewers) |
|
52 | 52 | from rhodecode.model.forms import PullRequestForm |
|
53 | 53 | from rhodecode.model.meta import Session |
|
54 | 54 | from rhodecode.model.pull_request import PullRequestModel, MergeCheck |
|
55 | 55 | from rhodecode.model.scm import ScmModel |
|
56 | 56 | |
|
57 | 57 | log = logging.getLogger(__name__) |
|
58 | 58 | |
|
59 | 59 | |
|
60 | 60 | class RepoPullRequestsView(RepoAppView, DataGridAppView): |
|
61 | 61 | |
|
62 | 62 | def load_default_context(self): |
|
63 | 63 | c = self._get_local_tmpl_context(include_app_defaults=True) |
|
64 | 64 | c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED |
|
65 | 65 | c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED |
|
66 | 66 | # backward compat., we use for OLD PRs a plain renderer |
|
67 | 67 | c.renderer = 'plain' |
|
68 | 68 | return c |
|
69 | 69 | |
|
70 | 70 | def _get_pull_requests_list( |
|
71 | 71 | self, repo_name, source, filter_type, opened_by, statuses): |
|
72 | 72 | |
|
73 | 73 | draw, start, limit = self._extract_chunk(self.request) |
|
74 | 74 | search_q, order_by, order_dir = self._extract_ordering(self.request) |
|
75 | 75 | _render = self.request.get_partial_renderer( |
|
76 | 76 | 'rhodecode:templates/data_table/_dt_elements.mako') |
|
77 | 77 | |
|
78 | 78 | # pagination |
|
79 | 79 | |
|
80 | 80 | if filter_type == 'awaiting_review': |
|
81 | 81 | pull_requests = PullRequestModel().get_awaiting_review( |
|
82 | 82 | repo_name, search_q=search_q, source=source, opened_by=opened_by, |
|
83 | 83 | statuses=statuses, offset=start, length=limit, |
|
84 | 84 | order_by=order_by, order_dir=order_dir) |
|
85 | 85 | pull_requests_total_count = PullRequestModel().count_awaiting_review( |
|
86 | 86 | repo_name, search_q=search_q, source=source, statuses=statuses, |
|
87 | 87 | opened_by=opened_by) |
|
88 | 88 | elif filter_type == 'awaiting_my_review': |
|
89 | 89 | pull_requests = PullRequestModel().get_awaiting_my_review( |
|
90 | 90 | repo_name, search_q=search_q, source=source, opened_by=opened_by, |
|
91 | 91 | user_id=self._rhodecode_user.user_id, statuses=statuses, |
|
92 | 92 | offset=start, length=limit, order_by=order_by, |
|
93 | 93 | order_dir=order_dir) |
|
94 | 94 | pull_requests_total_count = PullRequestModel().count_awaiting_my_review( |
|
95 | 95 | repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id, |
|
96 | 96 | statuses=statuses, opened_by=opened_by) |
|
97 | 97 | else: |
|
98 | 98 | pull_requests = PullRequestModel().get_all( |
|
99 | 99 | repo_name, search_q=search_q, source=source, opened_by=opened_by, |
|
100 | 100 | statuses=statuses, offset=start, length=limit, |
|
101 | 101 | order_by=order_by, order_dir=order_dir) |
|
102 | 102 | pull_requests_total_count = PullRequestModel().count_all( |
|
103 | 103 | repo_name, search_q=search_q, source=source, statuses=statuses, |
|
104 | 104 | opened_by=opened_by) |
|
105 | 105 | |
|
106 | 106 | data = [] |
|
107 | 107 | comments_model = CommentsModel() |
|
108 | 108 | for pr in pull_requests: |
|
109 | 109 | comments_count = comments_model.get_all_comments( |
|
110 | 110 | self.db_repo.repo_id, pull_request=pr, count_only=True) |
|
111 | 111 | |
|
112 | 112 | data.append({ |
|
113 | 113 | 'name': _render('pullrequest_name', |
|
114 | 114 | pr.pull_request_id, pr.pull_request_state, |
|
115 | 115 | pr.work_in_progress, pr.target_repo.repo_name, |
|
116 | 116 | short=True), |
|
117 | 117 | 'name_raw': pr.pull_request_id, |
|
118 | 118 | 'status': _render('pullrequest_status', |
|
119 | 119 | pr.calculated_review_status()), |
|
120 | 120 | 'title': _render('pullrequest_title', pr.title, pr.description), |
|
121 | 121 | 'description': h.escape(pr.description), |
|
122 | 122 | 'updated_on': _render('pullrequest_updated_on', |
|
123 | 123 | h.datetime_to_time(pr.updated_on)), |
|
124 | 124 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), |
|
125 | 125 | 'created_on': _render('pullrequest_updated_on', |
|
126 | 126 | h.datetime_to_time(pr.created_on)), |
|
127 | 127 | 'created_on_raw': h.datetime_to_time(pr.created_on), |
|
128 | 128 | 'state': pr.pull_request_state, |
|
129 | 129 | 'author': _render('pullrequest_author', |
|
130 | 130 | pr.author.full_contact, ), |
|
131 | 131 | 'author_raw': pr.author.full_name, |
|
132 | 132 | 'comments': _render('pullrequest_comments', comments_count), |
|
133 | 133 | 'comments_raw': comments_count, |
|
134 | 134 | 'closed': pr.is_closed(), |
|
135 | 135 | }) |
|
136 | 136 | |
|
137 | 137 | data = ({ |
|
138 | 138 | 'draw': draw, |
|
139 | 139 | 'data': data, |
|
140 | 140 | 'recordsTotal': pull_requests_total_count, |
|
141 | 141 | 'recordsFiltered': pull_requests_total_count, |
|
142 | 142 | }) |
|
143 | 143 | return data |
|
144 | 144 | |
|
145 | 145 | @LoginRequired() |
|
146 | 146 | @HasRepoPermissionAnyDecorator( |
|
147 | 147 | 'repository.read', 'repository.write', 'repository.admin') |
|
148 | 148 | @view_config( |
|
149 | 149 | route_name='pullrequest_show_all', request_method='GET', |
|
150 | 150 | renderer='rhodecode:templates/pullrequests/pullrequests.mako') |
|
151 | 151 | def pull_request_list(self): |
|
152 | 152 | c = self.load_default_context() |
|
153 | 153 | |
|
154 | 154 | req_get = self.request.GET |
|
155 | 155 | c.source = str2bool(req_get.get('source')) |
|
156 | 156 | c.closed = str2bool(req_get.get('closed')) |
|
157 | 157 | c.my = str2bool(req_get.get('my')) |
|
158 | 158 | c.awaiting_review = str2bool(req_get.get('awaiting_review')) |
|
159 | 159 | c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) |
|
160 | 160 | |
|
161 | 161 | c.active = 'open' |
|
162 | 162 | if c.my: |
|
163 | 163 | c.active = 'my' |
|
164 | 164 | if c.closed: |
|
165 | 165 | c.active = 'closed' |
|
166 | 166 | if c.awaiting_review and not c.source: |
|
167 | 167 | c.active = 'awaiting' |
|
168 | 168 | if c.source and not c.awaiting_review: |
|
169 | 169 | c.active = 'source' |
|
170 | 170 | if c.awaiting_my_review: |
|
171 | 171 | c.active = 'awaiting_my' |
|
172 | 172 | |
|
173 | 173 | return self._get_template_context(c) |
|
174 | 174 | |
|
175 | 175 | @LoginRequired() |
|
176 | 176 | @HasRepoPermissionAnyDecorator( |
|
177 | 177 | 'repository.read', 'repository.write', 'repository.admin') |
|
178 | 178 | @view_config( |
|
179 | 179 | route_name='pullrequest_show_all_data', request_method='GET', |
|
180 | 180 | renderer='json_ext', xhr=True) |
|
181 | 181 | def pull_request_list_data(self): |
|
182 | 182 | self.load_default_context() |
|
183 | 183 | |
|
184 | 184 | # additional filters |
|
185 | 185 | req_get = self.request.GET |
|
186 | 186 | source = str2bool(req_get.get('source')) |
|
187 | 187 | closed = str2bool(req_get.get('closed')) |
|
188 | 188 | my = str2bool(req_get.get('my')) |
|
189 | 189 | awaiting_review = str2bool(req_get.get('awaiting_review')) |
|
190 | 190 | awaiting_my_review = str2bool(req_get.get('awaiting_my_review')) |
|
191 | 191 | |
|
192 | 192 | filter_type = 'awaiting_review' if awaiting_review \ |
|
193 | 193 | else 'awaiting_my_review' if awaiting_my_review \ |
|
194 | 194 | else None |
|
195 | 195 | |
|
196 | 196 | opened_by = None |
|
197 | 197 | if my: |
|
198 | 198 | opened_by = [self._rhodecode_user.user_id] |
|
199 | 199 | |
|
200 | 200 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
201 | 201 | if closed: |
|
202 | 202 | statuses = [PullRequest.STATUS_CLOSED] |
|
203 | 203 | |
|
204 | 204 | data = self._get_pull_requests_list( |
|
205 | 205 | repo_name=self.db_repo_name, source=source, |
|
206 | 206 | filter_type=filter_type, opened_by=opened_by, statuses=statuses) |
|
207 | 207 | |
|
208 | 208 | return data |
|
209 | 209 | |
|
210 | 210 | def _is_diff_cache_enabled(self, target_repo): |
|
211 | 211 | caching_enabled = self._get_general_setting( |
|
212 | 212 | target_repo, 'rhodecode_diff_cache') |
|
213 | 213 | log.debug('Diff caching enabled: %s', caching_enabled) |
|
214 | 214 | return caching_enabled |
|
215 | 215 | |
|
216 | 216 | def _get_diffset(self, source_repo_name, source_repo, |
|
217 | 217 | ancestor_commit, |
|
218 | 218 | source_ref_id, target_ref_id, |
|
219 | 219 | target_commit, source_commit, diff_limit, file_limit, |
|
220 | 220 | fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True): |
|
221 | 221 | |
|
222 | 222 | if use_ancestor: |
|
223 | 223 | # we might want to not use it for versions |
|
224 | 224 | target_ref_id = ancestor_commit.raw_id |
|
225 | 225 | |
|
226 | 226 | vcs_diff = PullRequestModel().get_diff( |
|
227 | 227 | source_repo, source_ref_id, target_ref_id, |
|
228 | 228 | hide_whitespace_changes, diff_context) |
|
229 | 229 | |
|
230 | 230 | diff_processor = diffs.DiffProcessor( |
|
231 | 231 | vcs_diff, format='newdiff', diff_limit=diff_limit, |
|
232 | 232 | file_limit=file_limit, show_full_diff=fulldiff) |
|
233 | 233 | |
|
234 | 234 | _parsed = diff_processor.prepare() |
|
235 | 235 | |
|
236 | 236 | diffset = codeblocks.DiffSet( |
|
237 | 237 | repo_name=self.db_repo_name, |
|
238 | 238 | source_repo_name=source_repo_name, |
|
239 | 239 | source_node_getter=codeblocks.diffset_node_getter(target_commit), |
|
240 | 240 | target_node_getter=codeblocks.diffset_node_getter(source_commit), |
|
241 | 241 | ) |
|
242 | 242 | diffset = self.path_filter.render_patchset_filtered( |
|
243 | 243 | diffset, _parsed, target_commit.raw_id, source_commit.raw_id) |
|
244 | 244 | |
|
245 | 245 | return diffset |
|
246 | 246 | |
|
247 | 247 | def _get_range_diffset(self, source_scm, source_repo, |
|
248 | 248 | commit1, commit2, diff_limit, file_limit, |
|
249 | 249 | fulldiff, hide_whitespace_changes, diff_context): |
|
250 | 250 | vcs_diff = source_scm.get_diff( |
|
251 | 251 | commit1, commit2, |
|
252 | 252 | ignore_whitespace=hide_whitespace_changes, |
|
253 | 253 | context=diff_context) |
|
254 | 254 | |
|
255 | 255 | diff_processor = diffs.DiffProcessor( |
|
256 | 256 | vcs_diff, format='newdiff', diff_limit=diff_limit, |
|
257 | 257 | file_limit=file_limit, show_full_diff=fulldiff) |
|
258 | 258 | |
|
259 | 259 | _parsed = diff_processor.prepare() |
|
260 | 260 | |
|
261 | 261 | diffset = codeblocks.DiffSet( |
|
262 | 262 | repo_name=source_repo.repo_name, |
|
263 | 263 | source_node_getter=codeblocks.diffset_node_getter(commit1), |
|
264 | 264 | target_node_getter=codeblocks.diffset_node_getter(commit2)) |
|
265 | 265 | |
|
266 | 266 | diffset = self.path_filter.render_patchset_filtered( |
|
267 | 267 | diffset, _parsed, commit1.raw_id, commit2.raw_id) |
|
268 | 268 | |
|
269 | 269 | return diffset |
|
270 | 270 | |
|
271 | def register_comments_vars(self, c, pull_request, versions): | |
|
271 | def register_comments_vars(self, c, pull_request, versions, include_drafts=True): | |
|
272 | 272 | comments_model = CommentsModel() |
|
273 | 273 | |
|
274 | 274 | # GENERAL COMMENTS with versions # |
|
275 | 275 | q = comments_model._all_general_comments_of_pull_request(pull_request) |
|
276 | 276 | q = q.order_by(ChangesetComment.comment_id.asc()) |
|
277 | if not include_drafts: | |
|
278 | q = q.filter(ChangesetComment.draft == false()) | |
|
277 | 279 | general_comments = q |
|
278 | 280 | |
|
279 | 281 | # pick comments we want to render at current version |
|
280 | 282 | c.comment_versions = comments_model.aggregate_comments( |
|
281 | 283 | general_comments, versions, c.at_version_num) |
|
282 | 284 | |
|
283 | 285 | # INLINE COMMENTS with versions # |
|
284 | 286 | q = comments_model._all_inline_comments_of_pull_request(pull_request) |
|
285 | 287 | q = q.order_by(ChangesetComment.comment_id.asc()) |
|
288 | if not include_drafts: | |
|
289 | q = q.filter(ChangesetComment.draft == false()) | |
|
286 | 290 | inline_comments = q |
|
287 | 291 | |
|
288 | 292 | c.inline_versions = comments_model.aggregate_comments( |
|
289 | 293 | inline_comments, versions, c.at_version_num, inline=True) |
|
290 | 294 | |
|
291 | 295 | # Comments inline+general |
|
292 | 296 | if c.at_version: |
|
293 | 297 | c.inline_comments_flat = c.inline_versions[c.at_version_num]['display'] |
|
294 | 298 | c.comments = c.comment_versions[c.at_version_num]['display'] |
|
295 | 299 | else: |
|
296 | 300 | c.inline_comments_flat = c.inline_versions[c.at_version_num]['until'] |
|
297 | 301 | c.comments = c.comment_versions[c.at_version_num]['until'] |
|
298 | 302 | |
|
299 | 303 | return general_comments, inline_comments |
|
300 | 304 | |
|
301 | 305 | @LoginRequired() |
|
302 | 306 | @HasRepoPermissionAnyDecorator( |
|
303 | 307 | 'repository.read', 'repository.write', 'repository.admin') |
|
304 | 308 | @view_config( |
|
305 | 309 | route_name='pullrequest_show', request_method='GET', |
|
306 | 310 | renderer='rhodecode:templates/pullrequests/pullrequest_show.mako') |
|
307 | 311 | def pull_request_show(self): |
|
308 | 312 | _ = self.request.translate |
|
309 | 313 | c = self.load_default_context() |
|
310 | 314 | |
|
311 | 315 | pull_request = PullRequest.get_or_404( |
|
312 | 316 | self.request.matchdict['pull_request_id']) |
|
313 | 317 | pull_request_id = pull_request.pull_request_id |
|
314 | 318 | |
|
315 | 319 | c.state_progressing = pull_request.is_state_changing() |
|
316 | 320 | c.pr_broadcast_channel = channelstream.pr_channel(pull_request) |
|
317 | 321 | |
|
318 | 322 | _new_state = { |
|
319 | 323 | 'created': PullRequest.STATE_CREATED, |
|
320 | 324 | }.get(self.request.GET.get('force_state')) |
|
321 | 325 | |
|
322 | 326 | if c.is_super_admin and _new_state: |
|
323 | 327 | with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state): |
|
324 | 328 | h.flash( |
|
325 | 329 | _('Pull Request state was force changed to `{}`').format(_new_state), |
|
326 | 330 | category='success') |
|
327 | 331 | Session().commit() |
|
328 | 332 | |
|
329 | 333 | raise HTTPFound(h.route_path( |
|
330 | 334 | 'pullrequest_show', repo_name=self.db_repo_name, |
|
331 | 335 | pull_request_id=pull_request_id)) |
|
332 | 336 | |
|
333 | 337 | version = self.request.GET.get('version') |
|
334 | 338 | from_version = self.request.GET.get('from_version') or version |
|
335 | 339 | merge_checks = self.request.GET.get('merge_checks') |
|
336 | 340 | c.fulldiff = str2bool(self.request.GET.get('fulldiff')) |
|
337 | 341 | force_refresh = str2bool(self.request.GET.get('force_refresh')) |
|
338 | 342 | c.range_diff_on = self.request.GET.get('range-diff') == "1" |
|
339 | 343 | |
|
340 | 344 | # fetch global flags of ignore ws or context lines |
|
341 | 345 | diff_context = diffs.get_diff_context(self.request) |
|
342 | 346 | hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request) |
|
343 | 347 | |
|
344 | 348 | (pull_request_latest, |
|
345 | 349 | pull_request_at_ver, |
|
346 | 350 | pull_request_display_obj, |
|
347 | 351 | at_version) = PullRequestModel().get_pr_version( |
|
348 | 352 | pull_request_id, version=version) |
|
349 | 353 | |
|
350 | 354 | pr_closed = pull_request_latest.is_closed() |
|
351 | 355 | |
|
352 | 356 | if pr_closed and (version or from_version): |
|
353 | 357 | # not allow to browse versions for closed PR |
|
354 | 358 | raise HTTPFound(h.route_path( |
|
355 | 359 | 'pullrequest_show', repo_name=self.db_repo_name, |
|
356 | 360 | pull_request_id=pull_request_id)) |
|
357 | 361 | |
|
358 | 362 | versions = pull_request_display_obj.versions() |
|
359 | 363 | # used to store per-commit range diffs |
|
360 | 364 | c.changes = collections.OrderedDict() |
|
361 | 365 | |
|
362 | 366 | c.at_version = at_version |
|
363 | 367 | c.at_version_num = (at_version |
|
364 | 368 | if at_version and at_version != PullRequest.LATEST_VER |
|
365 | 369 | else None) |
|
366 | 370 | |
|
367 | 371 | c.at_version_index = ChangesetComment.get_index_from_version( |
|
368 | 372 | c.at_version_num, versions) |
|
369 | 373 | |
|
370 | 374 | (prev_pull_request_latest, |
|
371 | 375 | prev_pull_request_at_ver, |
|
372 | 376 | prev_pull_request_display_obj, |
|
373 | 377 | prev_at_version) = PullRequestModel().get_pr_version( |
|
374 | 378 | pull_request_id, version=from_version) |
|
375 | 379 | |
|
376 | 380 | c.from_version = prev_at_version |
|
377 | 381 | c.from_version_num = (prev_at_version |
|
378 | 382 | if prev_at_version and prev_at_version != PullRequest.LATEST_VER |
|
379 | 383 | else None) |
|
380 | 384 | c.from_version_index = ChangesetComment.get_index_from_version( |
|
381 | 385 | c.from_version_num, versions) |
|
382 | 386 | |
|
383 | 387 | # define if we're in COMPARE mode or VIEW at version mode |
|
384 | 388 | compare = at_version != prev_at_version |
|
385 | 389 | |
|
386 | 390 | # pull_requests repo_name we opened it against |
|
387 | 391 | # ie. target_repo must match |
|
388 | 392 | if self.db_repo_name != pull_request_at_ver.target_repo.repo_name: |
|
389 | 393 | log.warning('Mismatch between the current repo: %s, and target %s', |
|
390 | 394 | self.db_repo_name, pull_request_at_ver.target_repo.repo_name) |
|
391 | 395 | raise HTTPNotFound() |
|
392 | 396 | |
|
393 | 397 | c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver) |
|
394 | 398 | |
|
395 | 399 | c.pull_request = pull_request_display_obj |
|
396 | 400 | c.renderer = pull_request_at_ver.description_renderer or c.renderer |
|
397 | 401 | c.pull_request_latest = pull_request_latest |
|
398 | 402 | |
|
399 | 403 | # inject latest version |
|
400 | 404 | latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest) |
|
401 | 405 | c.versions = versions + [latest_ver] |
|
402 | 406 | |
|
403 | 407 | if compare or (at_version and not at_version == PullRequest.LATEST_VER): |
|
404 | 408 | c.allowed_to_change_status = False |
|
405 | 409 | c.allowed_to_update = False |
|
406 | 410 | c.allowed_to_merge = False |
|
407 | 411 | c.allowed_to_delete = False |
|
408 | 412 | c.allowed_to_comment = False |
|
409 | 413 | c.allowed_to_close = False |
|
410 | 414 | else: |
|
411 | 415 | can_change_status = PullRequestModel().check_user_change_status( |
|
412 | 416 | pull_request_at_ver, self._rhodecode_user) |
|
413 | 417 | c.allowed_to_change_status = can_change_status and not pr_closed |
|
414 | 418 | |
|
415 | 419 | c.allowed_to_update = PullRequestModel().check_user_update( |
|
416 | 420 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
417 | 421 | c.allowed_to_merge = PullRequestModel().check_user_merge( |
|
418 | 422 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
419 | 423 | c.allowed_to_delete = PullRequestModel().check_user_delete( |
|
420 | 424 | pull_request_latest, self._rhodecode_user) and not pr_closed |
|
421 | 425 | c.allowed_to_comment = not pr_closed |
|
422 | 426 | c.allowed_to_close = c.allowed_to_merge and not pr_closed |
|
423 | 427 | |
|
424 | 428 | c.forbid_adding_reviewers = False |
|
425 | 429 | c.forbid_author_to_review = False |
|
426 | 430 | c.forbid_commit_author_to_review = False |
|
427 | 431 | |
|
428 | 432 | if pull_request_latest.reviewer_data and \ |
|
429 | 433 | 'rules' in pull_request_latest.reviewer_data: |
|
430 | 434 | rules = pull_request_latest.reviewer_data['rules'] or {} |
|
431 | 435 | try: |
|
432 | 436 | c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers') |
|
433 | 437 | c.forbid_author_to_review = rules.get('forbid_author_to_review') |
|
434 | 438 | c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review') |
|
435 | 439 | except Exception: |
|
436 | 440 | pass |
|
437 | 441 | |
|
438 | 442 | # check merge capabilities |
|
439 | 443 | _merge_check = MergeCheck.validate( |
|
440 | 444 | pull_request_latest, auth_user=self._rhodecode_user, |
|
441 | 445 | translator=self.request.translate, |
|
442 | 446 | force_shadow_repo_refresh=force_refresh) |
|
443 | 447 | |
|
444 | 448 | c.pr_merge_errors = _merge_check.error_details |
|
445 | 449 | c.pr_merge_possible = not _merge_check.failed |
|
446 | 450 | c.pr_merge_message = _merge_check.merge_msg |
|
447 | 451 | c.pr_merge_source_commit = _merge_check.source_commit |
|
448 | 452 | c.pr_merge_target_commit = _merge_check.target_commit |
|
449 | 453 | |
|
450 | 454 | c.pr_merge_info = MergeCheck.get_merge_conditions( |
|
451 | 455 | pull_request_latest, translator=self.request.translate) |
|
452 | 456 | |
|
453 | 457 | c.pull_request_review_status = _merge_check.review_status |
|
454 | 458 | if merge_checks: |
|
455 | 459 | self.request.override_renderer = \ |
|
456 | 460 | 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako' |
|
457 | 461 | return self._get_template_context(c) |
|
458 | 462 | |
|
459 | 463 | c.reviewers_count = pull_request.reviewers_count |
|
460 | 464 | c.observers_count = pull_request.observers_count |
|
461 | 465 | |
|
462 | 466 | # reviewers and statuses |
|
463 | 467 | c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data) |
|
464 | 468 | c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []}) |
|
465 | 469 | c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []}) |
|
466 | 470 | |
|
467 | 471 | for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses(): |
|
468 | 472 | member_reviewer = h.reviewer_as_json( |
|
469 | 473 | member, reasons=reasons, mandatory=mandatory, |
|
470 | 474 | role=review_obj.role, |
|
471 | 475 | user_group=review_obj.rule_user_group_data() |
|
472 | 476 | ) |
|
473 | 477 | |
|
474 | 478 | current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED |
|
475 | 479 | member_reviewer['review_status'] = current_review_status |
|
476 | 480 | member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status) |
|
477 | 481 | member_reviewer['allowed_to_update'] = c.allowed_to_update |
|
478 | 482 | c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer) |
|
479 | 483 | |
|
480 | 484 | c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json) |
|
481 | 485 | |
|
482 | 486 | for observer_obj, member in pull_request_at_ver.observers(): |
|
483 | 487 | member_observer = h.reviewer_as_json( |
|
484 | 488 | member, reasons=[], mandatory=False, |
|
485 | 489 | role=observer_obj.role, |
|
486 | 490 | user_group=observer_obj.rule_user_group_data() |
|
487 | 491 | ) |
|
488 | 492 | member_observer['allowed_to_update'] = c.allowed_to_update |
|
489 | 493 | c.pull_request_set_observers_data_json['observers'].append(member_observer) |
|
490 | 494 | |
|
491 | 495 | c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json) |
|
492 | 496 | |
|
493 | 497 | general_comments, inline_comments = \ |
|
494 | 498 | self.register_comments_vars(c, pull_request_latest, versions) |
|
495 | 499 | |
|
496 | 500 | # TODOs |
|
497 | 501 | c.unresolved_comments = CommentsModel() \ |
|
498 | 502 | .get_pull_request_unresolved_todos(pull_request_latest) |
|
499 | 503 | c.resolved_comments = CommentsModel() \ |
|
500 | 504 | .get_pull_request_resolved_todos(pull_request_latest) |
|
501 | 505 | |
|
502 | 506 | # if we use version, then do not show later comments |
|
503 | 507 | # than current version |
|
504 | 508 | display_inline_comments = collections.defaultdict( |
|
505 | 509 | lambda: collections.defaultdict(list)) |
|
506 | 510 | for co in inline_comments: |
|
507 | 511 | if c.at_version_num: |
|
508 | 512 | # pick comments that are at least UPTO given version, so we |
|
509 | 513 | # don't render comments for higher version |
|
510 | 514 | should_render = co.pull_request_version_id and \ |
|
511 | 515 | co.pull_request_version_id <= c.at_version_num |
|
512 | 516 | else: |
|
513 | 517 | # showing all, for 'latest' |
|
514 | 518 | should_render = True |
|
515 | 519 | |
|
516 | 520 | if should_render: |
|
517 | 521 | display_inline_comments[co.f_path][co.line_no].append(co) |
|
518 | 522 | |
|
519 | 523 | # load diff data into template context, if we use compare mode then |
|
520 | 524 | # diff is calculated based on changes between versions of PR |
|
521 | 525 | |
|
522 | 526 | source_repo = pull_request_at_ver.source_repo |
|
523 | 527 | source_ref_id = pull_request_at_ver.source_ref_parts.commit_id |
|
524 | 528 | |
|
525 | 529 | target_repo = pull_request_at_ver.target_repo |
|
526 | 530 | target_ref_id = pull_request_at_ver.target_ref_parts.commit_id |
|
527 | 531 | |
|
528 | 532 | if compare: |
|
529 | 533 | # in compare switch the diff base to latest commit from prev version |
|
530 | 534 | target_ref_id = prev_pull_request_display_obj.revisions[0] |
|
531 | 535 | |
|
532 | 536 | # despite opening commits for bookmarks/branches/tags, we always |
|
533 | 537 | # convert this to rev to prevent changes after bookmark or branch change |
|
534 | 538 | c.source_ref_type = 'rev' |
|
535 | 539 | c.source_ref = source_ref_id |
|
536 | 540 | |
|
537 | 541 | c.target_ref_type = 'rev' |
|
538 | 542 | c.target_ref = target_ref_id |
|
539 | 543 | |
|
540 | 544 | c.source_repo = source_repo |
|
541 | 545 | c.target_repo = target_repo |
|
542 | 546 | |
|
543 | 547 | c.commit_ranges = [] |
|
544 | 548 | source_commit = EmptyCommit() |
|
545 | 549 | target_commit = EmptyCommit() |
|
546 | 550 | c.missing_requirements = False |
|
547 | 551 | |
|
548 | 552 | source_scm = source_repo.scm_instance() |
|
549 | 553 | target_scm = target_repo.scm_instance() |
|
550 | 554 | |
|
551 | 555 | shadow_scm = None |
|
552 | 556 | try: |
|
553 | 557 | shadow_scm = pull_request_latest.get_shadow_repo() |
|
554 | 558 | except Exception: |
|
555 | 559 | log.debug('Failed to get shadow repo', exc_info=True) |
|
556 | 560 | # try first the existing source_repo, and then shadow |
|
557 | 561 | # repo if we can obtain one |
|
558 | 562 | commits_source_repo = source_scm |
|
559 | 563 | if shadow_scm: |
|
560 | 564 | commits_source_repo = shadow_scm |
|
561 | 565 | |
|
562 | 566 | c.commits_source_repo = commits_source_repo |
|
563 | 567 | c.ancestor = None # set it to None, to hide it from PR view |
|
564 | 568 | |
|
565 | 569 | # empty version means latest, so we keep this to prevent |
|
566 | 570 | # double caching |
|
567 | 571 | version_normalized = version or PullRequest.LATEST_VER |
|
568 | 572 | from_version_normalized = from_version or PullRequest.LATEST_VER |
|
569 | 573 | |
|
570 | 574 | cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo) |
|
571 | 575 | cache_file_path = diff_cache_exist( |
|
572 | 576 | cache_path, 'pull_request', pull_request_id, version_normalized, |
|
573 | 577 | from_version_normalized, source_ref_id, target_ref_id, |
|
574 | 578 | hide_whitespace_changes, diff_context, c.fulldiff) |
|
575 | 579 | |
|
576 | 580 | caching_enabled = self._is_diff_cache_enabled(c.target_repo) |
|
577 | 581 | force_recache = self.get_recache_flag() |
|
578 | 582 | |
|
579 | 583 | cached_diff = None |
|
580 | 584 | if caching_enabled: |
|
581 | 585 | cached_diff = load_cached_diff(cache_file_path) |
|
582 | 586 | |
|
583 | 587 | has_proper_commit_cache = ( |
|
584 | 588 | cached_diff and cached_diff.get('commits') |
|
585 | 589 | and len(cached_diff.get('commits', [])) == 5 |
|
586 | 590 | and cached_diff.get('commits')[0] |
|
587 | 591 | and cached_diff.get('commits')[3]) |
|
588 | 592 | |
|
589 | 593 | if not force_recache and not c.range_diff_on and has_proper_commit_cache: |
|
590 | 594 | diff_commit_cache = \ |
|
591 | 595 | (ancestor_commit, commit_cache, missing_requirements, |
|
592 | 596 | source_commit, target_commit) = cached_diff['commits'] |
|
593 | 597 | else: |
|
594 | 598 | # NOTE(marcink): we reach potentially unreachable errors when a PR has |
|
595 | 599 | # merge errors resulting in potentially hidden commits in the shadow repo. |
|
596 | 600 | maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \ |
|
597 | 601 | and _merge_check.merge_response |
|
598 | 602 | maybe_unreachable = maybe_unreachable \ |
|
599 | 603 | and _merge_check.merge_response.metadata.get('unresolved_files') |
|
600 | 604 | log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation") |
|
601 | 605 | diff_commit_cache = \ |
|
602 | 606 | (ancestor_commit, commit_cache, missing_requirements, |
|
603 | 607 | source_commit, target_commit) = self.get_commits( |
|
604 | 608 | commits_source_repo, |
|
605 | 609 | pull_request_at_ver, |
|
606 | 610 | source_commit, |
|
607 | 611 | source_ref_id, |
|
608 | 612 | source_scm, |
|
609 | 613 | target_commit, |
|
610 | 614 | target_ref_id, |
|
611 | 615 | target_scm, |
|
612 | 616 | maybe_unreachable=maybe_unreachable) |
|
613 | 617 | |
|
614 | 618 | # register our commit range |
|
615 | 619 | for comm in commit_cache.values(): |
|
616 | 620 | c.commit_ranges.append(comm) |
|
617 | 621 | |
|
618 | 622 | c.missing_requirements = missing_requirements |
|
619 | 623 | c.ancestor_commit = ancestor_commit |
|
620 | 624 | c.statuses = source_repo.statuses( |
|
621 | 625 | [x.raw_id for x in c.commit_ranges]) |
|
622 | 626 | |
|
623 | 627 | # auto collapse if we have more than limit |
|
624 | 628 | collapse_limit = diffs.DiffProcessor._collapse_commits_over |
|
625 | 629 | c.collapse_all_commits = len(c.commit_ranges) > collapse_limit |
|
626 | 630 | c.compare_mode = compare |
|
627 | 631 | |
|
628 | 632 | # diff_limit is the old behavior, will cut off the whole diff |
|
629 | 633 | # if the limit is applied otherwise will just hide the |
|
630 | 634 | # big files from the front-end |
|
631 | 635 | diff_limit = c.visual.cut_off_limit_diff |
|
632 | 636 | file_limit = c.visual.cut_off_limit_file |
|
633 | 637 | |
|
634 | 638 | c.missing_commits = False |
|
635 | 639 | if (c.missing_requirements |
|
636 | 640 | or isinstance(source_commit, EmptyCommit) |
|
637 | 641 | or source_commit == target_commit): |
|
638 | 642 | |
|
639 | 643 | c.missing_commits = True |
|
640 | 644 | else: |
|
641 | 645 | c.inline_comments = display_inline_comments |
|
642 | 646 | |
|
643 | 647 | use_ancestor = True |
|
644 | 648 | if from_version_normalized != version_normalized: |
|
645 | 649 | use_ancestor = False |
|
646 | 650 | |
|
647 | 651 | has_proper_diff_cache = cached_diff and cached_diff.get('commits') |
|
648 | 652 | if not force_recache and has_proper_diff_cache: |
|
649 | 653 | c.diffset = cached_diff['diff'] |
|
650 | 654 | else: |
|
651 | 655 | try: |
|
652 | 656 | c.diffset = self._get_diffset( |
|
653 | 657 | c.source_repo.repo_name, commits_source_repo, |
|
654 | 658 | c.ancestor_commit, |
|
655 | 659 | source_ref_id, target_ref_id, |
|
656 | 660 | target_commit, source_commit, |
|
657 | 661 | diff_limit, file_limit, c.fulldiff, |
|
658 | 662 | hide_whitespace_changes, diff_context, |
|
659 | 663 | use_ancestor=use_ancestor |
|
660 | 664 | ) |
|
661 | 665 | |
|
662 | 666 | # save cached diff |
|
663 | 667 | if caching_enabled: |
|
664 | 668 | cache_diff(cache_file_path, c.diffset, diff_commit_cache) |
|
665 | 669 | except CommitDoesNotExistError: |
|
666 | 670 | log.exception('Failed to generate diffset') |
|
667 | 671 | c.missing_commits = True |
|
668 | 672 | |
|
669 | 673 | if not c.missing_commits: |
|
670 | 674 | |
|
671 | 675 | c.limited_diff = c.diffset.limited_diff |
|
672 | 676 | |
|
673 | 677 | # calculate removed files that are bound to comments |
|
674 | 678 | comment_deleted_files = [ |
|
675 | 679 | fname for fname in display_inline_comments |
|
676 | 680 | if fname not in c.diffset.file_stats] |
|
677 | 681 | |
|
678 | 682 | c.deleted_files_comments = collections.defaultdict(dict) |
|
679 | 683 | for fname, per_line_comments in display_inline_comments.items(): |
|
680 | 684 | if fname in comment_deleted_files: |
|
681 | 685 | c.deleted_files_comments[fname]['stats'] = 0 |
|
682 | 686 | c.deleted_files_comments[fname]['comments'] = list() |
|
683 | 687 | for lno, comments in per_line_comments.items(): |
|
684 | 688 | c.deleted_files_comments[fname]['comments'].extend(comments) |
|
685 | 689 | |
|
686 | 690 | # maybe calculate the range diff |
|
687 | 691 | if c.range_diff_on: |
|
688 | 692 | # TODO(marcink): set whitespace/context |
|
689 | 693 | context_lcl = 3 |
|
690 | 694 | ign_whitespace_lcl = False |
|
691 | 695 | |
|
692 | 696 | for commit in c.commit_ranges: |
|
693 | 697 | commit2 = commit |
|
694 | 698 | commit1 = commit.first_parent |
|
695 | 699 | |
|
696 | 700 | range_diff_cache_file_path = diff_cache_exist( |
|
697 | 701 | cache_path, 'diff', commit.raw_id, |
|
698 | 702 | ign_whitespace_lcl, context_lcl, c.fulldiff) |
|
699 | 703 | |
|
700 | 704 | cached_diff = None |
|
701 | 705 | if caching_enabled: |
|
702 | 706 | cached_diff = load_cached_diff(range_diff_cache_file_path) |
|
703 | 707 | |
|
704 | 708 | has_proper_diff_cache = cached_diff and cached_diff.get('diff') |
|
705 | 709 | if not force_recache and has_proper_diff_cache: |
|
706 | 710 | diffset = cached_diff['diff'] |
|
707 | 711 | else: |
|
708 | 712 | diffset = self._get_range_diffset( |
|
709 | 713 | commits_source_repo, source_repo, |
|
710 | 714 | commit1, commit2, diff_limit, file_limit, |
|
711 | 715 | c.fulldiff, ign_whitespace_lcl, context_lcl |
|
712 | 716 | ) |
|
713 | 717 | |
|
714 | 718 | # save cached diff |
|
715 | 719 | if caching_enabled: |
|
716 | 720 | cache_diff(range_diff_cache_file_path, diffset, None) |
|
717 | 721 | |
|
718 | 722 | c.changes[commit.raw_id] = diffset |
|
719 | 723 | |
|
720 | 724 | # this is a hack to properly display links, when creating PR, the |
|
721 | 725 | # compare view and others uses different notation, and |
|
722 | 726 | # compare_commits.mako renders links based on the target_repo. |
|
723 | 727 | # We need to swap that here to generate it properly on the html side |
|
724 | 728 | c.target_repo = c.source_repo |
|
725 | 729 | |
|
726 | 730 | c.commit_statuses = ChangesetStatus.STATUSES |
|
727 | 731 | |
|
728 | 732 | c.show_version_changes = not pr_closed |
|
729 | 733 | if c.show_version_changes: |
|
730 | 734 | cur_obj = pull_request_at_ver |
|
731 | 735 | prev_obj = prev_pull_request_at_ver |
|
732 | 736 | |
|
733 | 737 | old_commit_ids = prev_obj.revisions |
|
734 | 738 | new_commit_ids = cur_obj.revisions |
|
735 | 739 | commit_changes = PullRequestModel()._calculate_commit_id_changes( |
|
736 | 740 | old_commit_ids, new_commit_ids) |
|
737 | 741 | c.commit_changes_summary = commit_changes |
|
738 | 742 | |
|
739 | 743 | # calculate the diff for commits between versions |
|
740 | 744 | c.commit_changes = [] |
|
741 | 745 | |
|
742 | 746 | def mark(cs, fw): |
|
743 | 747 | return list(h.itertools.izip_longest([], cs, fillvalue=fw)) |
|
744 | 748 | |
|
745 | 749 | for c_type, raw_id in mark(commit_changes.added, 'a') \ |
|
746 | 750 | + mark(commit_changes.removed, 'r') \ |
|
747 | 751 | + mark(commit_changes.common, 'c'): |
|
748 | 752 | |
|
749 | 753 | if raw_id in commit_cache: |
|
750 | 754 | commit = commit_cache[raw_id] |
|
751 | 755 | else: |
|
752 | 756 | try: |
|
753 | 757 | commit = commits_source_repo.get_commit(raw_id) |
|
754 | 758 | except CommitDoesNotExistError: |
|
755 | 759 | # in case we fail extracting still use "dummy" commit |
|
756 | 760 | # for display in commit diff |
|
757 | 761 | commit = h.AttributeDict( |
|
758 | 762 | {'raw_id': raw_id, |
|
759 | 763 | 'message': 'EMPTY or MISSING COMMIT'}) |
|
760 | 764 | c.commit_changes.append([c_type, commit]) |
|
761 | 765 | |
|
762 | 766 | # current user review statuses for each version |
|
763 | 767 | c.review_versions = {} |
|
764 | 768 | is_reviewer = PullRequestModel().is_user_reviewer( |
|
765 | 769 | pull_request, self._rhodecode_user) |
|
766 | 770 | if is_reviewer: |
|
767 | 771 | for co in general_comments: |
|
768 | 772 | if co.author.user_id == self._rhodecode_user.user_id: |
|
769 | 773 | status = co.status_change |
|
770 | 774 | if status: |
|
771 | 775 | _ver_pr = status[0].comment.pull_request_version_id |
|
772 | 776 | c.review_versions[_ver_pr] = status[0] |
|
773 | 777 | |
|
774 | 778 | return self._get_template_context(c) |
|
775 | 779 | |
|
776 | 780 | def get_commits( |
|
777 | 781 | self, commits_source_repo, pull_request_at_ver, source_commit, |
|
778 | 782 | source_ref_id, source_scm, target_commit, target_ref_id, target_scm, |
|
779 | 783 | maybe_unreachable=False): |
|
780 | 784 | |
|
781 | 785 | commit_cache = collections.OrderedDict() |
|
782 | 786 | missing_requirements = False |
|
783 | 787 | |
|
784 | 788 | try: |
|
785 | 789 | pre_load = ["author", "date", "message", "branch", "parents"] |
|
786 | 790 | |
|
787 | 791 | pull_request_commits = pull_request_at_ver.revisions |
|
788 | 792 | log.debug('Loading %s commits from %s', |
|
789 | 793 | len(pull_request_commits), commits_source_repo) |
|
790 | 794 | |
|
791 | 795 | for rev in pull_request_commits: |
|
792 | 796 | comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load, |
|
793 | 797 | maybe_unreachable=maybe_unreachable) |
|
794 | 798 | commit_cache[comm.raw_id] = comm |
|
795 | 799 | |
|
796 | 800 | # Order here matters, we first need to get target, and then |
|
797 | 801 | # the source |
|
798 | 802 | target_commit = commits_source_repo.get_commit( |
|
799 | 803 | commit_id=safe_str(target_ref_id)) |
|
800 | 804 | |
|
801 | 805 | source_commit = commits_source_repo.get_commit( |
|
802 | 806 | commit_id=safe_str(source_ref_id), maybe_unreachable=True) |
|
803 | 807 | except CommitDoesNotExistError: |
|
804 | 808 | log.warning('Failed to get commit from `{}` repo'.format( |
|
805 | 809 | commits_source_repo), exc_info=True) |
|
806 | 810 | except RepositoryRequirementError: |
|
807 | 811 | log.warning('Failed to get all required data from repo', exc_info=True) |
|
808 | 812 | missing_requirements = True |
|
809 | 813 | |
|
810 | 814 | pr_ancestor_id = pull_request_at_ver.common_ancestor_id |
|
811 | 815 | |
|
812 | 816 | try: |
|
813 | 817 | ancestor_commit = source_scm.get_commit(pr_ancestor_id) |
|
814 | 818 | except Exception: |
|
815 | 819 | ancestor_commit = None |
|
816 | 820 | |
|
817 | 821 | return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit |
|
818 | 822 | |
|
819 | 823 | def assure_not_empty_repo(self): |
|
820 | 824 | _ = self.request.translate |
|
821 | 825 | |
|
822 | 826 | try: |
|
823 | 827 | self.db_repo.scm_instance().get_commit() |
|
824 | 828 | except EmptyRepositoryError: |
|
825 | 829 | h.flash(h.literal(_('There are no commits yet')), |
|
826 | 830 | category='warning') |
|
827 | 831 | raise HTTPFound( |
|
828 | 832 | h.route_path('repo_summary', repo_name=self.db_repo.repo_name)) |
|
829 | 833 | |
|
830 | 834 | @LoginRequired() |
|
831 | 835 | @NotAnonymous() |
|
832 | 836 | @HasRepoPermissionAnyDecorator( |
|
833 | 837 | 'repository.read', 'repository.write', 'repository.admin') |
|
834 | 838 | @view_config( |
|
835 | 839 | route_name='pullrequest_new', request_method='GET', |
|
836 | 840 | renderer='rhodecode:templates/pullrequests/pullrequest.mako') |
|
837 | 841 | def pull_request_new(self): |
|
838 | 842 | _ = self.request.translate |
|
839 | 843 | c = self.load_default_context() |
|
840 | 844 | |
|
841 | 845 | self.assure_not_empty_repo() |
|
842 | 846 | source_repo = self.db_repo |
|
843 | 847 | |
|
844 | 848 | commit_id = self.request.GET.get('commit') |
|
845 | 849 | branch_ref = self.request.GET.get('branch') |
|
846 | 850 | bookmark_ref = self.request.GET.get('bookmark') |
|
847 | 851 | |
|
848 | 852 | try: |
|
849 | 853 | source_repo_data = PullRequestModel().generate_repo_data( |
|
850 | 854 | source_repo, commit_id=commit_id, |
|
851 | 855 | branch=branch_ref, bookmark=bookmark_ref, |
|
852 | 856 | translator=self.request.translate) |
|
853 | 857 | except CommitDoesNotExistError as e: |
|
854 | 858 | log.exception(e) |
|
855 | 859 | h.flash(_('Commit does not exist'), 'error') |
|
856 | 860 | raise HTTPFound( |
|
857 | 861 | h.route_path('pullrequest_new', repo_name=source_repo.repo_name)) |
|
858 | 862 | |
|
859 | 863 | default_target_repo = source_repo |
|
860 | 864 | |
|
861 | 865 | if source_repo.parent and c.has_origin_repo_read_perm: |
|
862 | 866 | parent_vcs_obj = source_repo.parent.scm_instance() |
|
863 | 867 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
864 | 868 | # change default if we have a parent repo |
|
865 | 869 | default_target_repo = source_repo.parent |
|
866 | 870 | |
|
867 | 871 | target_repo_data = PullRequestModel().generate_repo_data( |
|
868 | 872 | default_target_repo, translator=self.request.translate) |
|
869 | 873 | |
|
870 | 874 | selected_source_ref = source_repo_data['refs']['selected_ref'] |
|
871 | 875 | title_source_ref = '' |
|
872 | 876 | if selected_source_ref: |
|
873 | 877 | title_source_ref = selected_source_ref.split(':', 2)[1] |
|
874 | 878 | c.default_title = PullRequestModel().generate_pullrequest_title( |
|
875 | 879 | source=source_repo.repo_name, |
|
876 | 880 | source_ref=title_source_ref, |
|
877 | 881 | target=default_target_repo.repo_name |
|
878 | 882 | ) |
|
879 | 883 | |
|
880 | 884 | c.default_repo_data = { |
|
881 | 885 | 'source_repo_name': source_repo.repo_name, |
|
882 | 886 | 'source_refs_json': json.dumps(source_repo_data), |
|
883 | 887 | 'target_repo_name': default_target_repo.repo_name, |
|
884 | 888 | 'target_refs_json': json.dumps(target_repo_data), |
|
885 | 889 | } |
|
886 | 890 | c.default_source_ref = selected_source_ref |
|
887 | 891 | |
|
888 | 892 | return self._get_template_context(c) |
|
889 | 893 | |
|
890 | 894 | @LoginRequired() |
|
891 | 895 | @NotAnonymous() |
|
892 | 896 | @HasRepoPermissionAnyDecorator( |
|
893 | 897 | 'repository.read', 'repository.write', 'repository.admin') |
|
894 | 898 | @view_config( |
|
895 | 899 | route_name='pullrequest_repo_refs', request_method='GET', |
|
896 | 900 | renderer='json_ext', xhr=True) |
|
897 | 901 | def pull_request_repo_refs(self): |
|
898 | 902 | self.load_default_context() |
|
899 | 903 | target_repo_name = self.request.matchdict['target_repo_name'] |
|
900 | 904 | repo = Repository.get_by_repo_name(target_repo_name) |
|
901 | 905 | if not repo: |
|
902 | 906 | raise HTTPNotFound() |
|
903 | 907 | |
|
904 | 908 | target_perm = HasRepoPermissionAny( |
|
905 | 909 | 'repository.read', 'repository.write', 'repository.admin')( |
|
906 | 910 | target_repo_name) |
|
907 | 911 | if not target_perm: |
|
908 | 912 | raise HTTPNotFound() |
|
909 | 913 | |
|
910 | 914 | return PullRequestModel().generate_repo_data( |
|
911 | 915 | repo, translator=self.request.translate) |
|
912 | 916 | |
|
913 | 917 | @LoginRequired() |
|
914 | 918 | @NotAnonymous() |
|
915 | 919 | @HasRepoPermissionAnyDecorator( |
|
916 | 920 | 'repository.read', 'repository.write', 'repository.admin') |
|
917 | 921 | @view_config( |
|
918 | 922 | route_name='pullrequest_repo_targets', request_method='GET', |
|
919 | 923 | renderer='json_ext', xhr=True) |
|
920 | 924 | def pullrequest_repo_targets(self): |
|
921 | 925 | _ = self.request.translate |
|
922 | 926 | filter_query = self.request.GET.get('query') |
|
923 | 927 | |
|
924 | 928 | # get the parents |
|
925 | 929 | parent_target_repos = [] |
|
926 | 930 | if self.db_repo.parent: |
|
927 | 931 | parents_query = Repository.query() \ |
|
928 | 932 | .order_by(func.length(Repository.repo_name)) \ |
|
929 | 933 | .filter(Repository.fork_id == self.db_repo.parent.repo_id) |
|
930 | 934 | |
|
931 | 935 | if filter_query: |
|
932 | 936 | ilike_expression = u'%{}%'.format(safe_unicode(filter_query)) |
|
933 | 937 | parents_query = parents_query.filter( |
|
934 | 938 | Repository.repo_name.ilike(ilike_expression)) |
|
935 | 939 | parents = parents_query.limit(20).all() |
|
936 | 940 | |
|
937 | 941 | for parent in parents: |
|
938 | 942 | parent_vcs_obj = parent.scm_instance() |
|
939 | 943 | if parent_vcs_obj and not parent_vcs_obj.is_empty(): |
|
940 | 944 | parent_target_repos.append(parent) |
|
941 | 945 | |
|
942 | 946 | # get other forks, and repo itself |
|
943 | 947 | query = Repository.query() \ |
|
944 | 948 | .order_by(func.length(Repository.repo_name)) \ |
|
945 | 949 | .filter( |
|
946 | 950 | or_(Repository.repo_id == self.db_repo.repo_id, # repo itself |
|
947 | 951 | Repository.fork_id == self.db_repo.repo_id) # forks of this repo |
|
948 | 952 | ) \ |
|
949 | 953 | .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos])) |
|
950 | 954 | |
|
951 | 955 | if filter_query: |
|
952 | 956 | ilike_expression = u'%{}%'.format(safe_unicode(filter_query)) |
|
953 | 957 | query = query.filter(Repository.repo_name.ilike(ilike_expression)) |
|
954 | 958 | |
|
955 | 959 | limit = max(20 - len(parent_target_repos), 5) # not less then 5 |
|
956 | 960 | target_repos = query.limit(limit).all() |
|
957 | 961 | |
|
958 | 962 | all_target_repos = target_repos + parent_target_repos |
|
959 | 963 | |
|
960 | 964 | repos = [] |
|
961 | 965 | # This checks permissions to the repositories |
|
962 | 966 | for obj in ScmModel().get_repos(all_target_repos): |
|
963 | 967 | repos.append({ |
|
964 | 968 | 'id': obj['name'], |
|
965 | 969 | 'text': obj['name'], |
|
966 | 970 | 'type': 'repo', |
|
967 | 971 | 'repo_id': obj['dbrepo']['repo_id'], |
|
968 | 972 | 'repo_type': obj['dbrepo']['repo_type'], |
|
969 | 973 | 'private': obj['dbrepo']['private'], |
|
970 | 974 | |
|
971 | 975 | }) |
|
972 | 976 | |
|
973 | 977 | data = { |
|
974 | 978 | 'more': False, |
|
975 | 979 | 'results': [{ |
|
976 | 980 | 'text': _('Repositories'), |
|
977 | 981 | 'children': repos |
|
978 | 982 | }] if repos else [] |
|
979 | 983 | } |
|
980 | 984 | return data |
|
981 | 985 | |
|
982 | 986 | def _get_existing_ids(self, post_data): |
|
983 | 987 | return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ','))) |
|
984 | 988 | |
|
985 | 989 | @LoginRequired() |
|
986 | 990 | @NotAnonymous() |
|
987 | 991 | @HasRepoPermissionAnyDecorator( |
|
988 | 992 | 'repository.read', 'repository.write', 'repository.admin') |
|
989 | 993 | @view_config( |
|
990 | 994 | route_name='pullrequest_comments', request_method='POST', |
|
991 | 995 | renderer='string_html', xhr=True) |
|
992 | 996 | def pullrequest_comments(self): |
|
993 | 997 | self.load_default_context() |
|
994 | 998 | |
|
995 | 999 | pull_request = PullRequest.get_or_404( |
|
996 | 1000 | self.request.matchdict['pull_request_id']) |
|
997 | 1001 | pull_request_id = pull_request.pull_request_id |
|
998 | 1002 | version = self.request.GET.get('version') |
|
999 | 1003 | |
|
1000 | 1004 | _render = self.request.get_partial_renderer( |
|
1001 | 1005 | 'rhodecode:templates/base/sidebar.mako') |
|
1002 | 1006 | c = _render.get_call_context() |
|
1003 | 1007 | |
|
1004 | 1008 | (pull_request_latest, |
|
1005 | 1009 | pull_request_at_ver, |
|
1006 | 1010 | pull_request_display_obj, |
|
1007 | 1011 | at_version) = PullRequestModel().get_pr_version( |
|
1008 | 1012 | pull_request_id, version=version) |
|
1009 | 1013 | versions = pull_request_display_obj.versions() |
|
1010 | 1014 | latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest) |
|
1011 | 1015 | c.versions = versions + [latest_ver] |
|
1012 | 1016 | |
|
1013 | 1017 | c.at_version = at_version |
|
1014 | 1018 | c.at_version_num = (at_version |
|
1015 | 1019 | if at_version and at_version != PullRequest.LATEST_VER |
|
1016 | 1020 | else None) |
|
1017 | 1021 | |
|
1018 | self.register_comments_vars(c, pull_request_latest, versions) | |
|
1022 | self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False) | |
|
1019 | 1023 | all_comments = c.inline_comments_flat + c.comments |
|
1020 | 1024 | |
|
1021 | 1025 | existing_ids = self._get_existing_ids(self.request.POST) |
|
1022 | 1026 | return _render('comments_table', all_comments, len(all_comments), |
|
1023 | 1027 | existing_ids=existing_ids) |
|
1024 | 1028 | |
|
1025 | 1029 | @LoginRequired() |
|
1026 | 1030 | @NotAnonymous() |
|
1027 | 1031 | @HasRepoPermissionAnyDecorator( |
|
1028 | 1032 | 'repository.read', 'repository.write', 'repository.admin') |
|
1029 | 1033 | @view_config( |
|
1030 | 1034 | route_name='pullrequest_todos', request_method='POST', |
|
1031 | 1035 | renderer='string_html', xhr=True) |
|
1032 | 1036 | def pullrequest_todos(self): |
|
1033 | 1037 | self.load_default_context() |
|
1034 | 1038 | |
|
1035 | 1039 | pull_request = PullRequest.get_or_404( |
|
1036 | 1040 | self.request.matchdict['pull_request_id']) |
|
1037 | 1041 | pull_request_id = pull_request.pull_request_id |
|
1038 | 1042 | version = self.request.GET.get('version') |
|
1039 | 1043 | |
|
1040 | 1044 | _render = self.request.get_partial_renderer( |
|
1041 | 1045 | 'rhodecode:templates/base/sidebar.mako') |
|
1042 | 1046 | c = _render.get_call_context() |
|
1043 | 1047 | (pull_request_latest, |
|
1044 | 1048 | pull_request_at_ver, |
|
1045 | 1049 | pull_request_display_obj, |
|
1046 | 1050 | at_version) = PullRequestModel().get_pr_version( |
|
1047 | 1051 | pull_request_id, version=version) |
|
1048 | 1052 | versions = pull_request_display_obj.versions() |
|
1049 | 1053 | latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest) |
|
1050 | 1054 | c.versions = versions + [latest_ver] |
|
1051 | 1055 | |
|
1052 | 1056 | c.at_version = at_version |
|
1053 | 1057 | c.at_version_num = (at_version |
|
1054 | 1058 | if at_version and at_version != PullRequest.LATEST_VER |
|
1055 | 1059 | else None) |
|
1056 | 1060 | |
|
1057 | 1061 | c.unresolved_comments = CommentsModel() \ |
|
1058 | .get_pull_request_unresolved_todos(pull_request) | |
|
1062 | .get_pull_request_unresolved_todos(pull_request, include_drafts=False) | |
|
1059 | 1063 | c.resolved_comments = CommentsModel() \ |
|
1060 | .get_pull_request_resolved_todos(pull_request) | |
|
1064 | .get_pull_request_resolved_todos(pull_request, include_drafts=False) | |
|
1061 | 1065 | |
|
1062 | 1066 | all_comments = c.unresolved_comments + c.resolved_comments |
|
1063 | 1067 | existing_ids = self._get_existing_ids(self.request.POST) |
|
1064 | 1068 | return _render('comments_table', all_comments, len(c.unresolved_comments), |
|
1065 | 1069 | todo_comments=True, existing_ids=existing_ids) |
|
1066 | 1070 | |
|
1067 | 1071 | @LoginRequired() |
|
1068 | 1072 | @NotAnonymous() |
|
1069 | 1073 | @HasRepoPermissionAnyDecorator( |
|
1070 | 1074 | 'repository.read', 'repository.write', 'repository.admin') |
|
1071 | 1075 | @CSRFRequired() |
|
1072 | 1076 | @view_config( |
|
1073 | 1077 | route_name='pullrequest_create', request_method='POST', |
|
1074 | 1078 | renderer=None) |
|
1075 | 1079 | def pull_request_create(self): |
|
1076 | 1080 | _ = self.request.translate |
|
1077 | 1081 | self.assure_not_empty_repo() |
|
1078 | 1082 | self.load_default_context() |
|
1079 | 1083 | |
|
1080 | 1084 | controls = peppercorn.parse(self.request.POST.items()) |
|
1081 | 1085 | |
|
1082 | 1086 | try: |
|
1083 | 1087 | form = PullRequestForm( |
|
1084 | 1088 | self.request.translate, self.db_repo.repo_id)() |
|
1085 | 1089 | _form = form.to_python(controls) |
|
1086 | 1090 | except formencode.Invalid as errors: |
|
1087 | 1091 | if errors.error_dict.get('revisions'): |
|
1088 | 1092 | msg = 'Revisions: %s' % errors.error_dict['revisions'] |
|
1089 | 1093 | elif errors.error_dict.get('pullrequest_title'): |
|
1090 | 1094 | msg = errors.error_dict.get('pullrequest_title') |
|
1091 | 1095 | else: |
|
1092 | 1096 | msg = _('Error creating pull request: {}').format(errors) |
|
1093 | 1097 | log.exception(msg) |
|
1094 | 1098 | h.flash(msg, 'error') |
|
1095 | 1099 | |
|
1096 | 1100 | # would rather just go back to form ... |
|
1097 | 1101 | raise HTTPFound( |
|
1098 | 1102 | h.route_path('pullrequest_new', repo_name=self.db_repo_name)) |
|
1099 | 1103 | |
|
1100 | 1104 | source_repo = _form['source_repo'] |
|
1101 | 1105 | source_ref = _form['source_ref'] |
|
1102 | 1106 | target_repo = _form['target_repo'] |
|
1103 | 1107 | target_ref = _form['target_ref'] |
|
1104 | 1108 | commit_ids = _form['revisions'][::-1] |
|
1105 | 1109 | common_ancestor_id = _form['common_ancestor'] |
|
1106 | 1110 | |
|
1107 | 1111 | # find the ancestor for this pr |
|
1108 | 1112 | source_db_repo = Repository.get_by_repo_name(_form['source_repo']) |
|
1109 | 1113 | target_db_repo = Repository.get_by_repo_name(_form['target_repo']) |
|
1110 | 1114 | |
|
1111 | 1115 | if not (source_db_repo or target_db_repo): |
|
1112 | 1116 | h.flash(_('source_repo or target repo not found'), category='error') |
|
1113 | 1117 | raise HTTPFound( |
|
1114 | 1118 | h.route_path('pullrequest_new', repo_name=self.db_repo_name)) |
|
1115 | 1119 | |
|
1116 | 1120 | # re-check permissions again here |
|
1117 | 1121 | # source_repo we must have read permissions |
|
1118 | 1122 | |
|
1119 | 1123 | source_perm = HasRepoPermissionAny( |
|
1120 | 1124 | 'repository.read', 'repository.write', 'repository.admin')( |
|
1121 | 1125 | source_db_repo.repo_name) |
|
1122 | 1126 | if not source_perm: |
|
1123 | 1127 | msg = _('Not Enough permissions to source repo `{}`.'.format( |
|
1124 | 1128 | source_db_repo.repo_name)) |
|
1125 | 1129 | h.flash(msg, category='error') |
|
1126 | 1130 | # copy the args back to redirect |
|
1127 | 1131 | org_query = self.request.GET.mixed() |
|
1128 | 1132 | raise HTTPFound( |
|
1129 | 1133 | h.route_path('pullrequest_new', repo_name=self.db_repo_name, |
|
1130 | 1134 | _query=org_query)) |
|
1131 | 1135 | |
|
1132 | 1136 | # target repo we must have read permissions, and also later on |
|
1133 | 1137 | # we want to check branch permissions here |
|
1134 | 1138 | target_perm = HasRepoPermissionAny( |
|
1135 | 1139 | 'repository.read', 'repository.write', 'repository.admin')( |
|
1136 | 1140 | target_db_repo.repo_name) |
|
1137 | 1141 | if not target_perm: |
|
1138 | 1142 | msg = _('Not Enough permissions to target repo `{}`.'.format( |
|
1139 | 1143 | target_db_repo.repo_name)) |
|
1140 | 1144 | h.flash(msg, category='error') |
|
1141 | 1145 | # copy the args back to redirect |
|
1142 | 1146 | org_query = self.request.GET.mixed() |
|
1143 | 1147 | raise HTTPFound( |
|
1144 | 1148 | h.route_path('pullrequest_new', repo_name=self.db_repo_name, |
|
1145 | 1149 | _query=org_query)) |
|
1146 | 1150 | |
|
1147 | 1151 | source_scm = source_db_repo.scm_instance() |
|
1148 | 1152 | target_scm = target_db_repo.scm_instance() |
|
1149 | 1153 | |
|
1150 | 1154 | source_ref_obj = unicode_to_reference(source_ref) |
|
1151 | 1155 | target_ref_obj = unicode_to_reference(target_ref) |
|
1152 | 1156 | |
|
1153 | 1157 | source_commit = source_scm.get_commit(source_ref_obj.commit_id) |
|
1154 | 1158 | target_commit = target_scm.get_commit(target_ref_obj.commit_id) |
|
1155 | 1159 | |
|
1156 | 1160 | ancestor = source_scm.get_common_ancestor( |
|
1157 | 1161 | source_commit.raw_id, target_commit.raw_id, target_scm) |
|
1158 | 1162 | |
|
1159 | 1163 | # recalculate target ref based on ancestor |
|
1160 | 1164 | target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor)) |
|
1161 | 1165 | |
|
1162 | 1166 | get_default_reviewers_data, validate_default_reviewers, validate_observers = \ |
|
1163 | 1167 | PullRequestModel().get_reviewer_functions() |
|
1164 | 1168 | |
|
1165 | 1169 | # recalculate reviewers logic, to make sure we can validate this |
|
1166 | 1170 | reviewer_rules = get_default_reviewers_data( |
|
1167 | 1171 | self._rhodecode_db_user, |
|
1168 | 1172 | source_db_repo, |
|
1169 | 1173 | source_ref_obj, |
|
1170 | 1174 | target_db_repo, |
|
1171 | 1175 | target_ref_obj, |
|
1172 | 1176 | include_diff_info=False) |
|
1173 | 1177 | |
|
1174 | 1178 | reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules) |
|
1175 | 1179 | observers = validate_observers(_form['observer_members'], reviewer_rules) |
|
1176 | 1180 | |
|
1177 | 1181 | pullrequest_title = _form['pullrequest_title'] |
|
1178 | 1182 | title_source_ref = source_ref_obj.name |
|
1179 | 1183 | if not pullrequest_title: |
|
1180 | 1184 | pullrequest_title = PullRequestModel().generate_pullrequest_title( |
|
1181 | 1185 | source=source_repo, |
|
1182 | 1186 | source_ref=title_source_ref, |
|
1183 | 1187 | target=target_repo |
|
1184 | 1188 | ) |
|
1185 | 1189 | |
|
1186 | 1190 | description = _form['pullrequest_desc'] |
|
1187 | 1191 | description_renderer = _form['description_renderer'] |
|
1188 | 1192 | |
|
1189 | 1193 | try: |
|
1190 | 1194 | pull_request = PullRequestModel().create( |
|
1191 | 1195 | created_by=self._rhodecode_user.user_id, |
|
1192 | 1196 | source_repo=source_repo, |
|
1193 | 1197 | source_ref=source_ref, |
|
1194 | 1198 | target_repo=target_repo, |
|
1195 | 1199 | target_ref=target_ref, |
|
1196 | 1200 | revisions=commit_ids, |
|
1197 | 1201 | common_ancestor_id=common_ancestor_id, |
|
1198 | 1202 | reviewers=reviewers, |
|
1199 | 1203 | observers=observers, |
|
1200 | 1204 | title=pullrequest_title, |
|
1201 | 1205 | description=description, |
|
1202 | 1206 | description_renderer=description_renderer, |
|
1203 | 1207 | reviewer_data=reviewer_rules, |
|
1204 | 1208 | auth_user=self._rhodecode_user |
|
1205 | 1209 | ) |
|
1206 | 1210 | Session().commit() |
|
1207 | 1211 | |
|
1208 | 1212 | h.flash(_('Successfully opened new pull request'), |
|
1209 | 1213 | category='success') |
|
1210 | 1214 | except Exception: |
|
1211 | 1215 | msg = _('Error occurred during creation of this pull request.') |
|
1212 | 1216 | log.exception(msg) |
|
1213 | 1217 | h.flash(msg, category='error') |
|
1214 | 1218 | |
|
1215 | 1219 | # copy the args back to redirect |
|
1216 | 1220 | org_query = self.request.GET.mixed() |
|
1217 | 1221 | raise HTTPFound( |
|
1218 | 1222 | h.route_path('pullrequest_new', repo_name=self.db_repo_name, |
|
1219 | 1223 | _query=org_query)) |
|
1220 | 1224 | |
|
1221 | 1225 | raise HTTPFound( |
|
1222 | 1226 | h.route_path('pullrequest_show', repo_name=target_repo, |
|
1223 | 1227 | pull_request_id=pull_request.pull_request_id)) |
|
1224 | 1228 | |
|
1225 | 1229 | @LoginRequired() |
|
1226 | 1230 | @NotAnonymous() |
|
1227 | 1231 | @HasRepoPermissionAnyDecorator( |
|
1228 | 1232 | 'repository.read', 'repository.write', 'repository.admin') |
|
1229 | 1233 | @CSRFRequired() |
|
1230 | 1234 | @view_config( |
|
1231 | 1235 | route_name='pullrequest_update', request_method='POST', |
|
1232 | 1236 | renderer='json_ext') |
|
1233 | 1237 | def pull_request_update(self): |
|
1234 | 1238 | pull_request = PullRequest.get_or_404( |
|
1235 | 1239 | self.request.matchdict['pull_request_id']) |
|
1236 | 1240 | _ = self.request.translate |
|
1237 | 1241 | |
|
1238 | 1242 | c = self.load_default_context() |
|
1239 | 1243 | redirect_url = None |
|
1240 | 1244 | |
|
1241 | 1245 | if pull_request.is_closed(): |
|
1242 | 1246 | log.debug('update: forbidden because pull request is closed') |
|
1243 | 1247 | msg = _(u'Cannot update closed pull requests.') |
|
1244 | 1248 | h.flash(msg, category='error') |
|
1245 | 1249 | return {'response': True, |
|
1246 | 1250 | 'redirect_url': redirect_url} |
|
1247 | 1251 | |
|
1248 | 1252 | is_state_changing = pull_request.is_state_changing() |
|
1249 | 1253 | c.pr_broadcast_channel = channelstream.pr_channel(pull_request) |
|
1250 | 1254 | |
|
1251 | 1255 | # only owner or admin can update it |
|
1252 | 1256 | allowed_to_update = PullRequestModel().check_user_update( |
|
1253 | 1257 | pull_request, self._rhodecode_user) |
|
1254 | 1258 | |
|
1255 | 1259 | if allowed_to_update: |
|
1256 | 1260 | controls = peppercorn.parse(self.request.POST.items()) |
|
1257 | 1261 | force_refresh = str2bool(self.request.POST.get('force_refresh')) |
|
1258 | 1262 | |
|
1259 | 1263 | if 'review_members' in controls: |
|
1260 | 1264 | self._update_reviewers( |
|
1261 | 1265 | c, |
|
1262 | 1266 | pull_request, controls['review_members'], |
|
1263 | 1267 | pull_request.reviewer_data, |
|
1264 | 1268 | PullRequestReviewers.ROLE_REVIEWER) |
|
1265 | 1269 | elif 'observer_members' in controls: |
|
1266 | 1270 | self._update_reviewers( |
|
1267 | 1271 | c, |
|
1268 | 1272 | pull_request, controls['observer_members'], |
|
1269 | 1273 | pull_request.reviewer_data, |
|
1270 | 1274 | PullRequestReviewers.ROLE_OBSERVER) |
|
1271 | 1275 | elif str2bool(self.request.POST.get('update_commits', 'false')): |
|
1272 | 1276 | if is_state_changing: |
|
1273 | 1277 | log.debug('commits update: forbidden because pull request is in state %s', |
|
1274 | 1278 | pull_request.pull_request_state) |
|
1275 | 1279 | msg = _(u'Cannot update pull requests commits in state other than `{}`. ' |
|
1276 | 1280 | u'Current state is: `{}`').format( |
|
1277 | 1281 | PullRequest.STATE_CREATED, pull_request.pull_request_state) |
|
1278 | 1282 | h.flash(msg, category='error') |
|
1279 | 1283 | return {'response': True, |
|
1280 | 1284 | 'redirect_url': redirect_url} |
|
1281 | 1285 | |
|
1282 | 1286 | self._update_commits(c, pull_request) |
|
1283 | 1287 | if force_refresh: |
|
1284 | 1288 | redirect_url = h.route_path( |
|
1285 | 1289 | 'pullrequest_show', repo_name=self.db_repo_name, |
|
1286 | 1290 | pull_request_id=pull_request.pull_request_id, |
|
1287 | 1291 | _query={"force_refresh": 1}) |
|
1288 | 1292 | elif str2bool(self.request.POST.get('edit_pull_request', 'false')): |
|
1289 | 1293 | self._edit_pull_request(pull_request) |
|
1290 | 1294 | else: |
|
1291 | 1295 | log.error('Unhandled update data.') |
|
1292 | 1296 | raise HTTPBadRequest() |
|
1293 | 1297 | |
|
1294 | 1298 | return {'response': True, |
|
1295 | 1299 | 'redirect_url': redirect_url} |
|
1296 | 1300 | raise HTTPForbidden() |
|
1297 | 1301 | |
|
1298 | 1302 | def _edit_pull_request(self, pull_request): |
|
1299 | 1303 | """ |
|
1300 | 1304 | Edit title and description |
|
1301 | 1305 | """ |
|
1302 | 1306 | _ = self.request.translate |
|
1303 | 1307 | |
|
1304 | 1308 | try: |
|
1305 | 1309 | PullRequestModel().edit( |
|
1306 | 1310 | pull_request, |
|
1307 | 1311 | self.request.POST.get('title'), |
|
1308 | 1312 | self.request.POST.get('description'), |
|
1309 | 1313 | self.request.POST.get('description_renderer'), |
|
1310 | 1314 | self._rhodecode_user) |
|
1311 | 1315 | except ValueError: |
|
1312 | 1316 | msg = _(u'Cannot update closed pull requests.') |
|
1313 | 1317 | h.flash(msg, category='error') |
|
1314 | 1318 | return |
|
1315 | 1319 | else: |
|
1316 | 1320 | Session().commit() |
|
1317 | 1321 | |
|
1318 | 1322 | msg = _(u'Pull request title & description updated.') |
|
1319 | 1323 | h.flash(msg, category='success') |
|
1320 | 1324 | return |
|
1321 | 1325 | |
|
1322 | 1326 | def _update_commits(self, c, pull_request): |
|
1323 | 1327 | _ = self.request.translate |
|
1324 | 1328 | |
|
1325 | 1329 | with pull_request.set_state(PullRequest.STATE_UPDATING): |
|
1326 | 1330 | resp = PullRequestModel().update_commits( |
|
1327 | 1331 | pull_request, self._rhodecode_db_user) |
|
1328 | 1332 | |
|
1329 | 1333 | if resp.executed: |
|
1330 | 1334 | |
|
1331 | 1335 | if resp.target_changed and resp.source_changed: |
|
1332 | 1336 | changed = 'target and source repositories' |
|
1333 | 1337 | elif resp.target_changed and not resp.source_changed: |
|
1334 | 1338 | changed = 'target repository' |
|
1335 | 1339 | elif not resp.target_changed and resp.source_changed: |
|
1336 | 1340 | changed = 'source repository' |
|
1337 | 1341 | else: |
|
1338 | 1342 | changed = 'nothing' |
|
1339 | 1343 | |
|
1340 | 1344 | msg = _(u'Pull request updated to "{source_commit_id}" with ' |
|
1341 | 1345 | u'{count_added} added, {count_removed} removed commits. ' |
|
1342 | 1346 | u'Source of changes: {change_source}.') |
|
1343 | 1347 | msg = msg.format( |
|
1344 | 1348 | source_commit_id=pull_request.source_ref_parts.commit_id, |
|
1345 | 1349 | count_added=len(resp.changes.added), |
|
1346 | 1350 | count_removed=len(resp.changes.removed), |
|
1347 | 1351 | change_source=changed) |
|
1348 | 1352 | h.flash(msg, category='success') |
|
1349 | 1353 | channelstream.pr_update_channelstream_push( |
|
1350 | 1354 | self.request, c.pr_broadcast_channel, self._rhodecode_user, msg) |
|
1351 | 1355 | else: |
|
1352 | 1356 | msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason] |
|
1353 | 1357 | warning_reasons = [ |
|
1354 | 1358 | UpdateFailureReason.NO_CHANGE, |
|
1355 | 1359 | UpdateFailureReason.WRONG_REF_TYPE, |
|
1356 | 1360 | ] |
|
1357 | 1361 | category = 'warning' if resp.reason in warning_reasons else 'error' |
|
1358 | 1362 | h.flash(msg, category=category) |
|
1359 | 1363 | |
|
1360 | 1364 | def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role): |
|
1361 | 1365 | _ = self.request.translate |
|
1362 | 1366 | |
|
1363 | 1367 | get_default_reviewers_data, validate_default_reviewers, validate_observers = \ |
|
1364 | 1368 | PullRequestModel().get_reviewer_functions() |
|
1365 | 1369 | |
|
1366 | 1370 | if role == PullRequestReviewers.ROLE_REVIEWER: |
|
1367 | 1371 | try: |
|
1368 | 1372 | reviewers = validate_default_reviewers(review_members, reviewer_rules) |
|
1369 | 1373 | except ValueError as e: |
|
1370 | 1374 | log.error('Reviewers Validation: {}'.format(e)) |
|
1371 | 1375 | h.flash(e, category='error') |
|
1372 | 1376 | return |
|
1373 | 1377 | |
|
1374 | 1378 | old_calculated_status = pull_request.calculated_review_status() |
|
1375 | 1379 | PullRequestModel().update_reviewers( |
|
1376 | 1380 | pull_request, reviewers, self._rhodecode_db_user) |
|
1377 | 1381 | |
|
1378 | 1382 | Session().commit() |
|
1379 | 1383 | |
|
1380 | 1384 | msg = _('Pull request reviewers updated.') |
|
1381 | 1385 | h.flash(msg, category='success') |
|
1382 | 1386 | channelstream.pr_update_channelstream_push( |
|
1383 | 1387 | self.request, c.pr_broadcast_channel, self._rhodecode_user, msg) |
|
1384 | 1388 | |
|
1385 | 1389 | # trigger status changed if change in reviewers changes the status |
|
1386 | 1390 | calculated_status = pull_request.calculated_review_status() |
|
1387 | 1391 | if old_calculated_status != calculated_status: |
|
1388 | 1392 | PullRequestModel().trigger_pull_request_hook( |
|
1389 | 1393 | pull_request, self._rhodecode_user, 'review_status_change', |
|
1390 | 1394 | data={'status': calculated_status}) |
|
1391 | 1395 | |
|
1392 | 1396 | elif role == PullRequestReviewers.ROLE_OBSERVER: |
|
1393 | 1397 | try: |
|
1394 | 1398 | observers = validate_observers(review_members, reviewer_rules) |
|
1395 | 1399 | except ValueError as e: |
|
1396 | 1400 | log.error('Observers Validation: {}'.format(e)) |
|
1397 | 1401 | h.flash(e, category='error') |
|
1398 | 1402 | return |
|
1399 | 1403 | |
|
1400 | 1404 | PullRequestModel().update_observers( |
|
1401 | 1405 | pull_request, observers, self._rhodecode_db_user) |
|
1402 | 1406 | |
|
1403 | 1407 | Session().commit() |
|
1404 | 1408 | msg = _('Pull request observers updated.') |
|
1405 | 1409 | h.flash(msg, category='success') |
|
1406 | 1410 | channelstream.pr_update_channelstream_push( |
|
1407 | 1411 | self.request, c.pr_broadcast_channel, self._rhodecode_user, msg) |
|
1408 | 1412 | |
|
1409 | 1413 | @LoginRequired() |
|
1410 | 1414 | @NotAnonymous() |
|
1411 | 1415 | @HasRepoPermissionAnyDecorator( |
|
1412 | 1416 | 'repository.read', 'repository.write', 'repository.admin') |
|
1413 | 1417 | @CSRFRequired() |
|
1414 | 1418 | @view_config( |
|
1415 | 1419 | route_name='pullrequest_merge', request_method='POST', |
|
1416 | 1420 | renderer='json_ext') |
|
1417 | 1421 | def pull_request_merge(self): |
|
1418 | 1422 | """ |
|
1419 | 1423 | Merge will perform a server-side merge of the specified |
|
1420 | 1424 | pull request, if the pull request is approved and mergeable. |
|
1421 | 1425 | After successful merging, the pull request is automatically |
|
1422 | 1426 | closed, with a relevant comment. |
|
1423 | 1427 | """ |
|
1424 | 1428 | pull_request = PullRequest.get_or_404( |
|
1425 | 1429 | self.request.matchdict['pull_request_id']) |
|
1426 | 1430 | _ = self.request.translate |
|
1427 | 1431 | |
|
1428 | 1432 | if pull_request.is_state_changing(): |
|
1429 | 1433 | log.debug('show: forbidden because pull request is in state %s', |
|
1430 | 1434 | pull_request.pull_request_state) |
|
1431 | 1435 | msg = _(u'Cannot merge pull requests in state other than `{}`. ' |
|
1432 | 1436 | u'Current state is: `{}`').format(PullRequest.STATE_CREATED, |
|
1433 | 1437 | pull_request.pull_request_state) |
|
1434 | 1438 | h.flash(msg, category='error') |
|
1435 | 1439 | raise HTTPFound( |
|
1436 | 1440 | h.route_path('pullrequest_show', |
|
1437 | 1441 | repo_name=pull_request.target_repo.repo_name, |
|
1438 | 1442 | pull_request_id=pull_request.pull_request_id)) |
|
1439 | 1443 | |
|
1440 | 1444 | self.load_default_context() |
|
1441 | 1445 | |
|
1442 | 1446 | with pull_request.set_state(PullRequest.STATE_UPDATING): |
|
1443 | 1447 | check = MergeCheck.validate( |
|
1444 | 1448 | pull_request, auth_user=self._rhodecode_user, |
|
1445 | 1449 | translator=self.request.translate) |
|
1446 | 1450 | merge_possible = not check.failed |
|
1447 | 1451 | |
|
1448 | 1452 | for err_type, error_msg in check.errors: |
|
1449 | 1453 | h.flash(error_msg, category=err_type) |
|
1450 | 1454 | |
|
1451 | 1455 | if merge_possible: |
|
1452 | 1456 | log.debug("Pre-conditions checked, trying to merge.") |
|
1453 | 1457 | extras = vcs_operation_context( |
|
1454 | 1458 | self.request.environ, repo_name=pull_request.target_repo.repo_name, |
|
1455 | 1459 | username=self._rhodecode_db_user.username, action='push', |
|
1456 | 1460 | scm=pull_request.target_repo.repo_type) |
|
1457 | 1461 | with pull_request.set_state(PullRequest.STATE_UPDATING): |
|
1458 | 1462 | self._merge_pull_request( |
|
1459 | 1463 | pull_request, self._rhodecode_db_user, extras) |
|
1460 | 1464 | else: |
|
1461 | 1465 | log.debug("Pre-conditions failed, NOT merging.") |
|
1462 | 1466 | |
|
1463 | 1467 | raise HTTPFound( |
|
1464 | 1468 | h.route_path('pullrequest_show', |
|
1465 | 1469 | repo_name=pull_request.target_repo.repo_name, |
|
1466 | 1470 | pull_request_id=pull_request.pull_request_id)) |
|
1467 | 1471 | |
|
1468 | 1472 | def _merge_pull_request(self, pull_request, user, extras): |
|
1469 | 1473 | _ = self.request.translate |
|
1470 | 1474 | merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras) |
|
1471 | 1475 | |
|
1472 | 1476 | if merge_resp.executed: |
|
1473 | 1477 | log.debug("The merge was successful, closing the pull request.") |
|
1474 | 1478 | PullRequestModel().close_pull_request( |
|
1475 | 1479 | pull_request.pull_request_id, user) |
|
1476 | 1480 | Session().commit() |
|
1477 | 1481 | msg = _('Pull request was successfully merged and closed.') |
|
1478 | 1482 | h.flash(msg, category='success') |
|
1479 | 1483 | else: |
|
1480 | 1484 | log.debug( |
|
1481 | 1485 | "The merge was not successful. Merge response: %s", merge_resp) |
|
1482 | 1486 | msg = merge_resp.merge_status_message |
|
1483 | 1487 | h.flash(msg, category='error') |
|
1484 | 1488 | |
|
1485 | 1489 | @LoginRequired() |
|
1486 | 1490 | @NotAnonymous() |
|
1487 | 1491 | @HasRepoPermissionAnyDecorator( |
|
1488 | 1492 | 'repository.read', 'repository.write', 'repository.admin') |
|
1489 | 1493 | @CSRFRequired() |
|
1490 | 1494 | @view_config( |
|
1491 | 1495 | route_name='pullrequest_delete', request_method='POST', |
|
1492 | 1496 | renderer='json_ext') |
|
1493 | 1497 | def pull_request_delete(self): |
|
1494 | 1498 | _ = self.request.translate |
|
1495 | 1499 | |
|
1496 | 1500 | pull_request = PullRequest.get_or_404( |
|
1497 | 1501 | self.request.matchdict['pull_request_id']) |
|
1498 | 1502 | self.load_default_context() |
|
1499 | 1503 | |
|
1500 | 1504 | pr_closed = pull_request.is_closed() |
|
1501 | 1505 | allowed_to_delete = PullRequestModel().check_user_delete( |
|
1502 | 1506 | pull_request, self._rhodecode_user) and not pr_closed |
|
1503 | 1507 | |
|
1504 | 1508 | # only owner can delete it ! |
|
1505 | 1509 | if allowed_to_delete: |
|
1506 | 1510 | PullRequestModel().delete(pull_request, self._rhodecode_user) |
|
1507 | 1511 | Session().commit() |
|
1508 | 1512 | h.flash(_('Successfully deleted pull request'), |
|
1509 | 1513 | category='success') |
|
1510 | 1514 | raise HTTPFound(h.route_path('pullrequest_show_all', |
|
1511 | 1515 | repo_name=self.db_repo_name)) |
|
1512 | 1516 | |
|
1513 | 1517 | log.warning('user %s tried to delete pull request without access', |
|
1514 | 1518 | self._rhodecode_user) |
|
1515 | 1519 | raise HTTPNotFound() |
|
1516 | 1520 | |
|
1517 | 1521 | @LoginRequired() |
|
1518 | 1522 | @NotAnonymous() |
|
1519 | 1523 | @HasRepoPermissionAnyDecorator( |
|
1520 | 1524 | 'repository.read', 'repository.write', 'repository.admin') |
|
1521 | 1525 | @CSRFRequired() |
|
1522 | 1526 | @view_config( |
|
1523 | 1527 | route_name='pullrequest_comment_create', request_method='POST', |
|
1524 | 1528 | renderer='json_ext') |
|
1525 | 1529 | def pull_request_comment_create(self): |
|
1526 | 1530 | _ = self.request.translate |
|
1527 | 1531 | |
|
1528 | 1532 | pull_request = PullRequest.get_or_404( |
|
1529 | 1533 | self.request.matchdict['pull_request_id']) |
|
1530 | 1534 | pull_request_id = pull_request.pull_request_id |
|
1531 | 1535 | |
|
1532 | 1536 | if pull_request.is_closed(): |
|
1533 | 1537 | log.debug('comment: forbidden because pull request is closed') |
|
1534 | 1538 | raise HTTPForbidden() |
|
1535 | 1539 | |
|
1536 | 1540 | allowed_to_comment = PullRequestModel().check_user_comment( |
|
1537 | 1541 | pull_request, self._rhodecode_user) |
|
1538 | 1542 | if not allowed_to_comment: |
|
1539 | 1543 | log.debug('comment: forbidden because pull request is from forbidden repo') |
|
1540 | 1544 | raise HTTPForbidden() |
|
1541 | 1545 | |
|
1542 | 1546 | c = self.load_default_context() |
|
1543 | 1547 | |
|
1544 | 1548 | status = self.request.POST.get('changeset_status', None) |
|
1545 | 1549 | text = self.request.POST.get('text') |
|
1546 | 1550 | comment_type = self.request.POST.get('comment_type') |
|
1551 | is_draft = str2bool(self.request.POST.get('draft')) | |
|
1547 | 1552 | resolves_comment_id = self.request.POST.get('resolves_comment_id', None) |
|
1548 | 1553 | close_pull_request = self.request.POST.get('close_pull_request') |
|
1549 | 1554 | |
|
1550 | 1555 | # the logic here should work like following, if we submit close |
|
1551 | 1556 | # pr comment, use `close_pull_request_with_comment` function |
|
1552 | 1557 | # else handle regular comment logic |
|
1553 | 1558 | |
|
1554 | 1559 | if close_pull_request: |
|
1555 | 1560 | # only owner or admin or person with write permissions |
|
1556 | 1561 | allowed_to_close = PullRequestModel().check_user_update( |
|
1557 | 1562 | pull_request, self._rhodecode_user) |
|
1558 | 1563 | if not allowed_to_close: |
|
1559 | 1564 | log.debug('comment: forbidden because not allowed to close ' |
|
1560 | 1565 | 'pull request %s', pull_request_id) |
|
1561 | 1566 | raise HTTPForbidden() |
|
1562 | 1567 | |
|
1563 | 1568 | # This also triggers `review_status_change` |
|
1564 | 1569 | comment, status = PullRequestModel().close_pull_request_with_comment( |
|
1565 | 1570 | pull_request, self._rhodecode_user, self.db_repo, message=text, |
|
1566 | 1571 | auth_user=self._rhodecode_user) |
|
1567 | 1572 | Session().flush() |
|
1568 | 1573 | is_inline = comment.is_inline |
|
1569 | 1574 | |
|
1570 | 1575 | PullRequestModel().trigger_pull_request_hook( |
|
1571 | 1576 | pull_request, self._rhodecode_user, 'comment', |
|
1572 | 1577 | data={'comment': comment}) |
|
1573 | 1578 | |
|
1574 | 1579 | else: |
|
1575 | 1580 | # regular comment case, could be inline, or one with status. |
|
1576 | 1581 | # for that one we check also permissions |
|
1577 | ||
|
1582 | # Additionally ENSURE if somehow draft is sent we're then unable to change status | |
|
1578 | 1583 | allowed_to_change_status = PullRequestModel().check_user_change_status( |
|
1579 | pull_request, self._rhodecode_user) | |
|
1584 | pull_request, self._rhodecode_user) and not is_draft | |
|
1580 | 1585 | |
|
1581 | 1586 | if status and allowed_to_change_status: |
|
1582 | 1587 | message = (_('Status change %(transition_icon)s %(status)s') |
|
1583 | 1588 | % {'transition_icon': '>', |
|
1584 | 1589 | 'status': ChangesetStatus.get_status_lbl(status)}) |
|
1585 | 1590 | text = text or message |
|
1586 | 1591 | |
|
1587 | 1592 | comment = CommentsModel().create( |
|
1588 | 1593 | text=text, |
|
1589 | 1594 | repo=self.db_repo.repo_id, |
|
1590 | 1595 | user=self._rhodecode_user.user_id, |
|
1591 | 1596 | pull_request=pull_request, |
|
1592 | 1597 | f_path=self.request.POST.get('f_path'), |
|
1593 | 1598 | line_no=self.request.POST.get('line'), |
|
1594 | 1599 | status_change=(ChangesetStatus.get_status_lbl(status) |
|
1595 | 1600 | if status and allowed_to_change_status else None), |
|
1596 | 1601 | status_change_type=(status |
|
1597 | 1602 | if status and allowed_to_change_status else None), |
|
1598 | 1603 | comment_type=comment_type, |
|
1604 | is_draft=is_draft, | |
|
1599 | 1605 | resolves_comment_id=resolves_comment_id, |
|
1600 | auth_user=self._rhodecode_user | |
|
1606 | auth_user=self._rhodecode_user, | |
|
1607 | send_email=not is_draft, # skip notification for draft comments | |
|
1601 | 1608 | ) |
|
1602 | 1609 | is_inline = comment.is_inline |
|
1603 | 1610 | |
|
1604 | 1611 | if allowed_to_change_status: |
|
1605 | 1612 | # calculate old status before we change it |
|
1606 | 1613 | old_calculated_status = pull_request.calculated_review_status() |
|
1607 | 1614 | |
|
1608 | 1615 | # get status if set ! |
|
1609 | 1616 | if status: |
|
1610 | 1617 | ChangesetStatusModel().set_status( |
|
1611 | 1618 | self.db_repo.repo_id, |
|
1612 | 1619 | status, |
|
1613 | 1620 | self._rhodecode_user.user_id, |
|
1614 | 1621 | comment, |
|
1615 | 1622 | pull_request=pull_request |
|
1616 | 1623 | ) |
|
1617 | 1624 | |
|
1618 | 1625 | Session().flush() |
|
1619 | 1626 | # this is somehow required to get access to some relationship |
|
1620 | 1627 | # loaded on comment |
|
1621 | 1628 | Session().refresh(comment) |
|
1622 | 1629 | |
|
1623 | 1630 | PullRequestModel().trigger_pull_request_hook( |
|
1624 | 1631 | pull_request, self._rhodecode_user, 'comment', |
|
1625 | 1632 | data={'comment': comment}) |
|
1626 | 1633 | |
|
1627 | 1634 | # we now calculate the status of pull request, and based on that |
|
1628 | 1635 | # calculation we set the commits status |
|
1629 | 1636 | calculated_status = pull_request.calculated_review_status() |
|
1630 | 1637 | if old_calculated_status != calculated_status: |
|
1631 | 1638 | PullRequestModel().trigger_pull_request_hook( |
|
1632 | 1639 | pull_request, self._rhodecode_user, 'review_status_change', |
|
1633 | 1640 | data={'status': calculated_status}) |
|
1634 | 1641 | |
|
1635 | 1642 | Session().commit() |
|
1636 | 1643 | |
|
1637 | 1644 | data = { |
|
1638 | 1645 | 'target_id': h.safeid(h.safe_unicode( |
|
1639 | 1646 | self.request.POST.get('f_path'))), |
|
1640 | 1647 | } |
|
1648 | ||
|
1641 | 1649 | if comment: |
|
1642 | 1650 | c.co = comment |
|
1643 | 1651 | c.at_version_num = None |
|
1644 | 1652 | rendered_comment = render( |
|
1645 | 1653 | 'rhodecode:templates/changeset/changeset_comment_block.mako', |
|
1646 | 1654 | self._get_template_context(c), self.request) |
|
1647 | 1655 | |
|
1648 | 1656 | data.update(comment.get_dict()) |
|
1649 | 1657 | data.update({'rendered_text': rendered_comment}) |
|
1650 | 1658 | |
|
1659 | # skip channelstream for draft comments | |
|
1660 | if not is_draft: | |
|
1651 | 1661 | comment_broadcast_channel = channelstream.comment_channel( |
|
1652 | 1662 | self.db_repo_name, pull_request_obj=pull_request) |
|
1653 | 1663 | |
|
1654 | 1664 | comment_data = data |
|
1655 | 1665 | comment_type = 'inline' if is_inline else 'general' |
|
1656 | 1666 | channelstream.comment_channelstream_push( |
|
1657 | 1667 | self.request, comment_broadcast_channel, self._rhodecode_user, |
|
1658 | 1668 | _('posted a new {} comment').format(comment_type), |
|
1659 | 1669 | comment_data=comment_data) |
|
1660 | 1670 | |
|
1661 | 1671 | return data |
|
1662 | 1672 | |
|
1663 | 1673 | @LoginRequired() |
|
1664 | 1674 | @NotAnonymous() |
|
1665 | 1675 | @HasRepoPermissionAnyDecorator( |
|
1666 | 1676 | 'repository.read', 'repository.write', 'repository.admin') |
|
1667 | 1677 | @CSRFRequired() |
|
1668 | 1678 | @view_config( |
|
1669 | 1679 | route_name='pullrequest_comment_delete', request_method='POST', |
|
1670 | 1680 | renderer='json_ext') |
|
1671 | 1681 | def pull_request_comment_delete(self): |
|
1672 | 1682 | pull_request = PullRequest.get_or_404( |
|
1673 | 1683 | self.request.matchdict['pull_request_id']) |
|
1674 | 1684 | |
|
1675 | 1685 | comment = ChangesetComment.get_or_404( |
|
1676 | 1686 | self.request.matchdict['comment_id']) |
|
1677 | 1687 | comment_id = comment.comment_id |
|
1678 | 1688 | |
|
1679 | 1689 | if comment.immutable: |
|
1680 | 1690 | # don't allow deleting comments that are immutable |
|
1681 | 1691 | raise HTTPForbidden() |
|
1682 | 1692 | |
|
1683 | 1693 | if pull_request.is_closed(): |
|
1684 | 1694 | log.debug('comment: forbidden because pull request is closed') |
|
1685 | 1695 | raise HTTPForbidden() |
|
1686 | 1696 | |
|
1687 | 1697 | if not comment: |
|
1688 | 1698 | log.debug('Comment with id:%s not found, skipping', comment_id) |
|
1689 | 1699 | # comment already deleted in another call probably |
|
1690 | 1700 | return True |
|
1691 | 1701 | |
|
1692 | 1702 | if comment.pull_request.is_closed(): |
|
1693 | 1703 | # don't allow deleting comments on closed pull request |
|
1694 | 1704 | raise HTTPForbidden() |
|
1695 | 1705 | |
|
1696 | 1706 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name) |
|
1697 | 1707 | super_admin = h.HasPermissionAny('hg.admin')() |
|
1698 | 1708 | comment_owner = comment.author.user_id == self._rhodecode_user.user_id |
|
1699 | 1709 | is_repo_comment = comment.repo.repo_name == self.db_repo_name |
|
1700 | 1710 | comment_repo_admin = is_repo_admin and is_repo_comment |
|
1701 | 1711 | |
|
1702 | 1712 | if super_admin or comment_owner or comment_repo_admin: |
|
1703 | 1713 | old_calculated_status = comment.pull_request.calculated_review_status() |
|
1704 | 1714 | CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user) |
|
1705 | 1715 | Session().commit() |
|
1706 | 1716 | calculated_status = comment.pull_request.calculated_review_status() |
|
1707 | 1717 | if old_calculated_status != calculated_status: |
|
1708 | 1718 | PullRequestModel().trigger_pull_request_hook( |
|
1709 | 1719 | comment.pull_request, self._rhodecode_user, 'review_status_change', |
|
1710 | 1720 | data={'status': calculated_status}) |
|
1711 | 1721 | return True |
|
1712 | 1722 | else: |
|
1713 | 1723 | log.warning('No permissions for user %s to delete comment_id: %s', |
|
1714 | 1724 | self._rhodecode_db_user, comment_id) |
|
1715 | 1725 | raise HTTPNotFound() |
|
1716 | 1726 | |
|
1717 | 1727 | @LoginRequired() |
|
1718 | 1728 | @NotAnonymous() |
|
1719 | 1729 | @HasRepoPermissionAnyDecorator( |
|
1720 | 1730 | 'repository.read', 'repository.write', 'repository.admin') |
|
1721 | 1731 | @CSRFRequired() |
|
1722 | 1732 | @view_config( |
|
1723 | 1733 | route_name='pullrequest_comment_edit', request_method='POST', |
|
1724 | 1734 | renderer='json_ext') |
|
1725 | 1735 | def pull_request_comment_edit(self): |
|
1726 | 1736 | self.load_default_context() |
|
1727 | 1737 | |
|
1728 | 1738 | pull_request = PullRequest.get_or_404( |
|
1729 | 1739 | self.request.matchdict['pull_request_id'] |
|
1730 | 1740 | ) |
|
1731 | 1741 | comment = ChangesetComment.get_or_404( |
|
1732 | 1742 | self.request.matchdict['comment_id'] |
|
1733 | 1743 | ) |
|
1734 | 1744 | comment_id = comment.comment_id |
|
1735 | 1745 | |
|
1736 | 1746 | if comment.immutable: |
|
1737 | 1747 | # don't allow deleting comments that are immutable |
|
1738 | 1748 | raise HTTPForbidden() |
|
1739 | 1749 | |
|
1740 | 1750 | if pull_request.is_closed(): |
|
1741 | 1751 | log.debug('comment: forbidden because pull request is closed') |
|
1742 | 1752 | raise HTTPForbidden() |
|
1743 | 1753 | |
|
1744 | 1754 | if not comment: |
|
1745 | 1755 | log.debug('Comment with id:%s not found, skipping', comment_id) |
|
1746 | 1756 | # comment already deleted in another call probably |
|
1747 | 1757 | return True |
|
1748 | 1758 | |
|
1749 | 1759 | if comment.pull_request.is_closed(): |
|
1750 | 1760 | # don't allow deleting comments on closed pull request |
|
1751 | 1761 | raise HTTPForbidden() |
|
1752 | 1762 | |
|
1753 | 1763 | is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name) |
|
1754 | 1764 | super_admin = h.HasPermissionAny('hg.admin')() |
|
1755 | 1765 | comment_owner = comment.author.user_id == self._rhodecode_user.user_id |
|
1756 | 1766 | is_repo_comment = comment.repo.repo_name == self.db_repo_name |
|
1757 | 1767 | comment_repo_admin = is_repo_admin and is_repo_comment |
|
1758 | 1768 | |
|
1759 | 1769 | if super_admin or comment_owner or comment_repo_admin: |
|
1760 | 1770 | text = self.request.POST.get('text') |
|
1761 | 1771 | version = self.request.POST.get('version') |
|
1762 | 1772 | if text == comment.text: |
|
1763 | 1773 | log.warning( |
|
1764 | 1774 | 'Comment(PR): ' |
|
1765 | 1775 | 'Trying to create new version ' |
|
1766 | 1776 | 'with the same comment body {}'.format( |
|
1767 | 1777 | comment_id, |
|
1768 | 1778 | ) |
|
1769 | 1779 | ) |
|
1770 | 1780 | raise HTTPNotFound() |
|
1771 | 1781 | |
|
1772 | 1782 | if version.isdigit(): |
|
1773 | 1783 | version = int(version) |
|
1774 | 1784 | else: |
|
1775 | 1785 | log.warning( |
|
1776 | 1786 | 'Comment(PR): Wrong version type {} {} ' |
|
1777 | 1787 | 'for comment {}'.format( |
|
1778 | 1788 | version, |
|
1779 | 1789 | type(version), |
|
1780 | 1790 | comment_id, |
|
1781 | 1791 | ) |
|
1782 | 1792 | ) |
|
1783 | 1793 | raise HTTPNotFound() |
|
1784 | 1794 | |
|
1785 | 1795 | try: |
|
1786 | 1796 | comment_history = CommentsModel().edit( |
|
1787 | 1797 | comment_id=comment_id, |
|
1788 | 1798 | text=text, |
|
1789 | 1799 | auth_user=self._rhodecode_user, |
|
1790 | 1800 | version=version, |
|
1791 | 1801 | ) |
|
1792 | 1802 | except CommentVersionMismatch: |
|
1793 | 1803 | raise HTTPConflict() |
|
1794 | 1804 | |
|
1795 | 1805 | if not comment_history: |
|
1796 | 1806 | raise HTTPNotFound() |
|
1797 | 1807 | |
|
1798 | 1808 | Session().commit() |
|
1799 | 1809 | |
|
1800 | 1810 | PullRequestModel().trigger_pull_request_hook( |
|
1801 | 1811 | pull_request, self._rhodecode_user, 'comment_edit', |
|
1802 | 1812 | data={'comment': comment}) |
|
1803 | 1813 | |
|
1804 | 1814 | return { |
|
1805 | 1815 | 'comment_history_id': comment_history.comment_history_id, |
|
1806 | 1816 | 'comment_id': comment.comment_id, |
|
1807 | 1817 | 'comment_version': comment_history.version, |
|
1808 | 1818 | 'comment_author_username': comment_history.author.username, |
|
1809 | 1819 | 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16), |
|
1810 | 1820 | 'comment_created_on': h.age_component(comment_history.created_on, |
|
1811 | 1821 | time_is_local=True), |
|
1812 | 1822 | } |
|
1813 | 1823 | else: |
|
1814 | 1824 | log.warning('No permissions for user %s to edit comment_id: %s', |
|
1815 | 1825 | self._rhodecode_db_user, comment_id) |
|
1816 | 1826 | raise HTTPNotFound() |
@@ -1,821 +1,843 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2011-2020 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | comments model for RhodeCode |
|
23 | 23 | """ |
|
24 | 24 | import datetime |
|
25 | 25 | |
|
26 | 26 | import logging |
|
27 | 27 | import traceback |
|
28 | 28 | import collections |
|
29 | 29 | |
|
30 | 30 | from pyramid.threadlocal import get_current_registry, get_current_request |
|
31 | 31 | from sqlalchemy.sql.expression import null |
|
32 | 32 | from sqlalchemy.sql.functions import coalesce |
|
33 | 33 | |
|
34 | 34 | from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils |
|
35 | 35 | from rhodecode.lib import audit_logger |
|
36 | 36 | from rhodecode.lib.exceptions import CommentVersionMismatch |
|
37 | 37 | from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int |
|
38 | 38 | from rhodecode.model import BaseModel |
|
39 | 39 | from rhodecode.model.db import ( |
|
40 | false, | |
|
40 | 41 | ChangesetComment, |
|
41 | 42 | User, |
|
42 | 43 | Notification, |
|
43 | 44 | PullRequest, |
|
44 | 45 | AttributeDict, |
|
45 | 46 | ChangesetCommentHistory, |
|
46 | 47 | ) |
|
47 | 48 | from rhodecode.model.notification import NotificationModel |
|
48 | 49 | from rhodecode.model.meta import Session |
|
49 | 50 | from rhodecode.model.settings import VcsSettingsModel |
|
50 | 51 | from rhodecode.model.notification import EmailNotificationModel |
|
51 | 52 | from rhodecode.model.validation_schema.schemas import comment_schema |
|
52 | 53 | |
|
53 | 54 | |
|
54 | 55 | log = logging.getLogger(__name__) |
|
55 | 56 | |
|
56 | 57 | |
|
57 | 58 | class CommentsModel(BaseModel): |
|
58 | 59 | |
|
59 | 60 | cls = ChangesetComment |
|
60 | 61 | |
|
61 | 62 | DIFF_CONTEXT_BEFORE = 3 |
|
62 | 63 | DIFF_CONTEXT_AFTER = 3 |
|
63 | 64 | |
|
64 | 65 | def __get_commit_comment(self, changeset_comment): |
|
65 | 66 | return self._get_instance(ChangesetComment, changeset_comment) |
|
66 | 67 | |
|
67 | 68 | def __get_pull_request(self, pull_request): |
|
68 | 69 | return self._get_instance(PullRequest, pull_request) |
|
69 | 70 | |
|
70 | 71 | def _extract_mentions(self, s): |
|
71 | 72 | user_objects = [] |
|
72 | 73 | for username in extract_mentioned_users(s): |
|
73 | 74 | user_obj = User.get_by_username(username, case_insensitive=True) |
|
74 | 75 | if user_obj: |
|
75 | 76 | user_objects.append(user_obj) |
|
76 | 77 | return user_objects |
|
77 | 78 | |
|
78 | 79 | def _get_renderer(self, global_renderer='rst', request=None): |
|
79 | 80 | request = request or get_current_request() |
|
80 | 81 | |
|
81 | 82 | try: |
|
82 | 83 | global_renderer = request.call_context.visual.default_renderer |
|
83 | 84 | except AttributeError: |
|
84 | 85 | log.debug("Renderer not set, falling back " |
|
85 | 86 | "to default renderer '%s'", global_renderer) |
|
86 | 87 | except Exception: |
|
87 | 88 | log.error(traceback.format_exc()) |
|
88 | 89 | return global_renderer |
|
89 | 90 | |
|
90 | 91 | def aggregate_comments(self, comments, versions, show_version, inline=False): |
|
91 | 92 | # group by versions, and count until, and display objects |
|
92 | 93 | |
|
93 | 94 | comment_groups = collections.defaultdict(list) |
|
94 | 95 | [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments] |
|
95 | 96 | |
|
96 | 97 | def yield_comments(pos): |
|
97 | 98 | for co in comment_groups[pos]: |
|
98 | 99 | yield co |
|
99 | 100 | |
|
100 | 101 | comment_versions = collections.defaultdict( |
|
101 | 102 | lambda: collections.defaultdict(list)) |
|
102 | 103 | prev_prvid = -1 |
|
103 | 104 | # fake last entry with None, to aggregate on "latest" version which |
|
104 | 105 | # doesn't have an pull_request_version_id |
|
105 | 106 | for ver in versions + [AttributeDict({'pull_request_version_id': None})]: |
|
106 | 107 | prvid = ver.pull_request_version_id |
|
107 | 108 | if prev_prvid == -1: |
|
108 | 109 | prev_prvid = prvid |
|
109 | 110 | |
|
110 | 111 | for co in yield_comments(prvid): |
|
111 | 112 | comment_versions[prvid]['at'].append(co) |
|
112 | 113 | |
|
113 | 114 | # save until |
|
114 | 115 | current = comment_versions[prvid]['at'] |
|
115 | 116 | prev_until = comment_versions[prev_prvid]['until'] |
|
116 | 117 | cur_until = prev_until + current |
|
117 | 118 | comment_versions[prvid]['until'].extend(cur_until) |
|
118 | 119 | |
|
119 | 120 | # save outdated |
|
120 | 121 | if inline: |
|
121 | 122 | outdated = [x for x in cur_until |
|
122 | 123 | if x.outdated_at_version(show_version)] |
|
123 | 124 | else: |
|
124 | 125 | outdated = [x for x in cur_until |
|
125 | 126 | if x.older_than_version(show_version)] |
|
126 | 127 | display = [x for x in cur_until if x not in outdated] |
|
127 | 128 | |
|
128 | 129 | comment_versions[prvid]['outdated'] = outdated |
|
129 | 130 | comment_versions[prvid]['display'] = display |
|
130 | 131 | |
|
131 | 132 | prev_prvid = prvid |
|
132 | 133 | |
|
133 | 134 | return comment_versions |
|
134 | 135 | |
|
135 | 136 | def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None): |
|
136 | 137 | qry = Session().query(ChangesetComment) \ |
|
137 | 138 | .filter(ChangesetComment.repo == repo) |
|
138 | 139 | |
|
139 | 140 | if comment_type and comment_type in ChangesetComment.COMMENT_TYPES: |
|
140 | 141 | qry = qry.filter(ChangesetComment.comment_type == comment_type) |
|
141 | 142 | |
|
142 | 143 | if user: |
|
143 | 144 | user = self._get_user(user) |
|
144 | 145 | if user: |
|
145 | 146 | qry = qry.filter(ChangesetComment.user_id == user.user_id) |
|
146 | 147 | |
|
147 | 148 | if commit_id: |
|
148 | 149 | qry = qry.filter(ChangesetComment.revision == commit_id) |
|
149 | 150 | |
|
150 | 151 | qry = qry.order_by(ChangesetComment.created_on) |
|
151 | 152 | return qry.all() |
|
152 | 153 | |
|
153 | 154 | def get_repository_unresolved_todos(self, repo): |
|
154 | 155 | todos = Session().query(ChangesetComment) \ |
|
155 | 156 | .filter(ChangesetComment.repo == repo) \ |
|
156 | 157 | .filter(ChangesetComment.resolved_by == None) \ |
|
157 | 158 | .filter(ChangesetComment.comment_type |
|
158 | 159 | == ChangesetComment.COMMENT_TYPE_TODO) |
|
159 | 160 | todos = todos.all() |
|
160 | 161 | |
|
161 | 162 | return todos |
|
162 | 163 | |
|
163 | def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True): | |
|
164 | def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True): | |
|
164 | 165 | |
|
165 | 166 | todos = Session().query(ChangesetComment) \ |
|
166 | 167 | .filter(ChangesetComment.pull_request == pull_request) \ |
|
167 | 168 | .filter(ChangesetComment.resolved_by == None) \ |
|
168 | 169 | .filter(ChangesetComment.comment_type |
|
169 | 170 | == ChangesetComment.COMMENT_TYPE_TODO) |
|
170 | 171 | |
|
172 | if not include_drafts: | |
|
173 | todos = todos.filter(ChangesetComment.draft == false()) | |
|
174 | ||
|
171 | 175 | if not show_outdated: |
|
172 | 176 | todos = todos.filter( |
|
173 | 177 | coalesce(ChangesetComment.display_state, '') != |
|
174 | 178 | ChangesetComment.COMMENT_OUTDATED) |
|
175 | 179 | |
|
176 | 180 | todos = todos.all() |
|
177 | 181 | |
|
178 | 182 | return todos |
|
179 | 183 | |
|
180 | def get_pull_request_resolved_todos(self, pull_request, show_outdated=True): | |
|
184 | def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True): | |
|
181 | 185 | |
|
182 | 186 | todos = Session().query(ChangesetComment) \ |
|
183 | 187 | .filter(ChangesetComment.pull_request == pull_request) \ |
|
184 | 188 | .filter(ChangesetComment.resolved_by != None) \ |
|
185 | 189 | .filter(ChangesetComment.comment_type |
|
186 | 190 | == ChangesetComment.COMMENT_TYPE_TODO) |
|
187 | 191 | |
|
192 | if not include_drafts: | |
|
193 | todos = todos.filter(ChangesetComment.draft == false()) | |
|
194 | ||
|
188 | 195 | if not show_outdated: |
|
189 | 196 | todos = todos.filter( |
|
190 | 197 | coalesce(ChangesetComment.display_state, '') != |
|
191 | 198 | ChangesetComment.COMMENT_OUTDATED) |
|
192 | 199 | |
|
193 | 200 | todos = todos.all() |
|
194 | 201 | |
|
195 | 202 | return todos |
|
196 | 203 | |
|
197 | def get_commit_unresolved_todos(self, commit_id, show_outdated=True): | |
|
204 | def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True): | |
|
198 | 205 | |
|
199 | 206 | todos = Session().query(ChangesetComment) \ |
|
200 | 207 | .filter(ChangesetComment.revision == commit_id) \ |
|
201 | 208 | .filter(ChangesetComment.resolved_by == None) \ |
|
202 | 209 | .filter(ChangesetComment.comment_type |
|
203 | 210 | == ChangesetComment.COMMENT_TYPE_TODO) |
|
204 | 211 | |
|
212 | if not include_drafts: | |
|
213 | todos = todos.filter(ChangesetComment.draft == false()) | |
|
214 | ||
|
205 | 215 | if not show_outdated: |
|
206 | 216 | todos = todos.filter( |
|
207 | 217 | coalesce(ChangesetComment.display_state, '') != |
|
208 | 218 | ChangesetComment.COMMENT_OUTDATED) |
|
209 | 219 | |
|
210 | 220 | todos = todos.all() |
|
211 | 221 | |
|
212 | 222 | return todos |
|
213 | 223 | |
|
214 | def get_commit_resolved_todos(self, commit_id, show_outdated=True): | |
|
224 | def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True): | |
|
215 | 225 | |
|
216 | 226 | todos = Session().query(ChangesetComment) \ |
|
217 | 227 | .filter(ChangesetComment.revision == commit_id) \ |
|
218 | 228 | .filter(ChangesetComment.resolved_by != None) \ |
|
219 | 229 | .filter(ChangesetComment.comment_type |
|
220 | 230 | == ChangesetComment.COMMENT_TYPE_TODO) |
|
221 | 231 | |
|
232 | if not include_drafts: | |
|
233 | todos = todos.filter(ChangesetComment.draft == false()) | |
|
234 | ||
|
222 | 235 | if not show_outdated: |
|
223 | 236 | todos = todos.filter( |
|
224 | 237 | coalesce(ChangesetComment.display_state, '') != |
|
225 | 238 | ChangesetComment.COMMENT_OUTDATED) |
|
226 | 239 | |
|
227 | 240 | todos = todos.all() |
|
228 | 241 | |
|
229 | 242 | return todos |
|
230 | 243 | |
|
231 | def get_commit_inline_comments(self, commit_id): | |
|
244 | def get_commit_inline_comments(self, commit_id, include_drafts=True): | |
|
232 | 245 | inline_comments = Session().query(ChangesetComment) \ |
|
233 | 246 | .filter(ChangesetComment.line_no != None) \ |
|
234 | 247 | .filter(ChangesetComment.f_path != None) \ |
|
235 | 248 | .filter(ChangesetComment.revision == commit_id) |
|
249 | ||
|
250 | if not include_drafts: | |
|
251 | inline_comments = inline_comments.filter(ChangesetComment.draft == false()) | |
|
252 | ||
|
236 | 253 | inline_comments = inline_comments.all() |
|
237 | 254 | return inline_comments |
|
238 | 255 | |
|
239 | 256 | def _log_audit_action(self, action, action_data, auth_user, comment): |
|
240 | 257 | audit_logger.store( |
|
241 | 258 | action=action, |
|
242 | 259 | action_data=action_data, |
|
243 | 260 | user=auth_user, |
|
244 | 261 | repo=comment.repo) |
|
245 | 262 | |
|
246 | 263 | def create(self, text, repo, user, commit_id=None, pull_request=None, |
|
247 | 264 | f_path=None, line_no=None, status_change=None, |
|
248 | status_change_type=None, comment_type=None, | |
|
265 | status_change_type=None, comment_type=None, is_draft=False, | |
|
249 | 266 | resolves_comment_id=None, closing_pr=False, send_email=True, |
|
250 | 267 | renderer=None, auth_user=None, extra_recipients=None): |
|
251 | 268 | """ |
|
252 | 269 | Creates new comment for commit or pull request. |
|
253 | 270 | IF status_change is not none this comment is associated with a |
|
254 | 271 | status change of commit or commit associated with pull request |
|
255 | 272 | |
|
256 | 273 | :param text: |
|
257 | 274 | :param repo: |
|
258 | 275 | :param user: |
|
259 | 276 | :param commit_id: |
|
260 | 277 | :param pull_request: |
|
261 | 278 | :param f_path: |
|
262 | 279 | :param line_no: |
|
263 | 280 | :param status_change: Label for status change |
|
264 | 281 | :param comment_type: Type of comment |
|
282 | :param is_draft: is comment a draft only | |
|
265 | 283 | :param resolves_comment_id: id of comment which this one will resolve |
|
266 | 284 | :param status_change_type: type of status change |
|
267 | 285 | :param closing_pr: |
|
268 | 286 | :param send_email: |
|
269 | 287 | :param renderer: pick renderer for this comment |
|
270 | 288 | :param auth_user: current authenticated user calling this method |
|
271 | 289 | :param extra_recipients: list of extra users to be added to recipients |
|
272 | 290 | """ |
|
273 | 291 | |
|
274 | 292 | if not text: |
|
275 | 293 | log.warning('Missing text for comment, skipping...') |
|
276 | 294 | return |
|
277 | 295 | request = get_current_request() |
|
278 | 296 | _ = request.translate |
|
279 | 297 | |
|
280 | 298 | if not renderer: |
|
281 | 299 | renderer = self._get_renderer(request=request) |
|
282 | 300 | |
|
283 | 301 | repo = self._get_repo(repo) |
|
284 | 302 | user = self._get_user(user) |
|
285 | 303 | auth_user = auth_user or user |
|
286 | 304 | |
|
287 | 305 | schema = comment_schema.CommentSchema() |
|
288 | 306 | validated_kwargs = schema.deserialize(dict( |
|
289 | 307 | comment_body=text, |
|
290 | 308 | comment_type=comment_type, |
|
309 | is_draft=is_draft, | |
|
291 | 310 | comment_file=f_path, |
|
292 | 311 | comment_line=line_no, |
|
293 | 312 | renderer_type=renderer, |
|
294 | 313 | status_change=status_change_type, |
|
295 | 314 | resolves_comment_id=resolves_comment_id, |
|
296 | 315 | repo=repo.repo_id, |
|
297 | 316 | user=user.user_id, |
|
298 | 317 | )) |
|
318 | is_draft = validated_kwargs['is_draft'] | |
|
299 | 319 | |
|
300 | 320 | comment = ChangesetComment() |
|
301 | 321 | comment.renderer = validated_kwargs['renderer_type'] |
|
302 | 322 | comment.text = validated_kwargs['comment_body'] |
|
303 | 323 | comment.f_path = validated_kwargs['comment_file'] |
|
304 | 324 | comment.line_no = validated_kwargs['comment_line'] |
|
305 | 325 | comment.comment_type = validated_kwargs['comment_type'] |
|
326 | comment.draft = is_draft | |
|
306 | 327 | |
|
307 | 328 | comment.repo = repo |
|
308 | 329 | comment.author = user |
|
309 | 330 | resolved_comment = self.__get_commit_comment( |
|
310 | 331 | validated_kwargs['resolves_comment_id']) |
|
311 | 332 | # check if the comment actually belongs to this PR |
|
312 | 333 | if resolved_comment and resolved_comment.pull_request and \ |
|
313 | 334 | resolved_comment.pull_request != pull_request: |
|
314 | 335 | log.warning('Comment tried to resolved unrelated todo comment: %s', |
|
315 | 336 | resolved_comment) |
|
316 | 337 | # comment not bound to this pull request, forbid |
|
317 | 338 | resolved_comment = None |
|
318 | 339 | |
|
319 | 340 | elif resolved_comment and resolved_comment.repo and \ |
|
320 | 341 | resolved_comment.repo != repo: |
|
321 | 342 | log.warning('Comment tried to resolved unrelated todo comment: %s', |
|
322 | 343 | resolved_comment) |
|
323 | 344 | # comment not bound to this repo, forbid |
|
324 | 345 | resolved_comment = None |
|
325 | 346 | |
|
326 | 347 | comment.resolved_comment = resolved_comment |
|
327 | 348 | |
|
328 | 349 | pull_request_id = pull_request |
|
329 | 350 | |
|
330 | 351 | commit_obj = None |
|
331 | 352 | pull_request_obj = None |
|
332 | 353 | |
|
333 | 354 | if commit_id: |
|
334 | 355 | notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT |
|
335 | 356 | # do a lookup, so we don't pass something bad here |
|
336 | 357 | commit_obj = repo.scm_instance().get_commit(commit_id=commit_id) |
|
337 | 358 | comment.revision = commit_obj.raw_id |
|
338 | 359 | |
|
339 | 360 | elif pull_request_id: |
|
340 | 361 | notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT |
|
341 | 362 | pull_request_obj = self.__get_pull_request(pull_request_id) |
|
342 | 363 | comment.pull_request = pull_request_obj |
|
343 | 364 | else: |
|
344 | 365 | raise Exception('Please specify commit or pull_request_id') |
|
345 | 366 | |
|
346 | 367 | Session().add(comment) |
|
347 | 368 | Session().flush() |
|
348 | 369 | kwargs = { |
|
349 | 370 | 'user': user, |
|
350 | 371 | 'renderer_type': renderer, |
|
351 | 372 | 'repo_name': repo.repo_name, |
|
352 | 373 | 'status_change': status_change, |
|
353 | 374 | 'status_change_type': status_change_type, |
|
354 | 375 | 'comment_body': text, |
|
355 | 376 | 'comment_file': f_path, |
|
356 | 377 | 'comment_line': line_no, |
|
357 | 378 | 'comment_type': comment_type or 'note', |
|
358 | 379 | 'comment_id': comment.comment_id |
|
359 | 380 | } |
|
360 | 381 | |
|
361 | 382 | if commit_obj: |
|
362 | 383 | recipients = ChangesetComment.get_users( |
|
363 | 384 | revision=commit_obj.raw_id) |
|
364 | 385 | # add commit author if it's in RhodeCode system |
|
365 | 386 | cs_author = User.get_from_cs_author(commit_obj.author) |
|
366 | 387 | if not cs_author: |
|
367 | 388 | # use repo owner if we cannot extract the author correctly |
|
368 | 389 | cs_author = repo.user |
|
369 | 390 | recipients += [cs_author] |
|
370 | 391 | |
|
371 | 392 | commit_comment_url = self.get_url(comment, request=request) |
|
372 | 393 | commit_comment_reply_url = self.get_url( |
|
373 | 394 | comment, request=request, |
|
374 | 395 | anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id)) |
|
375 | 396 | |
|
376 | 397 | target_repo_url = h.link_to( |
|
377 | 398 | repo.repo_name, |
|
378 | 399 | h.route_url('repo_summary', repo_name=repo.repo_name)) |
|
379 | 400 | |
|
380 | 401 | commit_url = h.route_url('repo_commit', repo_name=repo.repo_name, |
|
381 | 402 | commit_id=commit_id) |
|
382 | 403 | |
|
383 | 404 | # commit specifics |
|
384 | 405 | kwargs.update({ |
|
385 | 406 | 'commit': commit_obj, |
|
386 | 407 | 'commit_message': commit_obj.message, |
|
387 | 408 | 'commit_target_repo_url': target_repo_url, |
|
388 | 409 | 'commit_comment_url': commit_comment_url, |
|
389 | 410 | 'commit_comment_reply_url': commit_comment_reply_url, |
|
390 | 411 | 'commit_url': commit_url, |
|
391 | 412 | 'thread_ids': [commit_url, commit_comment_url], |
|
392 | 413 | }) |
|
393 | 414 | |
|
394 | 415 | elif pull_request_obj: |
|
395 | 416 | # get the current participants of this pull request |
|
396 | 417 | recipients = ChangesetComment.get_users( |
|
397 | 418 | pull_request_id=pull_request_obj.pull_request_id) |
|
398 | 419 | # add pull request author |
|
399 | 420 | recipients += [pull_request_obj.author] |
|
400 | 421 | |
|
401 | 422 | # add the reviewers to notification |
|
402 | 423 | recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()] |
|
403 | 424 | |
|
404 | 425 | pr_target_repo = pull_request_obj.target_repo |
|
405 | 426 | pr_source_repo = pull_request_obj.source_repo |
|
406 | 427 | |
|
407 | 428 | pr_comment_url = self.get_url(comment, request=request) |
|
408 | 429 | pr_comment_reply_url = self.get_url( |
|
409 | 430 | comment, request=request, |
|
410 | 431 | anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id)) |
|
411 | 432 | |
|
412 | 433 | pr_url = h.route_url( |
|
413 | 434 | 'pullrequest_show', |
|
414 | 435 | repo_name=pr_target_repo.repo_name, |
|
415 | 436 | pull_request_id=pull_request_obj.pull_request_id, ) |
|
416 | 437 | |
|
417 | 438 | # set some variables for email notification |
|
418 | 439 | pr_target_repo_url = h.route_url( |
|
419 | 440 | 'repo_summary', repo_name=pr_target_repo.repo_name) |
|
420 | 441 | |
|
421 | 442 | pr_source_repo_url = h.route_url( |
|
422 | 443 | 'repo_summary', repo_name=pr_source_repo.repo_name) |
|
423 | 444 | |
|
424 | 445 | # pull request specifics |
|
425 | 446 | kwargs.update({ |
|
426 | 447 | 'pull_request': pull_request_obj, |
|
427 | 448 | 'pr_id': pull_request_obj.pull_request_id, |
|
428 | 449 | 'pull_request_url': pr_url, |
|
429 | 450 | 'pull_request_target_repo': pr_target_repo, |
|
430 | 451 | 'pull_request_target_repo_url': pr_target_repo_url, |
|
431 | 452 | 'pull_request_source_repo': pr_source_repo, |
|
432 | 453 | 'pull_request_source_repo_url': pr_source_repo_url, |
|
433 | 454 | 'pr_comment_url': pr_comment_url, |
|
434 | 455 | 'pr_comment_reply_url': pr_comment_reply_url, |
|
435 | 456 | 'pr_closing': closing_pr, |
|
436 | 457 | 'thread_ids': [pr_url, pr_comment_url], |
|
437 | 458 | }) |
|
438 | 459 | |
|
439 | 460 | if send_email: |
|
440 | 461 | recipients += [self._get_user(u) for u in (extra_recipients or [])] |
|
441 | 462 | # pre-generate the subject for notification itself |
|
442 | 463 | (subject, _e, body_plaintext) = EmailNotificationModel().render_email( |
|
443 | 464 | notification_type, **kwargs) |
|
444 | 465 | |
|
445 | 466 | mention_recipients = set( |
|
446 | 467 | self._extract_mentions(text)).difference(recipients) |
|
447 | 468 | |
|
448 | 469 | # create notification objects, and emails |
|
449 | 470 | NotificationModel().create( |
|
450 | 471 | created_by=user, |
|
451 | 472 | notification_subject=subject, |
|
452 | 473 | notification_body=body_plaintext, |
|
453 | 474 | notification_type=notification_type, |
|
454 | 475 | recipients=recipients, |
|
455 | 476 | mention_recipients=mention_recipients, |
|
456 | 477 | email_kwargs=kwargs, |
|
457 | 478 | ) |
|
458 | 479 | |
|
459 | 480 | Session().flush() |
|
460 | 481 | if comment.pull_request: |
|
461 | 482 | action = 'repo.pull_request.comment.create' |
|
462 | 483 | else: |
|
463 | 484 | action = 'repo.commit.comment.create' |
|
464 | 485 | |
|
486 | if not is_draft: | |
|
465 | 487 | comment_data = comment.get_api_data() |
|
466 | 488 | |
|
467 | 489 | self._log_audit_action( |
|
468 | 490 | action, {'data': comment_data}, auth_user, comment) |
|
469 | 491 | |
|
470 | 492 | return comment |
|
471 | 493 | |
|
472 | 494 | def edit(self, comment_id, text, auth_user, version): |
|
473 | 495 | """ |
|
474 | 496 | Change existing comment for commit or pull request. |
|
475 | 497 | |
|
476 | 498 | :param comment_id: |
|
477 | 499 | :param text: |
|
478 | 500 | :param auth_user: current authenticated user calling this method |
|
479 | 501 | :param version: last comment version |
|
480 | 502 | """ |
|
481 | 503 | if not text: |
|
482 | 504 | log.warning('Missing text for comment, skipping...') |
|
483 | 505 | return |
|
484 | 506 | |
|
485 | 507 | comment = ChangesetComment.get(comment_id) |
|
486 | 508 | old_comment_text = comment.text |
|
487 | 509 | comment.text = text |
|
488 | 510 | comment.modified_at = datetime.datetime.now() |
|
489 | 511 | version = safe_int(version) |
|
490 | 512 | |
|
491 | 513 | # NOTE(marcink): this returns initial comment + edits, so v2 from ui |
|
492 | 514 | # would return 3 here |
|
493 | 515 | comment_version = ChangesetCommentHistory.get_version(comment_id) |
|
494 | 516 | |
|
495 | 517 | if isinstance(version, (int, long)) and (comment_version - version) != 1: |
|
496 | 518 | log.warning( |
|
497 | 519 | 'Version mismatch comment_version {} submitted {}, skipping'.format( |
|
498 | 520 | comment_version-1, # -1 since note above |
|
499 | 521 | version |
|
500 | 522 | ) |
|
501 | 523 | ) |
|
502 | 524 | raise CommentVersionMismatch() |
|
503 | 525 | |
|
504 | 526 | comment_history = ChangesetCommentHistory() |
|
505 | 527 | comment_history.comment_id = comment_id |
|
506 | 528 | comment_history.version = comment_version |
|
507 | 529 | comment_history.created_by_user_id = auth_user.user_id |
|
508 | 530 | comment_history.text = old_comment_text |
|
509 | 531 | # TODO add email notification |
|
510 | 532 | Session().add(comment_history) |
|
511 | 533 | Session().add(comment) |
|
512 | 534 | Session().flush() |
|
513 | 535 | |
|
514 | 536 | if comment.pull_request: |
|
515 | 537 | action = 'repo.pull_request.comment.edit' |
|
516 | 538 | else: |
|
517 | 539 | action = 'repo.commit.comment.edit' |
|
518 | 540 | |
|
519 | 541 | comment_data = comment.get_api_data() |
|
520 | 542 | comment_data['old_comment_text'] = old_comment_text |
|
521 | 543 | self._log_audit_action( |
|
522 | 544 | action, {'data': comment_data}, auth_user, comment) |
|
523 | 545 | |
|
524 | 546 | return comment_history |
|
525 | 547 | |
|
526 | 548 | def delete(self, comment, auth_user): |
|
527 | 549 | """ |
|
528 | 550 | Deletes given comment |
|
529 | 551 | """ |
|
530 | 552 | comment = self.__get_commit_comment(comment) |
|
531 | 553 | old_data = comment.get_api_data() |
|
532 | 554 | Session().delete(comment) |
|
533 | 555 | |
|
534 | 556 | if comment.pull_request: |
|
535 | 557 | action = 'repo.pull_request.comment.delete' |
|
536 | 558 | else: |
|
537 | 559 | action = 'repo.commit.comment.delete' |
|
538 | 560 | |
|
539 | 561 | self._log_audit_action( |
|
540 | 562 | action, {'old_data': old_data}, auth_user, comment) |
|
541 | 563 | |
|
542 | 564 | return comment |
|
543 | 565 | |
|
544 | 566 | def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False): |
|
545 | 567 | q = ChangesetComment.query()\ |
|
546 | 568 | .filter(ChangesetComment.repo_id == repo_id) |
|
547 | 569 | if revision: |
|
548 | 570 | q = q.filter(ChangesetComment.revision == revision) |
|
549 | 571 | elif pull_request: |
|
550 | 572 | pull_request = self.__get_pull_request(pull_request) |
|
551 | 573 | q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id) |
|
552 | 574 | else: |
|
553 | 575 | raise Exception('Please specify commit or pull_request') |
|
554 | 576 | q = q.order_by(ChangesetComment.created_on) |
|
555 | 577 | if count_only: |
|
556 | 578 | return q.count() |
|
557 | 579 | |
|
558 | 580 | return q.all() |
|
559 | 581 | |
|
560 | 582 | def get_url(self, comment, request=None, permalink=False, anchor=None): |
|
561 | 583 | if not request: |
|
562 | 584 | request = get_current_request() |
|
563 | 585 | |
|
564 | 586 | comment = self.__get_commit_comment(comment) |
|
565 | 587 | if anchor is None: |
|
566 | 588 | anchor = 'comment-{}'.format(comment.comment_id) |
|
567 | 589 | |
|
568 | 590 | if comment.pull_request: |
|
569 | 591 | pull_request = comment.pull_request |
|
570 | 592 | if permalink: |
|
571 | 593 | return request.route_url( |
|
572 | 594 | 'pull_requests_global', |
|
573 | 595 | pull_request_id=pull_request.pull_request_id, |
|
574 | 596 | _anchor=anchor) |
|
575 | 597 | else: |
|
576 | 598 | return request.route_url( |
|
577 | 599 | 'pullrequest_show', |
|
578 | 600 | repo_name=safe_str(pull_request.target_repo.repo_name), |
|
579 | 601 | pull_request_id=pull_request.pull_request_id, |
|
580 | 602 | _anchor=anchor) |
|
581 | 603 | |
|
582 | 604 | else: |
|
583 | 605 | repo = comment.repo |
|
584 | 606 | commit_id = comment.revision |
|
585 | 607 | |
|
586 | 608 | if permalink: |
|
587 | 609 | return request.route_url( |
|
588 | 610 | 'repo_commit', repo_name=safe_str(repo.repo_id), |
|
589 | 611 | commit_id=commit_id, |
|
590 | 612 | _anchor=anchor) |
|
591 | 613 | |
|
592 | 614 | else: |
|
593 | 615 | return request.route_url( |
|
594 | 616 | 'repo_commit', repo_name=safe_str(repo.repo_name), |
|
595 | 617 | commit_id=commit_id, |
|
596 | 618 | _anchor=anchor) |
|
597 | 619 | |
|
598 | 620 | def get_comments(self, repo_id, revision=None, pull_request=None): |
|
599 | 621 | """ |
|
600 | 622 | Gets main comments based on revision or pull_request_id |
|
601 | 623 | |
|
602 | 624 | :param repo_id: |
|
603 | 625 | :param revision: |
|
604 | 626 | :param pull_request: |
|
605 | 627 | """ |
|
606 | 628 | |
|
607 | 629 | q = ChangesetComment.query()\ |
|
608 | 630 | .filter(ChangesetComment.repo_id == repo_id)\ |
|
609 | 631 | .filter(ChangesetComment.line_no == None)\ |
|
610 | 632 | .filter(ChangesetComment.f_path == None) |
|
611 | 633 | if revision: |
|
612 | 634 | q = q.filter(ChangesetComment.revision == revision) |
|
613 | 635 | elif pull_request: |
|
614 | 636 | pull_request = self.__get_pull_request(pull_request) |
|
615 | 637 | q = q.filter(ChangesetComment.pull_request == pull_request) |
|
616 | 638 | else: |
|
617 | 639 | raise Exception('Please specify commit or pull_request') |
|
618 | 640 | q = q.order_by(ChangesetComment.created_on) |
|
619 | 641 | return q.all() |
|
620 | 642 | |
|
621 | 643 | def get_inline_comments(self, repo_id, revision=None, pull_request=None): |
|
622 | 644 | q = self._get_inline_comments_query(repo_id, revision, pull_request) |
|
623 | 645 | return self._group_comments_by_path_and_line_number(q) |
|
624 | 646 | |
|
625 | 647 | def get_inline_comments_as_list(self, inline_comments, skip_outdated=True, |
|
626 | 648 | version=None): |
|
627 | 649 | inline_comms = [] |
|
628 | 650 | for fname, per_line_comments in inline_comments.iteritems(): |
|
629 | 651 | for lno, comments in per_line_comments.iteritems(): |
|
630 | 652 | for comm in comments: |
|
631 | 653 | if not comm.outdated_at_version(version) and skip_outdated: |
|
632 | 654 | inline_comms.append(comm) |
|
633 | 655 | |
|
634 | 656 | return inline_comms |
|
635 | 657 | |
|
636 | 658 | def get_outdated_comments(self, repo_id, pull_request): |
|
637 | 659 | # TODO: johbo: Remove `repo_id`, it is not needed to find the comments |
|
638 | 660 | # of a pull request. |
|
639 | 661 | q = self._all_inline_comments_of_pull_request(pull_request) |
|
640 | 662 | q = q.filter( |
|
641 | 663 | ChangesetComment.display_state == |
|
642 | 664 | ChangesetComment.COMMENT_OUTDATED |
|
643 | 665 | ).order_by(ChangesetComment.comment_id.asc()) |
|
644 | 666 | |
|
645 | 667 | return self._group_comments_by_path_and_line_number(q) |
|
646 | 668 | |
|
647 | 669 | def _get_inline_comments_query(self, repo_id, revision, pull_request): |
|
648 | 670 | # TODO: johbo: Split this into two methods: One for PR and one for |
|
649 | 671 | # commit. |
|
650 | 672 | if revision: |
|
651 | 673 | q = Session().query(ChangesetComment).filter( |
|
652 | 674 | ChangesetComment.repo_id == repo_id, |
|
653 | 675 | ChangesetComment.line_no != null(), |
|
654 | 676 | ChangesetComment.f_path != null(), |
|
655 | 677 | ChangesetComment.revision == revision) |
|
656 | 678 | |
|
657 | 679 | elif pull_request: |
|
658 | 680 | pull_request = self.__get_pull_request(pull_request) |
|
659 | 681 | if not CommentsModel.use_outdated_comments(pull_request): |
|
660 | 682 | q = self._visible_inline_comments_of_pull_request(pull_request) |
|
661 | 683 | else: |
|
662 | 684 | q = self._all_inline_comments_of_pull_request(pull_request) |
|
663 | 685 | |
|
664 | 686 | else: |
|
665 | 687 | raise Exception('Please specify commit or pull_request_id') |
|
666 | 688 | q = q.order_by(ChangesetComment.comment_id.asc()) |
|
667 | 689 | return q |
|
668 | 690 | |
|
669 | 691 | def _group_comments_by_path_and_line_number(self, q): |
|
670 | 692 | comments = q.all() |
|
671 | 693 | paths = collections.defaultdict(lambda: collections.defaultdict(list)) |
|
672 | 694 | for co in comments: |
|
673 | 695 | paths[co.f_path][co.line_no].append(co) |
|
674 | 696 | return paths |
|
675 | 697 | |
|
676 | 698 | @classmethod |
|
677 | 699 | def needed_extra_diff_context(cls): |
|
678 | 700 | return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER) |
|
679 | 701 | |
|
680 | 702 | def outdate_comments(self, pull_request, old_diff_data, new_diff_data): |
|
681 | 703 | if not CommentsModel.use_outdated_comments(pull_request): |
|
682 | 704 | return |
|
683 | 705 | |
|
684 | 706 | comments = self._visible_inline_comments_of_pull_request(pull_request) |
|
685 | 707 | comments_to_outdate = comments.all() |
|
686 | 708 | |
|
687 | 709 | for comment in comments_to_outdate: |
|
688 | 710 | self._outdate_one_comment(comment, old_diff_data, new_diff_data) |
|
689 | 711 | |
|
690 | 712 | def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc): |
|
691 | 713 | diff_line = _parse_comment_line_number(comment.line_no) |
|
692 | 714 | |
|
693 | 715 | try: |
|
694 | 716 | old_context = old_diff_proc.get_context_of_line( |
|
695 | 717 | path=comment.f_path, diff_line=diff_line) |
|
696 | 718 | new_context = new_diff_proc.get_context_of_line( |
|
697 | 719 | path=comment.f_path, diff_line=diff_line) |
|
698 | 720 | except (diffs.LineNotInDiffException, |
|
699 | 721 | diffs.FileNotInDiffException): |
|
700 | 722 | comment.display_state = ChangesetComment.COMMENT_OUTDATED |
|
701 | 723 | return |
|
702 | 724 | |
|
703 | 725 | if old_context == new_context: |
|
704 | 726 | return |
|
705 | 727 | |
|
706 | 728 | if self._should_relocate_diff_line(diff_line): |
|
707 | 729 | new_diff_lines = new_diff_proc.find_context( |
|
708 | 730 | path=comment.f_path, context=old_context, |
|
709 | 731 | offset=self.DIFF_CONTEXT_BEFORE) |
|
710 | 732 | if not new_diff_lines: |
|
711 | 733 | comment.display_state = ChangesetComment.COMMENT_OUTDATED |
|
712 | 734 | else: |
|
713 | 735 | new_diff_line = self._choose_closest_diff_line( |
|
714 | 736 | diff_line, new_diff_lines) |
|
715 | 737 | comment.line_no = _diff_to_comment_line_number(new_diff_line) |
|
716 | 738 | else: |
|
717 | 739 | comment.display_state = ChangesetComment.COMMENT_OUTDATED |
|
718 | 740 | |
|
719 | 741 | def _should_relocate_diff_line(self, diff_line): |
|
720 | 742 | """ |
|
721 | 743 | Checks if relocation shall be tried for the given `diff_line`. |
|
722 | 744 | |
|
723 | 745 | If a comment points into the first lines, then we can have a situation |
|
724 | 746 | that after an update another line has been added on top. In this case |
|
725 | 747 | we would find the context still and move the comment around. This |
|
726 | 748 | would be wrong. |
|
727 | 749 | """ |
|
728 | 750 | should_relocate = ( |
|
729 | 751 | (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or |
|
730 | 752 | (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE)) |
|
731 | 753 | return should_relocate |
|
732 | 754 | |
|
733 | 755 | def _choose_closest_diff_line(self, diff_line, new_diff_lines): |
|
734 | 756 | candidate = new_diff_lines[0] |
|
735 | 757 | best_delta = _diff_line_delta(diff_line, candidate) |
|
736 | 758 | for new_diff_line in new_diff_lines[1:]: |
|
737 | 759 | delta = _diff_line_delta(diff_line, new_diff_line) |
|
738 | 760 | if delta < best_delta: |
|
739 | 761 | candidate = new_diff_line |
|
740 | 762 | best_delta = delta |
|
741 | 763 | return candidate |
|
742 | 764 | |
|
743 | 765 | def _visible_inline_comments_of_pull_request(self, pull_request): |
|
744 | 766 | comments = self._all_inline_comments_of_pull_request(pull_request) |
|
745 | 767 | comments = comments.filter( |
|
746 | 768 | coalesce(ChangesetComment.display_state, '') != |
|
747 | 769 | ChangesetComment.COMMENT_OUTDATED) |
|
748 | 770 | return comments |
|
749 | 771 | |
|
750 | 772 | def _all_inline_comments_of_pull_request(self, pull_request): |
|
751 | 773 | comments = Session().query(ChangesetComment)\ |
|
752 | 774 | .filter(ChangesetComment.line_no != None)\ |
|
753 | 775 | .filter(ChangesetComment.f_path != None)\ |
|
754 | 776 | .filter(ChangesetComment.pull_request == pull_request) |
|
755 | 777 | return comments |
|
756 | 778 | |
|
757 | 779 | def _all_general_comments_of_pull_request(self, pull_request): |
|
758 | 780 | comments = Session().query(ChangesetComment)\ |
|
759 | 781 | .filter(ChangesetComment.line_no == None)\ |
|
760 | 782 | .filter(ChangesetComment.f_path == None)\ |
|
761 | 783 | .filter(ChangesetComment.pull_request == pull_request) |
|
762 | 784 | |
|
763 | 785 | return comments |
|
764 | 786 | |
|
765 | 787 | @staticmethod |
|
766 | 788 | def use_outdated_comments(pull_request): |
|
767 | 789 | settings_model = VcsSettingsModel(repo=pull_request.target_repo) |
|
768 | 790 | settings = settings_model.get_general_settings() |
|
769 | 791 | return settings.get('rhodecode_use_outdated_comments', False) |
|
770 | 792 | |
|
771 | 793 | def trigger_commit_comment_hook(self, repo, user, action, data=None): |
|
772 | 794 | repo = self._get_repo(repo) |
|
773 | 795 | target_scm = repo.scm_instance() |
|
774 | 796 | if action == 'create': |
|
775 | 797 | trigger_hook = hooks_utils.trigger_comment_commit_hooks |
|
776 | 798 | elif action == 'edit': |
|
777 | 799 | trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks |
|
778 | 800 | else: |
|
779 | 801 | return |
|
780 | 802 | |
|
781 | 803 | log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s', |
|
782 | 804 | repo, action, trigger_hook) |
|
783 | 805 | trigger_hook( |
|
784 | 806 | username=user.username, |
|
785 | 807 | repo_name=repo.repo_name, |
|
786 | 808 | repo_type=target_scm.alias, |
|
787 | 809 | repo=repo, |
|
788 | 810 | data=data) |
|
789 | 811 | |
|
790 | 812 | |
|
791 | 813 | def _parse_comment_line_number(line_no): |
|
792 | 814 | """ |
|
793 | 815 | Parses line numbers of the form "(o|n)\d+" and returns them in a tuple. |
|
794 | 816 | """ |
|
795 | 817 | old_line = None |
|
796 | 818 | new_line = None |
|
797 | 819 | if line_no.startswith('o'): |
|
798 | 820 | old_line = int(line_no[1:]) |
|
799 | 821 | elif line_no.startswith('n'): |
|
800 | 822 | new_line = int(line_no[1:]) |
|
801 | 823 | else: |
|
802 | 824 | raise ValueError("Comment lines have to start with either 'o' or 'n'.") |
|
803 | 825 | return diffs.DiffLineNumber(old_line, new_line) |
|
804 | 826 | |
|
805 | 827 | |
|
806 | 828 | def _diff_to_comment_line_number(diff_line): |
|
807 | 829 | if diff_line.new is not None: |
|
808 | 830 | return u'n{}'.format(diff_line.new) |
|
809 | 831 | elif diff_line.old is not None: |
|
810 | 832 | return u'o{}'.format(diff_line.old) |
|
811 | 833 | return u'' |
|
812 | 834 | |
|
813 | 835 | |
|
814 | 836 | def _diff_line_delta(a, b): |
|
815 | 837 | if None not in (a.new, b.new): |
|
816 | 838 | return abs(a.new - b.new) |
|
817 | 839 | elif None not in (a.old, b.old): |
|
818 | 840 | return abs(a.old - b.old) |
|
819 | 841 | else: |
|
820 | 842 | raise ValueError( |
|
821 | 843 | "Cannot compute delta between {} and {}".format(a, b)) |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
@@ -1,74 +1,74 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2017-2020 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import os |
|
22 | 22 | |
|
23 | 23 | import colander |
|
24 | 24 | |
|
25 | 25 | from rhodecode.translation import _ |
|
26 | 26 | from rhodecode.model.validation_schema import preparers |
|
27 | 27 | from rhodecode.model.validation_schema import types |
|
28 | 28 | |
|
29 | 29 | |
|
30 | 30 | @colander.deferred |
|
31 | 31 | def deferred_lifetime_validator(node, kw): |
|
32 | 32 | options = kw.get('lifetime_options', []) |
|
33 | 33 | return colander.All( |
|
34 | 34 | colander.Range(min=-1, max=60 * 24 * 30 * 12), |
|
35 | 35 | colander.OneOf([x for x in options])) |
|
36 | 36 | |
|
37 | 37 | |
|
38 | 38 | def unique_gist_validator(node, value): |
|
39 | 39 | from rhodecode.model.db import Gist |
|
40 | 40 | existing = Gist.get_by_access_id(value) |
|
41 | 41 | if existing: |
|
42 | 42 | msg = _(u'Gist with name {} already exists').format(value) |
|
43 | 43 | raise colander.Invalid(node, msg) |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | def filename_validator(node, value): |
|
47 | 47 | if value != os.path.basename(value): |
|
48 | 48 | msg = _(u'Filename {} cannot be inside a directory').format(value) |
|
49 | 49 | raise colander.Invalid(node, msg) |
|
50 | 50 | |
|
51 | 51 | |
|
52 | 52 | comment_types = ['note', 'todo'] |
|
53 | 53 | |
|
54 | 54 | |
|
55 | 55 | class CommentSchema(colander.MappingSchema): |
|
56 | 56 | from rhodecode.model.db import ChangesetComment, ChangesetStatus |
|
57 | 57 | |
|
58 | 58 | comment_body = colander.SchemaNode(colander.String()) |
|
59 | 59 | comment_type = colander.SchemaNode( |
|
60 | 60 | colander.String(), |
|
61 | 61 | validator=colander.OneOf(ChangesetComment.COMMENT_TYPES), |
|
62 | 62 | missing=ChangesetComment.COMMENT_TYPE_NOTE) |
|
63 | ||
|
63 | is_draft = colander.SchemaNode(colander.Boolean(),missing=False) | |
|
64 | 64 | comment_file = colander.SchemaNode(colander.String(), missing=None) |
|
65 | 65 | comment_line = colander.SchemaNode(colander.String(), missing=None) |
|
66 | 66 | status_change = colander.SchemaNode( |
|
67 | 67 | colander.String(), missing=None, |
|
68 | 68 | validator=colander.OneOf([x[0] for x in ChangesetStatus.STATUSES])) |
|
69 | 69 | renderer_type = colander.SchemaNode(colander.String()) |
|
70 | 70 | |
|
71 | 71 | resolves_comment_id = colander.SchemaNode(colander.Integer(), missing=None) |
|
72 | 72 | |
|
73 | 73 | user = colander.SchemaNode(types.StrOrIntType()) |
|
74 | 74 | repo = colander.SchemaNode(types.StrOrIntType()) |
@@ -1,540 +1,600 b'' | |||
|
1 | 1 | |
|
2 | 2 | |
|
3 | 3 | //BUTTONS |
|
4 | 4 | button, |
|
5 | 5 | .btn, |
|
6 | 6 | input[type="button"] { |
|
7 | 7 | -webkit-appearance: none; |
|
8 | 8 | display: inline-block; |
|
9 | 9 | margin: 0 @padding/3 0 0; |
|
10 | 10 | padding: @button-padding; |
|
11 | 11 | text-align: center; |
|
12 | 12 | font-size: @basefontsize; |
|
13 | 13 | line-height: 1em; |
|
14 | 14 | font-family: @text-light; |
|
15 | 15 | text-decoration: none; |
|
16 | 16 | text-shadow: none; |
|
17 | 17 | color: @grey2; |
|
18 | 18 | background-color: white; |
|
19 | 19 | background-image: none; |
|
20 | 20 | border: none; |
|
21 | 21 | .border ( @border-thickness-buttons, @grey5 ); |
|
22 | 22 | .border-radius (@border-radius); |
|
23 | 23 | cursor: pointer; |
|
24 | 24 | white-space: nowrap; |
|
25 | 25 | -webkit-transition: background .3s,color .3s; |
|
26 | 26 | -moz-transition: background .3s,color .3s; |
|
27 | 27 | -o-transition: background .3s,color .3s; |
|
28 | 28 | transition: background .3s,color .3s; |
|
29 | 29 | box-shadow: @button-shadow; |
|
30 | 30 | -webkit-box-shadow: @button-shadow; |
|
31 | 31 | |
|
32 | 32 | |
|
33 | 33 | |
|
34 | 34 | a { |
|
35 | 35 | display: block; |
|
36 | 36 | margin: 0; |
|
37 | 37 | padding: 0; |
|
38 | 38 | color: inherit; |
|
39 | 39 | text-decoration: none; |
|
40 | 40 | |
|
41 | 41 | &:hover { |
|
42 | 42 | text-decoration: none; |
|
43 | 43 | } |
|
44 | 44 | } |
|
45 | 45 | |
|
46 | 46 | &:focus, |
|
47 | 47 | &:active { |
|
48 | 48 | outline:none; |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | &:hover { |
|
52 | 52 | color: @rcdarkblue; |
|
53 | 53 | background-color: @grey6; |
|
54 | 54 | |
|
55 | 55 | } |
|
56 | 56 | |
|
57 | 57 | &.btn-active { |
|
58 | 58 | color: @rcdarkblue; |
|
59 | 59 | background-color: @grey6; |
|
60 | 60 | } |
|
61 | 61 | |
|
62 | 62 | .icon-remove { |
|
63 | 63 | display: none; |
|
64 | 64 | } |
|
65 | 65 | |
|
66 | 66 | //disabled buttons |
|
67 | 67 | //last; overrides any other styles |
|
68 | 68 | &:disabled { |
|
69 | 69 | opacity: .7; |
|
70 | 70 | cursor: auto; |
|
71 | 71 | background-color: white; |
|
72 | 72 | color: @grey4; |
|
73 | 73 | text-shadow: none; |
|
74 | 74 | } |
|
75 | 75 | |
|
76 | 76 | &.no-margin { |
|
77 | 77 | margin: 0 0 0 0; |
|
78 | 78 | } |
|
79 | 79 | |
|
80 | 80 | |
|
81 | 81 | |
|
82 | 82 | } |
|
83 | 83 | |
|
84 | 84 | |
|
85 | 85 | .btn-default { |
|
86 | 86 | border: @border-thickness solid @grey5; |
|
87 | 87 | background-image: none; |
|
88 | 88 | color: @grey2; |
|
89 | 89 | |
|
90 | 90 | a { |
|
91 | 91 | color: @grey2; |
|
92 | 92 | } |
|
93 | 93 | |
|
94 | 94 | &:hover, |
|
95 | 95 | &.active { |
|
96 | 96 | color: @rcdarkblue; |
|
97 | 97 | background-color: @white; |
|
98 | 98 | .border ( @border-thickness, @grey4 ); |
|
99 | 99 | |
|
100 | 100 | a { |
|
101 | 101 | color: @grey2; |
|
102 | 102 | } |
|
103 | 103 | } |
|
104 | 104 | &:disabled { |
|
105 | 105 | .border ( @border-thickness-buttons, @grey5 ); |
|
106 | 106 | background-color: transparent; |
|
107 | 107 | } |
|
108 | 108 | &.btn-active { |
|
109 | 109 | color: @rcdarkblue; |
|
110 | 110 | background-color: @white; |
|
111 | 111 | .border ( @border-thickness, @rcdarkblue ); |
|
112 | 112 | } |
|
113 | 113 | } |
|
114 | 114 | |
|
115 | 115 | .btn-primary, |
|
116 | 116 | .btn-small, /* TODO: anderson: remove .btn-small to not mix with the new btn-sm */ |
|
117 | 117 | .btn-success { |
|
118 | 118 | .border ( @border-thickness, @rcblue ); |
|
119 | 119 | background-color: @rcblue; |
|
120 | 120 | color: white; |
|
121 | 121 | |
|
122 | 122 | a { |
|
123 | 123 | color: white; |
|
124 | 124 | } |
|
125 | 125 | |
|
126 | 126 | &:hover, |
|
127 | 127 | &.active { |
|
128 | 128 | .border ( @border-thickness, @rcdarkblue ); |
|
129 | 129 | color: white; |
|
130 | 130 | background-color: @rcdarkblue; |
|
131 | 131 | |
|
132 | 132 | a { |
|
133 | 133 | color: white; |
|
134 | 134 | } |
|
135 | 135 | } |
|
136 | 136 | &:disabled { |
|
137 | 137 | background-color: @rcblue; |
|
138 | 138 | } |
|
139 | 139 | } |
|
140 | 140 | |
|
141 | 141 | .btn-secondary { |
|
142 | 142 | &:extend(.btn-default); |
|
143 | 143 | |
|
144 | 144 | background-color: white; |
|
145 | 145 | |
|
146 | 146 | &:focus { |
|
147 | 147 | outline: 0; |
|
148 | 148 | } |
|
149 | 149 | |
|
150 | 150 | &:hover { |
|
151 | 151 | &:extend(.btn-default:hover); |
|
152 | 152 | } |
|
153 | 153 | |
|
154 | 154 | &.btn-link { |
|
155 | 155 | &:extend(.btn-link); |
|
156 | 156 | color: @rcblue; |
|
157 | 157 | } |
|
158 | 158 | |
|
159 | 159 | &:disabled { |
|
160 | 160 | color: @rcblue; |
|
161 | 161 | background-color: white; |
|
162 | 162 | } |
|
163 | 163 | } |
|
164 | 164 | |
|
165 | .btn-warning, | |
|
166 | 165 | .btn-danger, |
|
167 | 166 | .revoke_perm, |
|
168 | 167 | .btn-x, |
|
169 | 168 | .form .action_button.btn-x { |
|
170 | 169 | .border ( @border-thickness, @alert2 ); |
|
171 | 170 | background-color: white; |
|
172 | 171 | color: @alert2; |
|
173 | 172 | |
|
174 | 173 | a { |
|
175 | 174 | color: @alert2; |
|
176 | 175 | } |
|
177 | 176 | |
|
178 | 177 | &:hover, |
|
179 | 178 | &.active { |
|
180 | 179 | .border ( @border-thickness, @alert2 ); |
|
181 | 180 | color: white; |
|
182 | 181 | background-color: @alert2; |
|
183 | 182 | |
|
184 | 183 | a { |
|
185 | 184 | color: white; |
|
186 | 185 | } |
|
187 | 186 | } |
|
188 | 187 | |
|
189 | 188 | i { |
|
190 | 189 | display:none; |
|
191 | 190 | } |
|
192 | 191 | |
|
193 | 192 | &:disabled { |
|
194 | 193 | background-color: white; |
|
195 | 194 | color: @alert2; |
|
196 | 195 | } |
|
197 | 196 | } |
|
198 | 197 | |
|
198 | .btn-warning { | |
|
199 | .border ( @border-thickness, @alert3 ); | |
|
200 | background-color: white; | |
|
201 | color: @alert3; | |
|
202 | ||
|
203 | a { | |
|
204 | color: @alert3; | |
|
205 | } | |
|
206 | ||
|
207 | &:hover, | |
|
208 | &.active { | |
|
209 | .border ( @border-thickness, @alert3 ); | |
|
210 | color: white; | |
|
211 | background-color: @alert3; | |
|
212 | ||
|
213 | a { | |
|
214 | color: white; | |
|
215 | } | |
|
216 | } | |
|
217 | ||
|
218 | i { | |
|
219 | display:none; | |
|
220 | } | |
|
221 | ||
|
222 | &:disabled { | |
|
223 | background-color: white; | |
|
224 | color: @alert3; | |
|
225 | } | |
|
226 | } | |
|
227 | ||
|
199 | 228 | .btn-approved-status { |
|
200 | 229 | .border ( @border-thickness, @alert1 ); |
|
201 | 230 | background-color: white; |
|
202 | 231 | color: @alert1; |
|
203 | 232 | |
|
204 | 233 | } |
|
205 | 234 | |
|
206 | 235 | .btn-rejected-status { |
|
207 | 236 | .border ( @border-thickness, @alert2 ); |
|
208 | 237 | background-color: white; |
|
209 | 238 | color: @alert2; |
|
210 | 239 | } |
|
211 | 240 | |
|
212 | 241 | .btn-sm, |
|
213 | 242 | .btn-mini, |
|
214 | 243 | .field-sm .btn { |
|
215 | 244 | padding: @padding/3; |
|
216 | 245 | } |
|
217 | 246 | |
|
218 | 247 | .btn-xs { |
|
219 | 248 | padding: @padding/4; |
|
220 | 249 | } |
|
221 | 250 | |
|
222 | 251 | .btn-lg { |
|
223 | 252 | padding: @padding * 1.2; |
|
224 | 253 | } |
|
225 | 254 | |
|
226 | 255 | .btn-group { |
|
227 | 256 | display: inline-block; |
|
228 | 257 | .btn { |
|
229 | 258 | float: left; |
|
230 | 259 | margin: 0 0 0 0; |
|
231 | 260 | // first item |
|
232 | 261 | &:first-of-type:not(:last-of-type) { |
|
233 | 262 | border-radius: @border-radius 0 0 @border-radius; |
|
234 | 263 | |
|
235 | 264 | } |
|
236 | 265 | // middle elements |
|
237 | 266 | &:not(:first-of-type):not(:last-of-type) { |
|
238 | 267 | border-radius: 0; |
|
239 | 268 | border-left-width: 0; |
|
240 | 269 | border-right-width: 0; |
|
241 | 270 | } |
|
242 | 271 | // last item |
|
243 | 272 | &:last-of-type:not(:first-of-type) { |
|
244 | 273 | border-radius: 0 @border-radius @border-radius 0; |
|
245 | 274 | } |
|
246 | 275 | |
|
247 | 276 | &:only-child { |
|
248 | 277 | border-radius: @border-radius; |
|
249 | 278 | } |
|
250 | 279 | } |
|
251 | 280 | |
|
252 | 281 | } |
|
253 | 282 | |
|
254 | 283 | |
|
255 | 284 | .btn-group-actions { |
|
256 | 285 | position: relative; |
|
257 | 286 | z-index: 50; |
|
258 | 287 | |
|
259 | 288 | &:not(.open) .btn-action-switcher-container { |
|
260 | 289 | display: none; |
|
261 | 290 | } |
|
262 | 291 | |
|
263 | 292 | .btn-more-option { |
|
264 | 293 | margin-left: -1px; |
|
265 | 294 | padding-left: 2px; |
|
266 | 295 | padding-right: 2px; |
|
267 | 296 | } |
|
268 | 297 | } |
|
269 | 298 | |
|
270 | 299 | |
|
271 | 300 | .btn-action-switcher-container { |
|
272 | 301 | position: absolute; |
|
273 | 302 | top: 100%; |
|
274 | 303 | |
|
275 | 304 | &.left-align { |
|
276 | 305 | left: 0; |
|
277 | 306 | } |
|
278 | 307 | &.right-align { |
|
279 | 308 | right: 0; |
|
280 | 309 | } |
|
281 | 310 | |
|
282 | 311 | } |
|
283 | 312 | |
|
284 | 313 | .btn-action-switcher { |
|
285 | 314 | display: block; |
|
286 | 315 | position: relative; |
|
287 | 316 | z-index: 300; |
|
288 | 317 | max-width: 600px; |
|
289 | 318 | margin-top: 4px; |
|
290 | 319 | margin-bottom: 24px; |
|
291 | 320 | font-size: 14px; |
|
292 | 321 | font-weight: 400; |
|
293 | 322 | padding: 8px 0; |
|
294 | 323 | background-color: #fff; |
|
295 | 324 | border: 1px solid @grey4; |
|
296 | 325 | border-radius: 3px; |
|
297 | 326 | box-shadow: @dropdown-shadow; |
|
298 | 327 | overflow: auto; |
|
299 | 328 | |
|
300 | 329 | li { |
|
301 | 330 | display: block; |
|
302 | 331 | text-align: left; |
|
303 | 332 | list-style: none; |
|
304 | 333 | padding: 5px 10px; |
|
305 | 334 | } |
|
306 | 335 | |
|
307 | 336 | li .action-help-block { |
|
308 | 337 | font-size: 10px; |
|
309 | 338 | line-height: normal; |
|
310 | 339 | color: @grey4; |
|
311 | 340 | } |
|
312 | 341 | |
|
313 | 342 | } |
|
314 | 343 | |
|
315 | 344 | .btn-link { |
|
316 | 345 | background: transparent; |
|
317 | 346 | border: none; |
|
318 | 347 | padding: 0; |
|
319 | 348 | color: @rcblue; |
|
320 | 349 | |
|
321 | 350 | &:hover { |
|
322 | 351 | background: transparent; |
|
323 | 352 | border: none; |
|
324 | 353 | color: @rcdarkblue; |
|
325 | 354 | } |
|
326 | 355 | |
|
327 | 356 | //disabled buttons |
|
328 | 357 | //last; overrides any other styles |
|
329 | 358 | &:disabled { |
|
330 | 359 | opacity: .7; |
|
331 | 360 | cursor: auto; |
|
332 | 361 | background-color: white; |
|
333 | 362 | color: @grey4; |
|
334 | 363 | text-shadow: none; |
|
335 | 364 | } |
|
336 | 365 | |
|
337 | 366 | // TODO: johbo: Check if we can avoid this, indicates that the structure |
|
338 | 367 | // is not yet good. |
|
339 | 368 | // lisa: The button CSS reflects the button HTML; both need a cleanup. |
|
340 | 369 | &.btn-danger { |
|
341 | 370 | color: @alert2; |
|
342 | 371 | |
|
343 | 372 | &:hover { |
|
344 | 373 | color: darken(@alert2,30%); |
|
345 | 374 | } |
|
346 | 375 | |
|
347 | 376 | &:disabled { |
|
348 | 377 | color: @alert2; |
|
349 | 378 | } |
|
350 | 379 | } |
|
351 | 380 | } |
|
352 | 381 | |
|
353 | 382 | .btn-social { |
|
354 | 383 | &:extend(.btn-default); |
|
355 | 384 | margin: 5px 5px 5px 0px; |
|
356 | 385 | min-width: 160px; |
|
357 | 386 | } |
|
358 | 387 | |
|
359 | 388 | // TODO: johbo: check these exceptions |
|
360 | 389 | |
|
361 | 390 | .links { |
|
362 | 391 | |
|
363 | 392 | .btn + .btn { |
|
364 | 393 | margin-top: @padding; |
|
365 | 394 | } |
|
366 | 395 | } |
|
367 | 396 | |
|
368 | 397 | |
|
369 | 398 | .action_button { |
|
370 | 399 | display:inline; |
|
371 | 400 | margin: 0; |
|
372 | 401 | padding: 0 1em 0 0; |
|
373 | 402 | font-size: inherit; |
|
374 | 403 | color: @rcblue; |
|
375 | 404 | border: none; |
|
376 | 405 | border-radius: 0; |
|
377 | 406 | background-color: transparent; |
|
378 | 407 | |
|
379 | 408 | &.last-item { |
|
380 | 409 | border: none; |
|
381 | 410 | padding: 0 0 0 0; |
|
382 | 411 | } |
|
383 | 412 | |
|
384 | 413 | &:last-child { |
|
385 | 414 | border: none; |
|
386 | 415 | padding: 0 0 0 0; |
|
387 | 416 | } |
|
388 | 417 | |
|
389 | 418 | &:hover { |
|
390 | 419 | color: @rcdarkblue; |
|
391 | 420 | background-color: transparent; |
|
392 | 421 | border: none; |
|
393 | 422 | } |
|
394 | 423 | .noselect |
|
395 | 424 | } |
|
396 | 425 | |
|
397 | 426 | .grid_delete { |
|
398 | 427 | .action_button { |
|
399 | 428 | border: none; |
|
400 | 429 | } |
|
401 | 430 | } |
|
402 | 431 | |
|
403 | 432 | |
|
433 | input[type="submit"].btn-warning { | |
|
434 | &:extend(.btn-warning); | |
|
435 | ||
|
436 | &:focus { | |
|
437 | outline: 0; | |
|
438 | } | |
|
439 | ||
|
440 | &:hover { | |
|
441 | &:extend(.btn-warning:hover); | |
|
442 | } | |
|
443 | ||
|
444 | &.btn-link { | |
|
445 | &:extend(.btn-link); | |
|
446 | color: @alert3; | |
|
447 | ||
|
448 | &:disabled { | |
|
449 | color: @alert3; | |
|
450 | background-color: transparent; | |
|
451 | } | |
|
452 | } | |
|
453 | ||
|
454 | &:disabled { | |
|
455 | .border ( @border-thickness-buttons, @alert3 ); | |
|
456 | background-color: white; | |
|
457 | color: @alert3; | |
|
458 | opacity: 0.5; | |
|
459 | } | |
|
460 | } | |
|
461 | ||
|
462 | ||
|
463 | ||
|
404 | 464 | // TODO: johbo: Form button tweaks, check if we can use the classes instead |
|
405 | 465 | input[type="submit"] { |
|
406 | 466 | &:extend(.btn-primary); |
|
407 | 467 | |
|
408 | 468 | &:focus { |
|
409 | 469 | outline: 0; |
|
410 | 470 | } |
|
411 | 471 | |
|
412 | 472 | &:hover { |
|
413 | 473 | &:extend(.btn-primary:hover); |
|
414 | 474 | } |
|
415 | 475 | |
|
416 | 476 | &.btn-link { |
|
417 | 477 | &:extend(.btn-link); |
|
418 | 478 | color: @rcblue; |
|
419 | 479 | |
|
420 | 480 | &:disabled { |
|
421 | 481 | color: @rcblue; |
|
422 | 482 | background-color: transparent; |
|
423 | 483 | } |
|
424 | 484 | } |
|
425 | 485 | |
|
426 | 486 | &:disabled { |
|
427 | 487 | .border ( @border-thickness-buttons, @rcblue ); |
|
428 | 488 | background-color: @rcblue; |
|
429 | 489 | color: white; |
|
430 | 490 | opacity: 0.5; |
|
431 | 491 | } |
|
432 | 492 | } |
|
433 | 493 | |
|
434 | 494 | input[type="reset"] { |
|
435 | 495 | &:extend(.btn-default); |
|
436 | 496 | |
|
437 | 497 | // TODO: johbo: Check if this tweak can be avoided. |
|
438 | 498 | background: transparent; |
|
439 | 499 | |
|
440 | 500 | &:focus { |
|
441 | 501 | outline: 0; |
|
442 | 502 | } |
|
443 | 503 | |
|
444 | 504 | &:hover { |
|
445 | 505 | &:extend(.btn-default:hover); |
|
446 | 506 | } |
|
447 | 507 | |
|
448 | 508 | &.btn-link { |
|
449 | 509 | &:extend(.btn-link); |
|
450 | 510 | color: @rcblue; |
|
451 | 511 | |
|
452 | 512 | &:disabled { |
|
453 | 513 | border: none; |
|
454 | 514 | } |
|
455 | 515 | } |
|
456 | 516 | |
|
457 | 517 | &:disabled { |
|
458 | 518 | .border ( @border-thickness-buttons, @rcblue ); |
|
459 | 519 | background-color: white; |
|
460 | 520 | color: @rcblue; |
|
461 | 521 | } |
|
462 | 522 | } |
|
463 | 523 | |
|
464 | 524 | input[type="submit"], |
|
465 | 525 | input[type="reset"] { |
|
466 | 526 | &.btn-danger { |
|
467 | 527 | &:extend(.btn-danger); |
|
468 | 528 | |
|
469 | 529 | &:focus { |
|
470 | 530 | outline: 0; |
|
471 | 531 | } |
|
472 | 532 | |
|
473 | 533 | &:hover { |
|
474 | 534 | &:extend(.btn-danger:hover); |
|
475 | 535 | } |
|
476 | 536 | |
|
477 | 537 | &.btn-link { |
|
478 | 538 | &:extend(.btn-link); |
|
479 | 539 | color: @alert2; |
|
480 | 540 | |
|
481 | 541 | &:hover { |
|
482 | 542 | color: darken(@alert2,30%); |
|
483 | 543 | } |
|
484 | 544 | } |
|
485 | 545 | |
|
486 | 546 | &:disabled { |
|
487 | 547 | color: @alert2; |
|
488 | 548 | background-color: white; |
|
489 | 549 | } |
|
490 | 550 | } |
|
491 | 551 | &.btn-danger-action { |
|
492 | 552 | .border ( @border-thickness, @alert2 ); |
|
493 | 553 | background-color: @alert2; |
|
494 | 554 | color: white; |
|
495 | 555 | |
|
496 | 556 | a { |
|
497 | 557 | color: white; |
|
498 | 558 | } |
|
499 | 559 | |
|
500 | 560 | &:hover { |
|
501 | 561 | background-color: darken(@alert2,20%); |
|
502 | 562 | } |
|
503 | 563 | |
|
504 | 564 | &.active { |
|
505 | 565 | .border ( @border-thickness, @alert2 ); |
|
506 | 566 | color: white; |
|
507 | 567 | background-color: @alert2; |
|
508 | 568 | |
|
509 | 569 | a { |
|
510 | 570 | color: white; |
|
511 | 571 | } |
|
512 | 572 | } |
|
513 | 573 | |
|
514 | 574 | &:disabled { |
|
515 | 575 | background-color: white; |
|
516 | 576 | color: @alert2; |
|
517 | 577 | } |
|
518 | 578 | } |
|
519 | 579 | } |
|
520 | 580 | |
|
521 | 581 | |
|
522 | 582 | .button-links { |
|
523 | 583 | float: left; |
|
524 | 584 | display: inline; |
|
525 | 585 | margin: 0; |
|
526 | 586 | padding-left: 0; |
|
527 | 587 | list-style: none; |
|
528 | 588 | text-align: right; |
|
529 | 589 | |
|
530 | 590 | li { |
|
531 | 591 | |
|
532 | 592 | |
|
533 | 593 | } |
|
534 | 594 | |
|
535 | 595 | li.active { |
|
536 | 596 | background-color: @grey6; |
|
537 | 597 | .border ( @border-thickness, @grey4 ); |
|
538 | 598 | } |
|
539 | 599 | |
|
540 | 600 | } |
@@ -1,635 +1,642 b'' | |||
|
1 | 1 | // comments.less |
|
2 | 2 | // For use in RhodeCode applications; |
|
3 | 3 | // see style guide documentation for guidelines. |
|
4 | 4 | |
|
5 | 5 | |
|
6 | 6 | // Comments |
|
7 | 7 | @comment-outdated-opacity: 0.6; |
|
8 | 8 | |
|
9 | 9 | .comments { |
|
10 | 10 | width: 100%; |
|
11 | 11 | } |
|
12 | 12 | |
|
13 | 13 | .comments-heading { |
|
14 | 14 | margin-bottom: -1px; |
|
15 | 15 | background: @grey6; |
|
16 | 16 | display: block; |
|
17 | 17 | padding: 10px 0px; |
|
18 | 18 | font-size: 18px |
|
19 | 19 | } |
|
20 | 20 | |
|
21 | 21 | #comment-tr-show { |
|
22 | 22 | padding: 5px 0; |
|
23 | 23 | } |
|
24 | 24 | |
|
25 | 25 | tr.inline-comments div { |
|
26 | 26 | max-width: 100%; |
|
27 | 27 | |
|
28 | 28 | p { |
|
29 | 29 | white-space: normal; |
|
30 | 30 | } |
|
31 | 31 | |
|
32 | 32 | code, pre, .code, dd { |
|
33 | 33 | overflow-x: auto; |
|
34 | 34 | width: 1062px; |
|
35 | 35 | } |
|
36 | 36 | |
|
37 | 37 | dd { |
|
38 | 38 | width: auto; |
|
39 | 39 | } |
|
40 | 40 | } |
|
41 | 41 | |
|
42 | 42 | #injected_page_comments { |
|
43 | 43 | .comment-previous-link, |
|
44 | 44 | .comment-next-link, |
|
45 | 45 | .comment-links-divider { |
|
46 | 46 | display: none; |
|
47 | 47 | } |
|
48 | 48 | } |
|
49 | 49 | |
|
50 | 50 | .add-comment { |
|
51 | 51 | margin-bottom: 10px; |
|
52 | 52 | } |
|
53 | 53 | .hide-comment-button .add-comment { |
|
54 | 54 | display: none; |
|
55 | 55 | } |
|
56 | 56 | |
|
57 | 57 | .comment-bubble { |
|
58 | 58 | color: @grey4; |
|
59 | 59 | margin-top: 4px; |
|
60 | 60 | margin-right: 30px; |
|
61 | 61 | visibility: hidden; |
|
62 | 62 | } |
|
63 | 63 | |
|
64 | .comment-draft { | |
|
65 | float: left; | |
|
66 | margin-right: 10px; | |
|
67 | font-weight: 600; | |
|
68 | color: @alert3; | |
|
69 | } | |
|
70 | ||
|
64 | 71 | .comment-label { |
|
65 | 72 | float: left; |
|
66 | 73 | |
|
67 | 74 | padding: 0.4em 0.4em; |
|
68 | 75 | margin: 2px 4px 0px 0px; |
|
69 | 76 | display: inline-block; |
|
70 | 77 | min-height: 0; |
|
71 | 78 | |
|
72 | 79 | text-align: center; |
|
73 | 80 | font-size: 10px; |
|
74 | 81 | line-height: .8em; |
|
75 | 82 | |
|
76 | 83 | font-family: @text-italic; |
|
77 | 84 | font-style: italic; |
|
78 | 85 | background: #fff none; |
|
79 | 86 | color: @grey3; |
|
80 | 87 | border: 1px solid @grey4; |
|
81 | 88 | white-space: nowrap; |
|
82 | 89 | |
|
83 | 90 | text-transform: uppercase; |
|
84 | 91 | min-width: 50px; |
|
85 | 92 | border-radius: 4px; |
|
86 | 93 | |
|
87 | 94 | &.todo { |
|
88 | 95 | color: @color5; |
|
89 | 96 | font-style: italic; |
|
90 | 97 | font-weight: @text-bold-italic-weight; |
|
91 | 98 | font-family: @text-bold-italic; |
|
92 | 99 | } |
|
93 | 100 | |
|
94 | 101 | .resolve { |
|
95 | 102 | cursor: pointer; |
|
96 | 103 | text-decoration: underline; |
|
97 | 104 | } |
|
98 | 105 | |
|
99 | 106 | .resolved { |
|
100 | 107 | text-decoration: line-through; |
|
101 | 108 | color: @color1; |
|
102 | 109 | } |
|
103 | 110 | .resolved a { |
|
104 | 111 | text-decoration: line-through; |
|
105 | 112 | color: @color1; |
|
106 | 113 | } |
|
107 | 114 | .resolve-text { |
|
108 | 115 | color: @color1; |
|
109 | 116 | margin: 2px 8px; |
|
110 | 117 | font-family: @text-italic; |
|
111 | 118 | font-style: italic; |
|
112 | 119 | } |
|
113 | 120 | } |
|
114 | 121 | |
|
115 | 122 | .has-spacer-after { |
|
116 | 123 | &:after { |
|
117 | 124 | content: ' | '; |
|
118 | 125 | color: @grey5; |
|
119 | 126 | } |
|
120 | 127 | } |
|
121 | 128 | |
|
122 | 129 | .has-spacer-before { |
|
123 | 130 | &:before { |
|
124 | 131 | content: ' | '; |
|
125 | 132 | color: @grey5; |
|
126 | 133 | } |
|
127 | 134 | } |
|
128 | 135 | |
|
129 | 136 | .comment { |
|
130 | 137 | |
|
131 | 138 | &.comment-general { |
|
132 | 139 | border: 1px solid @grey5; |
|
133 | 140 | padding: 5px 5px 5px 5px; |
|
134 | 141 | } |
|
135 | 142 | |
|
136 | 143 | margin: @padding 0; |
|
137 | 144 | padding: 4px 0 0 0; |
|
138 | 145 | line-height: 1em; |
|
139 | 146 | |
|
140 | 147 | .rc-user { |
|
141 | 148 | min-width: 0; |
|
142 | 149 | margin: 0px .5em 0 0; |
|
143 | 150 | |
|
144 | 151 | .user { |
|
145 | 152 | display: inline; |
|
146 | 153 | } |
|
147 | 154 | } |
|
148 | 155 | |
|
149 | 156 | .meta { |
|
150 | 157 | position: relative; |
|
151 | 158 | width: 100%; |
|
152 | 159 | border-bottom: 1px solid @grey5; |
|
153 | 160 | margin: -5px 0px; |
|
154 | 161 | line-height: 24px; |
|
155 | 162 | |
|
156 | 163 | &:hover .permalink { |
|
157 | 164 | visibility: visible; |
|
158 | 165 | color: @rcblue; |
|
159 | 166 | } |
|
160 | 167 | } |
|
161 | 168 | |
|
162 | 169 | .author, |
|
163 | 170 | .date { |
|
164 | 171 | display: inline; |
|
165 | 172 | |
|
166 | 173 | &:after { |
|
167 | 174 | content: ' | '; |
|
168 | 175 | color: @grey5; |
|
169 | 176 | } |
|
170 | 177 | } |
|
171 | 178 | |
|
172 | 179 | .author-general img { |
|
173 | 180 | top: 3px; |
|
174 | 181 | } |
|
175 | 182 | .author-inline img { |
|
176 | 183 | top: 3px; |
|
177 | 184 | } |
|
178 | 185 | |
|
179 | 186 | .status-change, |
|
180 | 187 | .permalink, |
|
181 | 188 | .changeset-status-lbl { |
|
182 | 189 | display: inline; |
|
183 | 190 | } |
|
184 | 191 | |
|
185 | 192 | .permalink { |
|
186 | 193 | visibility: hidden; |
|
187 | 194 | } |
|
188 | 195 | |
|
189 | 196 | .comment-links-divider { |
|
190 | 197 | display: inline; |
|
191 | 198 | } |
|
192 | 199 | |
|
193 | 200 | .comment-links-block { |
|
194 | 201 | float:right; |
|
195 | 202 | text-align: right; |
|
196 | 203 | min-width: 85px; |
|
197 | 204 | |
|
198 | 205 | [class^="icon-"]:before, |
|
199 | 206 | [class*=" icon-"]:before { |
|
200 | 207 | margin-left: 0; |
|
201 | 208 | margin-right: 0; |
|
202 | 209 | } |
|
203 | 210 | } |
|
204 | 211 | |
|
205 | 212 | .comment-previous-link { |
|
206 | 213 | display: inline-block; |
|
207 | 214 | |
|
208 | 215 | .arrow_comment_link{ |
|
209 | 216 | cursor: pointer; |
|
210 | 217 | i { |
|
211 | 218 | font-size:10px; |
|
212 | 219 | } |
|
213 | 220 | } |
|
214 | 221 | .arrow_comment_link.disabled { |
|
215 | 222 | cursor: default; |
|
216 | 223 | color: @grey5; |
|
217 | 224 | } |
|
218 | 225 | } |
|
219 | 226 | |
|
220 | 227 | .comment-next-link { |
|
221 | 228 | display: inline-block; |
|
222 | 229 | |
|
223 | 230 | .arrow_comment_link{ |
|
224 | 231 | cursor: pointer; |
|
225 | 232 | i { |
|
226 | 233 | font-size:10px; |
|
227 | 234 | } |
|
228 | 235 | } |
|
229 | 236 | .arrow_comment_link.disabled { |
|
230 | 237 | cursor: default; |
|
231 | 238 | color: @grey5; |
|
232 | 239 | } |
|
233 | 240 | } |
|
234 | 241 | |
|
235 | 242 | .delete-comment { |
|
236 | 243 | display: inline-block; |
|
237 | 244 | color: @rcblue; |
|
238 | 245 | |
|
239 | 246 | &:hover { |
|
240 | 247 | cursor: pointer; |
|
241 | 248 | } |
|
242 | 249 | } |
|
243 | 250 | |
|
244 | 251 | .text { |
|
245 | 252 | clear: both; |
|
246 | 253 | .border-radius(@border-radius); |
|
247 | 254 | .box-sizing(border-box); |
|
248 | 255 | |
|
249 | 256 | .markdown-block p, |
|
250 | 257 | .rst-block p { |
|
251 | 258 | margin: .5em 0 !important; |
|
252 | 259 | // TODO: lisa: This is needed because of other rst !important rules :[ |
|
253 | 260 | } |
|
254 | 261 | } |
|
255 | 262 | |
|
256 | 263 | .pr-version { |
|
257 | 264 | display: inline-block; |
|
258 | 265 | } |
|
259 | 266 | .pr-version-inline { |
|
260 | 267 | display: inline-block; |
|
261 | 268 | } |
|
262 | 269 | .pr-version-num { |
|
263 | 270 | font-size: 10px; |
|
264 | 271 | } |
|
265 | 272 | } |
|
266 | 273 | |
|
267 | 274 | @comment-padding: 5px; |
|
268 | 275 | |
|
269 | 276 | .general-comments { |
|
270 | 277 | .comment-outdated { |
|
271 | 278 | opacity: @comment-outdated-opacity; |
|
272 | 279 | } |
|
273 | 280 | } |
|
274 | 281 | |
|
275 | 282 | .inline-comments { |
|
276 | 283 | border-radius: @border-radius; |
|
277 | 284 | .comment { |
|
278 | 285 | margin: 0; |
|
279 | 286 | border-radius: @border-radius; |
|
280 | 287 | } |
|
281 | 288 | .comment-outdated { |
|
282 | 289 | opacity: @comment-outdated-opacity; |
|
283 | 290 | } |
|
284 | 291 | |
|
285 | 292 | .comment-inline { |
|
286 | 293 | background: white; |
|
287 | 294 | padding: @comment-padding @comment-padding; |
|
288 | 295 | border: @comment-padding solid @grey6; |
|
289 | 296 | |
|
290 | 297 | .text { |
|
291 | 298 | border: none; |
|
292 | 299 | } |
|
293 | 300 | .meta { |
|
294 | 301 | border-bottom: 1px solid @grey6; |
|
295 | 302 | margin: -5px 0px; |
|
296 | 303 | line-height: 24px; |
|
297 | 304 | } |
|
298 | 305 | } |
|
299 | 306 | .comment-selected { |
|
300 | 307 | border-left: 6px solid @comment-highlight-color; |
|
301 | 308 | } |
|
302 | 309 | .comment-inline-form { |
|
303 | 310 | padding: @comment-padding; |
|
304 | 311 | display: none; |
|
305 | 312 | } |
|
306 | 313 | .cb-comment-add-button { |
|
307 | 314 | margin: @comment-padding; |
|
308 | 315 | } |
|
309 | 316 | /* hide add comment button when form is open */ |
|
310 | 317 | .comment-inline-form-open ~ .cb-comment-add-button { |
|
311 | 318 | display: none; |
|
312 | 319 | } |
|
313 | 320 | .comment-inline-form-open { |
|
314 | 321 | display: block; |
|
315 | 322 | } |
|
316 | 323 | /* hide add comment button when form but no comments */ |
|
317 | 324 | .comment-inline-form:first-child + .cb-comment-add-button { |
|
318 | 325 | display: none; |
|
319 | 326 | } |
|
320 | 327 | /* hide add comment button when no comments or form */ |
|
321 | 328 | .cb-comment-add-button:first-child { |
|
322 | 329 | display: none; |
|
323 | 330 | } |
|
324 | 331 | /* hide add comment button when only comment is being deleted */ |
|
325 | 332 | .comment-deleting:first-child + .cb-comment-add-button { |
|
326 | 333 | display: none; |
|
327 | 334 | } |
|
328 | 335 | } |
|
329 | 336 | |
|
330 | 337 | |
|
331 | 338 | .show-outdated-comments { |
|
332 | 339 | display: inline; |
|
333 | 340 | color: @rcblue; |
|
334 | 341 | } |
|
335 | 342 | |
|
336 | 343 | // Comment Form |
|
337 | 344 | div.comment-form { |
|
338 | 345 | margin-top: 20px; |
|
339 | 346 | } |
|
340 | 347 | |
|
341 | 348 | .comment-form strong { |
|
342 | 349 | display: block; |
|
343 | 350 | margin-bottom: 15px; |
|
344 | 351 | } |
|
345 | 352 | |
|
346 | 353 | .comment-form textarea { |
|
347 | 354 | width: 100%; |
|
348 | 355 | height: 100px; |
|
349 | 356 | font-family: @text-monospace; |
|
350 | 357 | } |
|
351 | 358 | |
|
352 | 359 | form.comment-form { |
|
353 | 360 | margin-top: 10px; |
|
354 | 361 | margin-left: 10px; |
|
355 | 362 | } |
|
356 | 363 | |
|
357 | 364 | .comment-inline-form .comment-block-ta, |
|
358 | 365 | .comment-form .comment-block-ta, |
|
359 | 366 | .comment-form .preview-box { |
|
360 | 367 | .border-radius(@border-radius); |
|
361 | 368 | .box-sizing(border-box); |
|
362 | 369 | background-color: white; |
|
363 | 370 | } |
|
364 | 371 | |
|
365 | 372 | .comment-form-submit { |
|
366 | 373 | margin-top: 5px; |
|
367 | 374 | margin-left: 525px; |
|
368 | 375 | } |
|
369 | 376 | |
|
370 | 377 | .file-comments { |
|
371 | 378 | display: none; |
|
372 | 379 | } |
|
373 | 380 | |
|
374 | 381 | .comment-form .preview-box.unloaded, |
|
375 | 382 | .comment-inline-form .preview-box.unloaded { |
|
376 | 383 | height: 50px; |
|
377 | 384 | text-align: center; |
|
378 | 385 | padding: 20px; |
|
379 | 386 | background-color: white; |
|
380 | 387 | } |
|
381 | 388 | |
|
382 | 389 | .comment-footer { |
|
383 | 390 | position: relative; |
|
384 | 391 | width: 100%; |
|
385 | 392 | min-height: 42px; |
|
386 | 393 | |
|
387 | 394 | .status_box, |
|
388 | 395 | .cancel-button { |
|
389 | 396 | float: left; |
|
390 | 397 | display: inline-block; |
|
391 | 398 | } |
|
392 | 399 | |
|
393 | 400 | .status_box { |
|
394 | 401 | margin-left: 10px; |
|
395 | 402 | } |
|
396 | 403 | |
|
397 | 404 | .action-buttons { |
|
398 | 405 | float: left; |
|
399 | 406 | display: inline-block; |
|
400 | 407 | } |
|
401 | 408 | |
|
402 | 409 | .action-buttons-extra { |
|
403 | 410 | display: inline-block; |
|
404 | 411 | } |
|
405 | 412 | } |
|
406 | 413 | |
|
407 | 414 | .comment-form { |
|
408 | 415 | |
|
409 | 416 | .comment { |
|
410 | 417 | margin-left: 10px; |
|
411 | 418 | } |
|
412 | 419 | |
|
413 | 420 | .comment-help { |
|
414 | 421 | color: @grey4; |
|
415 | 422 | padding: 5px 0 5px 0; |
|
416 | 423 | } |
|
417 | 424 | |
|
418 | 425 | .comment-title { |
|
419 | 426 | padding: 5px 0 5px 0; |
|
420 | 427 | } |
|
421 | 428 | |
|
422 | 429 | .comment-button { |
|
423 | 430 | display: inline-block; |
|
424 | 431 | } |
|
425 | 432 | |
|
426 | 433 | .comment-button-input { |
|
427 | 434 | margin-right: 0; |
|
428 | 435 | } |
|
429 | 436 | |
|
430 | 437 | .comment-footer { |
|
431 | 438 | margin-bottom: 50px; |
|
432 | 439 | margin-top: 10px; |
|
433 | 440 | } |
|
434 | 441 | } |
|
435 | 442 | |
|
436 | 443 | |
|
437 | 444 | .comment-form-login { |
|
438 | 445 | .comment-help { |
|
439 | 446 | padding: 0.7em; //same as the button |
|
440 | 447 | } |
|
441 | 448 | |
|
442 | 449 | div.clearfix { |
|
443 | 450 | clear: both; |
|
444 | 451 | width: 100%; |
|
445 | 452 | display: block; |
|
446 | 453 | } |
|
447 | 454 | } |
|
448 | 455 | |
|
449 | 456 | .comment-version-select { |
|
450 | 457 | margin: 0px; |
|
451 | 458 | border-radius: inherit; |
|
452 | 459 | border-color: @grey6; |
|
453 | 460 | height: 20px; |
|
454 | 461 | } |
|
455 | 462 | |
|
456 | 463 | .comment-type { |
|
457 | 464 | margin: 0px; |
|
458 | 465 | border-radius: inherit; |
|
459 | 466 | border-color: @grey6; |
|
460 | 467 | } |
|
461 | 468 | |
|
462 | 469 | .preview-box { |
|
463 | 470 | min-height: 105px; |
|
464 | 471 | margin-bottom: 15px; |
|
465 | 472 | background-color: white; |
|
466 | 473 | .border-radius(@border-radius); |
|
467 | 474 | .box-sizing(border-box); |
|
468 | 475 | } |
|
469 | 476 | |
|
470 | 477 | .add-another-button { |
|
471 | 478 | margin-left: 10px; |
|
472 | 479 | margin-top: 10px; |
|
473 | 480 | margin-bottom: 10px; |
|
474 | 481 | } |
|
475 | 482 | |
|
476 | 483 | .comment .buttons { |
|
477 | 484 | float: right; |
|
478 | 485 | margin: -1px 0px 0px 0px; |
|
479 | 486 | } |
|
480 | 487 | |
|
481 | 488 | // Inline Comment Form |
|
482 | 489 | .injected_diff .comment-inline-form, |
|
483 | 490 | .comment-inline-form { |
|
484 | 491 | background-color: white; |
|
485 | 492 | margin-top: 10px; |
|
486 | 493 | margin-bottom: 20px; |
|
487 | 494 | } |
|
488 | 495 | |
|
489 | 496 | .inline-form { |
|
490 | 497 | padding: 10px 7px; |
|
491 | 498 | } |
|
492 | 499 | |
|
493 | 500 | .inline-form div { |
|
494 | 501 | max-width: 100%; |
|
495 | 502 | } |
|
496 | 503 | |
|
497 | 504 | .overlay { |
|
498 | 505 | display: none; |
|
499 | 506 | position: absolute; |
|
500 | 507 | width: 100%; |
|
501 | 508 | text-align: center; |
|
502 | 509 | vertical-align: middle; |
|
503 | 510 | font-size: 16px; |
|
504 | 511 | background: none repeat scroll 0 0 white; |
|
505 | 512 | |
|
506 | 513 | &.submitting { |
|
507 | 514 | display: block; |
|
508 | 515 | opacity: 0.5; |
|
509 | 516 | z-index: 100; |
|
510 | 517 | } |
|
511 | 518 | } |
|
512 | 519 | .comment-inline-form .overlay.submitting .overlay-text { |
|
513 | 520 | margin-top: 5%; |
|
514 | 521 | } |
|
515 | 522 | |
|
516 | 523 | .comment-inline-form .clearfix, |
|
517 | 524 | .comment-form .clearfix { |
|
518 | 525 | .border-radius(@border-radius); |
|
519 | 526 | margin: 0px; |
|
520 | 527 | } |
|
521 | 528 | |
|
522 | 529 | .comment-inline-form .comment-footer { |
|
523 | 530 | margin: 10px 0px 0px 0px; |
|
524 | 531 | } |
|
525 | 532 | |
|
526 | 533 | .hide-inline-form-button { |
|
527 | 534 | margin-left: 5px; |
|
528 | 535 | } |
|
529 | 536 | .comment-button .hide-inline-form { |
|
530 | 537 | background: white; |
|
531 | 538 | } |
|
532 | 539 | |
|
533 | 540 | .comment-area { |
|
534 | 541 | padding: 6px 8px; |
|
535 | 542 | border: 1px solid @grey5; |
|
536 | 543 | .border-radius(@border-radius); |
|
537 | 544 | |
|
538 | 545 | .resolve-action { |
|
539 | 546 | padding: 1px 0px 0px 6px; |
|
540 | 547 | } |
|
541 | 548 | |
|
542 | 549 | } |
|
543 | 550 | |
|
544 | 551 | comment-area-text { |
|
545 | 552 | color: @grey3; |
|
546 | 553 | } |
|
547 | 554 | |
|
548 | 555 | .comment-area-header { |
|
549 | 556 | height: 35px; |
|
550 | 557 | } |
|
551 | 558 | |
|
552 | 559 | .comment-area-header .nav-links { |
|
553 | 560 | display: flex; |
|
554 | 561 | flex-flow: row wrap; |
|
555 | 562 | -webkit-flex-flow: row wrap; |
|
556 | 563 | width: 100%; |
|
557 | 564 | } |
|
558 | 565 | |
|
559 | 566 | .comment-area-footer { |
|
560 | 567 | min-height: 30px; |
|
561 | 568 | } |
|
562 | 569 | |
|
563 | 570 | .comment-footer .toolbar { |
|
564 | 571 | |
|
565 | 572 | } |
|
566 | 573 | |
|
567 | 574 | .comment-attachment-uploader { |
|
568 | 575 | border: 1px dashed white; |
|
569 | 576 | border-radius: @border-radius; |
|
570 | 577 | margin-top: -10px; |
|
571 | 578 | line-height: 30px; |
|
572 | 579 | &.dz-drag-hover { |
|
573 | 580 | border-color: @grey3; |
|
574 | 581 | } |
|
575 | 582 | |
|
576 | 583 | .dz-error-message { |
|
577 | 584 | padding-top: 0; |
|
578 | 585 | } |
|
579 | 586 | } |
|
580 | 587 | |
|
581 | 588 | .comment-attachment-text { |
|
582 | 589 | clear: both; |
|
583 | 590 | font-size: 11px; |
|
584 | 591 | color: #8F8F8F; |
|
585 | 592 | width: 100%; |
|
586 | 593 | .pick-attachment { |
|
587 | 594 | color: #8F8F8F; |
|
588 | 595 | } |
|
589 | 596 | .pick-attachment:hover { |
|
590 | 597 | color: @rcblue; |
|
591 | 598 | } |
|
592 | 599 | } |
|
593 | 600 | |
|
594 | 601 | .nav-links { |
|
595 | 602 | padding: 0; |
|
596 | 603 | margin: 0; |
|
597 | 604 | list-style: none; |
|
598 | 605 | height: auto; |
|
599 | 606 | border-bottom: 1px solid @grey5; |
|
600 | 607 | } |
|
601 | 608 | .nav-links li { |
|
602 | 609 | display: inline-block; |
|
603 | 610 | list-style-type: none; |
|
604 | 611 | } |
|
605 | 612 | |
|
606 | 613 | .nav-links li a.disabled { |
|
607 | 614 | cursor: not-allowed; |
|
608 | 615 | } |
|
609 | 616 | |
|
610 | 617 | .nav-links li.active a { |
|
611 | 618 | border-bottom: 2px solid @rcblue; |
|
612 | 619 | color: #000; |
|
613 | 620 | font-weight: 600; |
|
614 | 621 | } |
|
615 | 622 | .nav-links li a { |
|
616 | 623 | display: inline-block; |
|
617 | 624 | padding: 0px 10px 5px 10px; |
|
618 | 625 | margin-bottom: -1px; |
|
619 | 626 | font-size: 14px; |
|
620 | 627 | line-height: 28px; |
|
621 | 628 | color: #8f8f8f; |
|
622 | 629 | border-bottom: 2px solid transparent; |
|
623 | 630 | } |
|
624 | 631 | |
|
625 | 632 | .toolbar-text { |
|
626 | 633 | float: right; |
|
627 | 634 | font-size: 11px; |
|
628 | 635 | color: @grey4; |
|
629 | 636 | text-align: right; |
|
630 | 637 | |
|
631 | 638 | a { |
|
632 | 639 | color: @grey4; |
|
633 | 640 | } |
|
634 | 641 | } |
|
635 | 642 |
@@ -1,843 +1,850 b'' | |||
|
1 | 1 | // # Copyright (C) 2010-2020 RhodeCode GmbH |
|
2 | 2 | // # |
|
3 | 3 | // # This program is free software: you can redistribute it and/or modify |
|
4 | 4 | // # it under the terms of the GNU Affero General Public License, version 3 |
|
5 | 5 | // # (only), as published by the Free Software Foundation. |
|
6 | 6 | // # |
|
7 | 7 | // # This program is distributed in the hope that it will be useful, |
|
8 | 8 | // # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | // # GNU General Public License for more details. |
|
11 | 11 | // # |
|
12 | 12 | // # You should have received a copy of the GNU Affero General Public License |
|
13 | 13 | // # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | // # |
|
15 | 15 | // # This program is dual-licensed. If you wish to learn more about the |
|
16 | 16 | // # RhodeCode Enterprise Edition, including its added features, Support services, |
|
17 | 17 | // # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
18 | 18 | |
|
19 | 19 | /** |
|
20 | 20 | * Code Mirror |
|
21 | 21 | */ |
|
22 | 22 | // global code-mirror logger;, to enable run |
|
23 | 23 | // Logger.get('CodeMirror').setLevel(Logger.DEBUG) |
|
24 | 24 | |
|
25 | 25 | cmLog = Logger.get('CodeMirror'); |
|
26 | 26 | cmLog.setLevel(Logger.OFF); |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | //global cache for inline forms |
|
30 | 30 | var userHintsCache = {}; |
|
31 | 31 | |
|
32 | 32 | // global timer, used to cancel async loading |
|
33 | 33 | var CodeMirrorLoadUserHintTimer; |
|
34 | 34 | |
|
35 | 35 | var escapeRegExChars = function(value) { |
|
36 | 36 | return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); |
|
37 | 37 | }; |
|
38 | 38 | |
|
39 | 39 | /** |
|
40 | 40 | * Load hints from external source returns an array of objects in a format |
|
41 | 41 | * that hinting lib requires |
|
42 | 42 | * @returns {Array} |
|
43 | 43 | */ |
|
44 | 44 | var CodeMirrorLoadUserHints = function(query, triggerHints) { |
|
45 | 45 | cmLog.debug('Loading mentions users via AJAX'); |
|
46 | 46 | var _users = []; |
|
47 | 47 | $.ajax({ |
|
48 | 48 | type: 'GET', |
|
49 | 49 | data: {query: query}, |
|
50 | 50 | url: pyroutes.url('user_autocomplete_data'), |
|
51 | 51 | headers: {'X-PARTIAL-XHR': true}, |
|
52 | 52 | async: true |
|
53 | 53 | }) |
|
54 | 54 | .done(function(data) { |
|
55 | 55 | var tmpl = '<img class="gravatar" src="{0}"/>{1}'; |
|
56 | 56 | $.each(data.suggestions, function(i) { |
|
57 | 57 | var userObj = data.suggestions[i]; |
|
58 | 58 | |
|
59 | 59 | if (userObj.username !== "default") { |
|
60 | 60 | _users.push({ |
|
61 | 61 | text: userObj.username + " ", |
|
62 | 62 | org_text: userObj.username, |
|
63 | 63 | displayText: userObj.value_display, // search that field |
|
64 | 64 | // internal caches |
|
65 | 65 | _icon_link: userObj.icon_link, |
|
66 | 66 | _text: userObj.value_display, |
|
67 | 67 | |
|
68 | 68 | render: function(elt, data, completion) { |
|
69 | 69 | var el = document.createElement('div'); |
|
70 | 70 | el.className = "CodeMirror-hint-entry"; |
|
71 | 71 | el.innerHTML = tmpl.format( |
|
72 | 72 | completion._icon_link, completion._text); |
|
73 | 73 | elt.appendChild(el); |
|
74 | 74 | } |
|
75 | 75 | }); |
|
76 | 76 | } |
|
77 | 77 | }); |
|
78 | 78 | cmLog.debug('Mention users loaded'); |
|
79 | 79 | // set to global cache |
|
80 | 80 | userHintsCache[query] = _users; |
|
81 | 81 | triggerHints(userHintsCache[query]); |
|
82 | 82 | }) |
|
83 | 83 | .fail(function(data, textStatus, xhr) { |
|
84 | 84 | alert("error processing request. \n" + |
|
85 | 85 | "Error code {0} ({1}).".format(data.status, data.statusText)); |
|
86 | 86 | }); |
|
87 | 87 | }; |
|
88 | 88 | |
|
89 | 89 | /** |
|
90 | 90 | * filters the results based on the current context |
|
91 | 91 | * @param users |
|
92 | 92 | * @param context |
|
93 | 93 | * @returns {Array} |
|
94 | 94 | */ |
|
95 | 95 | var CodeMirrorFilterUsers = function(users, context) { |
|
96 | 96 | var MAX_LIMIT = 10; |
|
97 | 97 | var filtered_users = []; |
|
98 | 98 | var curWord = context.string; |
|
99 | 99 | |
|
100 | 100 | cmLog.debug('Filtering users based on query:', curWord); |
|
101 | 101 | $.each(users, function(i) { |
|
102 | 102 | var match = users[i]; |
|
103 | 103 | var searchText = match.displayText; |
|
104 | 104 | |
|
105 | 105 | if (!curWord || |
|
106 | 106 | searchText.toLowerCase().lastIndexOf(curWord) !== -1) { |
|
107 | 107 | // reset state |
|
108 | 108 | match._text = match.displayText; |
|
109 | 109 | if (curWord) { |
|
110 | 110 | // do highlighting |
|
111 | 111 | var pattern = '(' + escapeRegExChars(curWord) + ')'; |
|
112 | 112 | match._text = searchText.replace( |
|
113 | 113 | new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | filtered_users.push(match); |
|
117 | 117 | } |
|
118 | 118 | // to not return to many results, use limit of filtered results |
|
119 | 119 | if (filtered_users.length > MAX_LIMIT) { |
|
120 | 120 | return false; |
|
121 | 121 | } |
|
122 | 122 | }); |
|
123 | 123 | |
|
124 | 124 | return filtered_users; |
|
125 | 125 | }; |
|
126 | 126 | |
|
127 | 127 | var CodeMirrorMentionHint = function(editor, callback, options) { |
|
128 | 128 | var cur = editor.getCursor(); |
|
129 | 129 | var curLine = editor.getLine(cur.line).slice(0, cur.ch); |
|
130 | 130 | |
|
131 | 131 | // match on @ +1char |
|
132 | 132 | var tokenMatch = new RegExp( |
|
133 | 133 | '(^@| @)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]*)$').exec(curLine); |
|
134 | 134 | |
|
135 | 135 | var tokenStr = ''; |
|
136 | 136 | if (tokenMatch !== null && tokenMatch.length > 0){ |
|
137 | 137 | tokenStr = tokenMatch[0].strip(); |
|
138 | 138 | } else { |
|
139 | 139 | // skip if we didn't match our token |
|
140 | 140 | return; |
|
141 | 141 | } |
|
142 | 142 | |
|
143 | 143 | var context = { |
|
144 | 144 | start: (cur.ch - tokenStr.length) + 1, |
|
145 | 145 | end: cur.ch, |
|
146 | 146 | string: tokenStr.slice(1), |
|
147 | 147 | type: null |
|
148 | 148 | }; |
|
149 | 149 | |
|
150 | 150 | // case when we put the @sign in fron of a string, |
|
151 | 151 | // eg <@ we put it here>sometext then we need to prepend to text |
|
152 | 152 | if (context.end > cur.ch) { |
|
153 | 153 | context.start = context.start + 1; // we add to the @ sign |
|
154 | 154 | context.end = cur.ch; // don't eat front part just append |
|
155 | 155 | context.string = context.string.slice(1, cur.ch - context.start); |
|
156 | 156 | } |
|
157 | 157 | |
|
158 | 158 | cmLog.debug('Mention context', context); |
|
159 | 159 | |
|
160 | 160 | var triggerHints = function(userHints){ |
|
161 | 161 | return callback({ |
|
162 | 162 | list: CodeMirrorFilterUsers(userHints, context), |
|
163 | 163 | from: CodeMirror.Pos(cur.line, context.start), |
|
164 | 164 | to: CodeMirror.Pos(cur.line, context.end) |
|
165 | 165 | }); |
|
166 | 166 | }; |
|
167 | 167 | |
|
168 | 168 | var queryBasedHintsCache = undefined; |
|
169 | 169 | // if we have something in the cache, try to fetch the query based cache |
|
170 | 170 | if (userHintsCache !== {}){ |
|
171 | 171 | queryBasedHintsCache = userHintsCache[context.string]; |
|
172 | 172 | } |
|
173 | 173 | |
|
174 | 174 | if (queryBasedHintsCache !== undefined) { |
|
175 | 175 | cmLog.debug('Users loaded from cache'); |
|
176 | 176 | triggerHints(queryBasedHintsCache); |
|
177 | 177 | } else { |
|
178 | 178 | // this takes care for async loading, and then displaying results |
|
179 | 179 | // and also propagates the userHintsCache |
|
180 | 180 | window.clearTimeout(CodeMirrorLoadUserHintTimer); |
|
181 | 181 | CodeMirrorLoadUserHintTimer = setTimeout(function() { |
|
182 | 182 | CodeMirrorLoadUserHints(context.string, triggerHints); |
|
183 | 183 | }, 300); |
|
184 | 184 | } |
|
185 | 185 | }; |
|
186 | 186 | |
|
187 | 187 | var CodeMirrorCompleteAfter = function(cm, pred) { |
|
188 | 188 | var options = { |
|
189 | 189 | completeSingle: false, |
|
190 | 190 | async: true, |
|
191 | 191 | closeOnUnfocus: true |
|
192 | 192 | }; |
|
193 | 193 | var cur = cm.getCursor(); |
|
194 | 194 | setTimeout(function() { |
|
195 | 195 | if (!cm.state.completionActive) { |
|
196 | 196 | cmLog.debug('Trigger mentions hinting'); |
|
197 | 197 | CodeMirror.showHint(cm, CodeMirror.hint.mentions, options); |
|
198 | 198 | } |
|
199 | 199 | }, 100); |
|
200 | 200 | |
|
201 | 201 | // tell CodeMirror we didn't handle the key |
|
202 | 202 | // trick to trigger on a char but still complete it |
|
203 | 203 | return CodeMirror.Pass; |
|
204 | 204 | }; |
|
205 | 205 | |
|
206 | 206 | var initCodeMirror = function(textAreadId, resetUrl, focus, options) { |
|
207 | 207 | if (textAreadId.substr(0,1) === "#"){ |
|
208 | 208 | var ta = $(textAreadId).get(0); |
|
209 | 209 | }else { |
|
210 | 210 | var ta = $('#' + textAreadId).get(0); |
|
211 | 211 | } |
|
212 | 212 | |
|
213 | 213 | if (focus === undefined) { |
|
214 | 214 | focus = true; |
|
215 | 215 | } |
|
216 | 216 | |
|
217 | 217 | // default options |
|
218 | 218 | var codeMirrorOptions = { |
|
219 | 219 | mode: "null", |
|
220 | 220 | lineNumbers: true, |
|
221 | 221 | indentUnit: 4, |
|
222 | 222 | autofocus: focus |
|
223 | 223 | }; |
|
224 | 224 | |
|
225 | 225 | if (options !== undefined) { |
|
226 | 226 | // extend with custom options |
|
227 | 227 | codeMirrorOptions = $.extend(true, codeMirrorOptions, options); |
|
228 | 228 | } |
|
229 | 229 | |
|
230 | 230 | var myCodeMirror = CodeMirror.fromTextArea(ta, codeMirrorOptions); |
|
231 | 231 | |
|
232 | 232 | $('#reset').on('click', function(e) { |
|
233 | 233 | window.location = resetUrl; |
|
234 | 234 | }); |
|
235 | 235 | |
|
236 | 236 | return myCodeMirror; |
|
237 | 237 | }; |
|
238 | 238 | |
|
239 | 239 | |
|
240 | 240 | var initMarkupCodeMirror = function(textAreadId, focus, options) { |
|
241 | 241 | var initialHeight = 100; |
|
242 | 242 | |
|
243 | 243 | var ta = $(textAreadId).get(0); |
|
244 | 244 | if (focus === undefined) { |
|
245 | 245 | focus = true; |
|
246 | 246 | } |
|
247 | 247 | |
|
248 | 248 | // default options |
|
249 | 249 | var codeMirrorOptions = { |
|
250 | 250 | lineNumbers: false, |
|
251 | 251 | indentUnit: 4, |
|
252 | 252 | viewportMargin: 30, |
|
253 | 253 | // this is a trick to trigger some logic behind codemirror placeholder |
|
254 | 254 | // it influences styling and behaviour. |
|
255 | 255 | placeholder: " ", |
|
256 | 256 | lineWrapping: true, |
|
257 | 257 | autofocus: focus |
|
258 | 258 | }; |
|
259 | 259 | |
|
260 | 260 | if (options !== undefined) { |
|
261 | 261 | // extend with custom options |
|
262 | 262 | codeMirrorOptions = $.extend(true, codeMirrorOptions, options); |
|
263 | 263 | } |
|
264 | 264 | |
|
265 | 265 | var cm = CodeMirror.fromTextArea(ta, codeMirrorOptions); |
|
266 | 266 | cm.setSize(null, initialHeight); |
|
267 | 267 | cm.setOption("mode", DEFAULT_RENDERER); |
|
268 | 268 | CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode |
|
269 | 269 | cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER); |
|
270 | 270 | |
|
271 | 271 | // start listening on changes to make auto-expanded editor |
|
272 | 272 | cm.on("change", function (instance, changeObj) { |
|
273 | 273 | var height = initialHeight; |
|
274 | 274 | var lines = instance.lineCount(); |
|
275 | 275 | if (lines > 6 && lines < 20) { |
|
276 | 276 | height = "auto"; |
|
277 | 277 | } else if (lines >= 20) { |
|
278 | 278 | height = 20 * 15; |
|
279 | 279 | } |
|
280 | 280 | instance.setSize(null, height); |
|
281 | 281 | |
|
282 | 282 | // detect if the change was trigger by auto desc, or user input |
|
283 | 283 | var changeOrigin = changeObj.origin; |
|
284 | 284 | |
|
285 | 285 | if (changeOrigin === "setValue") { |
|
286 | 286 | cmLog.debug('Change triggered by setValue'); |
|
287 | 287 | } |
|
288 | 288 | else { |
|
289 | 289 | cmLog.debug('user triggered change !'); |
|
290 | 290 | // set special marker to indicate user has created an input. |
|
291 | 291 | instance._userDefinedValue = true; |
|
292 | 292 | } |
|
293 | 293 | |
|
294 | 294 | }); |
|
295 | 295 | |
|
296 | 296 | return cm; |
|
297 | 297 | }; |
|
298 | 298 | |
|
299 | 299 | |
|
300 | 300 | var initCommentBoxCodeMirror = function(CommentForm, textAreaId, triggerActions){ |
|
301 | 301 | var initialHeight = 100; |
|
302 | 302 | |
|
303 | 303 | if (typeof userHintsCache === "undefined") { |
|
304 | 304 | userHintsCache = {}; |
|
305 | 305 | cmLog.debug('Init empty cache for mentions'); |
|
306 | 306 | } |
|
307 | 307 | if (!$(textAreaId).get(0)) { |
|
308 | 308 | cmLog.debug('Element for textarea not found', textAreaId); |
|
309 | 309 | return; |
|
310 | 310 | } |
|
311 | 311 | /** |
|
312 | 312 | * Filter action based on typed in text |
|
313 | 313 | * @param actions |
|
314 | 314 | * @param context |
|
315 | 315 | * @returns {Array} |
|
316 | 316 | */ |
|
317 | 317 | |
|
318 | 318 | var filterActions = function(actions, context){ |
|
319 | 319 | |
|
320 | 320 | var MAX_LIMIT = 10; |
|
321 | 321 | var filtered_actions = []; |
|
322 | 322 | var curWord = context.string; |
|
323 | 323 | |
|
324 | 324 | cmLog.debug('Filtering actions based on query:', curWord); |
|
325 | 325 | $.each(actions, function(i) { |
|
326 | 326 | var match = actions[i]; |
|
327 | 327 | var searchText = match.searchText; |
|
328 | 328 | |
|
329 | 329 | if (!curWord || |
|
330 | 330 | searchText.toLowerCase().lastIndexOf(curWord) !== -1) { |
|
331 | 331 | // reset state |
|
332 | 332 | match._text = match.displayText; |
|
333 | 333 | if (curWord) { |
|
334 | 334 | // do highlighting |
|
335 | 335 | var pattern = '(' + escapeRegExChars(curWord) + ')'; |
|
336 | 336 | match._text = searchText.replace( |
|
337 | 337 | new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); |
|
338 | 338 | } |
|
339 | 339 | |
|
340 | 340 | filtered_actions.push(match); |
|
341 | 341 | } |
|
342 | 342 | // to not return to many results, use limit of filtered results |
|
343 | 343 | if (filtered_actions.length > MAX_LIMIT) { |
|
344 | 344 | return false; |
|
345 | 345 | } |
|
346 | 346 | }); |
|
347 | 347 | |
|
348 | 348 | return filtered_actions; |
|
349 | 349 | }; |
|
350 | 350 | |
|
351 | 351 | var submitForm = function(cm, pred) { |
|
352 | $(cm.display.input.textarea.form).submit(); | |
|
352 | $(cm.display.input.textarea.form).find('.submit-comment-action').click(); | |
|
353 | return CodeMirror.Pass; | |
|
354 | }; | |
|
355 | ||
|
356 | var submitFormAsDraft = function(cm, pred) { | |
|
357 | $(cm.display.input.textarea.form).find('.submit-draft-action').click(); | |
|
353 | 358 | return CodeMirror.Pass; |
|
354 | 359 | }; |
|
355 | 360 | |
|
356 | 361 | var completeActions = function(actions){ |
|
357 | 362 | |
|
358 | 363 | var registeredActions = []; |
|
359 | 364 | var allActions = [ |
|
360 | 365 | { |
|
361 | 366 | text: "approve", |
|
362 | 367 | searchText: "status approved", |
|
363 | 368 | displayText: _gettext('Set status to Approved'), |
|
364 | 369 | hint: function(CodeMirror, data, completion) { |
|
365 | 370 | CodeMirror.replaceRange("", completion.from || data.from, |
|
366 | 371 | completion.to || data.to, "complete"); |
|
367 | 372 | $(CommentForm.statusChange).select2("val", 'approved').trigger('change'); |
|
368 | 373 | }, |
|
369 | 374 | render: function(elt, data, completion) { |
|
370 | 375 | var el = document.createElement('i'); |
|
371 | 376 | |
|
372 | 377 | el.className = "icon-circle review-status-approved"; |
|
373 | 378 | elt.appendChild(el); |
|
374 | 379 | |
|
375 | 380 | el = document.createElement('span'); |
|
376 | 381 | el.innerHTML = completion.displayText; |
|
377 | 382 | elt.appendChild(el); |
|
378 | 383 | } |
|
379 | 384 | }, |
|
380 | 385 | { |
|
381 | 386 | text: "reject", |
|
382 | 387 | searchText: "status rejected", |
|
383 | 388 | displayText: _gettext('Set status to Rejected'), |
|
384 | 389 | hint: function(CodeMirror, data, completion) { |
|
385 | 390 | CodeMirror.replaceRange("", completion.from || data.from, |
|
386 | 391 | completion.to || data.to, "complete"); |
|
387 | 392 | $(CommentForm.statusChange).select2("val", 'rejected').trigger('change'); |
|
388 | 393 | }, |
|
389 | 394 | render: function(elt, data, completion) { |
|
390 | 395 | var el = document.createElement('i'); |
|
391 | 396 | el.className = "icon-circle review-status-rejected"; |
|
392 | 397 | elt.appendChild(el); |
|
393 | 398 | |
|
394 | 399 | el = document.createElement('span'); |
|
395 | 400 | el.innerHTML = completion.displayText; |
|
396 | 401 | elt.appendChild(el); |
|
397 | 402 | } |
|
398 | 403 | }, |
|
399 | 404 | { |
|
400 | 405 | text: "as_todo", |
|
401 | 406 | searchText: "todo comment", |
|
402 | 407 | displayText: _gettext('TODO comment'), |
|
403 | 408 | hint: function(CodeMirror, data, completion) { |
|
404 | 409 | CodeMirror.replaceRange("", completion.from || data.from, |
|
405 | 410 | completion.to || data.to, "complete"); |
|
406 | 411 | |
|
407 | 412 | $(CommentForm.commentType).val('todo'); |
|
408 | 413 | }, |
|
409 | 414 | render: function(elt, data, completion) { |
|
410 | 415 | var el = document.createElement('div'); |
|
411 | 416 | el.className = "pull-left"; |
|
412 | 417 | elt.appendChild(el); |
|
413 | 418 | |
|
414 | 419 | el = document.createElement('span'); |
|
415 | 420 | el.innerHTML = completion.displayText; |
|
416 | 421 | elt.appendChild(el); |
|
417 | 422 | } |
|
418 | 423 | }, |
|
419 | 424 | { |
|
420 | 425 | text: "as_note", |
|
421 | 426 | searchText: "note comment", |
|
422 | 427 | displayText: _gettext('Note Comment'), |
|
423 | 428 | hint: function(CodeMirror, data, completion) { |
|
424 | 429 | CodeMirror.replaceRange("", completion.from || data.from, |
|
425 | 430 | completion.to || data.to, "complete"); |
|
426 | 431 | |
|
427 | 432 | $(CommentForm.commentType).val('note'); |
|
428 | 433 | }, |
|
429 | 434 | render: function(elt, data, completion) { |
|
430 | 435 | var el = document.createElement('div'); |
|
431 | 436 | el.className = "pull-left"; |
|
432 | 437 | elt.appendChild(el); |
|
433 | 438 | |
|
434 | 439 | el = document.createElement('span'); |
|
435 | 440 | el.innerHTML = completion.displayText; |
|
436 | 441 | elt.appendChild(el); |
|
437 | 442 | } |
|
438 | 443 | } |
|
439 | 444 | ]; |
|
440 | 445 | |
|
441 | 446 | $.each(allActions, function(index, value){ |
|
442 | 447 | var actionData = allActions[index]; |
|
443 | 448 | if (actions.indexOf(actionData['text']) != -1) { |
|
444 | 449 | registeredActions.push(actionData); |
|
445 | 450 | } |
|
446 | 451 | }); |
|
447 | 452 | |
|
448 | 453 | return function(cm, pred) { |
|
449 | 454 | var cur = cm.getCursor(); |
|
450 | 455 | var options = { |
|
451 | 456 | closeOnUnfocus: true, |
|
452 | 457 | registeredActions: registeredActions |
|
453 | 458 | }; |
|
454 | 459 | setTimeout(function() { |
|
455 | 460 | if (!cm.state.completionActive) { |
|
456 | 461 | cmLog.debug('Trigger actions hinting'); |
|
457 | 462 | CodeMirror.showHint(cm, CodeMirror.hint.actions, options); |
|
458 | 463 | } |
|
459 | 464 | }, 100); |
|
460 | 465 | |
|
461 | 466 | // tell CodeMirror we didn't handle the key |
|
462 | 467 | // trick to trigger on a char but still complete it |
|
463 | 468 | return CodeMirror.Pass; |
|
464 | 469 | } |
|
465 | 470 | }; |
|
466 | 471 | |
|
467 | 472 | var extraKeys = { |
|
468 | 473 | "'@'": CodeMirrorCompleteAfter, |
|
469 | 474 | Tab: function(cm) { |
|
470 | 475 | // space indent instead of TABS |
|
471 | 476 | var spaces = new Array(cm.getOption("indentUnit") + 1).join(" "); |
|
472 | 477 | cm.replaceSelection(spaces); |
|
473 | 478 | } |
|
474 | 479 | }; |
|
475 | 480 | // submit form on Meta-Enter |
|
476 | 481 | if (OSType === "mac") { |
|
477 | 482 | extraKeys["Cmd-Enter"] = submitForm; |
|
483 | extraKeys["Shift-Cmd-Enter"] = submitFormAsDraft; | |
|
478 | 484 | } |
|
479 | 485 | else { |
|
480 | 486 | extraKeys["Ctrl-Enter"] = submitForm; |
|
487 | extraKeys["Shift-Ctrl-Enter"] = submitFormAsDraft; | |
|
481 | 488 | } |
|
482 | 489 | |
|
483 | 490 | if (triggerActions) { |
|
484 | 491 | // register triggerActions for this instance |
|
485 | 492 | extraKeys["'/'"] = completeActions(triggerActions); |
|
486 | 493 | } |
|
487 | 494 | |
|
488 | 495 | var cm = CodeMirror.fromTextArea($(textAreaId).get(0), { |
|
489 | 496 | lineNumbers: false, |
|
490 | 497 | indentUnit: 4, |
|
491 | 498 | viewportMargin: 30, |
|
492 | 499 | // this is a trick to trigger some logic behind codemirror placeholder |
|
493 | 500 | // it influences styling and behaviour. |
|
494 | 501 | placeholder: " ", |
|
495 | 502 | extraKeys: extraKeys, |
|
496 | 503 | lineWrapping: true |
|
497 | 504 | }); |
|
498 | 505 | |
|
499 | 506 | cm.setSize(null, initialHeight); |
|
500 | 507 | cm.setOption("mode", DEFAULT_RENDERER); |
|
501 | 508 | CodeMirror.autoLoadMode(cm, DEFAULT_RENDERER); // load rst or markdown mode |
|
502 | 509 | cmLog.debug('Loading codemirror mode', DEFAULT_RENDERER); |
|
503 | 510 | |
|
504 | 511 | // start listening on changes to make auto-expanded editor |
|
505 | 512 | cm.on("change", function (self) { |
|
506 | 513 | var height = initialHeight; |
|
507 | 514 | var lines = self.lineCount(); |
|
508 | 515 | if (lines > 6 && lines < 20) { |
|
509 | 516 | height = "auto"; |
|
510 | 517 | } else if (lines >= 20) { |
|
511 | 518 | height = 20 * 15; |
|
512 | 519 | } |
|
513 | 520 | self.setSize(null, height); |
|
514 | 521 | }); |
|
515 | 522 | |
|
516 | 523 | var actionHint = function(editor, options) { |
|
517 | 524 | |
|
518 | 525 | var cur = editor.getCursor(); |
|
519 | 526 | var curLine = editor.getLine(cur.line).slice(0, cur.ch); |
|
520 | 527 | |
|
521 | 528 | // match only on /+1 character minimum |
|
522 | 529 | var tokenMatch = new RegExp('(^/\|/\)([a-zA-Z]*)$').exec(curLine); |
|
523 | 530 | |
|
524 | 531 | var tokenStr = ''; |
|
525 | 532 | if (tokenMatch !== null && tokenMatch.length > 0){ |
|
526 | 533 | tokenStr = tokenMatch[2].strip(); |
|
527 | 534 | } |
|
528 | 535 | |
|
529 | 536 | var context = { |
|
530 | 537 | start: (cur.ch - tokenStr.length) - 1, |
|
531 | 538 | end: cur.ch, |
|
532 | 539 | string: tokenStr, |
|
533 | 540 | type: null |
|
534 | 541 | }; |
|
535 | 542 | |
|
536 | 543 | return { |
|
537 | 544 | list: filterActions(options.registeredActions, context), |
|
538 | 545 | from: CodeMirror.Pos(cur.line, context.start), |
|
539 | 546 | to: CodeMirror.Pos(cur.line, context.end) |
|
540 | 547 | }; |
|
541 | 548 | |
|
542 | 549 | }; |
|
543 | 550 | CodeMirror.registerHelper("hint", "mentions", CodeMirrorMentionHint); |
|
544 | 551 | CodeMirror.registerHelper("hint", "actions", actionHint); |
|
545 | 552 | return cm; |
|
546 | 553 | }; |
|
547 | 554 | |
|
548 | 555 | var setCodeMirrorMode = function(codeMirrorInstance, mode) { |
|
549 | 556 | CodeMirror.autoLoadMode(codeMirrorInstance, mode); |
|
550 | 557 | codeMirrorInstance.setOption("mode", mode); |
|
551 | 558 | }; |
|
552 | 559 | |
|
553 | 560 | var setCodeMirrorLineWrap = function(codeMirrorInstance, line_wrap) { |
|
554 | 561 | codeMirrorInstance.setOption("lineWrapping", line_wrap); |
|
555 | 562 | }; |
|
556 | 563 | |
|
557 | 564 | var setCodeMirrorModeFromSelect = function( |
|
558 | 565 | targetSelect, targetFileInput, codeMirrorInstance, callback){ |
|
559 | 566 | |
|
560 | 567 | $(targetSelect).on('change', function(e) { |
|
561 | 568 | cmLog.debug('codemirror select2 mode change event !'); |
|
562 | 569 | var selected = e.currentTarget; |
|
563 | 570 | var node = selected.options[selected.selectedIndex]; |
|
564 | 571 | var mimetype = node.value; |
|
565 | 572 | cmLog.debug('picked mimetype', mimetype); |
|
566 | 573 | var new_mode = $(node).attr('mode'); |
|
567 | 574 | setCodeMirrorMode(codeMirrorInstance, new_mode); |
|
568 | 575 | cmLog.debug('set new mode', new_mode); |
|
569 | 576 | |
|
570 | 577 | //propose filename from picked mode |
|
571 | 578 | cmLog.debug('setting mimetype', mimetype); |
|
572 | 579 | var proposed_ext = getExtFromMimeType(mimetype); |
|
573 | 580 | cmLog.debug('file input', $(targetFileInput).val()); |
|
574 | 581 | var file_data = getFilenameAndExt($(targetFileInput).val()); |
|
575 | 582 | var filename = file_data.filename || 'filename1'; |
|
576 | 583 | $(targetFileInput).val(filename + proposed_ext); |
|
577 | 584 | cmLog.debug('proposed file', filename + proposed_ext); |
|
578 | 585 | |
|
579 | 586 | |
|
580 | 587 | if (typeof(callback) === 'function') { |
|
581 | 588 | try { |
|
582 | 589 | cmLog.debug('running callback', callback); |
|
583 | 590 | callback(filename, mimetype, new_mode); |
|
584 | 591 | } catch (err) { |
|
585 | 592 | console.log('failed to run callback', callback, err); |
|
586 | 593 | } |
|
587 | 594 | } |
|
588 | 595 | cmLog.debug('finish iteration...'); |
|
589 | 596 | }); |
|
590 | 597 | }; |
|
591 | 598 | |
|
592 | 599 | var setCodeMirrorModeFromInput = function( |
|
593 | 600 | targetSelect, targetFileInput, codeMirrorInstance, callback) { |
|
594 | 601 | |
|
595 | 602 | // on type the new filename set mode |
|
596 | 603 | $(targetFileInput).on('keyup', function(e) { |
|
597 | 604 | var file_data = getFilenameAndExt(this.value); |
|
598 | 605 | if (file_data.ext === null) { |
|
599 | 606 | return; |
|
600 | 607 | } |
|
601 | 608 | |
|
602 | 609 | var mimetypes = getMimeTypeFromExt(file_data.ext, true); |
|
603 | 610 | cmLog.debug('mimetype from file', file_data, mimetypes); |
|
604 | 611 | var detected_mode; |
|
605 | 612 | var detected_option; |
|
606 | 613 | for (var i in mimetypes) { |
|
607 | 614 | var mt = mimetypes[i]; |
|
608 | 615 | if (!detected_mode) { |
|
609 | 616 | detected_mode = detectCodeMirrorMode(this.value, mt); |
|
610 | 617 | } |
|
611 | 618 | |
|
612 | 619 | if (!detected_option) { |
|
613 | 620 | cmLog.debug('#mimetype option[value="{0}"]'.format(mt)); |
|
614 | 621 | if ($(targetSelect).find('option[value="{0}"]'.format(mt)).length) { |
|
615 | 622 | detected_option = mt; |
|
616 | 623 | } |
|
617 | 624 | } |
|
618 | 625 | } |
|
619 | 626 | |
|
620 | 627 | cmLog.debug('detected mode', detected_mode); |
|
621 | 628 | cmLog.debug('detected option', detected_option); |
|
622 | 629 | if (detected_mode && detected_option){ |
|
623 | 630 | |
|
624 | 631 | $(targetSelect).select2("val", detected_option); |
|
625 | 632 | setCodeMirrorMode(codeMirrorInstance, detected_mode); |
|
626 | 633 | |
|
627 | 634 | if(typeof(callback) === 'function'){ |
|
628 | 635 | try{ |
|
629 | 636 | cmLog.debug('running callback', callback); |
|
630 | 637 | var filename = file_data.filename + "." + file_data.ext; |
|
631 | 638 | callback(filename, detected_option, detected_mode); |
|
632 | 639 | }catch (err){ |
|
633 | 640 | console.log('failed to run callback', callback, err); |
|
634 | 641 | } |
|
635 | 642 | } |
|
636 | 643 | } |
|
637 | 644 | |
|
638 | 645 | }); |
|
639 | 646 | }; |
|
640 | 647 | |
|
641 | 648 | var fillCodeMirrorOptions = function(targetSelect) { |
|
642 | 649 | //inject new modes, based on codeMirrors modeInfo object |
|
643 | 650 | var modes_select = $(targetSelect); |
|
644 | 651 | for (var i = 0; i < CodeMirror.modeInfo.length; i++) { |
|
645 | 652 | var m = CodeMirror.modeInfo[i]; |
|
646 | 653 | var opt = new Option(m.name, m.mime); |
|
647 | 654 | $(opt).attr('mode', m.mode); |
|
648 | 655 | modes_select.append(opt); |
|
649 | 656 | } |
|
650 | 657 | }; |
|
651 | 658 | |
|
652 | 659 | |
|
653 | 660 | /* markup form */ |
|
654 | 661 | (function(mod) { |
|
655 | 662 | |
|
656 | 663 | if (typeof exports == "object" && typeof module == "object") { |
|
657 | 664 | // CommonJS |
|
658 | 665 | module.exports = mod(); |
|
659 | 666 | } |
|
660 | 667 | else { |
|
661 | 668 | // Plain browser env |
|
662 | 669 | (this || window).MarkupForm = mod(); |
|
663 | 670 | } |
|
664 | 671 | |
|
665 | 672 | })(function() { |
|
666 | 673 | "use strict"; |
|
667 | 674 | |
|
668 | 675 | function MarkupForm(textareaId) { |
|
669 | 676 | if (!(this instanceof MarkupForm)) { |
|
670 | 677 | return new MarkupForm(textareaId); |
|
671 | 678 | } |
|
672 | 679 | |
|
673 | 680 | // bind the element instance to our Form |
|
674 | 681 | $('#' + textareaId).get(0).MarkupForm = this; |
|
675 | 682 | |
|
676 | 683 | this.withSelectorId = function(selector) { |
|
677 | 684 | var selectorId = textareaId; |
|
678 | 685 | return selector + '_' + selectorId; |
|
679 | 686 | }; |
|
680 | 687 | |
|
681 | 688 | this.previewButton = this.withSelectorId('#preview-btn'); |
|
682 | 689 | this.previewContainer = this.withSelectorId('#preview-container'); |
|
683 | 690 | |
|
684 | 691 | this.previewBoxSelector = this.withSelectorId('#preview-box'); |
|
685 | 692 | |
|
686 | 693 | this.editButton = this.withSelectorId('#edit-btn'); |
|
687 | 694 | this.editContainer = this.withSelectorId('#edit-container'); |
|
688 | 695 | |
|
689 | 696 | this.cmBox = textareaId; |
|
690 | 697 | this.cm = initMarkupCodeMirror('#' + textareaId); |
|
691 | 698 | |
|
692 | 699 | this.previewUrl = pyroutes.url('markup_preview'); |
|
693 | 700 | |
|
694 | 701 | // FUNCTIONS and helpers |
|
695 | 702 | var self = this; |
|
696 | 703 | |
|
697 | 704 | this.getCmInstance = function(){ |
|
698 | 705 | return this.cm |
|
699 | 706 | }; |
|
700 | 707 | |
|
701 | 708 | this.setPlaceholder = function(placeholder) { |
|
702 | 709 | var cm = this.getCmInstance(); |
|
703 | 710 | if (cm){ |
|
704 | 711 | cm.setOption('placeholder', placeholder); |
|
705 | 712 | } |
|
706 | 713 | }; |
|
707 | 714 | |
|
708 | 715 | this.initStatusChangeSelector = function(){ |
|
709 | 716 | var formatChangeStatus = function(state, escapeMarkup) { |
|
710 | 717 | var originalOption = state.element; |
|
711 | 718 | var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text)); |
|
712 | 719 | return tmpl |
|
713 | 720 | }; |
|
714 | 721 | var formatResult = function(result, container, query, escapeMarkup) { |
|
715 | 722 | return formatChangeStatus(result, escapeMarkup); |
|
716 | 723 | }; |
|
717 | 724 | |
|
718 | 725 | var formatSelection = function(data, container, escapeMarkup) { |
|
719 | 726 | return formatChangeStatus(data, escapeMarkup); |
|
720 | 727 | }; |
|
721 | 728 | |
|
722 | 729 | $(this.submitForm).find(this.statusChange).select2({ |
|
723 | 730 | placeholder: _gettext('Status Review'), |
|
724 | 731 | formatResult: formatResult, |
|
725 | 732 | formatSelection: formatSelection, |
|
726 | 733 | containerCssClass: "drop-menu status_box_menu", |
|
727 | 734 | dropdownCssClass: "drop-menu-dropdown", |
|
728 | 735 | dropdownAutoWidth: true, |
|
729 | 736 | minimumResultsForSearch: -1 |
|
730 | 737 | }); |
|
731 | 738 | $(this.submitForm).find(this.statusChange).on('change', function() { |
|
732 | 739 | var status = self.getCommentStatus(); |
|
733 | 740 | |
|
734 | 741 | if (status && !self.isInline()) { |
|
735 | 742 | $(self.submitButton).prop('disabled', false); |
|
736 | 743 | } |
|
737 | 744 | |
|
738 | 745 | var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status); |
|
739 | 746 | self.setPlaceholder(placeholderText) |
|
740 | 747 | }) |
|
741 | 748 | }; |
|
742 | 749 | |
|
743 | 750 | // reset the text area into it's original state |
|
744 | 751 | this.resetMarkupFormState = function(content) { |
|
745 | 752 | content = content || ''; |
|
746 | 753 | |
|
747 | 754 | $(this.editContainer).show(); |
|
748 | 755 | $(this.editButton).parent().addClass('active'); |
|
749 | 756 | |
|
750 | 757 | $(this.previewContainer).hide(); |
|
751 | 758 | $(this.previewButton).parent().removeClass('active'); |
|
752 | 759 | |
|
753 | 760 | this.setActionButtonsDisabled(true); |
|
754 | 761 | self.cm.setValue(content); |
|
755 | 762 | self.cm.setOption("readOnly", false); |
|
756 | 763 | }; |
|
757 | 764 | |
|
758 | 765 | this.previewSuccessCallback = function(o) { |
|
759 | 766 | $(self.previewBoxSelector).html(o); |
|
760 | 767 | $(self.previewBoxSelector).removeClass('unloaded'); |
|
761 | 768 | |
|
762 | 769 | // swap buttons, making preview active |
|
763 | 770 | $(self.previewButton).parent().addClass('active'); |
|
764 | 771 | $(self.editButton).parent().removeClass('active'); |
|
765 | 772 | |
|
766 | 773 | // unlock buttons |
|
767 | 774 | self.setActionButtonsDisabled(false); |
|
768 | 775 | }; |
|
769 | 776 | |
|
770 | 777 | this.setActionButtonsDisabled = function(state) { |
|
771 | 778 | $(this.editButton).prop('disabled', state); |
|
772 | 779 | $(this.previewButton).prop('disabled', state); |
|
773 | 780 | }; |
|
774 | 781 | |
|
775 | 782 | // lock preview/edit/submit buttons on load, but exclude cancel button |
|
776 | 783 | var excludeCancelBtn = true; |
|
777 | 784 | this.setActionButtonsDisabled(true); |
|
778 | 785 | |
|
779 | 786 | // anonymous users don't have access to initialized CM instance |
|
780 | 787 | if (this.cm !== undefined){ |
|
781 | 788 | this.cm.on('change', function(cMirror) { |
|
782 | 789 | if (cMirror.getValue() === "") { |
|
783 | 790 | self.setActionButtonsDisabled(true) |
|
784 | 791 | } else { |
|
785 | 792 | self.setActionButtonsDisabled(false) |
|
786 | 793 | } |
|
787 | 794 | }); |
|
788 | 795 | } |
|
789 | 796 | |
|
790 | 797 | $(this.editButton).on('click', function(e) { |
|
791 | 798 | e.preventDefault(); |
|
792 | 799 | |
|
793 | 800 | $(self.previewButton).parent().removeClass('active'); |
|
794 | 801 | $(self.previewContainer).hide(); |
|
795 | 802 | |
|
796 | 803 | $(self.editButton).parent().addClass('active'); |
|
797 | 804 | $(self.editContainer).show(); |
|
798 | 805 | |
|
799 | 806 | }); |
|
800 | 807 | |
|
801 | 808 | $(this.previewButton).on('click', function(e) { |
|
802 | 809 | e.preventDefault(); |
|
803 | 810 | var text = self.cm.getValue(); |
|
804 | 811 | |
|
805 | 812 | if (text === "") { |
|
806 | 813 | return; |
|
807 | 814 | } |
|
808 | 815 | |
|
809 | 816 | var postData = { |
|
810 | 817 | 'text': text, |
|
811 | 818 | 'renderer': templateContext.visual.default_renderer, |
|
812 | 819 | 'csrf_token': CSRF_TOKEN |
|
813 | 820 | }; |
|
814 | 821 | |
|
815 | 822 | // lock ALL buttons on preview |
|
816 | 823 | self.setActionButtonsDisabled(true); |
|
817 | 824 | |
|
818 | 825 | $(self.previewBoxSelector).addClass('unloaded'); |
|
819 | 826 | $(self.previewBoxSelector).html(_gettext('Loading ...')); |
|
820 | 827 | |
|
821 | 828 | $(self.editContainer).hide(); |
|
822 | 829 | $(self.previewContainer).show(); |
|
823 | 830 | |
|
824 | 831 | // by default we reset state of comment preserving the text |
|
825 | 832 | var previewFailCallback = function(data){ |
|
826 | 833 | alert( |
|
827 | 834 | "Error while submitting preview.\n" + |
|
828 | 835 | "Error code {0} ({1}).".format(data.status, data.statusText) |
|
829 | 836 | ); |
|
830 | 837 | self.resetMarkupFormState(text) |
|
831 | 838 | }; |
|
832 | 839 | _submitAjaxPOST( |
|
833 | 840 | self.previewUrl, postData, self.previewSuccessCallback, |
|
834 | 841 | previewFailCallback); |
|
835 | 842 | |
|
836 | 843 | $(self.previewButton).parent().addClass('active'); |
|
837 | 844 | $(self.editButton).parent().removeClass('active'); |
|
838 | 845 | }); |
|
839 | 846 | |
|
840 | 847 | } |
|
841 | 848 | |
|
842 | 849 | return MarkupForm; |
|
843 | 850 | }); |
@@ -1,1298 +1,1349 b'' | |||
|
1 | 1 | // # Copyright (C) 2010-2020 RhodeCode GmbH |
|
2 | 2 | // # |
|
3 | 3 | // # This program is free software: you can redistribute it and/or modify |
|
4 | 4 | // # it under the terms of the GNU Affero General Public License, version 3 |
|
5 | 5 | // # (only), as published by the Free Software Foundation. |
|
6 | 6 | // # |
|
7 | 7 | // # This program is distributed in the hope that it will be useful, |
|
8 | 8 | // # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
9 | 9 | // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
10 | 10 | // # GNU General Public License for more details. |
|
11 | 11 | // # |
|
12 | 12 | // # You should have received a copy of the GNU Affero General Public License |
|
13 | 13 | // # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
14 | 14 | // # |
|
15 | 15 | // # This program is dual-licensed. If you wish to learn more about the |
|
16 | 16 | // # RhodeCode Enterprise Edition, including its added features, Support services, |
|
17 | 17 | // # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
18 | 18 | |
|
19 | 19 | var firefoxAnchorFix = function() { |
|
20 | 20 | // hack to make anchor links behave properly on firefox, in our inline |
|
21 | 21 | // comments generation when comments are injected firefox is misbehaving |
|
22 | 22 | // when jumping to anchor links |
|
23 | 23 | if (location.href.indexOf('#') > -1) { |
|
24 | 24 | location.href += ''; |
|
25 | 25 | } |
|
26 | 26 | }; |
|
27 | 27 | |
|
28 | 28 | var linkifyComments = function(comments) { |
|
29 | 29 | var firstCommentId = null; |
|
30 | 30 | if (comments) { |
|
31 | 31 | firstCommentId = $(comments[0]).data('comment-id'); |
|
32 | 32 | } |
|
33 | 33 | |
|
34 | 34 | if (firstCommentId){ |
|
35 | 35 | $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId); |
|
36 | 36 | } |
|
37 | 37 | }; |
|
38 | 38 | |
|
39 | 39 | var bindToggleButtons = function() { |
|
40 | 40 | $('.comment-toggle').on('click', function() { |
|
41 | 41 | $(this).parent().nextUntil('tr.line').toggle('inline-comments'); |
|
42 | 42 | }); |
|
43 | 43 | }; |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | |
|
47 | 47 | var _submitAjaxPOST = function(url, postData, successHandler, failHandler) { |
|
48 | 48 | failHandler = failHandler || function() {}; |
|
49 | 49 | postData = toQueryString(postData); |
|
50 | 50 | var request = $.ajax({ |
|
51 | 51 | url: url, |
|
52 | 52 | type: 'POST', |
|
53 | 53 | data: postData, |
|
54 | 54 | headers: {'X-PARTIAL-XHR': true} |
|
55 | 55 | }) |
|
56 | 56 | .done(function (data) { |
|
57 | 57 | successHandler(data); |
|
58 | 58 | }) |
|
59 | 59 | .fail(function (data, textStatus, errorThrown) { |
|
60 | 60 | failHandler(data, textStatus, errorThrown) |
|
61 | 61 | }); |
|
62 | 62 | return request; |
|
63 | 63 | }; |
|
64 | 64 | |
|
65 | 65 | |
|
66 | 66 | |
|
67 | 67 | |
|
68 | 68 | /* Comment form for main and inline comments */ |
|
69 | 69 | (function(mod) { |
|
70 | 70 | |
|
71 | 71 | if (typeof exports == "object" && typeof module == "object") { |
|
72 | 72 | // CommonJS |
|
73 | 73 | module.exports = mod(); |
|
74 | 74 | } |
|
75 | 75 | else { |
|
76 | 76 | // Plain browser env |
|
77 | 77 | (this || window).CommentForm = mod(); |
|
78 | 78 | } |
|
79 | 79 | |
|
80 | 80 | })(function() { |
|
81 | 81 | "use strict"; |
|
82 | 82 | |
|
83 | 83 | function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) { |
|
84 | 84 | |
|
85 | 85 | if (!(this instanceof CommentForm)) { |
|
86 | 86 | return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id); |
|
87 | 87 | } |
|
88 | 88 | |
|
89 | 89 | // bind the element instance to our Form |
|
90 | 90 | $(formElement).get(0).CommentForm = this; |
|
91 | 91 | |
|
92 | 92 | this.withLineNo = function(selector) { |
|
93 | 93 | var lineNo = this.lineNo; |
|
94 | 94 | if (lineNo === undefined) { |
|
95 | 95 | return selector |
|
96 | 96 | } else { |
|
97 | 97 | return selector + '_' + lineNo; |
|
98 | 98 | } |
|
99 | 99 | }; |
|
100 | 100 | |
|
101 | 101 | this.commitId = commitId; |
|
102 | 102 | this.pullRequestId = pullRequestId; |
|
103 | 103 | this.lineNo = lineNo; |
|
104 | 104 | this.initAutocompleteActions = initAutocompleteActions; |
|
105 | 105 | |
|
106 | 106 | this.previewButton = this.withLineNo('#preview-btn'); |
|
107 | 107 | this.previewContainer = this.withLineNo('#preview-container'); |
|
108 | 108 | |
|
109 | 109 | this.previewBoxSelector = this.withLineNo('#preview-box'); |
|
110 | 110 | |
|
111 | 111 | this.editButton = this.withLineNo('#edit-btn'); |
|
112 | 112 | this.editContainer = this.withLineNo('#edit-container'); |
|
113 | 113 | this.cancelButton = this.withLineNo('#cancel-btn'); |
|
114 | 114 | this.commentType = this.withLineNo('#comment_type'); |
|
115 | 115 | |
|
116 | 116 | this.resolvesId = null; |
|
117 | 117 | this.resolvesActionId = null; |
|
118 | 118 | |
|
119 | 119 | this.closesPr = '#close_pull_request'; |
|
120 | 120 | |
|
121 | 121 | this.cmBox = this.withLineNo('#text'); |
|
122 | 122 | this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions); |
|
123 | 123 | |
|
124 | 124 | this.statusChange = this.withLineNo('#change_status'); |
|
125 | 125 | |
|
126 | 126 | this.submitForm = formElement; |
|
127 | this.submitButton = $(this.submitForm).find('input[type="submit"]'); | |
|
127 | ||
|
128 | this.submitButton = $(this.submitForm).find('.submit-comment-action'); | |
|
128 | 129 | this.submitButtonText = this.submitButton.val(); |
|
129 | 130 | |
|
131 | this.submitDraftButton = $(this.submitForm).find('.submit-draft-action'); | |
|
132 | this.submitDraftButtonText = this.submitDraftButton.val(); | |
|
130 | 133 | |
|
131 | 134 | this.previewUrl = pyroutes.url('repo_commit_comment_preview', |
|
132 | 135 | {'repo_name': templateContext.repo_name, |
|
133 | 136 | 'commit_id': templateContext.commit_data.commit_id}); |
|
134 | 137 | |
|
135 | 138 | if (edit){ |
|
136 |
this.submitButton |
|
|
139 | this.submitDraftButton.hide(); | |
|
140 | this.submitButtonText = _gettext('Update Comment'); | |
|
137 | 141 | $(this.commentType).prop('disabled', true); |
|
138 | 142 | $(this.commentType).addClass('disabled'); |
|
139 | 143 | var editInfo = |
|
140 | 144 | ''; |
|
141 | 145 | $(editInfo).insertBefore($(this.editButton).parent()); |
|
142 | 146 | } |
|
143 | 147 | |
|
144 | 148 | if (resolvesCommentId){ |
|
145 | 149 | this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId); |
|
146 | 150 | this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId); |
|
147 | 151 | $(this.commentType).prop('disabled', true); |
|
148 | 152 | $(this.commentType).addClass('disabled'); |
|
149 | 153 | |
|
150 | 154 | // disable select |
|
151 | 155 | setTimeout(function() { |
|
152 | 156 | $(self.statusChange).select2('readonly', true); |
|
153 | 157 | }, 10); |
|
154 | 158 | |
|
155 | 159 | var resolvedInfo = ( |
|
156 | 160 | '<li class="resolve-action">' + |
|
157 | 161 | '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' + |
|
158 | 162 | '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' + |
|
159 | 163 | '</li>' |
|
160 | 164 | ).format(resolvesCommentId, _gettext('resolve comment')); |
|
161 | 165 | $(resolvedInfo).insertAfter($(this.commentType).parent()); |
|
162 | 166 | } |
|
163 | 167 | |
|
164 | 168 | // based on commitId, or pullRequestId decide where do we submit |
|
165 | 169 | // out data |
|
166 | 170 | if (this.commitId){ |
|
167 | 171 | var pyurl = 'repo_commit_comment_create'; |
|
168 | 172 | if(edit){ |
|
169 | 173 | pyurl = 'repo_commit_comment_edit'; |
|
170 | 174 | } |
|
171 | 175 | this.submitUrl = pyroutes.url(pyurl, |
|
172 | 176 | {'repo_name': templateContext.repo_name, |
|
173 | 177 | 'commit_id': this.commitId, |
|
174 | 178 | 'comment_id': comment_id}); |
|
175 | 179 | this.selfUrl = pyroutes.url('repo_commit', |
|
176 | 180 | {'repo_name': templateContext.repo_name, |
|
177 | 181 | 'commit_id': this.commitId}); |
|
178 | 182 | |
|
179 | 183 | } else if (this.pullRequestId) { |
|
180 | 184 | var pyurl = 'pullrequest_comment_create'; |
|
181 | 185 | if(edit){ |
|
182 | 186 | pyurl = 'pullrequest_comment_edit'; |
|
183 | 187 | } |
|
184 | 188 | this.submitUrl = pyroutes.url(pyurl, |
|
185 | 189 | {'repo_name': templateContext.repo_name, |
|
186 | 190 | 'pull_request_id': this.pullRequestId, |
|
187 | 191 | 'comment_id': comment_id}); |
|
188 | 192 | this.selfUrl = pyroutes.url('pullrequest_show', |
|
189 | 193 | {'repo_name': templateContext.repo_name, |
|
190 | 194 | 'pull_request_id': this.pullRequestId}); |
|
191 | 195 | |
|
192 | 196 | } else { |
|
193 | 197 | throw new Error( |
|
194 | 198 | 'CommentForm requires pullRequestId, or commitId to be specified.') |
|
195 | 199 | } |
|
196 | 200 | |
|
197 | 201 | // FUNCTIONS and helpers |
|
198 | 202 | var self = this; |
|
199 | 203 | |
|
200 | 204 | this.isInline = function(){ |
|
201 | 205 | return this.lineNo && this.lineNo != 'general'; |
|
202 | 206 | }; |
|
203 | 207 | |
|
204 | 208 | this.getCmInstance = function(){ |
|
205 | 209 | return this.cm |
|
206 | 210 | }; |
|
207 | 211 | |
|
208 | 212 | this.setPlaceholder = function(placeholder) { |
|
209 | 213 | var cm = this.getCmInstance(); |
|
210 | 214 | if (cm){ |
|
211 | 215 | cm.setOption('placeholder', placeholder); |
|
212 | 216 | } |
|
213 | 217 | }; |
|
214 | 218 | |
|
215 | 219 | this.getCommentStatus = function() { |
|
216 | 220 | return $(this.submitForm).find(this.statusChange).val(); |
|
217 | 221 | }; |
|
222 | ||
|
218 | 223 | this.getCommentType = function() { |
|
219 | 224 | return $(this.submitForm).find(this.commentType).val(); |
|
220 | 225 | }; |
|
221 | 226 | |
|
227 | this.getDraftState = function () { | |
|
228 | var submitterElem = $(this.submitForm).find('input[type="submit"].submitter'); | |
|
229 | var data = $(submitterElem).data('isDraft'); | |
|
230 | return data | |
|
231 | } | |
|
232 | ||
|
222 | 233 | this.getResolvesId = function() { |
|
223 | 234 | return $(this.submitForm).find(this.resolvesId).val() || null; |
|
224 | 235 | }; |
|
225 | 236 | |
|
226 | 237 | this.getClosePr = function() { |
|
227 | 238 | return $(this.submitForm).find(this.closesPr).val() || null; |
|
228 | 239 | }; |
|
229 | 240 | |
|
230 | 241 | this.markCommentResolved = function(resolvedCommentId){ |
|
231 | 242 | $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show(); |
|
232 | 243 | $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide(); |
|
233 | 244 | }; |
|
234 | 245 | |
|
235 | 246 | this.isAllowedToSubmit = function() { |
|
236 |
r |
|
|
247 | var commentDisabled = $(this.submitButton).prop('disabled'); | |
|
248 | var draftDisabled = $(this.submitDraftButton).prop('disabled'); | |
|
249 | return !commentDisabled && !draftDisabled; | |
|
237 | 250 | }; |
|
238 | 251 | |
|
239 | 252 | this.initStatusChangeSelector = function(){ |
|
240 | 253 | var formatChangeStatus = function(state, escapeMarkup) { |
|
241 | 254 | var originalOption = state.element; |
|
242 | 255 | var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text)); |
|
243 | 256 | return tmpl |
|
244 | 257 | }; |
|
245 | 258 | var formatResult = function(result, container, query, escapeMarkup) { |
|
246 | 259 | return formatChangeStatus(result, escapeMarkup); |
|
247 | 260 | }; |
|
248 | 261 | |
|
249 | 262 | var formatSelection = function(data, container, escapeMarkup) { |
|
250 | 263 | return formatChangeStatus(data, escapeMarkup); |
|
251 | 264 | }; |
|
252 | 265 | |
|
253 | 266 | $(this.submitForm).find(this.statusChange).select2({ |
|
254 | 267 | placeholder: _gettext('Status Review'), |
|
255 | 268 | formatResult: formatResult, |
|
256 | 269 | formatSelection: formatSelection, |
|
257 | 270 | containerCssClass: "drop-menu status_box_menu", |
|
258 | 271 | dropdownCssClass: "drop-menu-dropdown", |
|
259 | 272 | dropdownAutoWidth: true, |
|
260 | 273 | minimumResultsForSearch: -1 |
|
261 | 274 | }); |
|
275 | ||
|
262 | 276 | $(this.submitForm).find(this.statusChange).on('change', function() { |
|
263 | 277 | var status = self.getCommentStatus(); |
|
264 | 278 | |
|
265 | 279 | if (status && !self.isInline()) { |
|
266 | 280 | $(self.submitButton).prop('disabled', false); |
|
281 | $(self.submitDraftButton).prop('disabled', false); | |
|
267 | 282 | } |
|
268 | 283 | |
|
269 | 284 | var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status); |
|
270 | 285 | self.setPlaceholder(placeholderText) |
|
271 | 286 | }) |
|
272 | 287 | }; |
|
273 | 288 | |
|
274 | 289 | // reset the comment form into it's original state |
|
275 | 290 | this.resetCommentFormState = function(content) { |
|
276 | 291 | content = content || ''; |
|
277 | 292 | |
|
278 | 293 | $(this.editContainer).show(); |
|
279 | 294 | $(this.editButton).parent().addClass('active'); |
|
280 | 295 | |
|
281 | 296 | $(this.previewContainer).hide(); |
|
282 | 297 | $(this.previewButton).parent().removeClass('active'); |
|
283 | 298 | |
|
284 | 299 | this.setActionButtonsDisabled(true); |
|
285 | 300 | self.cm.setValue(content); |
|
286 | 301 | self.cm.setOption("readOnly", false); |
|
287 | 302 | |
|
288 | 303 | if (this.resolvesId) { |
|
289 | 304 | // destroy the resolve action |
|
290 | 305 | $(this.resolvesId).parent().remove(); |
|
291 | 306 | } |
|
292 | 307 | // reset closingPR flag |
|
293 | 308 | $('.close-pr-input').remove(); |
|
294 | 309 | |
|
295 | 310 | $(this.statusChange).select2('readonly', false); |
|
296 | 311 | }; |
|
297 | 312 | |
|
298 | this.globalSubmitSuccessCallback = function(){ | |
|
313 | this.globalSubmitSuccessCallback = function(comment){ | |
|
299 | 314 | // default behaviour is to call GLOBAL hook, if it's registered. |
|
300 | 315 | if (window.commentFormGlobalSubmitSuccessCallback !== undefined){ |
|
301 | commentFormGlobalSubmitSuccessCallback(); | |
|
316 | commentFormGlobalSubmitSuccessCallback(comment); | |
|
302 | 317 | } |
|
303 | 318 | }; |
|
304 | 319 | |
|
305 | 320 | this.submitAjaxPOST = function(url, postData, successHandler, failHandler) { |
|
306 | 321 | return _submitAjaxPOST(url, postData, successHandler, failHandler); |
|
307 | 322 | }; |
|
308 | 323 | |
|
309 | 324 | // overwrite a submitHandler, we need to do it for inline comments |
|
310 | 325 | this.setHandleFormSubmit = function(callback) { |
|
311 | 326 | this.handleFormSubmit = callback; |
|
312 | 327 | }; |
|
313 | 328 | |
|
314 | 329 | // overwrite a submitSuccessHandler |
|
315 | 330 | this.setGlobalSubmitSuccessCallback = function(callback) { |
|
316 | 331 | this.globalSubmitSuccessCallback = callback; |
|
317 | 332 | }; |
|
318 | 333 | |
|
319 | 334 | // default handler for for submit for main comments |
|
320 | 335 | this.handleFormSubmit = function() { |
|
321 | 336 | var text = self.cm.getValue(); |
|
322 | 337 | var status = self.getCommentStatus(); |
|
323 | 338 | var commentType = self.getCommentType(); |
|
339 | var isDraft = self.getDraftState(); | |
|
324 | 340 | var resolvesCommentId = self.getResolvesId(); |
|
325 | 341 | var closePullRequest = self.getClosePr(); |
|
326 | 342 | |
|
327 | 343 | if (text === "" && !status) { |
|
328 | 344 | return; |
|
329 | 345 | } |
|
330 | 346 | |
|
331 | 347 | var excludeCancelBtn = false; |
|
332 | 348 | var submitEvent = true; |
|
333 | 349 | self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); |
|
334 | 350 | self.cm.setOption("readOnly", true); |
|
335 | 351 | |
|
336 | 352 | var postData = { |
|
337 | 353 | 'text': text, |
|
338 | 354 | 'changeset_status': status, |
|
339 | 355 | 'comment_type': commentType, |
|
340 | 356 | 'csrf_token': CSRF_TOKEN |
|
341 | 357 | }; |
|
342 | 358 | |
|
343 | 359 | if (resolvesCommentId) { |
|
344 | 360 | postData['resolves_comment_id'] = resolvesCommentId; |
|
345 | 361 | } |
|
346 | 362 | |
|
347 | 363 | if (closePullRequest) { |
|
348 | 364 | postData['close_pull_request'] = true; |
|
349 | 365 | } |
|
350 | 366 | |
|
351 | 367 | var submitSuccessCallback = function(o) { |
|
352 | 368 | // reload page if we change status for single commit. |
|
353 | 369 | if (status && self.commitId) { |
|
354 | 370 | location.reload(true); |
|
355 | 371 | } else { |
|
356 | 372 | $('#injected_page_comments').append(o.rendered_text); |
|
357 | 373 | self.resetCommentFormState(); |
|
358 | 374 | timeagoActivate(); |
|
359 | 375 | tooltipActivate(); |
|
360 | 376 | |
|
361 | 377 | // mark visually which comment was resolved |
|
362 | 378 | if (resolvesCommentId) { |
|
363 | 379 | self.markCommentResolved(resolvesCommentId); |
|
364 | 380 | } |
|
365 | 381 | } |
|
366 | 382 | |
|
367 | 383 | // run global callback on submit |
|
368 | self.globalSubmitSuccessCallback(); | |
|
384 | self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id}); | |
|
369 | 385 | |
|
370 | 386 | }; |
|
371 | 387 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { |
|
372 | 388 | var prefix = "Error while submitting comment.\n" |
|
373 | 389 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
374 | 390 | ajaxErrorSwal(message); |
|
375 | 391 | self.resetCommentFormState(text); |
|
376 | 392 | }; |
|
377 | 393 | self.submitAjaxPOST( |
|
378 | 394 | self.submitUrl, postData, submitSuccessCallback, submitFailCallback); |
|
379 | 395 | }; |
|
380 | 396 | |
|
381 | 397 | this.previewSuccessCallback = function(o) { |
|
382 | 398 | $(self.previewBoxSelector).html(o); |
|
383 | 399 | $(self.previewBoxSelector).removeClass('unloaded'); |
|
384 | 400 | |
|
385 | 401 | // swap buttons, making preview active |
|
386 | 402 | $(self.previewButton).parent().addClass('active'); |
|
387 | 403 | $(self.editButton).parent().removeClass('active'); |
|
388 | 404 | |
|
389 | 405 | // unlock buttons |
|
390 | 406 | self.setActionButtonsDisabled(false); |
|
391 | 407 | }; |
|
392 | 408 | |
|
393 | 409 | this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) { |
|
394 | 410 | excludeCancelBtn = excludeCancelBtn || false; |
|
395 | 411 | submitEvent = submitEvent || false; |
|
396 | 412 | |
|
397 | 413 | $(this.editButton).prop('disabled', state); |
|
398 | 414 | $(this.previewButton).prop('disabled', state); |
|
399 | 415 | |
|
400 | 416 | if (!excludeCancelBtn) { |
|
401 | 417 | $(this.cancelButton).prop('disabled', state); |
|
402 | 418 | } |
|
403 | 419 | |
|
404 | 420 | var submitState = state; |
|
405 | 421 | if (!submitEvent && this.getCommentStatus() && !self.isInline()) { |
|
406 | 422 | // if the value of commit review status is set, we allow |
|
407 | 423 | // submit button, but only on Main form, isInline means inline |
|
408 | 424 | submitState = false |
|
409 | 425 | } |
|
410 | 426 | |
|
411 | 427 | $(this.submitButton).prop('disabled', submitState); |
|
428 | $(this.submitDraftButton).prop('disabled', submitState); | |
|
429 | ||
|
412 | 430 | if (submitEvent) { |
|
431 | var isDraft = self.getDraftState(); | |
|
432 | ||
|
433 | if (isDraft) { | |
|
434 | $(this.submitDraftButton).val(_gettext('Saving Draft...')); | |
|
435 | } else { | |
|
413 | 436 | $(this.submitButton).val(_gettext('Submitting...')); |
|
437 | } | |
|
438 | ||
|
414 | 439 | } else { |
|
415 | 440 | $(this.submitButton).val(this.submitButtonText); |
|
441 | $(this.submitDraftButton).val(this.submitDraftButtonText); | |
|
416 | 442 | } |
|
417 | 443 | |
|
418 | 444 | }; |
|
419 | 445 | |
|
420 | 446 | // lock preview/edit/submit buttons on load, but exclude cancel button |
|
421 | 447 | var excludeCancelBtn = true; |
|
422 | 448 | this.setActionButtonsDisabled(true, excludeCancelBtn); |
|
423 | 449 | |
|
424 | 450 | // anonymous users don't have access to initialized CM instance |
|
425 | 451 | if (this.cm !== undefined){ |
|
426 | 452 | this.cm.on('change', function(cMirror) { |
|
427 | 453 | if (cMirror.getValue() === "") { |
|
428 | 454 | self.setActionButtonsDisabled(true, excludeCancelBtn) |
|
429 | 455 | } else { |
|
430 | 456 | self.setActionButtonsDisabled(false, excludeCancelBtn) |
|
431 | 457 | } |
|
432 | 458 | }); |
|
433 | 459 | } |
|
434 | 460 | |
|
435 | 461 | $(this.editButton).on('click', function(e) { |
|
436 | 462 | e.preventDefault(); |
|
437 | 463 | |
|
438 | 464 | $(self.previewButton).parent().removeClass('active'); |
|
439 | 465 | $(self.previewContainer).hide(); |
|
440 | 466 | |
|
441 | 467 | $(self.editButton).parent().addClass('active'); |
|
442 | 468 | $(self.editContainer).show(); |
|
443 | 469 | |
|
444 | 470 | }); |
|
445 | 471 | |
|
446 | 472 | $(this.previewButton).on('click', function(e) { |
|
447 | 473 | e.preventDefault(); |
|
448 | 474 | var text = self.cm.getValue(); |
|
449 | 475 | |
|
450 | 476 | if (text === "") { |
|
451 | 477 | return; |
|
452 | 478 | } |
|
453 | 479 | |
|
454 | 480 | var postData = { |
|
455 | 481 | 'text': text, |
|
456 | 482 | 'renderer': templateContext.visual.default_renderer, |
|
457 | 483 | 'csrf_token': CSRF_TOKEN |
|
458 | 484 | }; |
|
459 | 485 | |
|
460 | 486 | // lock ALL buttons on preview |
|
461 | 487 | self.setActionButtonsDisabled(true); |
|
462 | 488 | |
|
463 | 489 | $(self.previewBoxSelector).addClass('unloaded'); |
|
464 | 490 | $(self.previewBoxSelector).html(_gettext('Loading ...')); |
|
465 | 491 | |
|
466 | 492 | $(self.editContainer).hide(); |
|
467 | 493 | $(self.previewContainer).show(); |
|
468 | 494 | |
|
469 | 495 | // by default we reset state of comment preserving the text |
|
470 | 496 | var previewFailCallback = function(jqXHR, textStatus, errorThrown) { |
|
471 | 497 | var prefix = "Error while preview of comment.\n" |
|
472 | 498 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
473 | 499 | ajaxErrorSwal(message); |
|
474 | 500 | |
|
475 | 501 | self.resetCommentFormState(text) |
|
476 | 502 | }; |
|
477 | 503 | self.submitAjaxPOST( |
|
478 | 504 | self.previewUrl, postData, self.previewSuccessCallback, |
|
479 | 505 | previewFailCallback); |
|
480 | 506 | |
|
481 | 507 | $(self.previewButton).parent().addClass('active'); |
|
482 | 508 | $(self.editButton).parent().removeClass('active'); |
|
483 | 509 | }); |
|
484 | 510 | |
|
485 | 511 | $(this.submitForm).submit(function(e) { |
|
486 | 512 | e.preventDefault(); |
|
487 | 513 | var allowedToSubmit = self.isAllowedToSubmit(); |
|
488 | 514 | if (!allowedToSubmit){ |
|
489 | 515 | return false; |
|
490 | 516 | } |
|
517 | ||
|
491 | 518 | self.handleFormSubmit(); |
|
492 | 519 | }); |
|
493 | 520 | |
|
494 | 521 | } |
|
495 | 522 | |
|
496 | 523 | return CommentForm; |
|
497 | 524 | }); |
|
498 | 525 | |
|
499 | 526 | /* selector for comment versions */ |
|
500 | 527 | var initVersionSelector = function(selector, initialData) { |
|
501 | 528 | |
|
502 | 529 | var formatResult = function(result, container, query, escapeMarkup) { |
|
503 | 530 | |
|
504 | 531 | return renderTemplate('commentVersion', { |
|
505 | 532 | show_disabled: true, |
|
506 | 533 | version: result.comment_version, |
|
507 | 534 | user_name: result.comment_author_username, |
|
508 | 535 | gravatar_url: result.comment_author_gravatar, |
|
509 | 536 | size: 16, |
|
510 | 537 | timeago_component: result.comment_created_on, |
|
511 | 538 | }) |
|
512 | 539 | }; |
|
513 | 540 | |
|
514 | 541 | $(selector).select2({ |
|
515 | 542 | placeholder: "Edited", |
|
516 | 543 | containerCssClass: "drop-menu-comment-history", |
|
517 | 544 | dropdownCssClass: "drop-menu-dropdown", |
|
518 | 545 | dropdownAutoWidth: true, |
|
519 | 546 | minimumResultsForSearch: -1, |
|
520 | 547 | data: initialData, |
|
521 | 548 | formatResult: formatResult, |
|
522 | 549 | }); |
|
523 | 550 | |
|
524 | 551 | $(selector).on('select2-selecting', function (e) { |
|
525 | 552 | // hide the mast as we later do preventDefault() |
|
526 | 553 | $("#select2-drop-mask").click(); |
|
527 | 554 | e.preventDefault(); |
|
528 | 555 | e.choice.action(); |
|
529 | 556 | }); |
|
530 | 557 | |
|
531 | 558 | $(selector).on("select2-open", function() { |
|
532 | 559 | timeagoActivate(); |
|
533 | 560 | }); |
|
534 | 561 | }; |
|
535 | 562 | |
|
536 | 563 | /* comments controller */ |
|
537 | 564 | var CommentsController = function() { |
|
538 | 565 | var mainComment = '#text'; |
|
539 | 566 | var self = this; |
|
540 | 567 | |
|
541 | 568 | this.cancelComment = function (node) { |
|
542 | 569 | var $node = $(node); |
|
543 | 570 | var edit = $(this).attr('edit'); |
|
544 | 571 | if (edit) { |
|
545 | 572 | var $general_comments = null; |
|
546 | 573 | var $inline_comments = $node.closest('div.inline-comments'); |
|
547 | 574 | if (!$inline_comments.length) { |
|
548 | 575 | $general_comments = $('#comments'); |
|
549 | 576 | var $comment = $general_comments.parent().find('div.comment:hidden'); |
|
550 | 577 | // show hidden general comment form |
|
551 | 578 | $('#cb-comment-general-form-placeholder').show(); |
|
552 | 579 | } else { |
|
553 | 580 | var $comment = $inline_comments.find('div.comment:hidden'); |
|
554 | 581 | } |
|
555 | 582 | $comment.show(); |
|
556 | 583 | } |
|
557 | 584 | $node.closest('.comment-inline-form').remove(); |
|
558 | 585 | return false; |
|
559 | 586 | }; |
|
560 | 587 | |
|
561 | 588 | this.showVersion = function (comment_id, comment_history_id) { |
|
562 | 589 | |
|
563 | 590 | var historyViewUrl = pyroutes.url( |
|
564 | 591 | 'repo_commit_comment_history_view', |
|
565 | 592 | { |
|
566 | 593 | 'repo_name': templateContext.repo_name, |
|
567 | 594 | 'commit_id': comment_id, |
|
568 | 595 | 'comment_history_id': comment_history_id, |
|
569 | 596 | } |
|
570 | 597 | ); |
|
571 | 598 | successRenderCommit = function (data) { |
|
572 | 599 | SwalNoAnimation.fire({ |
|
573 | 600 | html: data, |
|
574 | 601 | title: '', |
|
575 | 602 | }); |
|
576 | 603 | }; |
|
577 | 604 | failRenderCommit = function () { |
|
578 | 605 | SwalNoAnimation.fire({ |
|
579 | 606 | html: 'Error while loading comment history', |
|
580 | 607 | title: '', |
|
581 | 608 | }); |
|
582 | 609 | }; |
|
583 | 610 | _submitAjaxPOST( |
|
584 | 611 | historyViewUrl, {'csrf_token': CSRF_TOKEN}, |
|
585 | 612 | successRenderCommit, |
|
586 | 613 | failRenderCommit |
|
587 | 614 | ); |
|
588 | 615 | }; |
|
589 | 616 | |
|
590 | 617 | this.getLineNumber = function(node) { |
|
591 | 618 | var $node = $(node); |
|
592 | 619 | var lineNo = $node.closest('td').attr('data-line-no'); |
|
593 | 620 | if (lineNo === undefined && $node.data('commentInline')){ |
|
594 | 621 | lineNo = $node.data('commentLineNo') |
|
595 | 622 | } |
|
596 | 623 | |
|
597 | 624 | return lineNo |
|
598 | 625 | }; |
|
599 | 626 | |
|
600 | 627 | this.scrollToComment = function(node, offset, outdated) { |
|
601 | 628 | if (offset === undefined) { |
|
602 | 629 | offset = 0; |
|
603 | 630 | } |
|
604 | 631 | var outdated = outdated || false; |
|
605 | 632 | var klass = outdated ? 'div.comment-outdated' : 'div.comment-current'; |
|
606 | 633 | |
|
607 | 634 | if (!node) { |
|
608 | 635 | node = $('.comment-selected'); |
|
609 | 636 | if (!node.length) { |
|
610 | 637 | node = $('comment-current') |
|
611 | 638 | } |
|
612 | 639 | } |
|
613 | 640 | |
|
614 | 641 | $wrapper = $(node).closest('div.comment'); |
|
615 | 642 | |
|
616 | 643 | // show hidden comment when referenced. |
|
617 | 644 | if (!$wrapper.is(':visible')){ |
|
618 | 645 | $wrapper.show(); |
|
619 | 646 | } |
|
620 | 647 | |
|
621 | 648 | $comment = $(node).closest(klass); |
|
622 | 649 | $comments = $(klass); |
|
623 | 650 | |
|
624 | 651 | $('.comment-selected').removeClass('comment-selected'); |
|
625 | 652 | |
|
626 | 653 | var nextIdx = $(klass).index($comment) + offset; |
|
627 | 654 | if (nextIdx >= $comments.length) { |
|
628 | 655 | nextIdx = 0; |
|
629 | 656 | } |
|
630 | 657 | var $next = $(klass).eq(nextIdx); |
|
631 | 658 | |
|
632 | 659 | var $cb = $next.closest('.cb'); |
|
633 | 660 | $cb.removeClass('cb-collapsed'); |
|
634 | 661 | |
|
635 | 662 | var $filediffCollapseState = $cb.closest('.filediff').prev(); |
|
636 | 663 | $filediffCollapseState.prop('checked', false); |
|
637 | 664 | $next.addClass('comment-selected'); |
|
638 | 665 | scrollToElement($next); |
|
639 | 666 | return false; |
|
640 | 667 | }; |
|
641 | 668 | |
|
642 | 669 | this.nextComment = function(node) { |
|
643 | 670 | return self.scrollToComment(node, 1); |
|
644 | 671 | }; |
|
645 | 672 | |
|
646 | 673 | this.prevComment = function(node) { |
|
647 | 674 | return self.scrollToComment(node, -1); |
|
648 | 675 | }; |
|
649 | 676 | |
|
650 | 677 | this.nextOutdatedComment = function(node) { |
|
651 | 678 | return self.scrollToComment(node, 1, true); |
|
652 | 679 | }; |
|
653 | 680 | |
|
654 | 681 | this.prevOutdatedComment = function(node) { |
|
655 | 682 | return self.scrollToComment(node, -1, true); |
|
656 | 683 | }; |
|
657 | 684 | |
|
658 | 685 | this._deleteComment = function(node) { |
|
659 | 686 | var $node = $(node); |
|
660 | 687 | var $td = $node.closest('td'); |
|
661 | 688 | var $comment = $node.closest('.comment'); |
|
662 |
var comment_id = $comment. |
|
|
689 | var comment_id = $($comment).data('commentId'); | |
|
690 | var isDraft = $($comment).data('commentDraft'); | |
|
663 | 691 | var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id); |
|
664 | 692 | var postData = { |
|
665 | 693 | 'csrf_token': CSRF_TOKEN |
|
666 | 694 | }; |
|
667 | 695 | |
|
668 | 696 | $comment.addClass('comment-deleting'); |
|
669 | 697 | $comment.hide('fast'); |
|
670 | 698 | |
|
671 | 699 | var success = function(response) { |
|
672 | 700 | $comment.remove(); |
|
673 | 701 | |
|
674 | 702 | if (window.updateSticky !== undefined) { |
|
675 | 703 | // potentially our comments change the active window size, so we |
|
676 | 704 | // notify sticky elements |
|
677 | 705 | updateSticky() |
|
678 | 706 | } |
|
679 | 707 | |
|
680 | if (window.refreshAllComments !== undefined) { | |
|
708 | if (window.refreshAllComments !== undefined && !isDraft) { | |
|
681 | 709 | // if we have this handler, run it, and refresh all comments boxes |
|
682 | 710 | refreshAllComments() |
|
683 | 711 | } |
|
684 | 712 | return false; |
|
685 | 713 | }; |
|
686 | 714 | |
|
687 | 715 | var failure = function(jqXHR, textStatus, errorThrown) { |
|
688 | 716 | var prefix = "Error while deleting this comment.\n" |
|
689 | 717 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
690 | 718 | ajaxErrorSwal(message); |
|
691 | 719 | |
|
692 | 720 | $comment.show('fast'); |
|
693 | 721 | $comment.removeClass('comment-deleting'); |
|
694 | 722 | return false; |
|
695 | 723 | }; |
|
696 | 724 | ajaxPOST(url, postData, success, failure); |
|
697 | 725 | |
|
698 | 726 | |
|
699 | 727 | |
|
700 | 728 | } |
|
701 | 729 | |
|
702 | 730 | this.deleteComment = function(node) { |
|
703 | 731 | var $comment = $(node).closest('.comment'); |
|
704 | 732 | var comment_id = $comment.attr('data-comment-id'); |
|
705 | 733 | |
|
706 | 734 | SwalNoAnimation.fire({ |
|
707 | 735 | title: 'Delete this comment?', |
|
708 | 736 | icon: 'warning', |
|
709 | 737 | showCancelButton: true, |
|
710 | 738 | confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id), |
|
711 | 739 | |
|
712 | 740 | }).then(function(result) { |
|
713 | 741 | if (result.value) { |
|
714 | 742 | self._deleteComment(node); |
|
715 | 743 | } |
|
716 | 744 | }) |
|
717 | 745 | }; |
|
718 | 746 | |
|
747 | this._finalizeDrafts = function(commentIds) { | |
|
748 | window.finalizeDrafts(commentIds) | |
|
749 | } | |
|
750 | ||
|
751 | this.finalizeDrafts = function(commentIds) { | |
|
752 | ||
|
753 | SwalNoAnimation.fire({ | |
|
754 | title: _ngettext('Submit {0} draft comment', 'Submit {0} draft comments', commentIds.length).format(commentIds.length), | |
|
755 | icon: 'warning', | |
|
756 | showCancelButton: true, | |
|
757 | confirmButtonText: _gettext('Yes, finalize drafts'), | |
|
758 | ||
|
759 | }).then(function(result) { | |
|
760 | if (result.value) { | |
|
761 | self._finalizeDrafts(commentIds); | |
|
762 | } | |
|
763 | }) | |
|
764 | }; | |
|
765 | ||
|
719 | 766 | this.toggleWideMode = function (node) { |
|
720 | 767 | if ($('#content').hasClass('wrapper')) { |
|
721 | 768 | $('#content').removeClass("wrapper"); |
|
722 | 769 | $('#content').addClass("wide-mode-wrapper"); |
|
723 | 770 | $(node).addClass('btn-success'); |
|
724 | 771 | return true |
|
725 | 772 | } else { |
|
726 | 773 | $('#content').removeClass("wide-mode-wrapper"); |
|
727 | 774 | $('#content').addClass("wrapper"); |
|
728 | 775 | $(node).removeClass('btn-success'); |
|
729 | 776 | return false |
|
730 | 777 | } |
|
731 | 778 | |
|
732 | 779 | }; |
|
733 | 780 | |
|
734 | 781 | this.toggleComments = function(node, show) { |
|
735 | 782 | var $filediff = $(node).closest('.filediff'); |
|
736 | 783 | if (show === true) { |
|
737 | 784 | $filediff.removeClass('hide-comments'); |
|
738 | 785 | } else if (show === false) { |
|
739 | 786 | $filediff.find('.hide-line-comments').removeClass('hide-line-comments'); |
|
740 | 787 | $filediff.addClass('hide-comments'); |
|
741 | 788 | } else { |
|
742 | 789 | $filediff.find('.hide-line-comments').removeClass('hide-line-comments'); |
|
743 | 790 | $filediff.toggleClass('hide-comments'); |
|
744 | 791 | } |
|
745 | 792 | |
|
746 | 793 | // since we change the height of the diff container that has anchor points for upper |
|
747 | 794 | // sticky header, we need to tell it to re-calculate those |
|
748 | 795 | if (window.updateSticky !== undefined) { |
|
749 | 796 | // potentially our comments change the active window size, so we |
|
750 | 797 | // notify sticky elements |
|
751 | 798 | updateSticky() |
|
752 | 799 | } |
|
753 | 800 | |
|
754 | 801 | return false; |
|
755 | 802 | }; |
|
756 | 803 | |
|
757 | 804 | this.toggleLineComments = function(node) { |
|
758 | 805 | self.toggleComments(node, true); |
|
759 | 806 | var $node = $(node); |
|
760 | 807 | // mark outdated comments as visible before the toggle; |
|
761 | 808 | $(node.closest('tr')).find('.comment-outdated').show(); |
|
762 | 809 | $node.closest('tr').toggleClass('hide-line-comments'); |
|
763 | 810 | }; |
|
764 | 811 | |
|
765 | 812 | this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){ |
|
766 | 813 | var pullRequestId = templateContext.pull_request_data.pull_request_id; |
|
767 | 814 | var commitId = templateContext.commit_data.commit_id; |
|
768 | 815 | |
|
769 | 816 | var commentForm = new CommentForm( |
|
770 | 817 | formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id); |
|
771 | 818 | var cm = commentForm.getCmInstance(); |
|
772 | 819 | |
|
773 | 820 | if (resolvesCommentId){ |
|
774 | 821 | placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId); |
|
775 | 822 | } |
|
776 | 823 | |
|
777 | 824 | setTimeout(function() { |
|
778 | 825 | // callbacks |
|
779 | 826 | if (cm !== undefined) { |
|
780 | 827 | commentForm.setPlaceholder(placeholderText); |
|
781 | 828 | if (commentForm.isInline()) { |
|
782 | 829 | cm.focus(); |
|
783 | 830 | cm.refresh(); |
|
784 | 831 | } |
|
785 | 832 | } |
|
786 | 833 | }, 10); |
|
787 | 834 | |
|
788 | 835 | // trigger scrolldown to the resolve comment, since it might be away |
|
789 | 836 | // from the clicked |
|
790 | 837 | if (resolvesCommentId){ |
|
791 | 838 | var actionNode = $(commentForm.resolvesActionId).offset(); |
|
792 | 839 | |
|
793 | 840 | setTimeout(function() { |
|
794 | 841 | if (actionNode) { |
|
795 | 842 | $('body, html').animate({scrollTop: actionNode.top}, 10); |
|
796 | 843 | } |
|
797 | 844 | }, 100); |
|
798 | 845 | } |
|
799 | 846 | |
|
800 | 847 | // add dropzone support |
|
801 | 848 | var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) { |
|
802 | 849 | var renderer = templateContext.visual.default_renderer; |
|
803 | 850 | if (renderer == 'rst') { |
|
804 | 851 | var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl); |
|
805 | 852 | if (isRendered){ |
|
806 | 853 | attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl); |
|
807 | 854 | } |
|
808 | 855 | } else if (renderer == 'markdown') { |
|
809 | 856 | var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl); |
|
810 | 857 | if (isRendered){ |
|
811 | 858 | attachmentUrl = '!' + attachmentUrl; |
|
812 | 859 | } |
|
813 | 860 | } else { |
|
814 | 861 | var attachmentUrl = '{}'.format(attachmentStoreUrl); |
|
815 | 862 | } |
|
816 | 863 | cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine())); |
|
817 | 864 | |
|
818 | 865 | return false; |
|
819 | 866 | }; |
|
820 | 867 | |
|
821 | 868 | //see: https://www.dropzonejs.com/#configuration |
|
822 | 869 | var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload', |
|
823 | 870 | {'repo_name': templateContext.repo_name, |
|
824 | 871 | 'commit_id': templateContext.commit_data.commit_id}) |
|
825 | 872 | |
|
826 | 873 | var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0); |
|
827 | 874 | if (previewTmpl !== undefined){ |
|
828 | 875 | var selectLink = $(formElement).find('.pick-attachment').get(0); |
|
829 | 876 | $(formElement).find('.comment-attachment-uploader').dropzone({ |
|
830 | 877 | url: storeUrl, |
|
831 | 878 | headers: {"X-CSRF-Token": CSRF_TOKEN}, |
|
832 | 879 | paramName: function () { |
|
833 | 880 | return "attachment" |
|
834 | 881 | }, // The name that will be used to transfer the file |
|
835 | 882 | clickable: selectLink, |
|
836 | 883 | parallelUploads: 1, |
|
837 | 884 | maxFiles: 10, |
|
838 | 885 | maxFilesize: templateContext.attachment_store.max_file_size_mb, |
|
839 | 886 | uploadMultiple: false, |
|
840 | 887 | autoProcessQueue: true, // if false queue will not be processed automatically. |
|
841 | 888 | createImageThumbnails: false, |
|
842 | 889 | previewTemplate: previewTmpl.innerHTML, |
|
843 | 890 | |
|
844 | 891 | accept: function (file, done) { |
|
845 | 892 | done(); |
|
846 | 893 | }, |
|
847 | 894 | init: function () { |
|
848 | 895 | |
|
849 | 896 | this.on("sending", function (file, xhr, formData) { |
|
850 | 897 | $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide(); |
|
851 | 898 | $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show(); |
|
852 | 899 | }); |
|
853 | 900 | |
|
854 | 901 | this.on("success", function (file, response) { |
|
855 | 902 | $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show(); |
|
856 | 903 | $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); |
|
857 | 904 | |
|
858 | 905 | var isRendered = false; |
|
859 | 906 | var ext = file.name.split('.').pop(); |
|
860 | 907 | var imageExts = templateContext.attachment_store.image_ext; |
|
861 | 908 | if (imageExts.indexOf(ext) !== -1){ |
|
862 | 909 | isRendered = true; |
|
863 | 910 | } |
|
864 | 911 | |
|
865 | 912 | insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered) |
|
866 | 913 | }); |
|
867 | 914 | |
|
868 | 915 | this.on("error", function (file, errorMessage, xhr) { |
|
869 | 916 | $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide(); |
|
870 | 917 | |
|
871 | 918 | var error = null; |
|
872 | 919 | |
|
873 | 920 | if (xhr !== undefined){ |
|
874 | 921 | var httpStatus = xhr.status + " " + xhr.statusText; |
|
875 | 922 | if (xhr !== undefined && xhr.status >= 500) { |
|
876 | 923 | error = httpStatus; |
|
877 | 924 | } |
|
878 | 925 | } |
|
879 | 926 | |
|
880 | 927 | if (error === null) { |
|
881 | 928 | error = errorMessage.error || errorMessage || httpStatus; |
|
882 | 929 | } |
|
883 | 930 | $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error)); |
|
884 | 931 | |
|
885 | 932 | }); |
|
886 | 933 | } |
|
887 | 934 | }); |
|
888 | 935 | } |
|
889 | 936 | return commentForm; |
|
890 | 937 | }; |
|
891 | 938 | |
|
892 | 939 | this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) { |
|
893 | 940 | |
|
894 | 941 | var tmpl = $('#cb-comment-general-form-template').html(); |
|
895 | 942 | tmpl = tmpl.format(null, 'general'); |
|
896 | 943 | var $form = $(tmpl); |
|
897 | 944 | |
|
898 | 945 | var $formPlaceholder = $('#cb-comment-general-form-placeholder'); |
|
899 | 946 | var curForm = $formPlaceholder.find('form'); |
|
900 | 947 | if (curForm){ |
|
901 | 948 | curForm.remove(); |
|
902 | 949 | } |
|
903 | 950 | $formPlaceholder.append($form); |
|
904 | 951 | |
|
905 | 952 | var _form = $($form[0]); |
|
906 | 953 | var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo']; |
|
907 | 954 | var edit = false; |
|
908 | 955 | var comment_id = null; |
|
909 | 956 | var commentForm = this.createCommentForm( |
|
910 | 957 | _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id); |
|
911 | 958 | commentForm.initStatusChangeSelector(); |
|
912 | 959 | |
|
913 | 960 | return commentForm; |
|
914 | 961 | }; |
|
915 | 962 | |
|
916 | 963 | this.editComment = function(node) { |
|
917 | 964 | var $node = $(node); |
|
918 | 965 | var $comment = $(node).closest('.comment'); |
|
919 |
var comment_id = $comment. |
|
|
966 | var comment_id = $($comment).data('commentId'); | |
|
967 | var isDraft = $($comment).data('commentDraft'); | |
|
920 | 968 | var $form = null |
|
921 | 969 | |
|
922 | 970 | var $comments = $node.closest('div.inline-comments'); |
|
923 | 971 | var $general_comments = null; |
|
924 | 972 | var lineno = null; |
|
925 | 973 | |
|
926 | 974 | if($comments.length){ |
|
927 | 975 | // inline comments setup |
|
928 | 976 | $form = $comments.find('.comment-inline-form'); |
|
929 | 977 | lineno = self.getLineNumber(node) |
|
930 | 978 | } |
|
931 | 979 | else{ |
|
932 | 980 | // general comments setup |
|
933 | 981 | $comments = $('#comments'); |
|
934 | 982 | $form = $comments.find('.comment-inline-form'); |
|
935 | 983 | lineno = $comment[0].id |
|
936 | 984 | $('#cb-comment-general-form-placeholder').hide(); |
|
937 | 985 | } |
|
938 | 986 | |
|
939 | 987 | this.edit = true; |
|
940 | 988 | |
|
941 | 989 | if (!$form.length) { |
|
942 | 990 | |
|
943 | 991 | var $filediff = $node.closest('.filediff'); |
|
944 | 992 | $filediff.removeClass('hide-comments'); |
|
945 | 993 | var f_path = $filediff.attr('data-f-path'); |
|
946 | 994 | |
|
947 | 995 | // create a new HTML from template |
|
948 | 996 | |
|
949 | 997 | var tmpl = $('#cb-comment-inline-form-template').html(); |
|
950 | 998 | tmpl = tmpl.format(escapeHtml(f_path), lineno); |
|
951 | 999 | $form = $(tmpl); |
|
952 | 1000 | $comment.after($form) |
|
953 | 1001 | |
|
954 | 1002 | var _form = $($form[0]).find('form'); |
|
955 | 1003 | var autocompleteActions = ['as_note',]; |
|
956 | 1004 | var commentForm = this.createCommentForm( |
|
957 | 1005 | _form, lineno, '', autocompleteActions, resolvesCommentId, |
|
958 | 1006 | this.edit, comment_id); |
|
959 | 1007 | var old_comment_text_binary = $comment.attr('data-comment-text'); |
|
960 | 1008 | var old_comment_text = b64DecodeUnicode(old_comment_text_binary); |
|
961 | 1009 | commentForm.cm.setValue(old_comment_text); |
|
962 | 1010 | $comment.hide(); |
|
963 | 1011 | |
|
964 | 1012 | $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({ |
|
965 | 1013 | form: _form, |
|
966 | 1014 | parent: $comments, |
|
967 | 1015 | lineno: lineno, |
|
968 | 1016 | f_path: f_path} |
|
969 | 1017 | ); |
|
970 | 1018 | |
|
971 | 1019 | // set a CUSTOM submit handler for inline comments. |
|
972 | 1020 | commentForm.setHandleFormSubmit(function(o) { |
|
973 | 1021 | var text = commentForm.cm.getValue(); |
|
974 | 1022 | var commentType = commentForm.getCommentType(); |
|
975 | 1023 | |
|
976 | 1024 | if (text === "") { |
|
977 | 1025 | return; |
|
978 | 1026 | } |
|
979 | 1027 | |
|
980 | 1028 | if (old_comment_text == text) { |
|
981 | 1029 | SwalNoAnimation.fire({ |
|
982 | 1030 | title: 'Unable to edit comment', |
|
983 | 1031 | html: _gettext('Comment body was not changed.'), |
|
984 | 1032 | }); |
|
985 | 1033 | return; |
|
986 | 1034 | } |
|
987 | 1035 | var excludeCancelBtn = false; |
|
988 | 1036 | var submitEvent = true; |
|
989 | 1037 | commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); |
|
990 | 1038 | commentForm.cm.setOption("readOnly", true); |
|
991 | 1039 | |
|
992 | 1040 | // Read last version known |
|
993 | 1041 | var versionSelector = $('#comment_versions_{0}'.format(comment_id)); |
|
994 | 1042 | var version = versionSelector.data('lastVersion'); |
|
995 | 1043 | |
|
996 | 1044 | if (!version) { |
|
997 | 1045 | version = 0; |
|
998 | 1046 | } |
|
999 | 1047 | |
|
1000 | 1048 | var postData = { |
|
1001 | 1049 | 'text': text, |
|
1002 | 1050 | 'f_path': f_path, |
|
1003 | 1051 | 'line': lineno, |
|
1004 | 1052 | 'comment_type': commentType, |
|
1053 | 'draft': isDraft, | |
|
1005 | 1054 | 'version': version, |
|
1006 | 1055 | 'csrf_token': CSRF_TOKEN |
|
1007 | 1056 | }; |
|
1008 | 1057 | |
|
1009 | 1058 | var submitSuccessCallback = function(json_data) { |
|
1010 | 1059 | $form.remove(); |
|
1011 | 1060 | $comment.show(); |
|
1012 | 1061 | var postData = { |
|
1013 | 1062 | 'text': text, |
|
1014 | 1063 | 'renderer': $comment.attr('data-comment-renderer'), |
|
1015 | 1064 | 'csrf_token': CSRF_TOKEN |
|
1016 | 1065 | }; |
|
1017 | 1066 | |
|
1018 | 1067 | /* Inject new edited version selector */ |
|
1019 | 1068 | var updateCommentVersionDropDown = function () { |
|
1020 | 1069 | var versionSelectId = '#comment_versions_'+comment_id; |
|
1021 | 1070 | var preLoadVersionData = [ |
|
1022 | 1071 | { |
|
1023 | 1072 | id: json_data['comment_version'], |
|
1024 | 1073 | text: "v{0}".format(json_data['comment_version']), |
|
1025 | 1074 | action: function () { |
|
1026 | 1075 | Rhodecode.comments.showVersion( |
|
1027 | 1076 | json_data['comment_id'], |
|
1028 | 1077 | json_data['comment_history_id'] |
|
1029 | 1078 | ) |
|
1030 | 1079 | }, |
|
1031 | 1080 | comment_version: json_data['comment_version'], |
|
1032 | 1081 | comment_author_username: json_data['comment_author_username'], |
|
1033 | 1082 | comment_author_gravatar: json_data['comment_author_gravatar'], |
|
1034 | 1083 | comment_created_on: json_data['comment_created_on'], |
|
1035 | 1084 | }, |
|
1036 | 1085 | ] |
|
1037 | 1086 | |
|
1038 | 1087 | |
|
1039 | 1088 | if ($(versionSelectId).data('select2')) { |
|
1040 | 1089 | var oldData = $(versionSelectId).data('select2').opts.data.results; |
|
1041 | 1090 | $(versionSelectId).select2("destroy"); |
|
1042 | 1091 | preLoadVersionData = oldData.concat(preLoadVersionData) |
|
1043 | 1092 | } |
|
1044 | 1093 | |
|
1045 | 1094 | initVersionSelector(versionSelectId, {results: preLoadVersionData}); |
|
1046 | 1095 | |
|
1047 | 1096 | $comment.attr('data-comment-text', utf8ToB64(text)); |
|
1048 | 1097 | |
|
1049 | 1098 | var versionSelector = $('#comment_versions_'+comment_id); |
|
1050 | 1099 | |
|
1051 | 1100 | // set lastVersion so we know our last edit version |
|
1052 | 1101 | versionSelector.data('lastVersion', json_data['comment_version']) |
|
1053 | 1102 | versionSelector.parent().show(); |
|
1054 | 1103 | } |
|
1055 | 1104 | updateCommentVersionDropDown(); |
|
1056 | 1105 | |
|
1057 | 1106 | // by default we reset state of comment preserving the text |
|
1058 | 1107 | var failRenderCommit = function(jqXHR, textStatus, errorThrown) { |
|
1059 | 1108 | var prefix = "Error while editing this comment.\n" |
|
1060 | 1109 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
1061 | 1110 | ajaxErrorSwal(message); |
|
1062 | 1111 | }; |
|
1063 | 1112 | |
|
1064 | 1113 | var successRenderCommit = function(o){ |
|
1065 | 1114 | $comment.show(); |
|
1066 | 1115 | $comment[0].lastElementChild.innerHTML = o; |
|
1067 | 1116 | }; |
|
1068 | 1117 | |
|
1069 | 1118 | var previewUrl = pyroutes.url( |
|
1070 | 1119 | 'repo_commit_comment_preview', |
|
1071 | 1120 | {'repo_name': templateContext.repo_name, |
|
1072 | 1121 | 'commit_id': templateContext.commit_data.commit_id}); |
|
1073 | 1122 | |
|
1074 | 1123 | _submitAjaxPOST( |
|
1075 | 1124 | previewUrl, postData, successRenderCommit, |
|
1076 | 1125 | failRenderCommit |
|
1077 | 1126 | ); |
|
1078 | 1127 | |
|
1079 | 1128 | try { |
|
1080 | 1129 | var html = json_data.rendered_text; |
|
1081 | 1130 | var lineno = json_data.line_no; |
|
1082 | 1131 | var target_id = json_data.target_id; |
|
1083 | 1132 | |
|
1084 | 1133 | $comments.find('.cb-comment-add-button').before(html); |
|
1085 | 1134 | |
|
1086 | 1135 | // run global callback on submit |
|
1087 | commentForm.globalSubmitSuccessCallback(); | |
|
1136 | commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id}); | |
|
1088 | 1137 | |
|
1089 | 1138 | } catch (e) { |
|
1090 | 1139 | console.error(e); |
|
1091 | 1140 | } |
|
1092 | 1141 | |
|
1093 | 1142 | // re trigger the linkification of next/prev navigation |
|
1094 | 1143 | linkifyComments($('.inline-comment-injected')); |
|
1095 | 1144 | timeagoActivate(); |
|
1096 | 1145 | tooltipActivate(); |
|
1097 | 1146 | |
|
1098 | 1147 | if (window.updateSticky !== undefined) { |
|
1099 | 1148 | // potentially our comments change the active window size, so we |
|
1100 | 1149 | // notify sticky elements |
|
1101 | 1150 | updateSticky() |
|
1102 | 1151 | } |
|
1103 | 1152 | |
|
1104 | if (window.refreshAllComments !== undefined) { | |
|
1153 | if (window.refreshAllComments !== undefined && !isDraft) { | |
|
1105 | 1154 | // if we have this handler, run it, and refresh all comments boxes |
|
1106 | 1155 | refreshAllComments() |
|
1107 | 1156 | } |
|
1108 | 1157 | |
|
1109 | 1158 | commentForm.setActionButtonsDisabled(false); |
|
1110 | 1159 | |
|
1111 | 1160 | }; |
|
1112 | 1161 | |
|
1113 | 1162 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { |
|
1114 | 1163 | var prefix = "Error while editing comment.\n" |
|
1115 | 1164 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
1116 | 1165 | if (jqXHR.status == 409){ |
|
1117 | 1166 | message = 'This comment was probably changed somewhere else. Please reload the content of this comment.' |
|
1118 | 1167 | ajaxErrorSwal(message, 'Comment version mismatch.'); |
|
1119 | 1168 | } else { |
|
1120 | 1169 | ajaxErrorSwal(message); |
|
1121 | 1170 | } |
|
1122 | 1171 | |
|
1123 | 1172 | commentForm.resetCommentFormState(text) |
|
1124 | 1173 | }; |
|
1125 | 1174 | commentForm.submitAjaxPOST( |
|
1126 | 1175 | commentForm.submitUrl, postData, |
|
1127 | 1176 | submitSuccessCallback, |
|
1128 | 1177 | submitFailCallback); |
|
1129 | 1178 | }); |
|
1130 | 1179 | } |
|
1131 | 1180 | |
|
1132 | 1181 | $form.addClass('comment-inline-form-open'); |
|
1133 | 1182 | }; |
|
1134 | 1183 | |
|
1135 | 1184 | this.createComment = function(node, resolutionComment) { |
|
1136 | 1185 | var resolvesCommentId = resolutionComment || null; |
|
1137 | 1186 | var $node = $(node); |
|
1138 | 1187 | var $td = $node.closest('td'); |
|
1139 | 1188 | var $form = $td.find('.comment-inline-form'); |
|
1140 | 1189 | this.edit = false; |
|
1141 | 1190 | |
|
1142 | 1191 | if (!$form.length) { |
|
1143 | 1192 | |
|
1144 | 1193 | var $filediff = $node.closest('.filediff'); |
|
1145 | 1194 | $filediff.removeClass('hide-comments'); |
|
1146 | 1195 | var f_path = $filediff.attr('data-f-path'); |
|
1147 | 1196 | var lineno = self.getLineNumber(node); |
|
1148 | 1197 | // create a new HTML from template |
|
1149 | 1198 | var tmpl = $('#cb-comment-inline-form-template').html(); |
|
1150 | 1199 | tmpl = tmpl.format(escapeHtml(f_path), lineno); |
|
1151 | 1200 | $form = $(tmpl); |
|
1152 | 1201 | |
|
1153 | 1202 | var $comments = $td.find('.inline-comments'); |
|
1154 | 1203 | if (!$comments.length) { |
|
1155 | 1204 | $comments = $( |
|
1156 | 1205 | $('#cb-comments-inline-container-template').html()); |
|
1157 | 1206 | $td.append($comments); |
|
1158 | 1207 | } |
|
1159 | 1208 | |
|
1160 | 1209 | $td.find('.cb-comment-add-button').before($form); |
|
1161 | 1210 | |
|
1162 | 1211 | var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno); |
|
1163 | 1212 | var _form = $($form[0]).find('form'); |
|
1164 | 1213 | var autocompleteActions = ['as_note', 'as_todo']; |
|
1165 | 1214 | var comment_id=null; |
|
1166 | 1215 | var commentForm = this.createCommentForm( |
|
1167 | 1216 | _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id); |
|
1168 | 1217 | |
|
1169 | 1218 | $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({ |
|
1170 | 1219 | form: _form, |
|
1171 | 1220 | parent: $td[0], |
|
1172 | 1221 | lineno: lineno, |
|
1173 | 1222 | f_path: f_path} |
|
1174 | 1223 | ); |
|
1175 | 1224 | |
|
1176 | 1225 | // set a CUSTOM submit handler for inline comments. |
|
1177 | 1226 | commentForm.setHandleFormSubmit(function(o) { |
|
1178 | 1227 | var text = commentForm.cm.getValue(); |
|
1179 | 1228 | var commentType = commentForm.getCommentType(); |
|
1180 | 1229 | var resolvesCommentId = commentForm.getResolvesId(); |
|
1230 | var isDraft = commentForm.getDraftState(); | |
|
1181 | 1231 | |
|
1182 | 1232 | if (text === "") { |
|
1183 | 1233 | return; |
|
1184 | 1234 | } |
|
1185 | 1235 | |
|
1186 | 1236 | if (lineno === undefined) { |
|
1187 | 1237 | alert('missing line !'); |
|
1188 | 1238 | return; |
|
1189 | 1239 | } |
|
1190 | 1240 | if (f_path === undefined) { |
|
1191 | 1241 | alert('missing file path !'); |
|
1192 | 1242 | return; |
|
1193 | 1243 | } |
|
1194 | 1244 | |
|
1195 | 1245 | var excludeCancelBtn = false; |
|
1196 | 1246 | var submitEvent = true; |
|
1197 | 1247 | commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent); |
|
1198 | 1248 | commentForm.cm.setOption("readOnly", true); |
|
1199 | 1249 | var postData = { |
|
1200 | 1250 | 'text': text, |
|
1201 | 1251 | 'f_path': f_path, |
|
1202 | 1252 | 'line': lineno, |
|
1203 | 1253 | 'comment_type': commentType, |
|
1254 | 'draft': isDraft, | |
|
1204 | 1255 | 'csrf_token': CSRF_TOKEN |
|
1205 | 1256 | }; |
|
1206 | 1257 | if (resolvesCommentId){ |
|
1207 | 1258 | postData['resolves_comment_id'] = resolvesCommentId; |
|
1208 | 1259 | } |
|
1209 | 1260 | |
|
1210 | 1261 | var submitSuccessCallback = function(json_data) { |
|
1211 | 1262 | $form.remove(); |
|
1212 | 1263 | try { |
|
1213 | 1264 | var html = json_data.rendered_text; |
|
1214 | 1265 | var lineno = json_data.line_no; |
|
1215 | 1266 | var target_id = json_data.target_id; |
|
1216 | 1267 | |
|
1217 | 1268 | $comments.find('.cb-comment-add-button').before(html); |
|
1218 | 1269 | |
|
1219 | 1270 | //mark visually which comment was resolved |
|
1220 | 1271 | if (resolvesCommentId) { |
|
1221 | 1272 | commentForm.markCommentResolved(resolvesCommentId); |
|
1222 | 1273 | } |
|
1223 | 1274 | |
|
1224 | 1275 | // run global callback on submit |
|
1225 | commentForm.globalSubmitSuccessCallback(); | |
|
1276 | commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id}); | |
|
1226 | 1277 | |
|
1227 | 1278 | } catch (e) { |
|
1228 | 1279 | console.error(e); |
|
1229 | 1280 | } |
|
1230 | 1281 | |
|
1231 | 1282 | // re trigger the linkification of next/prev navigation |
|
1232 | 1283 | linkifyComments($('.inline-comment-injected')); |
|
1233 | 1284 | timeagoActivate(); |
|
1234 | 1285 | tooltipActivate(); |
|
1235 | 1286 | |
|
1236 | 1287 | if (window.updateSticky !== undefined) { |
|
1237 | 1288 | // potentially our comments change the active window size, so we |
|
1238 | 1289 | // notify sticky elements |
|
1239 | 1290 | updateSticky() |
|
1240 | 1291 | } |
|
1241 | 1292 | |
|
1242 | if (window.refreshAllComments !== undefined) { | |
|
1293 | if (window.refreshAllComments !== undefined && !isDraft) { | |
|
1243 | 1294 | // if we have this handler, run it, and refresh all comments boxes |
|
1244 | 1295 | refreshAllComments() |
|
1245 | 1296 | } |
|
1246 | 1297 | |
|
1247 | 1298 | commentForm.setActionButtonsDisabled(false); |
|
1248 | 1299 | |
|
1249 | 1300 | }; |
|
1250 | 1301 | var submitFailCallback = function(jqXHR, textStatus, errorThrown) { |
|
1251 | 1302 | var prefix = "Error while submitting comment.\n" |
|
1252 | 1303 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix); |
|
1253 | 1304 | ajaxErrorSwal(message); |
|
1254 | 1305 | commentForm.resetCommentFormState(text) |
|
1255 | 1306 | }; |
|
1256 | 1307 | commentForm.submitAjaxPOST( |
|
1257 | 1308 | commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback); |
|
1258 | 1309 | }); |
|
1259 | 1310 | } |
|
1260 | 1311 | |
|
1261 | 1312 | $form.addClass('comment-inline-form-open'); |
|
1262 | 1313 | }; |
|
1263 | 1314 | |
|
1264 | 1315 | this.createResolutionComment = function(commentId){ |
|
1265 | 1316 | // hide the trigger text |
|
1266 | 1317 | $('#resolve-comment-{0}'.format(commentId)).hide(); |
|
1267 | 1318 | |
|
1268 | 1319 | var comment = $('#comment-'+commentId); |
|
1269 | 1320 | var commentData = comment.data(); |
|
1270 | 1321 | if (commentData.commentInline) { |
|
1271 | 1322 | this.createComment(comment, commentId) |
|
1272 | 1323 | } else { |
|
1273 | 1324 | Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId) |
|
1274 | 1325 | } |
|
1275 | 1326 | |
|
1276 | 1327 | return false; |
|
1277 | 1328 | }; |
|
1278 | 1329 | |
|
1279 | 1330 | this.submitResolution = function(commentId){ |
|
1280 | 1331 | var form = $('#resolve_comment_{0}'.format(commentId)).closest('form'); |
|
1281 | 1332 | var commentForm = form.get(0).CommentForm; |
|
1282 | 1333 | |
|
1283 | 1334 | var cm = commentForm.getCmInstance(); |
|
1284 | 1335 | var renderer = templateContext.visual.default_renderer; |
|
1285 | 1336 | if (renderer == 'rst'){ |
|
1286 | 1337 | var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl); |
|
1287 | 1338 | } else if (renderer == 'markdown') { |
|
1288 | 1339 | var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl); |
|
1289 | 1340 | } else { |
|
1290 | 1341 | var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl); |
|
1291 | 1342 | } |
|
1292 | 1343 | |
|
1293 | 1344 | cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl)); |
|
1294 | 1345 | form.submit(); |
|
1295 | 1346 | return false; |
|
1296 | 1347 | }; |
|
1297 | 1348 | |
|
1298 | 1349 | }; |
@@ -1,1254 +1,1262 b'' | |||
|
1 | 1 | ## -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | <%! |
|
4 | 4 | from rhodecode.lib import html_filters |
|
5 | 5 | %> |
|
6 | 6 | |
|
7 | 7 | <%inherit file="root.mako"/> |
|
8 | 8 | |
|
9 | 9 | <%include file="/ejs_templates/templates.html"/> |
|
10 | 10 | |
|
11 | 11 | <div class="outerwrapper"> |
|
12 | 12 | <!-- HEADER --> |
|
13 | 13 | <div class="header"> |
|
14 | 14 | <div id="header-inner" class="wrapper"> |
|
15 | 15 | <div id="logo"> |
|
16 | 16 | <div class="logo-wrapper"> |
|
17 | 17 | <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a> |
|
18 | 18 | </div> |
|
19 | 19 | % if c.rhodecode_name: |
|
20 | 20 | <div class="branding"> |
|
21 | 21 | <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a> |
|
22 | 22 | </div> |
|
23 | 23 | % endif |
|
24 | 24 | </div> |
|
25 | 25 | <!-- MENU BAR NAV --> |
|
26 | 26 | ${self.menu_bar_nav()} |
|
27 | 27 | <!-- END MENU BAR NAV --> |
|
28 | 28 | </div> |
|
29 | 29 | </div> |
|
30 | 30 | ${self.menu_bar_subnav()} |
|
31 | 31 | <!-- END HEADER --> |
|
32 | 32 | |
|
33 | 33 | <!-- CONTENT --> |
|
34 | 34 | <div id="content" class="wrapper"> |
|
35 | 35 | |
|
36 | 36 | <rhodecode-toast id="notifications"></rhodecode-toast> |
|
37 | 37 | |
|
38 | 38 | <div class="main"> |
|
39 | 39 | ${next.main()} |
|
40 | 40 | </div> |
|
41 | 41 | |
|
42 | 42 | </div> |
|
43 | 43 | <!-- END CONTENT --> |
|
44 | 44 | |
|
45 | 45 | </div> |
|
46 | 46 | |
|
47 | 47 | <!-- FOOTER --> |
|
48 | 48 | <div id="footer"> |
|
49 | 49 | <div id="footer-inner" class="title wrapper"> |
|
50 | 50 | <div> |
|
51 | 51 | <% sid = 'block' if request.GET.get('showrcid') else 'none' %> |
|
52 | 52 | |
|
53 | 53 | <p class="footer-link-right"> |
|
54 | 54 | <a class="grey-link-action" href="${h.route_path('home', _query={'showrcid': 1})}"> |
|
55 | 55 | RhodeCode |
|
56 | 56 | % if c.visual.show_version: |
|
57 | 57 | ${c.rhodecode_version} |
|
58 | 58 | % endif |
|
59 | 59 | ${c.rhodecode_edition} |
|
60 | 60 | </a> | |
|
61 | 61 | |
|
62 | 62 | % if c.visual.rhodecode_support_url: |
|
63 | 63 | <a class="grey-link-action" href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a> | |
|
64 | 64 | <a class="grey-link-action" href="https://docs.rhodecode.com" target="_blank">${_('Documentation')}</a> |
|
65 | 65 | % endif |
|
66 | 66 | |
|
67 | 67 | </p> |
|
68 | 68 | |
|
69 | 69 | <p class="server-instance" style="display:${sid}"> |
|
70 | 70 | ## display hidden instance ID if specially defined |
|
71 | 71 | © 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved. |
|
72 | 72 | % if c.rhodecode_instanceid: |
|
73 | 73 | ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)} |
|
74 | 74 | % endif |
|
75 | 75 | </p> |
|
76 | 76 | </div> |
|
77 | 77 | </div> |
|
78 | 78 | </div> |
|
79 | 79 | |
|
80 | 80 | <!-- END FOOTER --> |
|
81 | 81 | |
|
82 | 82 | ### MAKO DEFS ### |
|
83 | 83 | |
|
84 | 84 | <%def name="menu_bar_subnav()"> |
|
85 | 85 | </%def> |
|
86 | 86 | |
|
87 | 87 | <%def name="breadcrumbs(class_='breadcrumbs')"> |
|
88 | 88 | <div class="${class_}"> |
|
89 | 89 | ${self.breadcrumbs_links()} |
|
90 | 90 | </div> |
|
91 | 91 | </%def> |
|
92 | 92 | |
|
93 | 93 | <%def name="admin_menu(active=None)"> |
|
94 | 94 | |
|
95 | 95 | <div id="context-bar"> |
|
96 | 96 | <div class="wrapper"> |
|
97 | 97 | <div class="title"> |
|
98 | 98 | <div class="title-content"> |
|
99 | 99 | <div class="title-main"> |
|
100 | 100 | % if c.is_super_admin: |
|
101 | 101 | ${_('Super-admin Panel')} |
|
102 | 102 | % else: |
|
103 | 103 | ${_('Delegated Admin Panel')} |
|
104 | 104 | % endif |
|
105 | 105 | </div> |
|
106 | 106 | </div> |
|
107 | 107 | </div> |
|
108 | 108 | |
|
109 | 109 | <ul id="context-pages" class="navigation horizontal-list"> |
|
110 | 110 | |
|
111 | 111 | ## super-admin case |
|
112 | 112 | % if c.is_super_admin: |
|
113 | 113 | <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li> |
|
114 | 114 | <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li> |
|
115 | 115 | <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li> |
|
116 | 116 | <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li> |
|
117 | 117 | <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li> |
|
118 | 118 | <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li> |
|
119 | 119 | <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li> |
|
120 | 120 | <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li> |
|
121 | 121 | <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li> |
|
122 | 122 | <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li> |
|
123 | 123 | |
|
124 | 124 | ## delegated admin |
|
125 | 125 | % elif c.is_delegated_admin: |
|
126 | 126 | <% |
|
127 | 127 | repositories=c.auth_user.repositories_admin or c.can_create_repo |
|
128 | 128 | repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group |
|
129 | 129 | user_groups=c.auth_user.user_groups_admin or c.can_create_user_group |
|
130 | 130 | %> |
|
131 | 131 | |
|
132 | 132 | %if repositories: |
|
133 | 133 | <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li> |
|
134 | 134 | %endif |
|
135 | 135 | %if repository_groups: |
|
136 | 136 | <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li> |
|
137 | 137 | %endif |
|
138 | 138 | %if user_groups: |
|
139 | 139 | <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li> |
|
140 | 140 | %endif |
|
141 | 141 | % endif |
|
142 | 142 | </ul> |
|
143 | 143 | |
|
144 | 144 | </div> |
|
145 | 145 | <div class="clear"></div> |
|
146 | 146 | </div> |
|
147 | 147 | </%def> |
|
148 | 148 | |
|
149 | 149 | <%def name="dt_info_panel(elements)"> |
|
150 | 150 | <dl class="dl-horizontal"> |
|
151 | 151 | %for dt, dd, title, show_items in elements: |
|
152 | 152 | <dt>${dt}:</dt> |
|
153 | 153 | <dd title="${h.tooltip(title)}"> |
|
154 | 154 | %if callable(dd): |
|
155 | 155 | ## allow lazy evaluation of elements |
|
156 | 156 | ${dd()} |
|
157 | 157 | %else: |
|
158 | 158 | ${dd} |
|
159 | 159 | %endif |
|
160 | 160 | %if show_items: |
|
161 | 161 | <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span> |
|
162 | 162 | %endif |
|
163 | 163 | </dd> |
|
164 | 164 | |
|
165 | 165 | %if show_items: |
|
166 | 166 | <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none"> |
|
167 | 167 | %for item in show_items: |
|
168 | 168 | <dt></dt> |
|
169 | 169 | <dd>${item}</dd> |
|
170 | 170 | %endfor |
|
171 | 171 | </div> |
|
172 | 172 | %endif |
|
173 | 173 | |
|
174 | 174 | %endfor |
|
175 | 175 | </dl> |
|
176 | 176 | </%def> |
|
177 | 177 | |
|
178 | 178 | <%def name="tr_info_entry(element)"> |
|
179 | 179 | <% key, val, title, show_items = element %> |
|
180 | 180 | |
|
181 | 181 | <tr> |
|
182 | 182 | <td style="vertical-align: top">${key}</td> |
|
183 | 183 | <td title="${h.tooltip(title)}"> |
|
184 | 184 | %if callable(val): |
|
185 | 185 | ## allow lazy evaluation of elements |
|
186 | 186 | ${val()} |
|
187 | 187 | %else: |
|
188 | 188 | ${val} |
|
189 | 189 | %endif |
|
190 | 190 | %if show_items: |
|
191 | 191 | <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none"> |
|
192 | 192 | % for item in show_items: |
|
193 | 193 | <dt></dt> |
|
194 | 194 | <dd>${item}</dd> |
|
195 | 195 | % endfor |
|
196 | 196 | </div> |
|
197 | 197 | %endif |
|
198 | 198 | </td> |
|
199 | 199 | <td style="vertical-align: top"> |
|
200 | 200 | %if show_items: |
|
201 | 201 | <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span> |
|
202 | 202 | %endif |
|
203 | 203 | </td> |
|
204 | 204 | </tr> |
|
205 | 205 | |
|
206 | 206 | </%def> |
|
207 | 207 | |
|
208 | 208 | <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)"> |
|
209 | 209 | <% |
|
210 | 210 | if size > 16: |
|
211 | 211 | gravatar_class = ['gravatar','gravatar-large'] |
|
212 | 212 | else: |
|
213 | 213 | gravatar_class = ['gravatar'] |
|
214 | 214 | |
|
215 | 215 | data_hovercard_url = '' |
|
216 | 216 | data_hovercard_alt = tooltip_alt.replace('<', '<').replace('>', '>') if tooltip_alt else '' |
|
217 | 217 | |
|
218 | 218 | if tooltip: |
|
219 | 219 | gravatar_class += ['tooltip-hovercard'] |
|
220 | 220 | if extra_class: |
|
221 | 221 | gravatar_class += extra_class |
|
222 | 222 | if tooltip and user: |
|
223 | 223 | if user.username == h.DEFAULT_USER: |
|
224 | 224 | gravatar_class.pop(-1) |
|
225 | 225 | else: |
|
226 | 226 | data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', '')) |
|
227 | 227 | gravatar_class = ' '.join(gravatar_class) |
|
228 | 228 | |
|
229 | 229 | %> |
|
230 | 230 | <%doc> |
|
231 | 231 | TODO: johbo: For now we serve double size images to make it smooth |
|
232 | 232 | for retina. This is how it worked until now. Should be replaced |
|
233 | 233 | with a better solution at some point. |
|
234 | 234 | </%doc> |
|
235 | 235 | |
|
236 | 236 | <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" /> |
|
237 | 237 | </%def> |
|
238 | 238 | |
|
239 | 239 | |
|
240 | 240 | <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')"> |
|
241 | 241 | <% |
|
242 | 242 | email = h.email_or_none(contact) |
|
243 | 243 | rc_user = h.discover_user(contact) |
|
244 | 244 | %> |
|
245 | 245 | |
|
246 | 246 | <div class="${_class}"> |
|
247 | 247 | ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)} |
|
248 | 248 | <span class="${('user user-disabled' if show_disabled else 'user')}"> |
|
249 | 249 | ${h.link_to_user(rc_user or contact)} |
|
250 | 250 | </span> |
|
251 | 251 | </div> |
|
252 | 252 | </%def> |
|
253 | 253 | |
|
254 | 254 | |
|
255 | 255 | <%def name="user_group_icon(user_group=None, size=16, tooltip=False)"> |
|
256 | 256 | <% |
|
257 | 257 | if (size > 16): |
|
258 | 258 | gravatar_class = 'icon-user-group-alt' |
|
259 | 259 | else: |
|
260 | 260 | gravatar_class = 'icon-user-group-alt' |
|
261 | 261 | |
|
262 | 262 | if tooltip: |
|
263 | 263 | gravatar_class += ' tooltip-hovercard' |
|
264 | 264 | |
|
265 | 265 | data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id) |
|
266 | 266 | %> |
|
267 | 267 | <%doc> |
|
268 | 268 | TODO: johbo: For now we serve double size images to make it smooth |
|
269 | 269 | for retina. This is how it worked until now. Should be replaced |
|
270 | 270 | with a better solution at some point. |
|
271 | 271 | </%doc> |
|
272 | 272 | |
|
273 | 273 | <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i> |
|
274 | 274 | </%def> |
|
275 | 275 | |
|
276 | 276 | <%def name="repo_page_title(repo_instance)"> |
|
277 | 277 | <div class="title-content repo-title"> |
|
278 | 278 | |
|
279 | 279 | <div class="title-main"> |
|
280 | 280 | ## SVN/HG/GIT icons |
|
281 | 281 | %if h.is_hg(repo_instance): |
|
282 | 282 | <i class="icon-hg"></i> |
|
283 | 283 | %endif |
|
284 | 284 | %if h.is_git(repo_instance): |
|
285 | 285 | <i class="icon-git"></i> |
|
286 | 286 | %endif |
|
287 | 287 | %if h.is_svn(repo_instance): |
|
288 | 288 | <i class="icon-svn"></i> |
|
289 | 289 | %endif |
|
290 | 290 | |
|
291 | 291 | ## public/private |
|
292 | 292 | %if repo_instance.private: |
|
293 | 293 | <i class="icon-repo-private"></i> |
|
294 | 294 | %else: |
|
295 | 295 | <i class="icon-repo-public"></i> |
|
296 | 296 | %endif |
|
297 | 297 | |
|
298 | 298 | ## repo name with group name |
|
299 | 299 | ${h.breadcrumb_repo_link(repo_instance)} |
|
300 | 300 | |
|
301 | 301 | ## Context Actions |
|
302 | 302 | <div class="pull-right"> |
|
303 | 303 | %if c.rhodecode_user.username != h.DEFAULT_USER: |
|
304 | 304 | <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a> |
|
305 | 305 | |
|
306 | 306 | <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}"> |
|
307 | 307 | % if c.repository_is_user_following: |
|
308 | 308 | <i class="icon-eye-off"></i>${_('Unwatch')} |
|
309 | 309 | % else: |
|
310 | 310 | <i class="icon-eye"></i>${_('Watch')} |
|
311 | 311 | % endif |
|
312 | 312 | |
|
313 | 313 | </a> |
|
314 | 314 | %else: |
|
315 | 315 | <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a> |
|
316 | 316 | %endif |
|
317 | 317 | </div> |
|
318 | 318 | |
|
319 | 319 | </div> |
|
320 | 320 | |
|
321 | 321 | ## FORKED |
|
322 | 322 | %if repo_instance.fork: |
|
323 | 323 | <p class="discreet"> |
|
324 | 324 | <i class="icon-code-fork"></i> ${_('Fork of')} |
|
325 | 325 | ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))} |
|
326 | 326 | </p> |
|
327 | 327 | %endif |
|
328 | 328 | |
|
329 | 329 | ## IMPORTED FROM REMOTE |
|
330 | 330 | %if repo_instance.clone_uri: |
|
331 | 331 | <p class="discreet"> |
|
332 | 332 | <i class="icon-code-fork"></i> ${_('Clone from')} |
|
333 | 333 | <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a> |
|
334 | 334 | </p> |
|
335 | 335 | %endif |
|
336 | 336 | |
|
337 | 337 | ## LOCKING STATUS |
|
338 | 338 | %if repo_instance.locked[0]: |
|
339 | 339 | <p class="locking_locked discreet"> |
|
340 | 340 | <i class="icon-repo-lock"></i> |
|
341 | 341 | ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}} |
|
342 | 342 | </p> |
|
343 | 343 | %elif repo_instance.enable_locking: |
|
344 | 344 | <p class="locking_unlocked discreet"> |
|
345 | 345 | <i class="icon-repo-unlock"></i> |
|
346 | 346 | ${_('Repository not locked. Pull repository to lock it.')} |
|
347 | 347 | </p> |
|
348 | 348 | %endif |
|
349 | 349 | |
|
350 | 350 | </div> |
|
351 | 351 | </%def> |
|
352 | 352 | |
|
353 | 353 | <%def name="repo_menu(active=None)"> |
|
354 | 354 | <% |
|
355 | 355 | ## determine if we have "any" option available |
|
356 | 356 | can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking |
|
357 | 357 | has_actions = can_lock |
|
358 | 358 | |
|
359 | 359 | %> |
|
360 | 360 | % if c.rhodecode_db_repo.archived: |
|
361 | 361 | <div class="alert alert-warning text-center"> |
|
362 | 362 | <strong>${_('This repository has been archived. It is now read-only.')}</strong> |
|
363 | 363 | </div> |
|
364 | 364 | % endif |
|
365 | 365 | |
|
366 | 366 | <!--- REPO CONTEXT BAR --> |
|
367 | 367 | <div id="context-bar"> |
|
368 | 368 | <div class="wrapper"> |
|
369 | 369 | |
|
370 | 370 | <div class="title"> |
|
371 | 371 | ${self.repo_page_title(c.rhodecode_db_repo)} |
|
372 | 372 | </div> |
|
373 | 373 | |
|
374 | 374 | <ul id="context-pages" class="navigation horizontal-list"> |
|
375 | 375 | <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary_explicit', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li> |
|
376 | 376 | <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li> |
|
377 | 377 | <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.repo_files_by_ref_url(c.repo_name, c.rhodecode_db_repo.repo_type, f_path='', ref_name=c.rhodecode_db_repo.landing_ref_name, commit_id='tip', query={'at':c.rhodecode_db_repo.landing_ref_name})}"><div class="menulabel">${_('Files')}</div></a></li> |
|
378 | 378 | <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li> |
|
379 | 379 | |
|
380 | 380 | ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()" |
|
381 | 381 | %if c.rhodecode_db_repo.repo_type in ['git','hg']: |
|
382 | 382 | <li class="${h.is_active('showpullrequest', active)}"> |
|
383 | 383 | <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}"> |
|
384 | 384 | <div class="menulabel"> |
|
385 | 385 | ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span> |
|
386 | 386 | </div> |
|
387 | 387 | </a> |
|
388 | 388 | </li> |
|
389 | 389 | %endif |
|
390 | 390 | |
|
391 | 391 | <li class="${h.is_active('artifacts', active)}"> |
|
392 | 392 | <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}"> |
|
393 | 393 | <div class="menulabel"> |
|
394 | 394 | ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span> |
|
395 | 395 | </div> |
|
396 | 396 | </a> |
|
397 | 397 | </li> |
|
398 | 398 | |
|
399 | 399 | %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name): |
|
400 | 400 | <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li> |
|
401 | 401 | %endif |
|
402 | 402 | |
|
403 | 403 | <li class="${h.is_active('options', active)}"> |
|
404 | 404 | % if has_actions: |
|
405 | 405 | <a class="menulink dropdown"> |
|
406 | 406 | <div class="menulabel">${_('Options')}<div class="show_more"></div></div> |
|
407 | 407 | </a> |
|
408 | 408 | <ul class="submenu"> |
|
409 | 409 | %if can_lock: |
|
410 | 410 | %if c.rhodecode_db_repo.locked[0]: |
|
411 | 411 | <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li> |
|
412 | 412 | %else: |
|
413 | 413 | <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li> |
|
414 | 414 | %endif |
|
415 | 415 | %endif |
|
416 | 416 | </ul> |
|
417 | 417 | % endif |
|
418 | 418 | </li> |
|
419 | 419 | |
|
420 | 420 | </ul> |
|
421 | 421 | </div> |
|
422 | 422 | <div class="clear"></div> |
|
423 | 423 | </div> |
|
424 | 424 | |
|
425 | 425 | <!--- REPO END CONTEXT BAR --> |
|
426 | 426 | |
|
427 | 427 | </%def> |
|
428 | 428 | |
|
429 | 429 | <%def name="repo_group_page_title(repo_group_instance)"> |
|
430 | 430 | <div class="title-content"> |
|
431 | 431 | <div class="title-main"> |
|
432 | 432 | ## Repository Group icon |
|
433 | 433 | <i class="icon-repo-group"></i> |
|
434 | 434 | |
|
435 | 435 | ## repo name with group name |
|
436 | 436 | ${h.breadcrumb_repo_group_link(repo_group_instance)} |
|
437 | 437 | </div> |
|
438 | 438 | |
|
439 | 439 | <%namespace name="dt" file="/data_table/_dt_elements.mako"/> |
|
440 | 440 | <div class="repo-group-desc discreet"> |
|
441 | 441 | ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)} |
|
442 | 442 | </div> |
|
443 | 443 | |
|
444 | 444 | </div> |
|
445 | 445 | </%def> |
|
446 | 446 | |
|
447 | 447 | |
|
448 | 448 | <%def name="repo_group_menu(active=None)"> |
|
449 | 449 | <% |
|
450 | 450 | gr_name = c.repo_group.group_name if c.repo_group else None |
|
451 | 451 | # create repositories with write permission on group is set to true |
|
452 | 452 | group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page') |
|
453 | 453 | |
|
454 | 454 | %> |
|
455 | 455 | |
|
456 | 456 | |
|
457 | 457 | <!--- REPO GROUP CONTEXT BAR --> |
|
458 | 458 | <div id="context-bar"> |
|
459 | 459 | <div class="wrapper"> |
|
460 | 460 | <div class="title"> |
|
461 | 461 | ${self.repo_group_page_title(c.repo_group)} |
|
462 | 462 | </div> |
|
463 | 463 | |
|
464 | 464 | <ul id="context-pages" class="navigation horizontal-list"> |
|
465 | 465 | <li class="${h.is_active('home', active)}"> |
|
466 | 466 | <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a> |
|
467 | 467 | </li> |
|
468 | 468 | % if c.is_super_admin or group_admin: |
|
469 | 469 | <li class="${h.is_active('settings', active)}"> |
|
470 | 470 | <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a> |
|
471 | 471 | </li> |
|
472 | 472 | % endif |
|
473 | 473 | |
|
474 | 474 | </ul> |
|
475 | 475 | </div> |
|
476 | 476 | <div class="clear"></div> |
|
477 | 477 | </div> |
|
478 | 478 | |
|
479 | 479 | <!--- REPO GROUP CONTEXT BAR --> |
|
480 | 480 | |
|
481 | 481 | </%def> |
|
482 | 482 | |
|
483 | 483 | |
|
484 | 484 | <%def name="usermenu(active=False)"> |
|
485 | 485 | <% |
|
486 | 486 | not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER |
|
487 | 487 | |
|
488 | 488 | gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None |
|
489 | 489 | # create repositories with write permission on group is set to true |
|
490 | 490 | |
|
491 | 491 | can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')() |
|
492 | 492 | create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')() |
|
493 | 493 | group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page') |
|
494 | 494 | group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page') |
|
495 | 495 | |
|
496 | 496 | can_create_repos = c.is_super_admin or c.can_create_repo |
|
497 | 497 | can_create_repo_groups = c.is_super_admin or c.can_create_repo_group |
|
498 | 498 | |
|
499 | 499 | can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write) |
|
500 | 500 | can_create_repo_groups_in_group = c.is_super_admin or group_admin |
|
501 | 501 | %> |
|
502 | 502 | |
|
503 | 503 | % if not_anonymous: |
|
504 | 504 | <% |
|
505 | 505 | default_target_group = dict() |
|
506 | 506 | if c.rhodecode_user.personal_repo_group: |
|
507 | 507 | default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id) |
|
508 | 508 | %> |
|
509 | 509 | |
|
510 | 510 | ## create action |
|
511 | 511 | <li> |
|
512 | 512 | <a href="#create-actions" onclick="return false;" class="menulink childs"> |
|
513 | 513 | <i class="icon-plus-circled"></i> |
|
514 | 514 | </a> |
|
515 | 515 | |
|
516 | 516 | <div class="action-menu submenu"> |
|
517 | 517 | |
|
518 | 518 | <ol> |
|
519 | 519 | ## scope of within a repository |
|
520 | 520 | % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo: |
|
521 | 521 | <li class="submenu-title">${_('This Repository')}</li> |
|
522 | 522 | <li> |
|
523 | 523 | <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a> |
|
524 | 524 | </li> |
|
525 | 525 | % if can_fork: |
|
526 | 526 | <li> |
|
527 | 527 | <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a> |
|
528 | 528 | </li> |
|
529 | 529 | % endif |
|
530 | 530 | % endif |
|
531 | 531 | |
|
532 | 532 | ## scope of within repository groups |
|
533 | 533 | % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group): |
|
534 | 534 | <li class="submenu-title">${_('This Repository Group')}</li> |
|
535 | 535 | |
|
536 | 536 | % if can_create_repos_in_group: |
|
537 | 537 | <li> |
|
538 | 538 | <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a> |
|
539 | 539 | </li> |
|
540 | 540 | % endif |
|
541 | 541 | |
|
542 | 542 | % if can_create_repo_groups_in_group: |
|
543 | 543 | <li> |
|
544 | 544 | <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a> |
|
545 | 545 | </li> |
|
546 | 546 | % endif |
|
547 | 547 | % endif |
|
548 | 548 | |
|
549 | 549 | ## personal group |
|
550 | 550 | % if c.rhodecode_user.personal_repo_group: |
|
551 | 551 | <li class="submenu-title">Personal Group</li> |
|
552 | 552 | |
|
553 | 553 | <li> |
|
554 | 554 | <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a> |
|
555 | 555 | </li> |
|
556 | 556 | |
|
557 | 557 | <li> |
|
558 | 558 | <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a> |
|
559 | 559 | </li> |
|
560 | 560 | % endif |
|
561 | 561 | |
|
562 | 562 | ## Global actions |
|
563 | 563 | <li class="submenu-title">RhodeCode</li> |
|
564 | 564 | % if can_create_repos: |
|
565 | 565 | <li> |
|
566 | 566 | <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a> |
|
567 | 567 | </li> |
|
568 | 568 | % endif |
|
569 | 569 | |
|
570 | 570 | % if can_create_repo_groups: |
|
571 | 571 | <li> |
|
572 | 572 | <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a> |
|
573 | 573 | </li> |
|
574 | 574 | % endif |
|
575 | 575 | |
|
576 | 576 | <li> |
|
577 | 577 | <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a> |
|
578 | 578 | </li> |
|
579 | 579 | |
|
580 | 580 | </ol> |
|
581 | 581 | |
|
582 | 582 | </div> |
|
583 | 583 | </li> |
|
584 | 584 | |
|
585 | 585 | ## notifications |
|
586 | 586 | <li> |
|
587 | 587 | <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}"> |
|
588 | 588 | ${c.unread_notifications} |
|
589 | 589 | </a> |
|
590 | 590 | </li> |
|
591 | 591 | % endif |
|
592 | 592 | |
|
593 | 593 | ## USER MENU |
|
594 | 594 | <li id="quick_login_li" class="${'active' if active else ''}"> |
|
595 | 595 | % if c.rhodecode_user.username == h.DEFAULT_USER: |
|
596 | 596 | <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}"> |
|
597 | 597 | ${gravatar(c.rhodecode_user.email, 20)} |
|
598 | 598 | <span class="user"> |
|
599 | 599 | <span>${_('Sign in')}</span> |
|
600 | 600 | </span> |
|
601 | 601 | </a> |
|
602 | 602 | % else: |
|
603 | 603 | ## logged in user |
|
604 | 604 | <a id="quick_login_link" class="menulink childs"> |
|
605 | 605 | ${gravatar(c.rhodecode_user.email, 20)} |
|
606 | 606 | <span class="user"> |
|
607 | 607 | <span class="menu_link_user">${c.rhodecode_user.username}</span> |
|
608 | 608 | <div class="show_more"></div> |
|
609 | 609 | </span> |
|
610 | 610 | </a> |
|
611 | 611 | ## subnav with menu for logged in user |
|
612 | 612 | <div class="user-menu submenu"> |
|
613 | 613 | <div id="quick_login"> |
|
614 | 614 | %if c.rhodecode_user.username != h.DEFAULT_USER: |
|
615 | 615 | <div class=""> |
|
616 | 616 | <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div> |
|
617 | 617 | <div class="full_name">${c.rhodecode_user.full_name_or_username}</div> |
|
618 | 618 | <div class="email">${c.rhodecode_user.email}</div> |
|
619 | 619 | </div> |
|
620 | 620 | <div class=""> |
|
621 | 621 | <ol class="links"> |
|
622 | 622 | <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li> |
|
623 | 623 | % if c.rhodecode_user.personal_repo_group: |
|
624 | 624 | <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li> |
|
625 | 625 | % endif |
|
626 | 626 | <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li> |
|
627 | 627 | |
|
628 | 628 | % if c.debug_style: |
|
629 | 629 | <li> |
|
630 | 630 | <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}"> |
|
631 | 631 | <div class="menulabel">${_('[Style]')}</div> |
|
632 | 632 | </a> |
|
633 | 633 | </li> |
|
634 | 634 | % endif |
|
635 | 635 | |
|
636 | 636 | ## bookmark-items |
|
637 | 637 | <li class="bookmark-items"> |
|
638 | 638 | ${_('Bookmarks')} |
|
639 | 639 | <div class="pull-right"> |
|
640 | 640 | <a href="${h.route_path('my_account_bookmarks')}"> |
|
641 | 641 | |
|
642 | 642 | <i class="icon-cog"></i> |
|
643 | 643 | </a> |
|
644 | 644 | </div> |
|
645 | 645 | </li> |
|
646 | 646 | % if not c.bookmark_items: |
|
647 | 647 | <li> |
|
648 | 648 | <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a> |
|
649 | 649 | </li> |
|
650 | 650 | % endif |
|
651 | 651 | % for item in c.bookmark_items: |
|
652 | 652 | <li> |
|
653 | 653 | % if item.repository: |
|
654 | 654 | <div> |
|
655 | 655 | <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}"> |
|
656 | 656 | <code>${item.position}</code> |
|
657 | 657 | % if item.repository.repo_type == 'hg': |
|
658 | 658 | <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i> |
|
659 | 659 | % elif item.repository.repo_type == 'git': |
|
660 | 660 | <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i> |
|
661 | 661 | % elif item.repository.repo_type == 'svn': |
|
662 | 662 | <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i> |
|
663 | 663 | % endif |
|
664 | 664 | ${(item.title or h.shorter(item.repository.repo_name, 30))} |
|
665 | 665 | </a> |
|
666 | 666 | </div> |
|
667 | 667 | % elif item.repository_group: |
|
668 | 668 | <div> |
|
669 | 669 | <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}"> |
|
670 | 670 | <code>${item.position}</code> |
|
671 | 671 | <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i> |
|
672 | 672 | ${(item.title or h.shorter(item.repository_group.group_name, 30))} |
|
673 | 673 | </a> |
|
674 | 674 | </div> |
|
675 | 675 | % else: |
|
676 | 676 | <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}"> |
|
677 | 677 | <code>${item.position}</code> |
|
678 | 678 | ${item.title} |
|
679 | 679 | </a> |
|
680 | 680 | % endif |
|
681 | 681 | </li> |
|
682 | 682 | % endfor |
|
683 | 683 | |
|
684 | 684 | <li class="logout"> |
|
685 | 685 | ${h.secure_form(h.route_path('logout'), request=request)} |
|
686 | 686 | ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")} |
|
687 | 687 | ${h.end_form()} |
|
688 | 688 | </li> |
|
689 | 689 | </ol> |
|
690 | 690 | </div> |
|
691 | 691 | %endif |
|
692 | 692 | </div> |
|
693 | 693 | </div> |
|
694 | 694 | |
|
695 | 695 | % endif |
|
696 | 696 | </li> |
|
697 | 697 | </%def> |
|
698 | 698 | |
|
699 | 699 | <%def name="menu_items(active=None)"> |
|
700 | 700 | <% |
|
701 | 701 | notice_messages, notice_level = c.rhodecode_user.get_notice_messages() |
|
702 | 702 | notice_display = 'none' if len(notice_messages) == 0 else '' |
|
703 | 703 | %> |
|
704 | 704 | |
|
705 | 705 | <ul id="quick" class="main_nav navigation horizontal-list"> |
|
706 | 706 | ## notice box for important system messages |
|
707 | 707 | <li style="display: ${notice_display}"> |
|
708 | 708 | <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false"> |
|
709 | 709 | <div class="menulabel-notice ${notice_level}" > |
|
710 | 710 | ${len(notice_messages)} |
|
711 | 711 | </div> |
|
712 | 712 | </a> |
|
713 | 713 | </li> |
|
714 | 714 | <div class="notice-messages-container" style="display: none"> |
|
715 | 715 | <div class="notice-messages"> |
|
716 | 716 | <table class="rctable"> |
|
717 | 717 | % for notice in notice_messages: |
|
718 | 718 | <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}"> |
|
719 | 719 | <td style="vertical-align: text-top; width: 20px"> |
|
720 | 720 | <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i> |
|
721 | 721 | </td> |
|
722 | 722 | <td> |
|
723 | 723 | <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span> |
|
724 | 724 | ${notice['subject']} |
|
725 | 725 | |
|
726 | 726 | <div id="notice-${notice['msg_id']}" style="display: none"> |
|
727 | 727 | ${h.render(notice['body'], renderer='markdown')} |
|
728 | 728 | </div> |
|
729 | 729 | </td> |
|
730 | 730 | <td style="vertical-align: text-top; width: 35px;"> |
|
731 | 731 | <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false"> |
|
732 | 732 | <i class="icon-remove icon-filled-red"></i> |
|
733 | 733 | </a> |
|
734 | 734 | </td> |
|
735 | 735 | </tr> |
|
736 | 736 | |
|
737 | 737 | % endfor |
|
738 | 738 | </table> |
|
739 | 739 | </div> |
|
740 | 740 | </div> |
|
741 | 741 | ## Main filter |
|
742 | 742 | <li> |
|
743 | 743 | <div class="menulabel main_filter_box"> |
|
744 | 744 | <div class="main_filter_input_box"> |
|
745 | 745 | <ul class="searchItems"> |
|
746 | 746 | |
|
747 | 747 | <li class="searchTag searchTagIcon"> |
|
748 | 748 | <i class="icon-search"></i> |
|
749 | 749 | </li> |
|
750 | 750 | |
|
751 | 751 | % if c.template_context['search_context']['repo_id']: |
|
752 | 752 | <li class="searchTag searchTagFilter searchTagHidable" > |
|
753 | 753 | ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}"> |
|
754 | 754 | <span class="tag"> |
|
755 | 755 | This repo |
|
756 | 756 | <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a> |
|
757 | 757 | </span> |
|
758 | 758 | ##</a> |
|
759 | 759 | </li> |
|
760 | 760 | % elif c.template_context['search_context']['repo_group_id']: |
|
761 | 761 | <li class="searchTag searchTagFilter searchTagHidable"> |
|
762 | 762 | ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}"> |
|
763 | 763 | <span class="tag"> |
|
764 | 764 | This group |
|
765 | 765 | <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a> |
|
766 | 766 | </span> |
|
767 | 767 | ##</a> |
|
768 | 768 | </li> |
|
769 | 769 | % endif |
|
770 | 770 | |
|
771 | 771 | <li class="searchTagInput"> |
|
772 | 772 | <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" /> |
|
773 | 773 | </li> |
|
774 | 774 | <li class="searchTag searchTagHelp"> |
|
775 | 775 | <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a> |
|
776 | 776 | </li> |
|
777 | 777 | </ul> |
|
778 | 778 | </div> |
|
779 | 779 | </div> |
|
780 | 780 | |
|
781 | 781 | <div id="main_filter_help" style="display: none"> |
|
782 | 782 | - Use '/' key to quickly access this field. |
|
783 | 783 | |
|
784 | 784 | - Enter a name of repository, or repository group for quick search. |
|
785 | 785 | |
|
786 | 786 | - Prefix query to allow special search: |
|
787 | 787 | |
|
788 | 788 | <strong>user:</strong>admin, to search for usernames, always global |
|
789 | 789 | |
|
790 | 790 | <strong>user_group:</strong>devops, to search for user groups, always global |
|
791 | 791 | |
|
792 | 792 | <strong>pr:</strong>303, to search for pull request number, title, or description, always global |
|
793 | 793 | |
|
794 | 794 | <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups |
|
795 | 795 | |
|
796 | 796 | <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups |
|
797 | 797 | |
|
798 | 798 | % if c.template_context['search_context']['repo_id']: |
|
799 | 799 | For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a> |
|
800 | 800 | % elif c.template_context['search_context']['repo_group_id']: |
|
801 | 801 | For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a> |
|
802 | 802 | % else: |
|
803 | 803 | For advanced full text search visit: <a href="${h.route_path('search')}">global search</a> |
|
804 | 804 | % endif |
|
805 | 805 | </div> |
|
806 | 806 | </li> |
|
807 | 807 | |
|
808 | 808 | ## ROOT MENU |
|
809 | 809 | <li class="${h.is_active('home', active)}"> |
|
810 | 810 | <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}"> |
|
811 | 811 | <div class="menulabel">${_('Home')}</div> |
|
812 | 812 | </a> |
|
813 | 813 | </li> |
|
814 | 814 | |
|
815 | 815 | %if c.rhodecode_user.username != h.DEFAULT_USER: |
|
816 | 816 | <li class="${h.is_active('journal', active)}"> |
|
817 | 817 | <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}"> |
|
818 | 818 | <div class="menulabel">${_('Journal')}</div> |
|
819 | 819 | </a> |
|
820 | 820 | </li> |
|
821 | 821 | %else: |
|
822 | 822 | <li class="${h.is_active('journal', active)}"> |
|
823 | 823 | <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}"> |
|
824 | 824 | <div class="menulabel">${_('Public journal')}</div> |
|
825 | 825 | </a> |
|
826 | 826 | </li> |
|
827 | 827 | %endif |
|
828 | 828 | |
|
829 | 829 | <li class="${h.is_active('gists', active)}"> |
|
830 | 830 | <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}"> |
|
831 | 831 | <div class="menulabel">${_('Gists')}</div> |
|
832 | 832 | </a> |
|
833 | 833 | </li> |
|
834 | 834 | |
|
835 | 835 | % if c.is_super_admin or c.is_delegated_admin: |
|
836 | 836 | <li class="${h.is_active('admin', active)}"> |
|
837 | 837 | <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}"> |
|
838 | 838 | <div class="menulabel">${_('Admin')} </div> |
|
839 | 839 | </a> |
|
840 | 840 | </li> |
|
841 | 841 | % endif |
|
842 | 842 | |
|
843 | 843 | ## render extra user menu |
|
844 | 844 | ${usermenu(active=(active=='my_account'))} |
|
845 | 845 | |
|
846 | 846 | </ul> |
|
847 | 847 | |
|
848 | 848 | <script type="text/javascript"> |
|
849 | 849 | var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True"; |
|
850 | 850 | |
|
851 | 851 | var formatRepoResult = function(result, container, query, escapeMarkup) { |
|
852 | 852 | return function(data, escapeMarkup) { |
|
853 | 853 | if (!data.repo_id){ |
|
854 | 854 | return data.text; // optgroup text Repositories |
|
855 | 855 | } |
|
856 | 856 | |
|
857 | 857 | var tmpl = ''; |
|
858 | 858 | var repoType = data['repo_type']; |
|
859 | 859 | var repoName = data['text']; |
|
860 | 860 | |
|
861 | 861 | if(data && data.type == 'repo'){ |
|
862 | 862 | if(repoType === 'hg'){ |
|
863 | 863 | tmpl += '<i class="icon-hg"></i> '; |
|
864 | 864 | } |
|
865 | 865 | else if(repoType === 'git'){ |
|
866 | 866 | tmpl += '<i class="icon-git"></i> '; |
|
867 | 867 | } |
|
868 | 868 | else if(repoType === 'svn'){ |
|
869 | 869 | tmpl += '<i class="icon-svn"></i> '; |
|
870 | 870 | } |
|
871 | 871 | if(data['private']){ |
|
872 | 872 | tmpl += '<i class="icon-lock" ></i> '; |
|
873 | 873 | } |
|
874 | 874 | else if(visualShowPublicIcon){ |
|
875 | 875 | tmpl += '<i class="icon-unlock-alt"></i> '; |
|
876 | 876 | } |
|
877 | 877 | } |
|
878 | 878 | tmpl += escapeMarkup(repoName); |
|
879 | 879 | return tmpl; |
|
880 | 880 | |
|
881 | 881 | }(result, escapeMarkup); |
|
882 | 882 | }; |
|
883 | 883 | |
|
884 | 884 | var formatRepoGroupResult = function(result, container, query, escapeMarkup) { |
|
885 | 885 | return function(data, escapeMarkup) { |
|
886 | 886 | if (!data.repo_group_id){ |
|
887 | 887 | return data.text; // optgroup text Repositories |
|
888 | 888 | } |
|
889 | 889 | |
|
890 | 890 | var tmpl = ''; |
|
891 | 891 | var repoGroupName = data['text']; |
|
892 | 892 | |
|
893 | 893 | if(data){ |
|
894 | 894 | |
|
895 | 895 | tmpl += '<i class="icon-repo-group"></i> '; |
|
896 | 896 | |
|
897 | 897 | } |
|
898 | 898 | tmpl += escapeMarkup(repoGroupName); |
|
899 | 899 | return tmpl; |
|
900 | 900 | |
|
901 | 901 | }(result, escapeMarkup); |
|
902 | 902 | }; |
|
903 | 903 | |
|
904 | 904 | var escapeRegExChars = function (value) { |
|
905 | 905 | return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); |
|
906 | 906 | }; |
|
907 | 907 | |
|
908 | 908 | var getRepoIcon = function(repo_type) { |
|
909 | 909 | if (repo_type === 'hg') { |
|
910 | 910 | return '<i class="icon-hg"></i> '; |
|
911 | 911 | } |
|
912 | 912 | else if (repo_type === 'git') { |
|
913 | 913 | return '<i class="icon-git"></i> '; |
|
914 | 914 | } |
|
915 | 915 | else if (repo_type === 'svn') { |
|
916 | 916 | return '<i class="icon-svn"></i> '; |
|
917 | 917 | } |
|
918 | 918 | return '' |
|
919 | 919 | }; |
|
920 | 920 | |
|
921 | 921 | var autocompleteMainFilterFormatResult = function (data, value, org_formatter) { |
|
922 | 922 | |
|
923 | 923 | if (value.split(':').length === 2) { |
|
924 | 924 | value = value.split(':')[1] |
|
925 | 925 | } |
|
926 | 926 | |
|
927 | 927 | var searchType = data['type']; |
|
928 | 928 | var searchSubType = data['subtype']; |
|
929 | 929 | var valueDisplay = data['value_display']; |
|
930 | 930 | var valueIcon = data['value_icon']; |
|
931 | 931 | |
|
932 | 932 | var pattern = '(' + escapeRegExChars(value) + ')'; |
|
933 | 933 | |
|
934 | 934 | valueDisplay = Select2.util.escapeMarkup(valueDisplay); |
|
935 | 935 | |
|
936 | 936 | // highlight match |
|
937 | 937 | if (searchType != 'text') { |
|
938 | 938 | valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>'); |
|
939 | 939 | } |
|
940 | 940 | |
|
941 | 941 | var icon = ''; |
|
942 | 942 | |
|
943 | 943 | if (searchType === 'hint') { |
|
944 | 944 | icon += '<i class="icon-repo-group"></i> '; |
|
945 | 945 | } |
|
946 | 946 | // full text search/hints |
|
947 | 947 | else if (searchType === 'search') { |
|
948 | 948 | if (valueIcon === undefined) { |
|
949 | 949 | icon += '<i class="icon-more"></i> '; |
|
950 | 950 | } else { |
|
951 | 951 | icon += valueIcon + ' '; |
|
952 | 952 | } |
|
953 | 953 | |
|
954 | 954 | if (searchSubType !== undefined && searchSubType == 'repo') { |
|
955 | 955 | valueDisplay += '<div class="pull-right tag">repository</div>'; |
|
956 | 956 | } |
|
957 | 957 | else if (searchSubType !== undefined && searchSubType == 'repo_group') { |
|
958 | 958 | valueDisplay += '<div class="pull-right tag">repo group</div>'; |
|
959 | 959 | } |
|
960 | 960 | } |
|
961 | 961 | // repository |
|
962 | 962 | else if (searchType === 'repo') { |
|
963 | 963 | |
|
964 | 964 | var repoIcon = getRepoIcon(data['repo_type']); |
|
965 | 965 | icon += repoIcon; |
|
966 | 966 | |
|
967 | 967 | if (data['private']) { |
|
968 | 968 | icon += '<i class="icon-lock" ></i> '; |
|
969 | 969 | } |
|
970 | 970 | else if (visualShowPublicIcon) { |
|
971 | 971 | icon += '<i class="icon-unlock-alt"></i> '; |
|
972 | 972 | } |
|
973 | 973 | } |
|
974 | 974 | // repository groups |
|
975 | 975 | else if (searchType === 'repo_group') { |
|
976 | 976 | icon += '<i class="icon-repo-group"></i> '; |
|
977 | 977 | } |
|
978 | 978 | // user group |
|
979 | 979 | else if (searchType === 'user_group') { |
|
980 | 980 | icon += '<i class="icon-group"></i> '; |
|
981 | 981 | } |
|
982 | 982 | // user |
|
983 | 983 | else if (searchType === 'user') { |
|
984 | 984 | icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']); |
|
985 | 985 | } |
|
986 | 986 | // pull request |
|
987 | 987 | else if (searchType === 'pull_request') { |
|
988 | 988 | icon += '<i class="icon-merge"></i> '; |
|
989 | 989 | } |
|
990 | 990 | // commit |
|
991 | 991 | else if (searchType === 'commit') { |
|
992 | 992 | var repo_data = data['repo_data']; |
|
993 | 993 | var repoIcon = getRepoIcon(repo_data['repository_type']); |
|
994 | 994 | if (repoIcon) { |
|
995 | 995 | icon += repoIcon; |
|
996 | 996 | } else { |
|
997 | 997 | icon += '<i class="icon-tag"></i>'; |
|
998 | 998 | } |
|
999 | 999 | } |
|
1000 | 1000 | // file |
|
1001 | 1001 | else if (searchType === 'file') { |
|
1002 | 1002 | var repo_data = data['repo_data']; |
|
1003 | 1003 | var repoIcon = getRepoIcon(repo_data['repository_type']); |
|
1004 | 1004 | if (repoIcon) { |
|
1005 | 1005 | icon += repoIcon; |
|
1006 | 1006 | } else { |
|
1007 | 1007 | icon += '<i class="icon-tag"></i>'; |
|
1008 | 1008 | } |
|
1009 | 1009 | } |
|
1010 | 1010 | // generic text |
|
1011 | 1011 | else if (searchType === 'text') { |
|
1012 | 1012 | icon = ''; |
|
1013 | 1013 | } |
|
1014 | 1014 | |
|
1015 | 1015 | var tmpl = '<div class="ac-container-wrap">{0}{1}</div>'; |
|
1016 | 1016 | return tmpl.format(icon, valueDisplay); |
|
1017 | 1017 | }; |
|
1018 | 1018 | |
|
1019 | 1019 | var handleSelect = function(element, suggestion) { |
|
1020 | 1020 | if (suggestion.type === "hint") { |
|
1021 | 1021 | // we skip action |
|
1022 | 1022 | $('#main_filter').focus(); |
|
1023 | 1023 | } |
|
1024 | 1024 | else if (suggestion.type === "text") { |
|
1025 | 1025 | // we skip action |
|
1026 | 1026 | $('#main_filter').focus(); |
|
1027 | 1027 | |
|
1028 | 1028 | } else { |
|
1029 | 1029 | window.location = suggestion['url']; |
|
1030 | 1030 | } |
|
1031 | 1031 | }; |
|
1032 | 1032 | |
|
1033 | 1033 | var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) { |
|
1034 | 1034 | if (queryLowerCase.split(':').length === 2) { |
|
1035 | 1035 | queryLowerCase = queryLowerCase.split(':')[1] |
|
1036 | 1036 | } |
|
1037 | 1037 | if (suggestion.type === "text") { |
|
1038 | 1038 | // special case we don't want to "skip" display for |
|
1039 | 1039 | return true |
|
1040 | 1040 | } |
|
1041 | 1041 | return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1; |
|
1042 | 1042 | }; |
|
1043 | 1043 | |
|
1044 | 1044 | var cleanContext = { |
|
1045 | 1045 | repo_view_type: null, |
|
1046 | 1046 | |
|
1047 | 1047 | repo_id: null, |
|
1048 | 1048 | repo_name: "", |
|
1049 | 1049 | |
|
1050 | 1050 | repo_group_id: null, |
|
1051 | 1051 | repo_group_name: null |
|
1052 | 1052 | }; |
|
1053 | 1053 | var removeGoToFilter = function () { |
|
1054 | 1054 | $('.searchTagHidable').hide(); |
|
1055 | 1055 | $('#main_filter').autocomplete( |
|
1056 | 1056 | 'setOptions', {params:{search_context: cleanContext}}); |
|
1057 | 1057 | }; |
|
1058 | 1058 | |
|
1059 | 1059 | $('#main_filter').autocomplete({ |
|
1060 | 1060 | serviceUrl: pyroutes.url('goto_switcher_data'), |
|
1061 | 1061 | params: { |
|
1062 | 1062 | "search_context": templateContext.search_context |
|
1063 | 1063 | }, |
|
1064 | 1064 | minChars:2, |
|
1065 | 1065 | maxHeight:400, |
|
1066 | 1066 | deferRequestBy: 300, //miliseconds |
|
1067 | 1067 | tabDisabled: true, |
|
1068 | 1068 | autoSelectFirst: false, |
|
1069 | 1069 | containerClass: 'autocomplete-qfilter-suggestions', |
|
1070 | 1070 | formatResult: autocompleteMainFilterFormatResult, |
|
1071 | 1071 | lookupFilter: autocompleteMainFilterResult, |
|
1072 | 1072 | onSelect: function (element, suggestion) { |
|
1073 | 1073 | handleSelect(element, suggestion); |
|
1074 | 1074 | return false; |
|
1075 | 1075 | }, |
|
1076 | 1076 | onSearchError: function (element, query, jqXHR, textStatus, errorThrown) { |
|
1077 | 1077 | if (jqXHR !== 'abort') { |
|
1078 | 1078 | var message = formatErrorMessage(jqXHR, textStatus, errorThrown); |
|
1079 | 1079 | SwalNoAnimation.fire({ |
|
1080 | 1080 | icon: 'error', |
|
1081 | 1081 | title: _gettext('Error during search operation'), |
|
1082 | 1082 | html: '<span style="white-space: pre-line">{0}</span>'.format(message), |
|
1083 | 1083 | }).then(function(result) { |
|
1084 | 1084 | window.location.reload(); |
|
1085 | 1085 | }) |
|
1086 | 1086 | } |
|
1087 | 1087 | }, |
|
1088 | 1088 | onSearchStart: function (params) { |
|
1089 | 1089 | $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>') |
|
1090 | 1090 | }, |
|
1091 | 1091 | onSearchComplete: function (query, suggestions) { |
|
1092 | 1092 | $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>') |
|
1093 | 1093 | }, |
|
1094 | 1094 | }); |
|
1095 | 1095 | |
|
1096 | 1096 | showMainFilterBox = function () { |
|
1097 | 1097 | $('#main_filter_help').toggle(); |
|
1098 | 1098 | }; |
|
1099 | 1099 | |
|
1100 | 1100 | $('#main_filter').on('keydown.autocomplete', function (e) { |
|
1101 | 1101 | |
|
1102 | 1102 | var BACKSPACE = 8; |
|
1103 | 1103 | var el = $(e.currentTarget); |
|
1104 | 1104 | if(e.which === BACKSPACE){ |
|
1105 | 1105 | var inputVal = el.val(); |
|
1106 | 1106 | if (inputVal === ""){ |
|
1107 | 1107 | removeGoToFilter() |
|
1108 | 1108 | } |
|
1109 | 1109 | } |
|
1110 | 1110 | }); |
|
1111 | 1111 | |
|
1112 | 1112 | var dismissNotice = function(noticeId) { |
|
1113 | 1113 | |
|
1114 | 1114 | var url = pyroutes.url('user_notice_dismiss', |
|
1115 | 1115 | {"user_id": templateContext.rhodecode_user.user_id}); |
|
1116 | 1116 | |
|
1117 | 1117 | var postData = { |
|
1118 | 1118 | 'csrf_token': CSRF_TOKEN, |
|
1119 | 1119 | 'notice_id': noticeId, |
|
1120 | 1120 | }; |
|
1121 | 1121 | |
|
1122 | 1122 | var success = function(response) { |
|
1123 | 1123 | $('#notice-message-' + noticeId).remove(); |
|
1124 | 1124 | return false; |
|
1125 | 1125 | }; |
|
1126 | 1126 | var failure = function(data, textStatus, xhr) { |
|
1127 | 1127 | alert("error processing request: " + textStatus); |
|
1128 | 1128 | return false; |
|
1129 | 1129 | }; |
|
1130 | 1130 | ajaxPOST(url, postData, success, failure); |
|
1131 | 1131 | } |
|
1132 | 1132 | |
|
1133 | 1133 | var hideLicenseWarning = function () { |
|
1134 | 1134 | var fingerprint = templateContext.session_attrs.license_fingerprint; |
|
1135 | 1135 | storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint); |
|
1136 | 1136 | $('#notifications').hide(); |
|
1137 | 1137 | } |
|
1138 | 1138 | |
|
1139 | 1139 | var hideLicenseError = function () { |
|
1140 | 1140 | var fingerprint = templateContext.session_attrs.license_fingerprint; |
|
1141 | 1141 | storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint); |
|
1142 | 1142 | $('#notifications').hide(); |
|
1143 | 1143 | } |
|
1144 | 1144 | |
|
1145 | 1145 | </script> |
|
1146 | 1146 | <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script> |
|
1147 | 1147 | </%def> |
|
1148 | 1148 | |
|
1149 | 1149 | <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> |
|
1150 | 1150 | <div class="modal-dialog"> |
|
1151 | 1151 | <div class="modal-content"> |
|
1152 | 1152 | <div class="modal-header"> |
|
1153 | 1153 | <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> |
|
1154 | 1154 | <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4> |
|
1155 | 1155 | </div> |
|
1156 | 1156 | <div class="modal-body"> |
|
1157 | 1157 | <div class="block-left"> |
|
1158 | 1158 | <table class="keyboard-mappings"> |
|
1159 | 1159 | <tbody> |
|
1160 | 1160 | <tr> |
|
1161 | 1161 | <th></th> |
|
1162 | 1162 | <th>${_('Site-wide shortcuts')}</th> |
|
1163 | 1163 | </tr> |
|
1164 | 1164 | <% |
|
1165 | 1165 | elems = [ |
|
1166 | 1166 | ('/', 'Use quick search box'), |
|
1167 | 1167 | ('g h', 'Goto home page'), |
|
1168 | 1168 | ('g g', 'Goto my private gists page'), |
|
1169 | 1169 | ('g G', 'Goto my public gists page'), |
|
1170 | 1170 | ('g 0-9', 'Goto bookmarked items from 0-9'), |
|
1171 | 1171 | ('n r', 'New repository page'), |
|
1172 | 1172 | ('n g', 'New gist page'), |
|
1173 | 1173 | ] |
|
1174 | 1174 | %> |
|
1175 | 1175 | %for key, desc in elems: |
|
1176 | 1176 | <tr> |
|
1177 | 1177 | <td class="keys"> |
|
1178 | 1178 | <span class="key tag">${key}</span> |
|
1179 | 1179 | </td> |
|
1180 | 1180 | <td>${desc}</td> |
|
1181 | 1181 | </tr> |
|
1182 | 1182 | %endfor |
|
1183 | 1183 | </tbody> |
|
1184 | 1184 | </table> |
|
1185 | 1185 | </div> |
|
1186 | 1186 | <div class="block-left"> |
|
1187 | 1187 | <table class="keyboard-mappings"> |
|
1188 | 1188 | <tbody> |
|
1189 | 1189 | <tr> |
|
1190 | 1190 | <th></th> |
|
1191 | 1191 | <th>${_('Repositories')}</th> |
|
1192 | 1192 | </tr> |
|
1193 | 1193 | <% |
|
1194 | 1194 | elems = [ |
|
1195 | 1195 | ('g s', 'Goto summary page'), |
|
1196 | 1196 | ('g c', 'Goto changelog page'), |
|
1197 | 1197 | ('g f', 'Goto files page'), |
|
1198 | 1198 | ('g F', 'Goto files page with file search activated'), |
|
1199 | 1199 | ('g p', 'Goto pull requests page'), |
|
1200 | 1200 | ('g o', 'Goto repository settings'), |
|
1201 | 1201 | ('g O', 'Goto repository access permissions settings'), |
|
1202 | 1202 | ('t s', 'Toggle sidebar on some pages'), |
|
1203 | 1203 | ] |
|
1204 | 1204 | %> |
|
1205 | 1205 | %for key, desc in elems: |
|
1206 | 1206 | <tr> |
|
1207 | 1207 | <td class="keys"> |
|
1208 | 1208 | <span class="key tag">${key}</span> |
|
1209 | 1209 | </td> |
|
1210 | 1210 | <td>${desc}</td> |
|
1211 | 1211 | </tr> |
|
1212 | 1212 | %endfor |
|
1213 | 1213 | </tbody> |
|
1214 | 1214 | </table> |
|
1215 | 1215 | </div> |
|
1216 | 1216 | </div> |
|
1217 | 1217 | <div class="modal-footer"> |
|
1218 | 1218 | </div> |
|
1219 | 1219 | </div><!-- /.modal-content --> |
|
1220 | 1220 | </div><!-- /.modal-dialog --> |
|
1221 | 1221 | </div><!-- /.modal --> |
|
1222 | 1222 | |
|
1223 | 1223 | |
|
1224 | 1224 | <script type="text/javascript"> |
|
1225 | 1225 | (function () { |
|
1226 | 1226 | "use sctrict"; |
|
1227 | 1227 | |
|
1228 | // details block auto-hide menu | |
|
1229 | $(document).mouseup(function(e) { | |
|
1230 | var container = $('.details-inline-block'); | |
|
1231 | if (!container.is(e.target) && container.has(e.target).length === 0) { | |
|
1232 | $('.details-inline-block[open]').removeAttr('open') | |
|
1233 | } | |
|
1234 | }); | |
|
1235 | ||
|
1228 | 1236 | var $sideBar = $('.right-sidebar'); |
|
1229 | 1237 | var expanded = $sideBar.hasClass('right-sidebar-expanded'); |
|
1230 | 1238 | var sidebarState = templateContext.session_attrs.sidebarState; |
|
1231 | 1239 | var sidebarEnabled = $('aside.right-sidebar').get(0); |
|
1232 | 1240 | |
|
1233 | 1241 | if (sidebarState === 'expanded') { |
|
1234 | 1242 | expanded = true |
|
1235 | 1243 | } else if (sidebarState === 'collapsed') { |
|
1236 | 1244 | expanded = false |
|
1237 | 1245 | } |
|
1238 | 1246 | if (sidebarEnabled) { |
|
1239 | 1247 | // show sidebar since it's hidden on load |
|
1240 | 1248 | $('.right-sidebar').show(); |
|
1241 | 1249 | |
|
1242 | 1250 | // init based on set initial class, or if defined user session attrs |
|
1243 | 1251 | if (expanded) { |
|
1244 | 1252 | window.expandSidebar(); |
|
1245 | 1253 | window.updateStickyHeader(); |
|
1246 | 1254 | |
|
1247 | 1255 | } else { |
|
1248 | 1256 | window.collapseSidebar(); |
|
1249 | 1257 | window.updateStickyHeader(); |
|
1250 | 1258 | } |
|
1251 | 1259 | } |
|
1252 | 1260 | })() |
|
1253 | 1261 | |
|
1254 | 1262 | </script> |
@@ -1,147 +1,151 b'' | |||
|
1 | 1 | ## snippet for sidebar elements |
|
2 | 2 | ## usage: |
|
3 | 3 | ## <%namespace name="sidebar" file="/base/sidebar.mako"/> |
|
4 | 4 | ## ${sidebar.comments_table()} |
|
5 | 5 | <%namespace name="base" file="/base/base.mako"/> |
|
6 | 6 | |
|
7 | 7 | <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)"> |
|
8 | 8 | <% |
|
9 | 9 | if todo_comments: |
|
10 | 10 | cls_ = 'todos-content-table' |
|
11 | 11 | def sorter(entry): |
|
12 | 12 | user_id = entry.author.user_id |
|
13 | 13 | resolved = '1' if entry.resolved else '0' |
|
14 | 14 | if user_id == c.rhodecode_user.user_id: |
|
15 | 15 | # own comments first |
|
16 | 16 | user_id = 0 |
|
17 | 17 | return '{}'.format(str(entry.comment_id).zfill(10000)) |
|
18 | 18 | else: |
|
19 | 19 | cls_ = 'comments-content-table' |
|
20 | 20 | def sorter(entry): |
|
21 | 21 | user_id = entry.author.user_id |
|
22 | 22 | return '{}'.format(str(entry.comment_id).zfill(10000)) |
|
23 | 23 | |
|
24 | 24 | existing_ids = existing_ids or [] |
|
25 | 25 | |
|
26 | 26 | %> |
|
27 | 27 | |
|
28 | 28 | <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}"> |
|
29 | 29 | |
|
30 | 30 | % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))): |
|
31 | 31 | <% |
|
32 | 32 | display = '' |
|
33 | 33 | _cls = '' |
|
34 | ## Extra precaution to not show drafts in the sidebar for todo/comments | |
|
35 | if comment_obj.draft: | |
|
36 | continue | |
|
34 | 37 | %> |
|
35 | 38 | |
|
39 | ||
|
36 | 40 | <% |
|
37 | 41 | comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', [])) |
|
38 | 42 | prev_comment_ver_index = 0 |
|
39 | 43 | if loop_obj.previous: |
|
40 | 44 | prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', [])) |
|
41 | 45 | |
|
42 | 46 | ver_info = None |
|
43 | 47 | if getattr(c, 'versions', []): |
|
44 | 48 | ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None |
|
45 | 49 | %> |
|
46 | 50 | <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %> |
|
47 | 51 | <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %> |
|
48 | 52 | <% |
|
49 | 53 | if (prev_comment_ver_index > comment_ver_index): |
|
50 | 54 | comments_ver_divider = comment_ver_index |
|
51 | 55 | else: |
|
52 | 56 | comments_ver_divider = None |
|
53 | 57 | %> |
|
54 | 58 | |
|
55 | 59 | % if todo_comments: |
|
56 | 60 | % if comment_obj.resolved: |
|
57 | 61 | <% _cls = 'resolved-todo' %> |
|
58 | 62 | <% display = 'none' %> |
|
59 | 63 | % endif |
|
60 | 64 | % else: |
|
61 | 65 | ## SKIP TODOs we display them in other area |
|
62 | 66 | % if comment_obj.is_todo: |
|
63 | 67 | <% display = 'none' %> |
|
64 | 68 | % endif |
|
65 | 69 | ## Skip outdated comments |
|
66 | 70 | % if comment_obj.outdated: |
|
67 | 71 | <% display = 'none' %> |
|
68 | 72 | <% _cls = 'hidden-comment' %> |
|
69 | 73 | % endif |
|
70 | 74 | % endif |
|
71 | 75 | |
|
72 | 76 | % if not todo_comments and comments_ver_divider: |
|
73 | 77 | <tr class="old-comments-marker"> |
|
74 | 78 | <td colspan="3"> |
|
75 | 79 | % if ver_info: |
|
76 | 80 | <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code> |
|
77 | 81 | % else: |
|
78 | 82 | <code>v${comments_ver_divider}</code> |
|
79 | 83 | % endif |
|
80 | 84 | </td> |
|
81 | 85 | </tr> |
|
82 | 86 | |
|
83 | 87 | % endif |
|
84 | 88 | |
|
85 | 89 | <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}"> |
|
86 | 90 | <td class="td-todo-number"> |
|
87 | 91 | <% |
|
88 | 92 | version_info = '' |
|
89 | 93 | if is_pr: |
|
90 | 94 | version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version') |
|
91 | 95 | %> |
|
92 | 96 | ## new comments, since refresh |
|
93 | 97 | % if existing_ids and comment_obj.comment_id not in existing_ids: |
|
94 | 98 | <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment"> |
|
95 | 99 | ! |
|
96 | 100 | </div> |
|
97 | 101 | % endif |
|
98 | 102 | |
|
99 | 103 | <% |
|
100 | 104 | data = h.json.dumps({ |
|
101 | 105 | 'comment_id': comment_obj.comment_id, |
|
102 | 106 | 'version_info': version_info, |
|
103 | 107 | 'file_name': comment_obj.f_path, |
|
104 | 108 | 'line_no': comment_obj.line_no, |
|
105 | 109 | 'outdated': comment_obj.outdated, |
|
106 | 110 | 'inline': comment_obj.is_inline, |
|
107 | 111 | 'is_todo': comment_obj.is_todo, |
|
108 | 112 | 'created_on': h.format_date(comment_obj.created_on), |
|
109 | 113 | 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)), |
|
110 | 114 | 'review_status': (comment_obj.review_status or '') |
|
111 | 115 | }) |
|
112 | 116 | |
|
113 | 117 | if comment_obj.outdated: |
|
114 | 118 | icon = 'icon-comment-toggle' |
|
115 | 119 | elif comment_obj.is_inline: |
|
116 | 120 | icon = 'icon-code' |
|
117 | 121 | else: |
|
118 | 122 | icon = 'icon-comment' |
|
119 | 123 | %> |
|
120 | 124 | |
|
121 | 125 | <i id="commentHovercard${comment_obj.comment_id}" |
|
122 | 126 | class="${icon} tooltip-hovercard" |
|
123 | 127 | data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})" |
|
124 | 128 | data-comment-json-b64='${h.b64(data)}'> |
|
125 | 129 | </i> |
|
126 | 130 | |
|
127 | 131 | </td> |
|
128 | 132 | |
|
129 | 133 | <td class="td-todo-gravatar"> |
|
130 | 134 | ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])} |
|
131 | 135 | </td> |
|
132 | 136 | <td class="todo-comment-text-wrapper"> |
|
133 | 137 | <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}"> |
|
134 | 138 | <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink" |
|
135 | 139 | href="#comment-${comment_obj.comment_id}" |
|
136 | 140 | onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})"> |
|
137 | 141 | |
|
138 | 142 | ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')} |
|
139 | 143 | </a> |
|
140 | 144 | </div> |
|
141 | 145 | </td> |
|
142 | 146 | </tr> |
|
143 | 147 | % endfor |
|
144 | 148 | |
|
145 | 149 | </table> |
|
146 | 150 | |
|
147 | 151 | </%def> No newline at end of file |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now