##// END OF EJS Templates
pull-requests: added retry mechanism for updating pull requests.
super-admin -
r4696:7a5e2fc4 stable
parent child Browse files
Show More
@@ -1,1868 +1,1872 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29
29
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist, retry
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name,
82 repo_name,
83 search_q=search_q, statuses=statuses,
83 search_q=search_q, statuses=statuses,
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
84 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name,
86 repo_name,
87 search_q=search_q, statuses=statuses)
87 search_q=search_q, statuses=statuses)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, self._rhodecode_user.user_id,
90 repo_name, self._rhodecode_user.user_id,
91 search_q=search_q, statuses=statuses,
91 search_q=search_q, statuses=statuses,
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
92 offset=start, length=limit, order_by=order_by, order_dir=order_dir)
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 repo_name, self._rhodecode_user.user_id,
94 repo_name, self._rhodecode_user.user_id,
95 search_q=search_q, statuses=statuses)
95 search_q=search_q, statuses=statuses)
96 else:
96 else:
97 pull_requests = PullRequestModel().get_all(
97 pull_requests = PullRequestModel().get_all(
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 statuses=statuses, offset=start, length=limit,
99 statuses=statuses, offset=start, length=limit,
100 order_by=order_by, order_dir=order_dir)
100 order_by=order_by, order_dir=order_dir)
101 pull_requests_total_count = PullRequestModel().count_all(
101 pull_requests_total_count = PullRequestModel().count_all(
102 repo_name, search_q=search_q, source=source, statuses=statuses,
102 repo_name, search_q=search_q, source=source, statuses=statuses,
103 opened_by=opened_by)
103 opened_by=opened_by)
104
104
105 data = []
105 data = []
106 comments_model = CommentsModel()
106 comments_model = CommentsModel()
107 for pr in pull_requests:
107 for pr in pull_requests:
108 comments_count = comments_model.get_all_comments(
108 comments_count = comments_model.get_all_comments(
109 self.db_repo.repo_id, pull_request=pr,
109 self.db_repo.repo_id, pull_request=pr,
110 include_drafts=False, count_only=True)
110 include_drafts=False, count_only=True)
111
111
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
112 review_statuses = pr.reviewers_statuses(user=self._rhodecode_db_user)
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
113 my_review_status = ChangesetStatus.STATUS_NOT_REVIEWED
114 if review_statuses and review_statuses[4]:
114 if review_statuses and review_statuses[4]:
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
115 _review_obj, _user, _reasons, _mandatory, statuses = review_statuses
116 my_review_status = statuses[0][1].status
116 my_review_status = statuses[0][1].status
117
117
118 data.append({
118 data.append({
119 'name': _render('pullrequest_name',
119 'name': _render('pullrequest_name',
120 pr.pull_request_id, pr.pull_request_state,
120 pr.pull_request_id, pr.pull_request_state,
121 pr.work_in_progress, pr.target_repo.repo_name,
121 pr.work_in_progress, pr.target_repo.repo_name,
122 short=True),
122 short=True),
123 'name_raw': pr.pull_request_id,
123 'name_raw': pr.pull_request_id,
124 'status': _render('pullrequest_status',
124 'status': _render('pullrequest_status',
125 pr.calculated_review_status()),
125 pr.calculated_review_status()),
126 'my_status': _render('pullrequest_status',
126 'my_status': _render('pullrequest_status',
127 my_review_status),
127 my_review_status),
128 'title': _render('pullrequest_title', pr.title, pr.description),
128 'title': _render('pullrequest_title', pr.title, pr.description),
129 'description': h.escape(pr.description),
129 'description': h.escape(pr.description),
130 'updated_on': _render('pullrequest_updated_on',
130 'updated_on': _render('pullrequest_updated_on',
131 h.datetime_to_time(pr.updated_on),
131 h.datetime_to_time(pr.updated_on),
132 pr.versions_count),
132 pr.versions_count),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
133 'updated_on_raw': h.datetime_to_time(pr.updated_on),
134 'created_on': _render('pullrequest_updated_on',
134 'created_on': _render('pullrequest_updated_on',
135 h.datetime_to_time(pr.created_on)),
135 h.datetime_to_time(pr.created_on)),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
136 'created_on_raw': h.datetime_to_time(pr.created_on),
137 'state': pr.pull_request_state,
137 'state': pr.pull_request_state,
138 'author': _render('pullrequest_author',
138 'author': _render('pullrequest_author',
139 pr.author.full_contact, ),
139 pr.author.full_contact, ),
140 'author_raw': pr.author.full_name,
140 'author_raw': pr.author.full_name,
141 'comments': _render('pullrequest_comments', comments_count),
141 'comments': _render('pullrequest_comments', comments_count),
142 'comments_raw': comments_count,
142 'comments_raw': comments_count,
143 'closed': pr.is_closed(),
143 'closed': pr.is_closed(),
144 })
144 })
145
145
146 data = ({
146 data = ({
147 'draw': draw,
147 'draw': draw,
148 'data': data,
148 'data': data,
149 'recordsTotal': pull_requests_total_count,
149 'recordsTotal': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
150 'recordsFiltered': pull_requests_total_count,
151 })
151 })
152 return data
152 return data
153
153
154 @LoginRequired()
154 @LoginRequired()
155 @HasRepoPermissionAnyDecorator(
155 @HasRepoPermissionAnyDecorator(
156 'repository.read', 'repository.write', 'repository.admin')
156 'repository.read', 'repository.write', 'repository.admin')
157 def pull_request_list(self):
157 def pull_request_list(self):
158 c = self.load_default_context()
158 c = self.load_default_context()
159
159
160 req_get = self.request.GET
160 req_get = self.request.GET
161 c.source = str2bool(req_get.get('source'))
161 c.source = str2bool(req_get.get('source'))
162 c.closed = str2bool(req_get.get('closed'))
162 c.closed = str2bool(req_get.get('closed'))
163 c.my = str2bool(req_get.get('my'))
163 c.my = str2bool(req_get.get('my'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
164 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
165 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
166
166
167 c.active = 'open'
167 c.active = 'open'
168 if c.my:
168 if c.my:
169 c.active = 'my'
169 c.active = 'my'
170 if c.closed:
170 if c.closed:
171 c.active = 'closed'
171 c.active = 'closed'
172 if c.awaiting_review and not c.source:
172 if c.awaiting_review and not c.source:
173 c.active = 'awaiting'
173 c.active = 'awaiting'
174 if c.source and not c.awaiting_review:
174 if c.source and not c.awaiting_review:
175 c.active = 'source'
175 c.active = 'source'
176 if c.awaiting_my_review:
176 if c.awaiting_my_review:
177 c.active = 'awaiting_my'
177 c.active = 'awaiting_my'
178
178
179 return self._get_template_context(c)
179 return self._get_template_context(c)
180
180
181 @LoginRequired()
181 @LoginRequired()
182 @HasRepoPermissionAnyDecorator(
182 @HasRepoPermissionAnyDecorator(
183 'repository.read', 'repository.write', 'repository.admin')
183 'repository.read', 'repository.write', 'repository.admin')
184 def pull_request_list_data(self):
184 def pull_request_list_data(self):
185 self.load_default_context()
185 self.load_default_context()
186
186
187 # additional filters
187 # additional filters
188 req_get = self.request.GET
188 req_get = self.request.GET
189 source = str2bool(req_get.get('source'))
189 source = str2bool(req_get.get('source'))
190 closed = str2bool(req_get.get('closed'))
190 closed = str2bool(req_get.get('closed'))
191 my = str2bool(req_get.get('my'))
191 my = str2bool(req_get.get('my'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_review = str2bool(req_get.get('awaiting_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
194
194
195 filter_type = 'awaiting_review' if awaiting_review \
195 filter_type = 'awaiting_review' if awaiting_review \
196 else 'awaiting_my_review' if awaiting_my_review \
196 else 'awaiting_my_review' if awaiting_my_review \
197 else None
197 else None
198
198
199 opened_by = None
199 opened_by = None
200 if my:
200 if my:
201 opened_by = [self._rhodecode_user.user_id]
201 opened_by = [self._rhodecode_user.user_id]
202
202
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
204 if closed:
204 if closed:
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 data = self._get_pull_requests_list(
207 data = self._get_pull_requests_list(
208 repo_name=self.db_repo_name, source=source,
208 repo_name=self.db_repo_name, source=source,
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
210
210
211 return data
211 return data
212
212
213 def _is_diff_cache_enabled(self, target_repo):
213 def _is_diff_cache_enabled(self, target_repo):
214 caching_enabled = self._get_general_setting(
214 caching_enabled = self._get_general_setting(
215 target_repo, 'rhodecode_diff_cache')
215 target_repo, 'rhodecode_diff_cache')
216 log.debug('Diff caching enabled: %s', caching_enabled)
216 log.debug('Diff caching enabled: %s', caching_enabled)
217 return caching_enabled
217 return caching_enabled
218
218
219 def _get_diffset(self, source_repo_name, source_repo,
219 def _get_diffset(self, source_repo_name, source_repo,
220 ancestor_commit,
220 ancestor_commit,
221 source_ref_id, target_ref_id,
221 source_ref_id, target_ref_id,
222 target_commit, source_commit, diff_limit, file_limit,
222 target_commit, source_commit, diff_limit, file_limit,
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
224
224
225 target_commit_final = target_commit
225 target_commit_final = target_commit
226 source_commit_final = source_commit
226 source_commit_final = source_commit
227
227
228 if use_ancestor:
228 if use_ancestor:
229 # we might want to not use it for versions
229 # we might want to not use it for versions
230 target_ref_id = ancestor_commit.raw_id
230 target_ref_id = ancestor_commit.raw_id
231 target_commit_final = ancestor_commit
231 target_commit_final = ancestor_commit
232
232
233 vcs_diff = PullRequestModel().get_diff(
233 vcs_diff = PullRequestModel().get_diff(
234 source_repo, source_ref_id, target_ref_id,
234 source_repo, source_ref_id, target_ref_id,
235 hide_whitespace_changes, diff_context)
235 hide_whitespace_changes, diff_context)
236
236
237 diff_processor = diffs.DiffProcessor(
237 diff_processor = diffs.DiffProcessor(
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
238 vcs_diff, format='newdiff', diff_limit=diff_limit,
239 file_limit=file_limit, show_full_diff=fulldiff)
239 file_limit=file_limit, show_full_diff=fulldiff)
240
240
241 _parsed = diff_processor.prepare()
241 _parsed = diff_processor.prepare()
242
242
243 diffset = codeblocks.DiffSet(
243 diffset = codeblocks.DiffSet(
244 repo_name=self.db_repo_name,
244 repo_name=self.db_repo_name,
245 source_repo_name=source_repo_name,
245 source_repo_name=source_repo_name,
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
246 source_node_getter=codeblocks.diffset_node_getter(target_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
247 target_node_getter=codeblocks.diffset_node_getter(source_commit_final),
248 )
248 )
249 diffset = self.path_filter.render_patchset_filtered(
249 diffset = self.path_filter.render_patchset_filtered(
250 diffset, _parsed, target_ref_id, source_ref_id)
250 diffset, _parsed, target_ref_id, source_ref_id)
251
251
252 return diffset
252 return diffset
253
253
254 def _get_range_diffset(self, source_scm, source_repo,
254 def _get_range_diffset(self, source_scm, source_repo,
255 commit1, commit2, diff_limit, file_limit,
255 commit1, commit2, diff_limit, file_limit,
256 fulldiff, hide_whitespace_changes, diff_context):
256 fulldiff, hide_whitespace_changes, diff_context):
257 vcs_diff = source_scm.get_diff(
257 vcs_diff = source_scm.get_diff(
258 commit1, commit2,
258 commit1, commit2,
259 ignore_whitespace=hide_whitespace_changes,
259 ignore_whitespace=hide_whitespace_changes,
260 context=diff_context)
260 context=diff_context)
261
261
262 diff_processor = diffs.DiffProcessor(
262 diff_processor = diffs.DiffProcessor(
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
263 vcs_diff, format='newdiff', diff_limit=diff_limit,
264 file_limit=file_limit, show_full_diff=fulldiff)
264 file_limit=file_limit, show_full_diff=fulldiff)
265
265
266 _parsed = diff_processor.prepare()
266 _parsed = diff_processor.prepare()
267
267
268 diffset = codeblocks.DiffSet(
268 diffset = codeblocks.DiffSet(
269 repo_name=source_repo.repo_name,
269 repo_name=source_repo.repo_name,
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
270 source_node_getter=codeblocks.diffset_node_getter(commit1),
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
271 target_node_getter=codeblocks.diffset_node_getter(commit2))
272
272
273 diffset = self.path_filter.render_patchset_filtered(
273 diffset = self.path_filter.render_patchset_filtered(
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
274 diffset, _parsed, commit1.raw_id, commit2.raw_id)
275
275
276 return diffset
276 return diffset
277
277
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
278 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
279 comments_model = CommentsModel()
279 comments_model = CommentsModel()
280
280
281 # GENERAL COMMENTS with versions #
281 # GENERAL COMMENTS with versions #
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
282 q = comments_model._all_general_comments_of_pull_request(pull_request)
283 q = q.order_by(ChangesetComment.comment_id.asc())
283 q = q.order_by(ChangesetComment.comment_id.asc())
284 if not include_drafts:
284 if not include_drafts:
285 q = q.filter(ChangesetComment.draft == false())
285 q = q.filter(ChangesetComment.draft == false())
286 general_comments = q
286 general_comments = q
287
287
288 # pick comments we want to render at current version
288 # pick comments we want to render at current version
289 c.comment_versions = comments_model.aggregate_comments(
289 c.comment_versions = comments_model.aggregate_comments(
290 general_comments, versions, c.at_version_num)
290 general_comments, versions, c.at_version_num)
291
291
292 # INLINE COMMENTS with versions #
292 # INLINE COMMENTS with versions #
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
293 q = comments_model._all_inline_comments_of_pull_request(pull_request)
294 q = q.order_by(ChangesetComment.comment_id.asc())
294 q = q.order_by(ChangesetComment.comment_id.asc())
295 if not include_drafts:
295 if not include_drafts:
296 q = q.filter(ChangesetComment.draft == false())
296 q = q.filter(ChangesetComment.draft == false())
297 inline_comments = q
297 inline_comments = q
298
298
299 c.inline_versions = comments_model.aggregate_comments(
299 c.inline_versions = comments_model.aggregate_comments(
300 inline_comments, versions, c.at_version_num, inline=True)
300 inline_comments, versions, c.at_version_num, inline=True)
301
301
302 # Comments inline+general
302 # Comments inline+general
303 if c.at_version:
303 if c.at_version:
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
304 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
305 c.comments = c.comment_versions[c.at_version_num]['display']
306 else:
306 else:
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
307 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
308 c.comments = c.comment_versions[c.at_version_num]['until']
309
309
310 return general_comments, inline_comments
310 return general_comments, inline_comments
311
311
312 @LoginRequired()
312 @LoginRequired()
313 @HasRepoPermissionAnyDecorator(
313 @HasRepoPermissionAnyDecorator(
314 'repository.read', 'repository.write', 'repository.admin')
314 'repository.read', 'repository.write', 'repository.admin')
315 def pull_request_show(self):
315 def pull_request_show(self):
316 _ = self.request.translate
316 _ = self.request.translate
317 c = self.load_default_context()
317 c = self.load_default_context()
318
318
319 pull_request = PullRequest.get_or_404(
319 pull_request = PullRequest.get_or_404(
320 self.request.matchdict['pull_request_id'])
320 self.request.matchdict['pull_request_id'])
321 pull_request_id = pull_request.pull_request_id
321 pull_request_id = pull_request.pull_request_id
322
322
323 c.state_progressing = pull_request.is_state_changing()
323 c.state_progressing = pull_request.is_state_changing()
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
324 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
325
325
326 _new_state = {
326 _new_state = {
327 'created': PullRequest.STATE_CREATED,
327 'created': PullRequest.STATE_CREATED,
328 }.get(self.request.GET.get('force_state'))
328 }.get(self.request.GET.get('force_state'))
329
329
330 if c.is_super_admin and _new_state:
330 if c.is_super_admin and _new_state:
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
331 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
332 h.flash(
332 h.flash(
333 _('Pull Request state was force changed to `{}`').format(_new_state),
333 _('Pull Request state was force changed to `{}`').format(_new_state),
334 category='success')
334 category='success')
335 Session().commit()
335 Session().commit()
336
336
337 raise HTTPFound(h.route_path(
337 raise HTTPFound(h.route_path(
338 'pullrequest_show', repo_name=self.db_repo_name,
338 'pullrequest_show', repo_name=self.db_repo_name,
339 pull_request_id=pull_request_id))
339 pull_request_id=pull_request_id))
340
340
341 version = self.request.GET.get('version')
341 version = self.request.GET.get('version')
342 from_version = self.request.GET.get('from_version') or version
342 from_version = self.request.GET.get('from_version') or version
343 merge_checks = self.request.GET.get('merge_checks')
343 merge_checks = self.request.GET.get('merge_checks')
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
344 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
345 force_refresh = str2bool(self.request.GET.get('force_refresh'))
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
346 c.range_diff_on = self.request.GET.get('range-diff') == "1"
347
347
348 # fetch global flags of ignore ws or context lines
348 # fetch global flags of ignore ws or context lines
349 diff_context = diffs.get_diff_context(self.request)
349 diff_context = diffs.get_diff_context(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
350 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
351
351
352 (pull_request_latest,
352 (pull_request_latest,
353 pull_request_at_ver,
353 pull_request_at_ver,
354 pull_request_display_obj,
354 pull_request_display_obj,
355 at_version) = PullRequestModel().get_pr_version(
355 at_version) = PullRequestModel().get_pr_version(
356 pull_request_id, version=version)
356 pull_request_id, version=version)
357
357
358 pr_closed = pull_request_latest.is_closed()
358 pr_closed = pull_request_latest.is_closed()
359
359
360 if pr_closed and (version or from_version):
360 if pr_closed and (version or from_version):
361 # not allow to browse versions for closed PR
361 # not allow to browse versions for closed PR
362 raise HTTPFound(h.route_path(
362 raise HTTPFound(h.route_path(
363 'pullrequest_show', repo_name=self.db_repo_name,
363 'pullrequest_show', repo_name=self.db_repo_name,
364 pull_request_id=pull_request_id))
364 pull_request_id=pull_request_id))
365
365
366 versions = pull_request_display_obj.versions()
366 versions = pull_request_display_obj.versions()
367
367
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
368 c.commit_versions = PullRequestModel().pr_commits_versions(versions)
369
369
370 # used to store per-commit range diffs
370 # used to store per-commit range diffs
371 c.changes = collections.OrderedDict()
371 c.changes = collections.OrderedDict()
372
372
373 c.at_version = at_version
373 c.at_version = at_version
374 c.at_version_num = (at_version
374 c.at_version_num = (at_version
375 if at_version and at_version != PullRequest.LATEST_VER
375 if at_version and at_version != PullRequest.LATEST_VER
376 else None)
376 else None)
377
377
378 c.at_version_index = ChangesetComment.get_index_from_version(
378 c.at_version_index = ChangesetComment.get_index_from_version(
379 c.at_version_num, versions)
379 c.at_version_num, versions)
380
380
381 (prev_pull_request_latest,
381 (prev_pull_request_latest,
382 prev_pull_request_at_ver,
382 prev_pull_request_at_ver,
383 prev_pull_request_display_obj,
383 prev_pull_request_display_obj,
384 prev_at_version) = PullRequestModel().get_pr_version(
384 prev_at_version) = PullRequestModel().get_pr_version(
385 pull_request_id, version=from_version)
385 pull_request_id, version=from_version)
386
386
387 c.from_version = prev_at_version
387 c.from_version = prev_at_version
388 c.from_version_num = (prev_at_version
388 c.from_version_num = (prev_at_version
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
389 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
390 else None)
390 else None)
391 c.from_version_index = ChangesetComment.get_index_from_version(
391 c.from_version_index = ChangesetComment.get_index_from_version(
392 c.from_version_num, versions)
392 c.from_version_num, versions)
393
393
394 # define if we're in COMPARE mode or VIEW at version mode
394 # define if we're in COMPARE mode or VIEW at version mode
395 compare = at_version != prev_at_version
395 compare = at_version != prev_at_version
396
396
397 # pull_requests repo_name we opened it against
397 # pull_requests repo_name we opened it against
398 # ie. target_repo must match
398 # ie. target_repo must match
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
399 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
400 log.warning('Mismatch between the current repo: %s, and target %s',
400 log.warning('Mismatch between the current repo: %s, and target %s',
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
401 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
402 raise HTTPNotFound()
402 raise HTTPNotFound()
403
403
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
404 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
405
405
406 c.pull_request = pull_request_display_obj
406 c.pull_request = pull_request_display_obj
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
407 c.renderer = pull_request_at_ver.description_renderer or c.renderer
408 c.pull_request_latest = pull_request_latest
408 c.pull_request_latest = pull_request_latest
409
409
410 # inject latest version
410 # inject latest version
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
411 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
412 c.versions = versions + [latest_ver]
412 c.versions = versions + [latest_ver]
413
413
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
414 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
415 c.allowed_to_change_status = False
415 c.allowed_to_change_status = False
416 c.allowed_to_update = False
416 c.allowed_to_update = False
417 c.allowed_to_merge = False
417 c.allowed_to_merge = False
418 c.allowed_to_delete = False
418 c.allowed_to_delete = False
419 c.allowed_to_comment = False
419 c.allowed_to_comment = False
420 c.allowed_to_close = False
420 c.allowed_to_close = False
421 else:
421 else:
422 can_change_status = PullRequestModel().check_user_change_status(
422 can_change_status = PullRequestModel().check_user_change_status(
423 pull_request_at_ver, self._rhodecode_user)
423 pull_request_at_ver, self._rhodecode_user)
424 c.allowed_to_change_status = can_change_status and not pr_closed
424 c.allowed_to_change_status = can_change_status and not pr_closed
425
425
426 c.allowed_to_update = PullRequestModel().check_user_update(
426 c.allowed_to_update = PullRequestModel().check_user_update(
427 pull_request_latest, self._rhodecode_user) and not pr_closed
427 pull_request_latest, self._rhodecode_user) and not pr_closed
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
428 c.allowed_to_merge = PullRequestModel().check_user_merge(
429 pull_request_latest, self._rhodecode_user) and not pr_closed
429 pull_request_latest, self._rhodecode_user) and not pr_closed
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
430 c.allowed_to_delete = PullRequestModel().check_user_delete(
431 pull_request_latest, self._rhodecode_user) and not pr_closed
431 pull_request_latest, self._rhodecode_user) and not pr_closed
432 c.allowed_to_comment = not pr_closed
432 c.allowed_to_comment = not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
433 c.allowed_to_close = c.allowed_to_merge and not pr_closed
434
434
435 c.forbid_adding_reviewers = False
435 c.forbid_adding_reviewers = False
436
436
437 if pull_request_latest.reviewer_data and \
437 if pull_request_latest.reviewer_data and \
438 'rules' in pull_request_latest.reviewer_data:
438 'rules' in pull_request_latest.reviewer_data:
439 rules = pull_request_latest.reviewer_data['rules'] or {}
439 rules = pull_request_latest.reviewer_data['rules'] or {}
440 try:
440 try:
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
441 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
442 except Exception:
442 except Exception:
443 pass
443 pass
444
444
445 # check merge capabilities
445 # check merge capabilities
446 _merge_check = MergeCheck.validate(
446 _merge_check = MergeCheck.validate(
447 pull_request_latest, auth_user=self._rhodecode_user,
447 pull_request_latest, auth_user=self._rhodecode_user,
448 translator=self.request.translate,
448 translator=self.request.translate,
449 force_shadow_repo_refresh=force_refresh)
449 force_shadow_repo_refresh=force_refresh)
450
450
451 c.pr_merge_errors = _merge_check.error_details
451 c.pr_merge_errors = _merge_check.error_details
452 c.pr_merge_possible = not _merge_check.failed
452 c.pr_merge_possible = not _merge_check.failed
453 c.pr_merge_message = _merge_check.merge_msg
453 c.pr_merge_message = _merge_check.merge_msg
454 c.pr_merge_source_commit = _merge_check.source_commit
454 c.pr_merge_source_commit = _merge_check.source_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
455 c.pr_merge_target_commit = _merge_check.target_commit
456
456
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
457 c.pr_merge_info = MergeCheck.get_merge_conditions(
458 pull_request_latest, translator=self.request.translate)
458 pull_request_latest, translator=self.request.translate)
459
459
460 c.pull_request_review_status = _merge_check.review_status
460 c.pull_request_review_status = _merge_check.review_status
461 if merge_checks:
461 if merge_checks:
462 self.request.override_renderer = \
462 self.request.override_renderer = \
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
463 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
464 return self._get_template_context(c)
464 return self._get_template_context(c)
465
465
466 c.reviewers_count = pull_request.reviewers_count
466 c.reviewers_count = pull_request.reviewers_count
467 c.observers_count = pull_request.observers_count
467 c.observers_count = pull_request.observers_count
468
468
469 # reviewers and statuses
469 # reviewers and statuses
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
470 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
471 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
472 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
473
473
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
474 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
475 member_reviewer = h.reviewer_as_json(
475 member_reviewer = h.reviewer_as_json(
476 member, reasons=reasons, mandatory=mandatory,
476 member, reasons=reasons, mandatory=mandatory,
477 role=review_obj.role,
477 role=review_obj.role,
478 user_group=review_obj.rule_user_group_data()
478 user_group=review_obj.rule_user_group_data()
479 )
479 )
480
480
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
481 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
482 member_reviewer['review_status'] = current_review_status
482 member_reviewer['review_status'] = current_review_status
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
483 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
484 member_reviewer['allowed_to_update'] = c.allowed_to_update
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
485 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
486
486
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
487 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
488
488
489 for observer_obj, member in pull_request_at_ver.observers():
489 for observer_obj, member in pull_request_at_ver.observers():
490 member_observer = h.reviewer_as_json(
490 member_observer = h.reviewer_as_json(
491 member, reasons=[], mandatory=False,
491 member, reasons=[], mandatory=False,
492 role=observer_obj.role,
492 role=observer_obj.role,
493 user_group=observer_obj.rule_user_group_data()
493 user_group=observer_obj.rule_user_group_data()
494 )
494 )
495 member_observer['allowed_to_update'] = c.allowed_to_update
495 member_observer['allowed_to_update'] = c.allowed_to_update
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
496 c.pull_request_set_observers_data_json['observers'].append(member_observer)
497
497
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
498 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
499
499
500 general_comments, inline_comments = \
500 general_comments, inline_comments = \
501 self.register_comments_vars(c, pull_request_latest, versions)
501 self.register_comments_vars(c, pull_request_latest, versions)
502
502
503 # TODOs
503 # TODOs
504 c.unresolved_comments = CommentsModel() \
504 c.unresolved_comments = CommentsModel() \
505 .get_pull_request_unresolved_todos(pull_request_latest)
505 .get_pull_request_unresolved_todos(pull_request_latest)
506 c.resolved_comments = CommentsModel() \
506 c.resolved_comments = CommentsModel() \
507 .get_pull_request_resolved_todos(pull_request_latest)
507 .get_pull_request_resolved_todos(pull_request_latest)
508
508
509 # Drafts
509 # Drafts
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
510 c.draft_comments = CommentsModel().get_pull_request_drafts(
511 self._rhodecode_db_user.user_id,
511 self._rhodecode_db_user.user_id,
512 pull_request_latest)
512 pull_request_latest)
513
513
514 # if we use version, then do not show later comments
514 # if we use version, then do not show later comments
515 # than current version
515 # than current version
516 display_inline_comments = collections.defaultdict(
516 display_inline_comments = collections.defaultdict(
517 lambda: collections.defaultdict(list))
517 lambda: collections.defaultdict(list))
518 for co in inline_comments:
518 for co in inline_comments:
519 if c.at_version_num:
519 if c.at_version_num:
520 # pick comments that are at least UPTO given version, so we
520 # pick comments that are at least UPTO given version, so we
521 # don't render comments for higher version
521 # don't render comments for higher version
522 should_render = co.pull_request_version_id and \
522 should_render = co.pull_request_version_id and \
523 co.pull_request_version_id <= c.at_version_num
523 co.pull_request_version_id <= c.at_version_num
524 else:
524 else:
525 # showing all, for 'latest'
525 # showing all, for 'latest'
526 should_render = True
526 should_render = True
527
527
528 if should_render:
528 if should_render:
529 display_inline_comments[co.f_path][co.line_no].append(co)
529 display_inline_comments[co.f_path][co.line_no].append(co)
530
530
531 # load diff data into template context, if we use compare mode then
531 # load diff data into template context, if we use compare mode then
532 # diff is calculated based on changes between versions of PR
532 # diff is calculated based on changes between versions of PR
533
533
534 source_repo = pull_request_at_ver.source_repo
534 source_repo = pull_request_at_ver.source_repo
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
535 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
536
536
537 target_repo = pull_request_at_ver.target_repo
537 target_repo = pull_request_at_ver.target_repo
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
538 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
539
539
540 if compare:
540 if compare:
541 # in compare switch the diff base to latest commit from prev version
541 # in compare switch the diff base to latest commit from prev version
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
542 target_ref_id = prev_pull_request_display_obj.revisions[0]
543
543
544 # despite opening commits for bookmarks/branches/tags, we always
544 # despite opening commits for bookmarks/branches/tags, we always
545 # convert this to rev to prevent changes after bookmark or branch change
545 # convert this to rev to prevent changes after bookmark or branch change
546 c.source_ref_type = 'rev'
546 c.source_ref_type = 'rev'
547 c.source_ref = source_ref_id
547 c.source_ref = source_ref_id
548
548
549 c.target_ref_type = 'rev'
549 c.target_ref_type = 'rev'
550 c.target_ref = target_ref_id
550 c.target_ref = target_ref_id
551
551
552 c.source_repo = source_repo
552 c.source_repo = source_repo
553 c.target_repo = target_repo
553 c.target_repo = target_repo
554
554
555 c.commit_ranges = []
555 c.commit_ranges = []
556 source_commit = EmptyCommit()
556 source_commit = EmptyCommit()
557 target_commit = EmptyCommit()
557 target_commit = EmptyCommit()
558 c.missing_requirements = False
558 c.missing_requirements = False
559
559
560 source_scm = source_repo.scm_instance()
560 source_scm = source_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
561 target_scm = target_repo.scm_instance()
562
562
563 shadow_scm = None
563 shadow_scm = None
564 try:
564 try:
565 shadow_scm = pull_request_latest.get_shadow_repo()
565 shadow_scm = pull_request_latest.get_shadow_repo()
566 except Exception:
566 except Exception:
567 log.debug('Failed to get shadow repo', exc_info=True)
567 log.debug('Failed to get shadow repo', exc_info=True)
568 # try first the existing source_repo, and then shadow
568 # try first the existing source_repo, and then shadow
569 # repo if we can obtain one
569 # repo if we can obtain one
570 commits_source_repo = source_scm
570 commits_source_repo = source_scm
571 if shadow_scm:
571 if shadow_scm:
572 commits_source_repo = shadow_scm
572 commits_source_repo = shadow_scm
573
573
574 c.commits_source_repo = commits_source_repo
574 c.commits_source_repo = commits_source_repo
575 c.ancestor = None # set it to None, to hide it from PR view
575 c.ancestor = None # set it to None, to hide it from PR view
576
576
577 # empty version means latest, so we keep this to prevent
577 # empty version means latest, so we keep this to prevent
578 # double caching
578 # double caching
579 version_normalized = version or PullRequest.LATEST_VER
579 version_normalized = version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
580 from_version_normalized = from_version or PullRequest.LATEST_VER
581
581
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
582 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
583 cache_file_path = diff_cache_exist(
583 cache_file_path = diff_cache_exist(
584 cache_path, 'pull_request', pull_request_id, version_normalized,
584 cache_path, 'pull_request', pull_request_id, version_normalized,
585 from_version_normalized, source_ref_id, target_ref_id,
585 from_version_normalized, source_ref_id, target_ref_id,
586 hide_whitespace_changes, diff_context, c.fulldiff)
586 hide_whitespace_changes, diff_context, c.fulldiff)
587
587
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
588 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
589 force_recache = self.get_recache_flag()
589 force_recache = self.get_recache_flag()
590
590
591 cached_diff = None
591 cached_diff = None
592 if caching_enabled:
592 if caching_enabled:
593 cached_diff = load_cached_diff(cache_file_path)
593 cached_diff = load_cached_diff(cache_file_path)
594
594
595 has_proper_commit_cache = (
595 has_proper_commit_cache = (
596 cached_diff and cached_diff.get('commits')
596 cached_diff and cached_diff.get('commits')
597 and len(cached_diff.get('commits', [])) == 5
597 and len(cached_diff.get('commits', [])) == 5
598 and cached_diff.get('commits')[0]
598 and cached_diff.get('commits')[0]
599 and cached_diff.get('commits')[3])
599 and cached_diff.get('commits')[3])
600
600
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
601 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
602 diff_commit_cache = \
602 diff_commit_cache = \
603 (ancestor_commit, commit_cache, missing_requirements,
603 (ancestor_commit, commit_cache, missing_requirements,
604 source_commit, target_commit) = cached_diff['commits']
604 source_commit, target_commit) = cached_diff['commits']
605 else:
605 else:
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
606 # NOTE(marcink): we reach potentially unreachable errors when a PR has
607 # merge errors resulting in potentially hidden commits in the shadow repo.
607 # merge errors resulting in potentially hidden commits in the shadow repo.
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
608 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
609 and _merge_check.merge_response
609 and _merge_check.merge_response
610 maybe_unreachable = maybe_unreachable \
610 maybe_unreachable = maybe_unreachable \
611 and _merge_check.merge_response.metadata.get('unresolved_files')
611 and _merge_check.merge_response.metadata.get('unresolved_files')
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
612 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
613 diff_commit_cache = \
613 diff_commit_cache = \
614 (ancestor_commit, commit_cache, missing_requirements,
614 (ancestor_commit, commit_cache, missing_requirements,
615 source_commit, target_commit) = self.get_commits(
615 source_commit, target_commit) = self.get_commits(
616 commits_source_repo,
616 commits_source_repo,
617 pull_request_at_ver,
617 pull_request_at_ver,
618 source_commit,
618 source_commit,
619 source_ref_id,
619 source_ref_id,
620 source_scm,
620 source_scm,
621 target_commit,
621 target_commit,
622 target_ref_id,
622 target_ref_id,
623 target_scm,
623 target_scm,
624 maybe_unreachable=maybe_unreachable)
624 maybe_unreachable=maybe_unreachable)
625
625
626 # register our commit range
626 # register our commit range
627 for comm in commit_cache.values():
627 for comm in commit_cache.values():
628 c.commit_ranges.append(comm)
628 c.commit_ranges.append(comm)
629
629
630 c.missing_requirements = missing_requirements
630 c.missing_requirements = missing_requirements
631 c.ancestor_commit = ancestor_commit
631 c.ancestor_commit = ancestor_commit
632 c.statuses = source_repo.statuses(
632 c.statuses = source_repo.statuses(
633 [x.raw_id for x in c.commit_ranges])
633 [x.raw_id for x in c.commit_ranges])
634
634
635 # auto collapse if we have more than limit
635 # auto collapse if we have more than limit
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
636 collapse_limit = diffs.DiffProcessor._collapse_commits_over
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
637 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
638 c.compare_mode = compare
638 c.compare_mode = compare
639
639
640 # diff_limit is the old behavior, will cut off the whole diff
640 # diff_limit is the old behavior, will cut off the whole diff
641 # if the limit is applied otherwise will just hide the
641 # if the limit is applied otherwise will just hide the
642 # big files from the front-end
642 # big files from the front-end
643 diff_limit = c.visual.cut_off_limit_diff
643 diff_limit = c.visual.cut_off_limit_diff
644 file_limit = c.visual.cut_off_limit_file
644 file_limit = c.visual.cut_off_limit_file
645
645
646 c.missing_commits = False
646 c.missing_commits = False
647 if (c.missing_requirements
647 if (c.missing_requirements
648 or isinstance(source_commit, EmptyCommit)
648 or isinstance(source_commit, EmptyCommit)
649 or source_commit == target_commit):
649 or source_commit == target_commit):
650
650
651 c.missing_commits = True
651 c.missing_commits = True
652 else:
652 else:
653 c.inline_comments = display_inline_comments
653 c.inline_comments = display_inline_comments
654
654
655 use_ancestor = True
655 use_ancestor = True
656 if from_version_normalized != version_normalized:
656 if from_version_normalized != version_normalized:
657 use_ancestor = False
657 use_ancestor = False
658
658
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
659 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
660 if not force_recache and has_proper_diff_cache:
660 if not force_recache and has_proper_diff_cache:
661 c.diffset = cached_diff['diff']
661 c.diffset = cached_diff['diff']
662 else:
662 else:
663 try:
663 try:
664 c.diffset = self._get_diffset(
664 c.diffset = self._get_diffset(
665 c.source_repo.repo_name, commits_source_repo,
665 c.source_repo.repo_name, commits_source_repo,
666 c.ancestor_commit,
666 c.ancestor_commit,
667 source_ref_id, target_ref_id,
667 source_ref_id, target_ref_id,
668 target_commit, source_commit,
668 target_commit, source_commit,
669 diff_limit, file_limit, c.fulldiff,
669 diff_limit, file_limit, c.fulldiff,
670 hide_whitespace_changes, diff_context,
670 hide_whitespace_changes, diff_context,
671 use_ancestor=use_ancestor
671 use_ancestor=use_ancestor
672 )
672 )
673
673
674 # save cached diff
674 # save cached diff
675 if caching_enabled:
675 if caching_enabled:
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
676 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
677 except CommitDoesNotExistError:
677 except CommitDoesNotExistError:
678 log.exception('Failed to generate diffset')
678 log.exception('Failed to generate diffset')
679 c.missing_commits = True
679 c.missing_commits = True
680
680
681 if not c.missing_commits:
681 if not c.missing_commits:
682
682
683 c.limited_diff = c.diffset.limited_diff
683 c.limited_diff = c.diffset.limited_diff
684
684
685 # calculate removed files that are bound to comments
685 # calculate removed files that are bound to comments
686 comment_deleted_files = [
686 comment_deleted_files = [
687 fname for fname in display_inline_comments
687 fname for fname in display_inline_comments
688 if fname not in c.diffset.file_stats]
688 if fname not in c.diffset.file_stats]
689
689
690 c.deleted_files_comments = collections.defaultdict(dict)
690 c.deleted_files_comments = collections.defaultdict(dict)
691 for fname, per_line_comments in display_inline_comments.items():
691 for fname, per_line_comments in display_inline_comments.items():
692 if fname in comment_deleted_files:
692 if fname in comment_deleted_files:
693 c.deleted_files_comments[fname]['stats'] = 0
693 c.deleted_files_comments[fname]['stats'] = 0
694 c.deleted_files_comments[fname]['comments'] = list()
694 c.deleted_files_comments[fname]['comments'] = list()
695 for lno, comments in per_line_comments.items():
695 for lno, comments in per_line_comments.items():
696 c.deleted_files_comments[fname]['comments'].extend(comments)
696 c.deleted_files_comments[fname]['comments'].extend(comments)
697
697
698 # maybe calculate the range diff
698 # maybe calculate the range diff
699 if c.range_diff_on:
699 if c.range_diff_on:
700 # TODO(marcink): set whitespace/context
700 # TODO(marcink): set whitespace/context
701 context_lcl = 3
701 context_lcl = 3
702 ign_whitespace_lcl = False
702 ign_whitespace_lcl = False
703
703
704 for commit in c.commit_ranges:
704 for commit in c.commit_ranges:
705 commit2 = commit
705 commit2 = commit
706 commit1 = commit.first_parent
706 commit1 = commit.first_parent
707
707
708 range_diff_cache_file_path = diff_cache_exist(
708 range_diff_cache_file_path = diff_cache_exist(
709 cache_path, 'diff', commit.raw_id,
709 cache_path, 'diff', commit.raw_id,
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
710 ign_whitespace_lcl, context_lcl, c.fulldiff)
711
711
712 cached_diff = None
712 cached_diff = None
713 if caching_enabled:
713 if caching_enabled:
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
714 cached_diff = load_cached_diff(range_diff_cache_file_path)
715
715
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
716 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
717 if not force_recache and has_proper_diff_cache:
717 if not force_recache and has_proper_diff_cache:
718 diffset = cached_diff['diff']
718 diffset = cached_diff['diff']
719 else:
719 else:
720 diffset = self._get_range_diffset(
720 diffset = self._get_range_diffset(
721 commits_source_repo, source_repo,
721 commits_source_repo, source_repo,
722 commit1, commit2, diff_limit, file_limit,
722 commit1, commit2, diff_limit, file_limit,
723 c.fulldiff, ign_whitespace_lcl, context_lcl
723 c.fulldiff, ign_whitespace_lcl, context_lcl
724 )
724 )
725
725
726 # save cached diff
726 # save cached diff
727 if caching_enabled:
727 if caching_enabled:
728 cache_diff(range_diff_cache_file_path, diffset, None)
728 cache_diff(range_diff_cache_file_path, diffset, None)
729
729
730 c.changes[commit.raw_id] = diffset
730 c.changes[commit.raw_id] = diffset
731
731
732 # this is a hack to properly display links, when creating PR, the
732 # this is a hack to properly display links, when creating PR, the
733 # compare view and others uses different notation, and
733 # compare view and others uses different notation, and
734 # compare_commits.mako renders links based on the target_repo.
734 # compare_commits.mako renders links based on the target_repo.
735 # We need to swap that here to generate it properly on the html side
735 # We need to swap that here to generate it properly on the html side
736 c.target_repo = c.source_repo
736 c.target_repo = c.source_repo
737
737
738 c.commit_statuses = ChangesetStatus.STATUSES
738 c.commit_statuses = ChangesetStatus.STATUSES
739
739
740 c.show_version_changes = not pr_closed
740 c.show_version_changes = not pr_closed
741 if c.show_version_changes:
741 if c.show_version_changes:
742 cur_obj = pull_request_at_ver
742 cur_obj = pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
743 prev_obj = prev_pull_request_at_ver
744
744
745 old_commit_ids = prev_obj.revisions
745 old_commit_ids = prev_obj.revisions
746 new_commit_ids = cur_obj.revisions
746 new_commit_ids = cur_obj.revisions
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
747 commit_changes = PullRequestModel()._calculate_commit_id_changes(
748 old_commit_ids, new_commit_ids)
748 old_commit_ids, new_commit_ids)
749 c.commit_changes_summary = commit_changes
749 c.commit_changes_summary = commit_changes
750
750
751 # calculate the diff for commits between versions
751 # calculate the diff for commits between versions
752 c.commit_changes = []
752 c.commit_changes = []
753
753
754 def mark(cs, fw):
754 def mark(cs, fw):
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
755 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
756
756
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
757 for c_type, raw_id in mark(commit_changes.added, 'a') \
758 + mark(commit_changes.removed, 'r') \
758 + mark(commit_changes.removed, 'r') \
759 + mark(commit_changes.common, 'c'):
759 + mark(commit_changes.common, 'c'):
760
760
761 if raw_id in commit_cache:
761 if raw_id in commit_cache:
762 commit = commit_cache[raw_id]
762 commit = commit_cache[raw_id]
763 else:
763 else:
764 try:
764 try:
765 commit = commits_source_repo.get_commit(raw_id)
765 commit = commits_source_repo.get_commit(raw_id)
766 except CommitDoesNotExistError:
766 except CommitDoesNotExistError:
767 # in case we fail extracting still use "dummy" commit
767 # in case we fail extracting still use "dummy" commit
768 # for display in commit diff
768 # for display in commit diff
769 commit = h.AttributeDict(
769 commit = h.AttributeDict(
770 {'raw_id': raw_id,
770 {'raw_id': raw_id,
771 'message': 'EMPTY or MISSING COMMIT'})
771 'message': 'EMPTY or MISSING COMMIT'})
772 c.commit_changes.append([c_type, commit])
772 c.commit_changes.append([c_type, commit])
773
773
774 # current user review statuses for each version
774 # current user review statuses for each version
775 c.review_versions = {}
775 c.review_versions = {}
776 is_reviewer = PullRequestModel().is_user_reviewer(
776 is_reviewer = PullRequestModel().is_user_reviewer(
777 pull_request, self._rhodecode_user)
777 pull_request, self._rhodecode_user)
778 if is_reviewer:
778 if is_reviewer:
779 for co in general_comments:
779 for co in general_comments:
780 if co.author.user_id == self._rhodecode_user.user_id:
780 if co.author.user_id == self._rhodecode_user.user_id:
781 status = co.status_change
781 status = co.status_change
782 if status:
782 if status:
783 _ver_pr = status[0].comment.pull_request_version_id
783 _ver_pr = status[0].comment.pull_request_version_id
784 c.review_versions[_ver_pr] = status[0]
784 c.review_versions[_ver_pr] = status[0]
785
785
786 return self._get_template_context(c)
786 return self._get_template_context(c)
787
787
788 def get_commits(
788 def get_commits(
789 self, commits_source_repo, pull_request_at_ver, source_commit,
789 self, commits_source_repo, pull_request_at_ver, source_commit,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
790 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
791 maybe_unreachable=False):
791 maybe_unreachable=False):
792
792
793 commit_cache = collections.OrderedDict()
793 commit_cache = collections.OrderedDict()
794 missing_requirements = False
794 missing_requirements = False
795
795
796 try:
796 try:
797 pre_load = ["author", "date", "message", "branch", "parents"]
797 pre_load = ["author", "date", "message", "branch", "parents"]
798
798
799 pull_request_commits = pull_request_at_ver.revisions
799 pull_request_commits = pull_request_at_ver.revisions
800 log.debug('Loading %s commits from %s',
800 log.debug('Loading %s commits from %s',
801 len(pull_request_commits), commits_source_repo)
801 len(pull_request_commits), commits_source_repo)
802
802
803 for rev in pull_request_commits:
803 for rev in pull_request_commits:
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
804 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
805 maybe_unreachable=maybe_unreachable)
805 maybe_unreachable=maybe_unreachable)
806 commit_cache[comm.raw_id] = comm
806 commit_cache[comm.raw_id] = comm
807
807
808 # Order here matters, we first need to get target, and then
808 # Order here matters, we first need to get target, and then
809 # the source
809 # the source
810 target_commit = commits_source_repo.get_commit(
810 target_commit = commits_source_repo.get_commit(
811 commit_id=safe_str(target_ref_id))
811 commit_id=safe_str(target_ref_id))
812
812
813 source_commit = commits_source_repo.get_commit(
813 source_commit = commits_source_repo.get_commit(
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
814 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
815 except CommitDoesNotExistError:
815 except CommitDoesNotExistError:
816 log.warning('Failed to get commit from `{}` repo'.format(
816 log.warning('Failed to get commit from `{}` repo'.format(
817 commits_source_repo), exc_info=True)
817 commits_source_repo), exc_info=True)
818 except RepositoryRequirementError:
818 except RepositoryRequirementError:
819 log.warning('Failed to get all required data from repo', exc_info=True)
819 log.warning('Failed to get all required data from repo', exc_info=True)
820 missing_requirements = True
820 missing_requirements = True
821
821
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
822 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
823
823
824 try:
824 try:
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
825 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
826 except Exception:
826 except Exception:
827 ancestor_commit = None
827 ancestor_commit = None
828
828
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
829 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
830
830
831 def assure_not_empty_repo(self):
831 def assure_not_empty_repo(self):
832 _ = self.request.translate
832 _ = self.request.translate
833
833
834 try:
834 try:
835 self.db_repo.scm_instance().get_commit()
835 self.db_repo.scm_instance().get_commit()
836 except EmptyRepositoryError:
836 except EmptyRepositoryError:
837 h.flash(h.literal(_('There are no commits yet')),
837 h.flash(h.literal(_('There are no commits yet')),
838 category='warning')
838 category='warning')
839 raise HTTPFound(
839 raise HTTPFound(
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
840 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
841
841
842 @LoginRequired()
842 @LoginRequired()
843 @NotAnonymous()
843 @NotAnonymous()
844 @HasRepoPermissionAnyDecorator(
844 @HasRepoPermissionAnyDecorator(
845 'repository.read', 'repository.write', 'repository.admin')
845 'repository.read', 'repository.write', 'repository.admin')
846 def pull_request_new(self):
846 def pull_request_new(self):
847 _ = self.request.translate
847 _ = self.request.translate
848 c = self.load_default_context()
848 c = self.load_default_context()
849
849
850 self.assure_not_empty_repo()
850 self.assure_not_empty_repo()
851 source_repo = self.db_repo
851 source_repo = self.db_repo
852
852
853 commit_id = self.request.GET.get('commit')
853 commit_id = self.request.GET.get('commit')
854 branch_ref = self.request.GET.get('branch')
854 branch_ref = self.request.GET.get('branch')
855 bookmark_ref = self.request.GET.get('bookmark')
855 bookmark_ref = self.request.GET.get('bookmark')
856
856
857 try:
857 try:
858 source_repo_data = PullRequestModel().generate_repo_data(
858 source_repo_data = PullRequestModel().generate_repo_data(
859 source_repo, commit_id=commit_id,
859 source_repo, commit_id=commit_id,
860 branch=branch_ref, bookmark=bookmark_ref,
860 branch=branch_ref, bookmark=bookmark_ref,
861 translator=self.request.translate)
861 translator=self.request.translate)
862 except CommitDoesNotExistError as e:
862 except CommitDoesNotExistError as e:
863 log.exception(e)
863 log.exception(e)
864 h.flash(_('Commit does not exist'), 'error')
864 h.flash(_('Commit does not exist'), 'error')
865 raise HTTPFound(
865 raise HTTPFound(
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
866 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
867
867
868 default_target_repo = source_repo
868 default_target_repo = source_repo
869
869
870 if source_repo.parent and c.has_origin_repo_read_perm:
870 if source_repo.parent and c.has_origin_repo_read_perm:
871 parent_vcs_obj = source_repo.parent.scm_instance()
871 parent_vcs_obj = source_repo.parent.scm_instance()
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
872 if parent_vcs_obj and not parent_vcs_obj.is_empty():
873 # change default if we have a parent repo
873 # change default if we have a parent repo
874 default_target_repo = source_repo.parent
874 default_target_repo = source_repo.parent
875
875
876 target_repo_data = PullRequestModel().generate_repo_data(
876 target_repo_data = PullRequestModel().generate_repo_data(
877 default_target_repo, translator=self.request.translate)
877 default_target_repo, translator=self.request.translate)
878
878
879 selected_source_ref = source_repo_data['refs']['selected_ref']
879 selected_source_ref = source_repo_data['refs']['selected_ref']
880 title_source_ref = ''
880 title_source_ref = ''
881 if selected_source_ref:
881 if selected_source_ref:
882 title_source_ref = selected_source_ref.split(':', 2)[1]
882 title_source_ref = selected_source_ref.split(':', 2)[1]
883 c.default_title = PullRequestModel().generate_pullrequest_title(
883 c.default_title = PullRequestModel().generate_pullrequest_title(
884 source=source_repo.repo_name,
884 source=source_repo.repo_name,
885 source_ref=title_source_ref,
885 source_ref=title_source_ref,
886 target=default_target_repo.repo_name
886 target=default_target_repo.repo_name
887 )
887 )
888
888
889 c.default_repo_data = {
889 c.default_repo_data = {
890 'source_repo_name': source_repo.repo_name,
890 'source_repo_name': source_repo.repo_name,
891 'source_refs_json': json.dumps(source_repo_data),
891 'source_refs_json': json.dumps(source_repo_data),
892 'target_repo_name': default_target_repo.repo_name,
892 'target_repo_name': default_target_repo.repo_name,
893 'target_refs_json': json.dumps(target_repo_data),
893 'target_refs_json': json.dumps(target_repo_data),
894 }
894 }
895 c.default_source_ref = selected_source_ref
895 c.default_source_ref = selected_source_ref
896
896
897 return self._get_template_context(c)
897 return self._get_template_context(c)
898
898
899 @LoginRequired()
899 @LoginRequired()
900 @NotAnonymous()
900 @NotAnonymous()
901 @HasRepoPermissionAnyDecorator(
901 @HasRepoPermissionAnyDecorator(
902 'repository.read', 'repository.write', 'repository.admin')
902 'repository.read', 'repository.write', 'repository.admin')
903 def pull_request_repo_refs(self):
903 def pull_request_repo_refs(self):
904 self.load_default_context()
904 self.load_default_context()
905 target_repo_name = self.request.matchdict['target_repo_name']
905 target_repo_name = self.request.matchdict['target_repo_name']
906 repo = Repository.get_by_repo_name(target_repo_name)
906 repo = Repository.get_by_repo_name(target_repo_name)
907 if not repo:
907 if not repo:
908 raise HTTPNotFound()
908 raise HTTPNotFound()
909
909
910 target_perm = HasRepoPermissionAny(
910 target_perm = HasRepoPermissionAny(
911 'repository.read', 'repository.write', 'repository.admin')(
911 'repository.read', 'repository.write', 'repository.admin')(
912 target_repo_name)
912 target_repo_name)
913 if not target_perm:
913 if not target_perm:
914 raise HTTPNotFound()
914 raise HTTPNotFound()
915
915
916 return PullRequestModel().generate_repo_data(
916 return PullRequestModel().generate_repo_data(
917 repo, translator=self.request.translate)
917 repo, translator=self.request.translate)
918
918
919 @LoginRequired()
919 @LoginRequired()
920 @NotAnonymous()
920 @NotAnonymous()
921 @HasRepoPermissionAnyDecorator(
921 @HasRepoPermissionAnyDecorator(
922 'repository.read', 'repository.write', 'repository.admin')
922 'repository.read', 'repository.write', 'repository.admin')
923 def pullrequest_repo_targets(self):
923 def pullrequest_repo_targets(self):
924 _ = self.request.translate
924 _ = self.request.translate
925 filter_query = self.request.GET.get('query')
925 filter_query = self.request.GET.get('query')
926
926
927 # get the parents
927 # get the parents
928 parent_target_repos = []
928 parent_target_repos = []
929 if self.db_repo.parent:
929 if self.db_repo.parent:
930 parents_query = Repository.query() \
930 parents_query = Repository.query() \
931 .order_by(func.length(Repository.repo_name)) \
931 .order_by(func.length(Repository.repo_name)) \
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
933
933
934 if filter_query:
934 if filter_query:
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
936 parents_query = parents_query.filter(
936 parents_query = parents_query.filter(
937 Repository.repo_name.ilike(ilike_expression))
937 Repository.repo_name.ilike(ilike_expression))
938 parents = parents_query.limit(20).all()
938 parents = parents_query.limit(20).all()
939
939
940 for parent in parents:
940 for parent in parents:
941 parent_vcs_obj = parent.scm_instance()
941 parent_vcs_obj = parent.scm_instance()
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 if parent_vcs_obj and not parent_vcs_obj.is_empty():
943 parent_target_repos.append(parent)
943 parent_target_repos.append(parent)
944
944
945 # get other forks, and repo itself
945 # get other forks, and repo itself
946 query = Repository.query() \
946 query = Repository.query() \
947 .order_by(func.length(Repository.repo_name)) \
947 .order_by(func.length(Repository.repo_name)) \
948 .filter(
948 .filter(
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
951 ) \
951 ) \
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
953
953
954 if filter_query:
954 if filter_query:
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 query = query.filter(Repository.repo_name.ilike(ilike_expression))
957
957
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 limit = max(20 - len(parent_target_repos), 5) # not less then 5
959 target_repos = query.limit(limit).all()
959 target_repos = query.limit(limit).all()
960
960
961 all_target_repos = target_repos + parent_target_repos
961 all_target_repos = target_repos + parent_target_repos
962
962
963 repos = []
963 repos = []
964 # This checks permissions to the repositories
964 # This checks permissions to the repositories
965 for obj in ScmModel().get_repos(all_target_repos):
965 for obj in ScmModel().get_repos(all_target_repos):
966 repos.append({
966 repos.append({
967 'id': obj['name'],
967 'id': obj['name'],
968 'text': obj['name'],
968 'text': obj['name'],
969 'type': 'repo',
969 'type': 'repo',
970 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_id': obj['dbrepo']['repo_id'],
971 'repo_type': obj['dbrepo']['repo_type'],
971 'repo_type': obj['dbrepo']['repo_type'],
972 'private': obj['dbrepo']['private'],
972 'private': obj['dbrepo']['private'],
973
973
974 })
974 })
975
975
976 data = {
976 data = {
977 'more': False,
977 'more': False,
978 'results': [{
978 'results': [{
979 'text': _('Repositories'),
979 'text': _('Repositories'),
980 'children': repos
980 'children': repos
981 }] if repos else []
981 }] if repos else []
982 }
982 }
983 return data
983 return data
984
984
985 @classmethod
985 @classmethod
986 def get_comment_ids(cls, post_data):
986 def get_comment_ids(cls, post_data):
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
988
988
989 @LoginRequired()
989 @LoginRequired()
990 @NotAnonymous()
990 @NotAnonymous()
991 @HasRepoPermissionAnyDecorator(
991 @HasRepoPermissionAnyDecorator(
992 'repository.read', 'repository.write', 'repository.admin')
992 'repository.read', 'repository.write', 'repository.admin')
993 def pullrequest_comments(self):
993 def pullrequest_comments(self):
994 self.load_default_context()
994 self.load_default_context()
995
995
996 pull_request = PullRequest.get_or_404(
996 pull_request = PullRequest.get_or_404(
997 self.request.matchdict['pull_request_id'])
997 self.request.matchdict['pull_request_id'])
998 pull_request_id = pull_request.pull_request_id
998 pull_request_id = pull_request.pull_request_id
999 version = self.request.GET.get('version')
999 version = self.request.GET.get('version')
1000
1000
1001 _render = self.request.get_partial_renderer(
1001 _render = self.request.get_partial_renderer(
1002 'rhodecode:templates/base/sidebar.mako')
1002 'rhodecode:templates/base/sidebar.mako')
1003 c = _render.get_call_context()
1003 c = _render.get_call_context()
1004
1004
1005 (pull_request_latest,
1005 (pull_request_latest,
1006 pull_request_at_ver,
1006 pull_request_at_ver,
1007 pull_request_display_obj,
1007 pull_request_display_obj,
1008 at_version) = PullRequestModel().get_pr_version(
1008 at_version) = PullRequestModel().get_pr_version(
1009 pull_request_id, version=version)
1009 pull_request_id, version=version)
1010 versions = pull_request_display_obj.versions()
1010 versions = pull_request_display_obj.versions()
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1011 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1012 c.versions = versions + [latest_ver]
1012 c.versions = versions + [latest_ver]
1013
1013
1014 c.at_version = at_version
1014 c.at_version = at_version
1015 c.at_version_num = (at_version
1015 c.at_version_num = (at_version
1016 if at_version and at_version != PullRequest.LATEST_VER
1016 if at_version and at_version != PullRequest.LATEST_VER
1017 else None)
1017 else None)
1018
1018
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1019 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1020 all_comments = c.inline_comments_flat + c.comments
1020 all_comments = c.inline_comments_flat + c.comments
1021
1021
1022 existing_ids = self.get_comment_ids(self.request.POST)
1022 existing_ids = self.get_comment_ids(self.request.POST)
1023 return _render('comments_table', all_comments, len(all_comments),
1023 return _render('comments_table', all_comments, len(all_comments),
1024 existing_ids=existing_ids)
1024 existing_ids=existing_ids)
1025
1025
1026 @LoginRequired()
1026 @LoginRequired()
1027 @NotAnonymous()
1027 @NotAnonymous()
1028 @HasRepoPermissionAnyDecorator(
1028 @HasRepoPermissionAnyDecorator(
1029 'repository.read', 'repository.write', 'repository.admin')
1029 'repository.read', 'repository.write', 'repository.admin')
1030 def pullrequest_todos(self):
1030 def pullrequest_todos(self):
1031 self.load_default_context()
1031 self.load_default_context()
1032
1032
1033 pull_request = PullRequest.get_or_404(
1033 pull_request = PullRequest.get_or_404(
1034 self.request.matchdict['pull_request_id'])
1034 self.request.matchdict['pull_request_id'])
1035 pull_request_id = pull_request.pull_request_id
1035 pull_request_id = pull_request.pull_request_id
1036 version = self.request.GET.get('version')
1036 version = self.request.GET.get('version')
1037
1037
1038 _render = self.request.get_partial_renderer(
1038 _render = self.request.get_partial_renderer(
1039 'rhodecode:templates/base/sidebar.mako')
1039 'rhodecode:templates/base/sidebar.mako')
1040 c = _render.get_call_context()
1040 c = _render.get_call_context()
1041 (pull_request_latest,
1041 (pull_request_latest,
1042 pull_request_at_ver,
1042 pull_request_at_ver,
1043 pull_request_display_obj,
1043 pull_request_display_obj,
1044 at_version) = PullRequestModel().get_pr_version(
1044 at_version) = PullRequestModel().get_pr_version(
1045 pull_request_id, version=version)
1045 pull_request_id, version=version)
1046 versions = pull_request_display_obj.versions()
1046 versions = pull_request_display_obj.versions()
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1047 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1048 c.versions = versions + [latest_ver]
1048 c.versions = versions + [latest_ver]
1049
1049
1050 c.at_version = at_version
1050 c.at_version = at_version
1051 c.at_version_num = (at_version
1051 c.at_version_num = (at_version
1052 if at_version and at_version != PullRequest.LATEST_VER
1052 if at_version and at_version != PullRequest.LATEST_VER
1053 else None)
1053 else None)
1054
1054
1055 c.unresolved_comments = CommentsModel() \
1055 c.unresolved_comments = CommentsModel() \
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1056 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1057 c.resolved_comments = CommentsModel() \
1057 c.resolved_comments = CommentsModel() \
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1058 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1059
1059
1060 all_comments = c.unresolved_comments + c.resolved_comments
1060 all_comments = c.unresolved_comments + c.resolved_comments
1061 existing_ids = self.get_comment_ids(self.request.POST)
1061 existing_ids = self.get_comment_ids(self.request.POST)
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1062 return _render('comments_table', all_comments, len(c.unresolved_comments),
1063 todo_comments=True, existing_ids=existing_ids)
1063 todo_comments=True, existing_ids=existing_ids)
1064
1064
1065 @LoginRequired()
1065 @LoginRequired()
1066 @NotAnonymous()
1066 @NotAnonymous()
1067 @HasRepoPermissionAnyDecorator(
1067 @HasRepoPermissionAnyDecorator(
1068 'repository.read', 'repository.write', 'repository.admin')
1068 'repository.read', 'repository.write', 'repository.admin')
1069 def pullrequest_drafts(self):
1069 def pullrequest_drafts(self):
1070 self.load_default_context()
1070 self.load_default_context()
1071
1071
1072 pull_request = PullRequest.get_or_404(
1072 pull_request = PullRequest.get_or_404(
1073 self.request.matchdict['pull_request_id'])
1073 self.request.matchdict['pull_request_id'])
1074 pull_request_id = pull_request.pull_request_id
1074 pull_request_id = pull_request.pull_request_id
1075 version = self.request.GET.get('version')
1075 version = self.request.GET.get('version')
1076
1076
1077 _render = self.request.get_partial_renderer(
1077 _render = self.request.get_partial_renderer(
1078 'rhodecode:templates/base/sidebar.mako')
1078 'rhodecode:templates/base/sidebar.mako')
1079 c = _render.get_call_context()
1079 c = _render.get_call_context()
1080
1080
1081 (pull_request_latest,
1081 (pull_request_latest,
1082 pull_request_at_ver,
1082 pull_request_at_ver,
1083 pull_request_display_obj,
1083 pull_request_display_obj,
1084 at_version) = PullRequestModel().get_pr_version(
1084 at_version) = PullRequestModel().get_pr_version(
1085 pull_request_id, version=version)
1085 pull_request_id, version=version)
1086 versions = pull_request_display_obj.versions()
1086 versions = pull_request_display_obj.versions()
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1087 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1088 c.versions = versions + [latest_ver]
1088 c.versions = versions + [latest_ver]
1089
1089
1090 c.at_version = at_version
1090 c.at_version = at_version
1091 c.at_version_num = (at_version
1091 c.at_version_num = (at_version
1092 if at_version and at_version != PullRequest.LATEST_VER
1092 if at_version and at_version != PullRequest.LATEST_VER
1093 else None)
1093 else None)
1094
1094
1095 c.draft_comments = CommentsModel() \
1095 c.draft_comments = CommentsModel() \
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1096 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1097
1097
1098 all_comments = c.draft_comments
1098 all_comments = c.draft_comments
1099
1099
1100 existing_ids = self.get_comment_ids(self.request.POST)
1100 existing_ids = self.get_comment_ids(self.request.POST)
1101 return _render('comments_table', all_comments, len(all_comments),
1101 return _render('comments_table', all_comments, len(all_comments),
1102 existing_ids=existing_ids, draft_comments=True)
1102 existing_ids=existing_ids, draft_comments=True)
1103
1103
1104 @LoginRequired()
1104 @LoginRequired()
1105 @NotAnonymous()
1105 @NotAnonymous()
1106 @HasRepoPermissionAnyDecorator(
1106 @HasRepoPermissionAnyDecorator(
1107 'repository.read', 'repository.write', 'repository.admin')
1107 'repository.read', 'repository.write', 'repository.admin')
1108 @CSRFRequired()
1108 @CSRFRequired()
1109 def pull_request_create(self):
1109 def pull_request_create(self):
1110 _ = self.request.translate
1110 _ = self.request.translate
1111 self.assure_not_empty_repo()
1111 self.assure_not_empty_repo()
1112 self.load_default_context()
1112 self.load_default_context()
1113
1113
1114 controls = peppercorn.parse(self.request.POST.items())
1114 controls = peppercorn.parse(self.request.POST.items())
1115
1115
1116 try:
1116 try:
1117 form = PullRequestForm(
1117 form = PullRequestForm(
1118 self.request.translate, self.db_repo.repo_id)()
1118 self.request.translate, self.db_repo.repo_id)()
1119 _form = form.to_python(controls)
1119 _form = form.to_python(controls)
1120 except formencode.Invalid as errors:
1120 except formencode.Invalid as errors:
1121 if errors.error_dict.get('revisions'):
1121 if errors.error_dict.get('revisions'):
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1122 msg = 'Revisions: %s' % errors.error_dict['revisions']
1123 elif errors.error_dict.get('pullrequest_title'):
1123 elif errors.error_dict.get('pullrequest_title'):
1124 msg = errors.error_dict.get('pullrequest_title')
1124 msg = errors.error_dict.get('pullrequest_title')
1125 else:
1125 else:
1126 msg = _('Error creating pull request: {}').format(errors)
1126 msg = _('Error creating pull request: {}').format(errors)
1127 log.exception(msg)
1127 log.exception(msg)
1128 h.flash(msg, 'error')
1128 h.flash(msg, 'error')
1129
1129
1130 # would rather just go back to form ...
1130 # would rather just go back to form ...
1131 raise HTTPFound(
1131 raise HTTPFound(
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1133
1133
1134 source_repo = _form['source_repo']
1134 source_repo = _form['source_repo']
1135 source_ref = _form['source_ref']
1135 source_ref = _form['source_ref']
1136 target_repo = _form['target_repo']
1136 target_repo = _form['target_repo']
1137 target_ref = _form['target_ref']
1137 target_ref = _form['target_ref']
1138 commit_ids = _form['revisions'][::-1]
1138 commit_ids = _form['revisions'][::-1]
1139 common_ancestor_id = _form['common_ancestor']
1139 common_ancestor_id = _form['common_ancestor']
1140
1140
1141 # find the ancestor for this pr
1141 # find the ancestor for this pr
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1142 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1143 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1144
1144
1145 if not (source_db_repo or target_db_repo):
1145 if not (source_db_repo or target_db_repo):
1146 h.flash(_('source_repo or target repo not found'), category='error')
1146 h.flash(_('source_repo or target repo not found'), category='error')
1147 raise HTTPFound(
1147 raise HTTPFound(
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1149
1149
1150 # re-check permissions again here
1150 # re-check permissions again here
1151 # source_repo we must have read permissions
1151 # source_repo we must have read permissions
1152
1152
1153 source_perm = HasRepoPermissionAny(
1153 source_perm = HasRepoPermissionAny(
1154 'repository.read', 'repository.write', 'repository.admin')(
1154 'repository.read', 'repository.write', 'repository.admin')(
1155 source_db_repo.repo_name)
1155 source_db_repo.repo_name)
1156 if not source_perm:
1156 if not source_perm:
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1157 msg = _('Not Enough permissions to source repo `{}`.'.format(
1158 source_db_repo.repo_name))
1158 source_db_repo.repo_name))
1159 h.flash(msg, category='error')
1159 h.flash(msg, category='error')
1160 # copy the args back to redirect
1160 # copy the args back to redirect
1161 org_query = self.request.GET.mixed()
1161 org_query = self.request.GET.mixed()
1162 raise HTTPFound(
1162 raise HTTPFound(
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1163 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1164 _query=org_query))
1164 _query=org_query))
1165
1165
1166 # target repo we must have read permissions, and also later on
1166 # target repo we must have read permissions, and also later on
1167 # we want to check branch permissions here
1167 # we want to check branch permissions here
1168 target_perm = HasRepoPermissionAny(
1168 target_perm = HasRepoPermissionAny(
1169 'repository.read', 'repository.write', 'repository.admin')(
1169 'repository.read', 'repository.write', 'repository.admin')(
1170 target_db_repo.repo_name)
1170 target_db_repo.repo_name)
1171 if not target_perm:
1171 if not target_perm:
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1172 msg = _('Not Enough permissions to target repo `{}`.'.format(
1173 target_db_repo.repo_name))
1173 target_db_repo.repo_name))
1174 h.flash(msg, category='error')
1174 h.flash(msg, category='error')
1175 # copy the args back to redirect
1175 # copy the args back to redirect
1176 org_query = self.request.GET.mixed()
1176 org_query = self.request.GET.mixed()
1177 raise HTTPFound(
1177 raise HTTPFound(
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1178 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 _query=org_query))
1179 _query=org_query))
1180
1180
1181 source_scm = source_db_repo.scm_instance()
1181 source_scm = source_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1182 target_scm = target_db_repo.scm_instance()
1183
1183
1184 source_ref_obj = unicode_to_reference(source_ref)
1184 source_ref_obj = unicode_to_reference(source_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1185 target_ref_obj = unicode_to_reference(target_ref)
1186
1186
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1187 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1188 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1189
1189
1190 ancestor = source_scm.get_common_ancestor(
1190 ancestor = source_scm.get_common_ancestor(
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1191 source_commit.raw_id, target_commit.raw_id, target_scm)
1192
1192
1193 # recalculate target ref based on ancestor
1193 # recalculate target ref based on ancestor
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1194 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1195
1195
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1196 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1197 PullRequestModel().get_reviewer_functions()
1197 PullRequestModel().get_reviewer_functions()
1198
1198
1199 # recalculate reviewers logic, to make sure we can validate this
1199 # recalculate reviewers logic, to make sure we can validate this
1200 reviewer_rules = get_default_reviewers_data(
1200 reviewer_rules = get_default_reviewers_data(
1201 self._rhodecode_db_user,
1201 self._rhodecode_db_user,
1202 source_db_repo,
1202 source_db_repo,
1203 source_ref_obj,
1203 source_ref_obj,
1204 target_db_repo,
1204 target_db_repo,
1205 target_ref_obj,
1205 target_ref_obj,
1206 include_diff_info=False)
1206 include_diff_info=False)
1207
1207
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1208 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1209 observers = validate_observers(_form['observer_members'], reviewer_rules)
1210
1210
1211 pullrequest_title = _form['pullrequest_title']
1211 pullrequest_title = _form['pullrequest_title']
1212 title_source_ref = source_ref_obj.name
1212 title_source_ref = source_ref_obj.name
1213 if not pullrequest_title:
1213 if not pullrequest_title:
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1214 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1215 source=source_repo,
1215 source=source_repo,
1216 source_ref=title_source_ref,
1216 source_ref=title_source_ref,
1217 target=target_repo
1217 target=target_repo
1218 )
1218 )
1219
1219
1220 description = _form['pullrequest_desc']
1220 description = _form['pullrequest_desc']
1221 description_renderer = _form['description_renderer']
1221 description_renderer = _form['description_renderer']
1222
1222
1223 try:
1223 try:
1224 pull_request = PullRequestModel().create(
1224 pull_request = PullRequestModel().create(
1225 created_by=self._rhodecode_user.user_id,
1225 created_by=self._rhodecode_user.user_id,
1226 source_repo=source_repo,
1226 source_repo=source_repo,
1227 source_ref=source_ref,
1227 source_ref=source_ref,
1228 target_repo=target_repo,
1228 target_repo=target_repo,
1229 target_ref=target_ref,
1229 target_ref=target_ref,
1230 revisions=commit_ids,
1230 revisions=commit_ids,
1231 common_ancestor_id=common_ancestor_id,
1231 common_ancestor_id=common_ancestor_id,
1232 reviewers=reviewers,
1232 reviewers=reviewers,
1233 observers=observers,
1233 observers=observers,
1234 title=pullrequest_title,
1234 title=pullrequest_title,
1235 description=description,
1235 description=description,
1236 description_renderer=description_renderer,
1236 description_renderer=description_renderer,
1237 reviewer_data=reviewer_rules,
1237 reviewer_data=reviewer_rules,
1238 auth_user=self._rhodecode_user
1238 auth_user=self._rhodecode_user
1239 )
1239 )
1240 Session().commit()
1240 Session().commit()
1241
1241
1242 h.flash(_('Successfully opened new pull request'),
1242 h.flash(_('Successfully opened new pull request'),
1243 category='success')
1243 category='success')
1244 except Exception:
1244 except Exception:
1245 msg = _('Error occurred during creation of this pull request.')
1245 msg = _('Error occurred during creation of this pull request.')
1246 log.exception(msg)
1246 log.exception(msg)
1247 h.flash(msg, category='error')
1247 h.flash(msg, category='error')
1248
1248
1249 # copy the args back to redirect
1249 # copy the args back to redirect
1250 org_query = self.request.GET.mixed()
1250 org_query = self.request.GET.mixed()
1251 raise HTTPFound(
1251 raise HTTPFound(
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1252 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1253 _query=org_query))
1253 _query=org_query))
1254
1254
1255 raise HTTPFound(
1255 raise HTTPFound(
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1256 h.route_path('pullrequest_show', repo_name=target_repo,
1257 pull_request_id=pull_request.pull_request_id))
1257 pull_request_id=pull_request.pull_request_id))
1258
1258
1259 @LoginRequired()
1259 @LoginRequired()
1260 @NotAnonymous()
1260 @NotAnonymous()
1261 @HasRepoPermissionAnyDecorator(
1261 @HasRepoPermissionAnyDecorator(
1262 'repository.read', 'repository.write', 'repository.admin')
1262 'repository.read', 'repository.write', 'repository.admin')
1263 @CSRFRequired()
1263 @CSRFRequired()
1264 def pull_request_update(self):
1264 def pull_request_update(self):
1265 pull_request = PullRequest.get_or_404(
1265 pull_request = PullRequest.get_or_404(
1266 self.request.matchdict['pull_request_id'])
1266 self.request.matchdict['pull_request_id'])
1267 _ = self.request.translate
1267 _ = self.request.translate
1268
1268
1269 c = self.load_default_context()
1269 c = self.load_default_context()
1270 redirect_url = None
1270 redirect_url = None
1271
1271
1272 if pull_request.is_closed():
1272 if pull_request.is_closed():
1273 log.debug('update: forbidden because pull request is closed')
1273 log.debug('update: forbidden because pull request is closed')
1274 msg = _(u'Cannot update closed pull requests.')
1274 msg = _(u'Cannot update closed pull requests.')
1275 h.flash(msg, category='error')
1275 h.flash(msg, category='error')
1276 return {'response': True,
1276 return {'response': True,
1277 'redirect_url': redirect_url}
1277 'redirect_url': redirect_url}
1278
1278
1279 is_state_changing = pull_request.is_state_changing()
1279 is_state_changing = pull_request.is_state_changing()
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1280 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1281
1281
1282 # only owner or admin can update it
1282 # only owner or admin can update it
1283 allowed_to_update = PullRequestModel().check_user_update(
1283 allowed_to_update = PullRequestModel().check_user_update(
1284 pull_request, self._rhodecode_user)
1284 pull_request, self._rhodecode_user)
1285
1285
1286 if allowed_to_update:
1286 if allowed_to_update:
1287 controls = peppercorn.parse(self.request.POST.items())
1287 controls = peppercorn.parse(self.request.POST.items())
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1288 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1289
1289
1290 if 'review_members' in controls:
1290 if 'review_members' in controls:
1291 self._update_reviewers(
1291 self._update_reviewers(
1292 c,
1292 c,
1293 pull_request, controls['review_members'],
1293 pull_request, controls['review_members'],
1294 pull_request.reviewer_data,
1294 pull_request.reviewer_data,
1295 PullRequestReviewers.ROLE_REVIEWER)
1295 PullRequestReviewers.ROLE_REVIEWER)
1296 elif 'observer_members' in controls:
1296 elif 'observer_members' in controls:
1297 self._update_reviewers(
1297 self._update_reviewers(
1298 c,
1298 c,
1299 pull_request, controls['observer_members'],
1299 pull_request, controls['observer_members'],
1300 pull_request.reviewer_data,
1300 pull_request.reviewer_data,
1301 PullRequestReviewers.ROLE_OBSERVER)
1301 PullRequestReviewers.ROLE_OBSERVER)
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1302 elif str2bool(self.request.POST.get('update_commits', 'false')):
1303 if is_state_changing:
1303 if is_state_changing:
1304 log.debug('commits update: forbidden because pull request is in state %s',
1304 log.debug('commits update: forbidden because pull request is in state %s',
1305 pull_request.pull_request_state)
1305 pull_request.pull_request_state)
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1306 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1307 u'Current state is: `{}`').format(
1307 u'Current state is: `{}`').format(
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1308 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1309 h.flash(msg, category='error')
1309 h.flash(msg, category='error')
1310 return {'response': True,
1310 return {'response': True,
1311 'redirect_url': redirect_url}
1311 'redirect_url': redirect_url}
1312
1312
1313 self._update_commits(c, pull_request)
1313 self._update_commits(c, pull_request)
1314 if force_refresh:
1314 if force_refresh:
1315 redirect_url = h.route_path(
1315 redirect_url = h.route_path(
1316 'pullrequest_show', repo_name=self.db_repo_name,
1316 'pullrequest_show', repo_name=self.db_repo_name,
1317 pull_request_id=pull_request.pull_request_id,
1317 pull_request_id=pull_request.pull_request_id,
1318 _query={"force_refresh": 1})
1318 _query={"force_refresh": 1})
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1319 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1320 self._edit_pull_request(pull_request)
1320 self._edit_pull_request(pull_request)
1321 else:
1321 else:
1322 log.error('Unhandled update data.')
1322 log.error('Unhandled update data.')
1323 raise HTTPBadRequest()
1323 raise HTTPBadRequest()
1324
1324
1325 return {'response': True,
1325 return {'response': True,
1326 'redirect_url': redirect_url}
1326 'redirect_url': redirect_url}
1327 raise HTTPForbidden()
1327 raise HTTPForbidden()
1328
1328
1329 def _edit_pull_request(self, pull_request):
1329 def _edit_pull_request(self, pull_request):
1330 """
1330 """
1331 Edit title and description
1331 Edit title and description
1332 """
1332 """
1333 _ = self.request.translate
1333 _ = self.request.translate
1334
1334
1335 try:
1335 try:
1336 PullRequestModel().edit(
1336 PullRequestModel().edit(
1337 pull_request,
1337 pull_request,
1338 self.request.POST.get('title'),
1338 self.request.POST.get('title'),
1339 self.request.POST.get('description'),
1339 self.request.POST.get('description'),
1340 self.request.POST.get('description_renderer'),
1340 self.request.POST.get('description_renderer'),
1341 self._rhodecode_user)
1341 self._rhodecode_user)
1342 except ValueError:
1342 except ValueError:
1343 msg = _(u'Cannot update closed pull requests.')
1343 msg = _(u'Cannot update closed pull requests.')
1344 h.flash(msg, category='error')
1344 h.flash(msg, category='error')
1345 return
1345 return
1346 else:
1346 else:
1347 Session().commit()
1347 Session().commit()
1348
1348
1349 msg = _(u'Pull request title & description updated.')
1349 msg = _(u'Pull request title & description updated.')
1350 h.flash(msg, category='success')
1350 h.flash(msg, category='success')
1351 return
1351 return
1352
1352
1353 def _update_commits(self, c, pull_request):
1353 def _update_commits(self, c, pull_request):
1354 _ = self.request.translate
1354 _ = self.request.translate
1355
1355
1356 @retry(exception=Exception, n_tries=3)
1357 def commits_update():
1358 return PullRequestModel().update_commits(
1359 pull_request, self._rhodecode_db_user)
1360
1356 with pull_request.set_state(PullRequest.STATE_UPDATING):
1361 with pull_request.set_state(PullRequest.STATE_UPDATING):
1357 resp = PullRequestModel().update_commits(
1362 resp = commits_update() # retry x3
1358 pull_request, self._rhodecode_db_user)
1359
1363
1360 if resp.executed:
1364 if resp.executed:
1361
1365
1362 if resp.target_changed and resp.source_changed:
1366 if resp.target_changed and resp.source_changed:
1363 changed = 'target and source repositories'
1367 changed = 'target and source repositories'
1364 elif resp.target_changed and not resp.source_changed:
1368 elif resp.target_changed and not resp.source_changed:
1365 changed = 'target repository'
1369 changed = 'target repository'
1366 elif not resp.target_changed and resp.source_changed:
1370 elif not resp.target_changed and resp.source_changed:
1367 changed = 'source repository'
1371 changed = 'source repository'
1368 else:
1372 else:
1369 changed = 'nothing'
1373 changed = 'nothing'
1370
1374
1371 msg = _(u'Pull request updated to "{source_commit_id}" with '
1375 msg = _(u'Pull request updated to "{source_commit_id}" with '
1372 u'{count_added} added, {count_removed} removed commits. '
1376 u'{count_added} added, {count_removed} removed commits. '
1373 u'Source of changes: {change_source}.')
1377 u'Source of changes: {change_source}.')
1374 msg = msg.format(
1378 msg = msg.format(
1375 source_commit_id=pull_request.source_ref_parts.commit_id,
1379 source_commit_id=pull_request.source_ref_parts.commit_id,
1376 count_added=len(resp.changes.added),
1380 count_added=len(resp.changes.added),
1377 count_removed=len(resp.changes.removed),
1381 count_removed=len(resp.changes.removed),
1378 change_source=changed)
1382 change_source=changed)
1379 h.flash(msg, category='success')
1383 h.flash(msg, category='success')
1380 channelstream.pr_update_channelstream_push(
1384 channelstream.pr_update_channelstream_push(
1381 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1385 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1382 else:
1386 else:
1383 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1387 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1384 warning_reasons = [
1388 warning_reasons = [
1385 UpdateFailureReason.NO_CHANGE,
1389 UpdateFailureReason.NO_CHANGE,
1386 UpdateFailureReason.WRONG_REF_TYPE,
1390 UpdateFailureReason.WRONG_REF_TYPE,
1387 ]
1391 ]
1388 category = 'warning' if resp.reason in warning_reasons else 'error'
1392 category = 'warning' if resp.reason in warning_reasons else 'error'
1389 h.flash(msg, category=category)
1393 h.flash(msg, category=category)
1390
1394
1391 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1395 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1392 _ = self.request.translate
1396 _ = self.request.translate
1393
1397
1394 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1398 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1395 PullRequestModel().get_reviewer_functions()
1399 PullRequestModel().get_reviewer_functions()
1396
1400
1397 if role == PullRequestReviewers.ROLE_REVIEWER:
1401 if role == PullRequestReviewers.ROLE_REVIEWER:
1398 try:
1402 try:
1399 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1403 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1400 except ValueError as e:
1404 except ValueError as e:
1401 log.error('Reviewers Validation: {}'.format(e))
1405 log.error('Reviewers Validation: {}'.format(e))
1402 h.flash(e, category='error')
1406 h.flash(e, category='error')
1403 return
1407 return
1404
1408
1405 old_calculated_status = pull_request.calculated_review_status()
1409 old_calculated_status = pull_request.calculated_review_status()
1406 PullRequestModel().update_reviewers(
1410 PullRequestModel().update_reviewers(
1407 pull_request, reviewers, self._rhodecode_db_user)
1411 pull_request, reviewers, self._rhodecode_db_user)
1408
1412
1409 Session().commit()
1413 Session().commit()
1410
1414
1411 msg = _('Pull request reviewers updated.')
1415 msg = _('Pull request reviewers updated.')
1412 h.flash(msg, category='success')
1416 h.flash(msg, category='success')
1413 channelstream.pr_update_channelstream_push(
1417 channelstream.pr_update_channelstream_push(
1414 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1418 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1415
1419
1416 # trigger status changed if change in reviewers changes the status
1420 # trigger status changed if change in reviewers changes the status
1417 calculated_status = pull_request.calculated_review_status()
1421 calculated_status = pull_request.calculated_review_status()
1418 if old_calculated_status != calculated_status:
1422 if old_calculated_status != calculated_status:
1419 PullRequestModel().trigger_pull_request_hook(
1423 PullRequestModel().trigger_pull_request_hook(
1420 pull_request, self._rhodecode_user, 'review_status_change',
1424 pull_request, self._rhodecode_user, 'review_status_change',
1421 data={'status': calculated_status})
1425 data={'status': calculated_status})
1422
1426
1423 elif role == PullRequestReviewers.ROLE_OBSERVER:
1427 elif role == PullRequestReviewers.ROLE_OBSERVER:
1424 try:
1428 try:
1425 observers = validate_observers(review_members, reviewer_rules)
1429 observers = validate_observers(review_members, reviewer_rules)
1426 except ValueError as e:
1430 except ValueError as e:
1427 log.error('Observers Validation: {}'.format(e))
1431 log.error('Observers Validation: {}'.format(e))
1428 h.flash(e, category='error')
1432 h.flash(e, category='error')
1429 return
1433 return
1430
1434
1431 PullRequestModel().update_observers(
1435 PullRequestModel().update_observers(
1432 pull_request, observers, self._rhodecode_db_user)
1436 pull_request, observers, self._rhodecode_db_user)
1433
1437
1434 Session().commit()
1438 Session().commit()
1435 msg = _('Pull request observers updated.')
1439 msg = _('Pull request observers updated.')
1436 h.flash(msg, category='success')
1440 h.flash(msg, category='success')
1437 channelstream.pr_update_channelstream_push(
1441 channelstream.pr_update_channelstream_push(
1438 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1442 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1439
1443
1440 @LoginRequired()
1444 @LoginRequired()
1441 @NotAnonymous()
1445 @NotAnonymous()
1442 @HasRepoPermissionAnyDecorator(
1446 @HasRepoPermissionAnyDecorator(
1443 'repository.read', 'repository.write', 'repository.admin')
1447 'repository.read', 'repository.write', 'repository.admin')
1444 @CSRFRequired()
1448 @CSRFRequired()
1445 def pull_request_merge(self):
1449 def pull_request_merge(self):
1446 """
1450 """
1447 Merge will perform a server-side merge of the specified
1451 Merge will perform a server-side merge of the specified
1448 pull request, if the pull request is approved and mergeable.
1452 pull request, if the pull request is approved and mergeable.
1449 After successful merging, the pull request is automatically
1453 After successful merging, the pull request is automatically
1450 closed, with a relevant comment.
1454 closed, with a relevant comment.
1451 """
1455 """
1452 pull_request = PullRequest.get_or_404(
1456 pull_request = PullRequest.get_or_404(
1453 self.request.matchdict['pull_request_id'])
1457 self.request.matchdict['pull_request_id'])
1454 _ = self.request.translate
1458 _ = self.request.translate
1455
1459
1456 if pull_request.is_state_changing():
1460 if pull_request.is_state_changing():
1457 log.debug('show: forbidden because pull request is in state %s',
1461 log.debug('show: forbidden because pull request is in state %s',
1458 pull_request.pull_request_state)
1462 pull_request.pull_request_state)
1459 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1463 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1460 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1464 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1461 pull_request.pull_request_state)
1465 pull_request.pull_request_state)
1462 h.flash(msg, category='error')
1466 h.flash(msg, category='error')
1463 raise HTTPFound(
1467 raise HTTPFound(
1464 h.route_path('pullrequest_show',
1468 h.route_path('pullrequest_show',
1465 repo_name=pull_request.target_repo.repo_name,
1469 repo_name=pull_request.target_repo.repo_name,
1466 pull_request_id=pull_request.pull_request_id))
1470 pull_request_id=pull_request.pull_request_id))
1467
1471
1468 self.load_default_context()
1472 self.load_default_context()
1469
1473
1470 with pull_request.set_state(PullRequest.STATE_UPDATING):
1474 with pull_request.set_state(PullRequest.STATE_UPDATING):
1471 check = MergeCheck.validate(
1475 check = MergeCheck.validate(
1472 pull_request, auth_user=self._rhodecode_user,
1476 pull_request, auth_user=self._rhodecode_user,
1473 translator=self.request.translate)
1477 translator=self.request.translate)
1474 merge_possible = not check.failed
1478 merge_possible = not check.failed
1475
1479
1476 for err_type, error_msg in check.errors:
1480 for err_type, error_msg in check.errors:
1477 h.flash(error_msg, category=err_type)
1481 h.flash(error_msg, category=err_type)
1478
1482
1479 if merge_possible:
1483 if merge_possible:
1480 log.debug("Pre-conditions checked, trying to merge.")
1484 log.debug("Pre-conditions checked, trying to merge.")
1481 extras = vcs_operation_context(
1485 extras = vcs_operation_context(
1482 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1486 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1483 username=self._rhodecode_db_user.username, action='push',
1487 username=self._rhodecode_db_user.username, action='push',
1484 scm=pull_request.target_repo.repo_type)
1488 scm=pull_request.target_repo.repo_type)
1485 with pull_request.set_state(PullRequest.STATE_UPDATING):
1489 with pull_request.set_state(PullRequest.STATE_UPDATING):
1486 self._merge_pull_request(
1490 self._merge_pull_request(
1487 pull_request, self._rhodecode_db_user, extras)
1491 pull_request, self._rhodecode_db_user, extras)
1488 else:
1492 else:
1489 log.debug("Pre-conditions failed, NOT merging.")
1493 log.debug("Pre-conditions failed, NOT merging.")
1490
1494
1491 raise HTTPFound(
1495 raise HTTPFound(
1492 h.route_path('pullrequest_show',
1496 h.route_path('pullrequest_show',
1493 repo_name=pull_request.target_repo.repo_name,
1497 repo_name=pull_request.target_repo.repo_name,
1494 pull_request_id=pull_request.pull_request_id))
1498 pull_request_id=pull_request.pull_request_id))
1495
1499
1496 def _merge_pull_request(self, pull_request, user, extras):
1500 def _merge_pull_request(self, pull_request, user, extras):
1497 _ = self.request.translate
1501 _ = self.request.translate
1498 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1502 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1499
1503
1500 if merge_resp.executed:
1504 if merge_resp.executed:
1501 log.debug("The merge was successful, closing the pull request.")
1505 log.debug("The merge was successful, closing the pull request.")
1502 PullRequestModel().close_pull_request(
1506 PullRequestModel().close_pull_request(
1503 pull_request.pull_request_id, user)
1507 pull_request.pull_request_id, user)
1504 Session().commit()
1508 Session().commit()
1505 msg = _('Pull request was successfully merged and closed.')
1509 msg = _('Pull request was successfully merged and closed.')
1506 h.flash(msg, category='success')
1510 h.flash(msg, category='success')
1507 else:
1511 else:
1508 log.debug(
1512 log.debug(
1509 "The merge was not successful. Merge response: %s", merge_resp)
1513 "The merge was not successful. Merge response: %s", merge_resp)
1510 msg = merge_resp.merge_status_message
1514 msg = merge_resp.merge_status_message
1511 h.flash(msg, category='error')
1515 h.flash(msg, category='error')
1512
1516
1513 @LoginRequired()
1517 @LoginRequired()
1514 @NotAnonymous()
1518 @NotAnonymous()
1515 @HasRepoPermissionAnyDecorator(
1519 @HasRepoPermissionAnyDecorator(
1516 'repository.read', 'repository.write', 'repository.admin')
1520 'repository.read', 'repository.write', 'repository.admin')
1517 @CSRFRequired()
1521 @CSRFRequired()
1518 def pull_request_delete(self):
1522 def pull_request_delete(self):
1519 _ = self.request.translate
1523 _ = self.request.translate
1520
1524
1521 pull_request = PullRequest.get_or_404(
1525 pull_request = PullRequest.get_or_404(
1522 self.request.matchdict['pull_request_id'])
1526 self.request.matchdict['pull_request_id'])
1523 self.load_default_context()
1527 self.load_default_context()
1524
1528
1525 pr_closed = pull_request.is_closed()
1529 pr_closed = pull_request.is_closed()
1526 allowed_to_delete = PullRequestModel().check_user_delete(
1530 allowed_to_delete = PullRequestModel().check_user_delete(
1527 pull_request, self._rhodecode_user) and not pr_closed
1531 pull_request, self._rhodecode_user) and not pr_closed
1528
1532
1529 # only owner can delete it !
1533 # only owner can delete it !
1530 if allowed_to_delete:
1534 if allowed_to_delete:
1531 PullRequestModel().delete(pull_request, self._rhodecode_user)
1535 PullRequestModel().delete(pull_request, self._rhodecode_user)
1532 Session().commit()
1536 Session().commit()
1533 h.flash(_('Successfully deleted pull request'),
1537 h.flash(_('Successfully deleted pull request'),
1534 category='success')
1538 category='success')
1535 raise HTTPFound(h.route_path('pullrequest_show_all',
1539 raise HTTPFound(h.route_path('pullrequest_show_all',
1536 repo_name=self.db_repo_name))
1540 repo_name=self.db_repo_name))
1537
1541
1538 log.warning('user %s tried to delete pull request without access',
1542 log.warning('user %s tried to delete pull request without access',
1539 self._rhodecode_user)
1543 self._rhodecode_user)
1540 raise HTTPNotFound()
1544 raise HTTPNotFound()
1541
1545
1542 def _pull_request_comments_create(self, pull_request, comments):
1546 def _pull_request_comments_create(self, pull_request, comments):
1543 _ = self.request.translate
1547 _ = self.request.translate
1544 data = {}
1548 data = {}
1545 if not comments:
1549 if not comments:
1546 return
1550 return
1547 pull_request_id = pull_request.pull_request_id
1551 pull_request_id = pull_request.pull_request_id
1548
1552
1549 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1553 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1550
1554
1551 for entry in comments:
1555 for entry in comments:
1552 c = self.load_default_context()
1556 c = self.load_default_context()
1553 comment_type = entry['comment_type']
1557 comment_type = entry['comment_type']
1554 text = entry['text']
1558 text = entry['text']
1555 status = entry['status']
1559 status = entry['status']
1556 is_draft = str2bool(entry['is_draft'])
1560 is_draft = str2bool(entry['is_draft'])
1557 resolves_comment_id = entry['resolves_comment_id']
1561 resolves_comment_id = entry['resolves_comment_id']
1558 close_pull_request = entry['close_pull_request']
1562 close_pull_request = entry['close_pull_request']
1559 f_path = entry['f_path']
1563 f_path = entry['f_path']
1560 line_no = entry['line']
1564 line_no = entry['line']
1561 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1565 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1562
1566
1563 # the logic here should work like following, if we submit close
1567 # the logic here should work like following, if we submit close
1564 # pr comment, use `close_pull_request_with_comment` function
1568 # pr comment, use `close_pull_request_with_comment` function
1565 # else handle regular comment logic
1569 # else handle regular comment logic
1566
1570
1567 if close_pull_request:
1571 if close_pull_request:
1568 # only owner or admin or person with write permissions
1572 # only owner or admin or person with write permissions
1569 allowed_to_close = PullRequestModel().check_user_update(
1573 allowed_to_close = PullRequestModel().check_user_update(
1570 pull_request, self._rhodecode_user)
1574 pull_request, self._rhodecode_user)
1571 if not allowed_to_close:
1575 if not allowed_to_close:
1572 log.debug('comment: forbidden because not allowed to close '
1576 log.debug('comment: forbidden because not allowed to close '
1573 'pull request %s', pull_request_id)
1577 'pull request %s', pull_request_id)
1574 raise HTTPForbidden()
1578 raise HTTPForbidden()
1575
1579
1576 # This also triggers `review_status_change`
1580 # This also triggers `review_status_change`
1577 comment, status = PullRequestModel().close_pull_request_with_comment(
1581 comment, status = PullRequestModel().close_pull_request_with_comment(
1578 pull_request, self._rhodecode_user, self.db_repo, message=text,
1582 pull_request, self._rhodecode_user, self.db_repo, message=text,
1579 auth_user=self._rhodecode_user)
1583 auth_user=self._rhodecode_user)
1580 Session().flush()
1584 Session().flush()
1581 is_inline = comment.is_inline
1585 is_inline = comment.is_inline
1582
1586
1583 PullRequestModel().trigger_pull_request_hook(
1587 PullRequestModel().trigger_pull_request_hook(
1584 pull_request, self._rhodecode_user, 'comment',
1588 pull_request, self._rhodecode_user, 'comment',
1585 data={'comment': comment})
1589 data={'comment': comment})
1586
1590
1587 else:
1591 else:
1588 # regular comment case, could be inline, or one with status.
1592 # regular comment case, could be inline, or one with status.
1589 # for that one we check also permissions
1593 # for that one we check also permissions
1590 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1594 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1591 allowed_to_change_status = PullRequestModel().check_user_change_status(
1595 allowed_to_change_status = PullRequestModel().check_user_change_status(
1592 pull_request, self._rhodecode_user) and not is_draft
1596 pull_request, self._rhodecode_user) and not is_draft
1593
1597
1594 if status and allowed_to_change_status:
1598 if status and allowed_to_change_status:
1595 message = (_('Status change %(transition_icon)s %(status)s')
1599 message = (_('Status change %(transition_icon)s %(status)s')
1596 % {'transition_icon': '>',
1600 % {'transition_icon': '>',
1597 'status': ChangesetStatus.get_status_lbl(status)})
1601 'status': ChangesetStatus.get_status_lbl(status)})
1598 text = text or message
1602 text = text or message
1599
1603
1600 comment = CommentsModel().create(
1604 comment = CommentsModel().create(
1601 text=text,
1605 text=text,
1602 repo=self.db_repo.repo_id,
1606 repo=self.db_repo.repo_id,
1603 user=self._rhodecode_user.user_id,
1607 user=self._rhodecode_user.user_id,
1604 pull_request=pull_request,
1608 pull_request=pull_request,
1605 f_path=f_path,
1609 f_path=f_path,
1606 line_no=line_no,
1610 line_no=line_no,
1607 status_change=(ChangesetStatus.get_status_lbl(status)
1611 status_change=(ChangesetStatus.get_status_lbl(status)
1608 if status and allowed_to_change_status else None),
1612 if status and allowed_to_change_status else None),
1609 status_change_type=(status
1613 status_change_type=(status
1610 if status and allowed_to_change_status else None),
1614 if status and allowed_to_change_status else None),
1611 comment_type=comment_type,
1615 comment_type=comment_type,
1612 is_draft=is_draft,
1616 is_draft=is_draft,
1613 resolves_comment_id=resolves_comment_id,
1617 resolves_comment_id=resolves_comment_id,
1614 auth_user=self._rhodecode_user,
1618 auth_user=self._rhodecode_user,
1615 send_email=not is_draft, # skip notification for draft comments
1619 send_email=not is_draft, # skip notification for draft comments
1616 )
1620 )
1617 is_inline = comment.is_inline
1621 is_inline = comment.is_inline
1618
1622
1619 if allowed_to_change_status:
1623 if allowed_to_change_status:
1620 # calculate old status before we change it
1624 # calculate old status before we change it
1621 old_calculated_status = pull_request.calculated_review_status()
1625 old_calculated_status = pull_request.calculated_review_status()
1622
1626
1623 # get status if set !
1627 # get status if set !
1624 if status:
1628 if status:
1625 ChangesetStatusModel().set_status(
1629 ChangesetStatusModel().set_status(
1626 self.db_repo.repo_id,
1630 self.db_repo.repo_id,
1627 status,
1631 status,
1628 self._rhodecode_user.user_id,
1632 self._rhodecode_user.user_id,
1629 comment,
1633 comment,
1630 pull_request=pull_request
1634 pull_request=pull_request
1631 )
1635 )
1632
1636
1633 Session().flush()
1637 Session().flush()
1634 # this is somehow required to get access to some relationship
1638 # this is somehow required to get access to some relationship
1635 # loaded on comment
1639 # loaded on comment
1636 Session().refresh(comment)
1640 Session().refresh(comment)
1637
1641
1638 # skip notifications for drafts
1642 # skip notifications for drafts
1639 if not is_draft:
1643 if not is_draft:
1640 PullRequestModel().trigger_pull_request_hook(
1644 PullRequestModel().trigger_pull_request_hook(
1641 pull_request, self._rhodecode_user, 'comment',
1645 pull_request, self._rhodecode_user, 'comment',
1642 data={'comment': comment})
1646 data={'comment': comment})
1643
1647
1644 # we now calculate the status of pull request, and based on that
1648 # we now calculate the status of pull request, and based on that
1645 # calculation we set the commits status
1649 # calculation we set the commits status
1646 calculated_status = pull_request.calculated_review_status()
1650 calculated_status = pull_request.calculated_review_status()
1647 if old_calculated_status != calculated_status:
1651 if old_calculated_status != calculated_status:
1648 PullRequestModel().trigger_pull_request_hook(
1652 PullRequestModel().trigger_pull_request_hook(
1649 pull_request, self._rhodecode_user, 'review_status_change',
1653 pull_request, self._rhodecode_user, 'review_status_change',
1650 data={'status': calculated_status})
1654 data={'status': calculated_status})
1651
1655
1652 comment_id = comment.comment_id
1656 comment_id = comment.comment_id
1653 data[comment_id] = {
1657 data[comment_id] = {
1654 'target_id': target_elem_id
1658 'target_id': target_elem_id
1655 }
1659 }
1656 Session().flush()
1660 Session().flush()
1657
1661
1658 c.co = comment
1662 c.co = comment
1659 c.at_version_num = None
1663 c.at_version_num = None
1660 c.is_new = True
1664 c.is_new = True
1661 rendered_comment = render(
1665 rendered_comment = render(
1662 'rhodecode:templates/changeset/changeset_comment_block.mako',
1666 'rhodecode:templates/changeset/changeset_comment_block.mako',
1663 self._get_template_context(c), self.request)
1667 self._get_template_context(c), self.request)
1664
1668
1665 data[comment_id].update(comment.get_dict())
1669 data[comment_id].update(comment.get_dict())
1666 data[comment_id].update({'rendered_text': rendered_comment})
1670 data[comment_id].update({'rendered_text': rendered_comment})
1667
1671
1668 Session().commit()
1672 Session().commit()
1669
1673
1670 # skip channelstream for draft comments
1674 # skip channelstream for draft comments
1671 if not all_drafts:
1675 if not all_drafts:
1672 comment_broadcast_channel = channelstream.comment_channel(
1676 comment_broadcast_channel = channelstream.comment_channel(
1673 self.db_repo_name, pull_request_obj=pull_request)
1677 self.db_repo_name, pull_request_obj=pull_request)
1674
1678
1675 comment_data = data
1679 comment_data = data
1676 posted_comment_type = 'inline' if is_inline else 'general'
1680 posted_comment_type = 'inline' if is_inline else 'general'
1677 if len(data) == 1:
1681 if len(data) == 1:
1678 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1682 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1679 else:
1683 else:
1680 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1684 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1681
1685
1682 channelstream.comment_channelstream_push(
1686 channelstream.comment_channelstream_push(
1683 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1687 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1684 comment_data=comment_data)
1688 comment_data=comment_data)
1685
1689
1686 return data
1690 return data
1687
1691
1688 @LoginRequired()
1692 @LoginRequired()
1689 @NotAnonymous()
1693 @NotAnonymous()
1690 @HasRepoPermissionAnyDecorator(
1694 @HasRepoPermissionAnyDecorator(
1691 'repository.read', 'repository.write', 'repository.admin')
1695 'repository.read', 'repository.write', 'repository.admin')
1692 @CSRFRequired()
1696 @CSRFRequired()
1693 def pull_request_comment_create(self):
1697 def pull_request_comment_create(self):
1694 _ = self.request.translate
1698 _ = self.request.translate
1695
1699
1696 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1700 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1697
1701
1698 if pull_request.is_closed():
1702 if pull_request.is_closed():
1699 log.debug('comment: forbidden because pull request is closed')
1703 log.debug('comment: forbidden because pull request is closed')
1700 raise HTTPForbidden()
1704 raise HTTPForbidden()
1701
1705
1702 allowed_to_comment = PullRequestModel().check_user_comment(
1706 allowed_to_comment = PullRequestModel().check_user_comment(
1703 pull_request, self._rhodecode_user)
1707 pull_request, self._rhodecode_user)
1704 if not allowed_to_comment:
1708 if not allowed_to_comment:
1705 log.debug('comment: forbidden because pull request is from forbidden repo')
1709 log.debug('comment: forbidden because pull request is from forbidden repo')
1706 raise HTTPForbidden()
1710 raise HTTPForbidden()
1707
1711
1708 comment_data = {
1712 comment_data = {
1709 'comment_type': self.request.POST.get('comment_type'),
1713 'comment_type': self.request.POST.get('comment_type'),
1710 'text': self.request.POST.get('text'),
1714 'text': self.request.POST.get('text'),
1711 'status': self.request.POST.get('changeset_status', None),
1715 'status': self.request.POST.get('changeset_status', None),
1712 'is_draft': self.request.POST.get('draft'),
1716 'is_draft': self.request.POST.get('draft'),
1713 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1717 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1714 'close_pull_request': self.request.POST.get('close_pull_request'),
1718 'close_pull_request': self.request.POST.get('close_pull_request'),
1715 'f_path': self.request.POST.get('f_path'),
1719 'f_path': self.request.POST.get('f_path'),
1716 'line': self.request.POST.get('line'),
1720 'line': self.request.POST.get('line'),
1717 }
1721 }
1718 data = self._pull_request_comments_create(pull_request, [comment_data])
1722 data = self._pull_request_comments_create(pull_request, [comment_data])
1719
1723
1720 return data
1724 return data
1721
1725
1722 @LoginRequired()
1726 @LoginRequired()
1723 @NotAnonymous()
1727 @NotAnonymous()
1724 @HasRepoPermissionAnyDecorator(
1728 @HasRepoPermissionAnyDecorator(
1725 'repository.read', 'repository.write', 'repository.admin')
1729 'repository.read', 'repository.write', 'repository.admin')
1726 @CSRFRequired()
1730 @CSRFRequired()
1727 def pull_request_comment_delete(self):
1731 def pull_request_comment_delete(self):
1728 pull_request = PullRequest.get_or_404(
1732 pull_request = PullRequest.get_or_404(
1729 self.request.matchdict['pull_request_id'])
1733 self.request.matchdict['pull_request_id'])
1730
1734
1731 comment = ChangesetComment.get_or_404(
1735 comment = ChangesetComment.get_or_404(
1732 self.request.matchdict['comment_id'])
1736 self.request.matchdict['comment_id'])
1733 comment_id = comment.comment_id
1737 comment_id = comment.comment_id
1734
1738
1735 if comment.immutable:
1739 if comment.immutable:
1736 # don't allow deleting comments that are immutable
1740 # don't allow deleting comments that are immutable
1737 raise HTTPForbidden()
1741 raise HTTPForbidden()
1738
1742
1739 if pull_request.is_closed():
1743 if pull_request.is_closed():
1740 log.debug('comment: forbidden because pull request is closed')
1744 log.debug('comment: forbidden because pull request is closed')
1741 raise HTTPForbidden()
1745 raise HTTPForbidden()
1742
1746
1743 if not comment:
1747 if not comment:
1744 log.debug('Comment with id:%s not found, skipping', comment_id)
1748 log.debug('Comment with id:%s not found, skipping', comment_id)
1745 # comment already deleted in another call probably
1749 # comment already deleted in another call probably
1746 return True
1750 return True
1747
1751
1748 if comment.pull_request.is_closed():
1752 if comment.pull_request.is_closed():
1749 # don't allow deleting comments on closed pull request
1753 # don't allow deleting comments on closed pull request
1750 raise HTTPForbidden()
1754 raise HTTPForbidden()
1751
1755
1752 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1756 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1753 super_admin = h.HasPermissionAny('hg.admin')()
1757 super_admin = h.HasPermissionAny('hg.admin')()
1754 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1758 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1755 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1759 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1756 comment_repo_admin = is_repo_admin and is_repo_comment
1760 comment_repo_admin = is_repo_admin and is_repo_comment
1757
1761
1758 if comment.draft and not comment_owner:
1762 if comment.draft and not comment_owner:
1759 # We never allow to delete draft comments for other than owners
1763 # We never allow to delete draft comments for other than owners
1760 raise HTTPNotFound()
1764 raise HTTPNotFound()
1761
1765
1762 if super_admin or comment_owner or comment_repo_admin:
1766 if super_admin or comment_owner or comment_repo_admin:
1763 old_calculated_status = comment.pull_request.calculated_review_status()
1767 old_calculated_status = comment.pull_request.calculated_review_status()
1764 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1768 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1765 Session().commit()
1769 Session().commit()
1766 calculated_status = comment.pull_request.calculated_review_status()
1770 calculated_status = comment.pull_request.calculated_review_status()
1767 if old_calculated_status != calculated_status:
1771 if old_calculated_status != calculated_status:
1768 PullRequestModel().trigger_pull_request_hook(
1772 PullRequestModel().trigger_pull_request_hook(
1769 comment.pull_request, self._rhodecode_user, 'review_status_change',
1773 comment.pull_request, self._rhodecode_user, 'review_status_change',
1770 data={'status': calculated_status})
1774 data={'status': calculated_status})
1771 return True
1775 return True
1772 else:
1776 else:
1773 log.warning('No permissions for user %s to delete comment_id: %s',
1777 log.warning('No permissions for user %s to delete comment_id: %s',
1774 self._rhodecode_db_user, comment_id)
1778 self._rhodecode_db_user, comment_id)
1775 raise HTTPNotFound()
1779 raise HTTPNotFound()
1776
1780
1777 @LoginRequired()
1781 @LoginRequired()
1778 @NotAnonymous()
1782 @NotAnonymous()
1779 @HasRepoPermissionAnyDecorator(
1783 @HasRepoPermissionAnyDecorator(
1780 'repository.read', 'repository.write', 'repository.admin')
1784 'repository.read', 'repository.write', 'repository.admin')
1781 @CSRFRequired()
1785 @CSRFRequired()
1782 def pull_request_comment_edit(self):
1786 def pull_request_comment_edit(self):
1783 self.load_default_context()
1787 self.load_default_context()
1784
1788
1785 pull_request = PullRequest.get_or_404(
1789 pull_request = PullRequest.get_or_404(
1786 self.request.matchdict['pull_request_id']
1790 self.request.matchdict['pull_request_id']
1787 )
1791 )
1788 comment = ChangesetComment.get_or_404(
1792 comment = ChangesetComment.get_or_404(
1789 self.request.matchdict['comment_id']
1793 self.request.matchdict['comment_id']
1790 )
1794 )
1791 comment_id = comment.comment_id
1795 comment_id = comment.comment_id
1792
1796
1793 if comment.immutable:
1797 if comment.immutable:
1794 # don't allow deleting comments that are immutable
1798 # don't allow deleting comments that are immutable
1795 raise HTTPForbidden()
1799 raise HTTPForbidden()
1796
1800
1797 if pull_request.is_closed():
1801 if pull_request.is_closed():
1798 log.debug('comment: forbidden because pull request is closed')
1802 log.debug('comment: forbidden because pull request is closed')
1799 raise HTTPForbidden()
1803 raise HTTPForbidden()
1800
1804
1801 if comment.pull_request.is_closed():
1805 if comment.pull_request.is_closed():
1802 # don't allow deleting comments on closed pull request
1806 # don't allow deleting comments on closed pull request
1803 raise HTTPForbidden()
1807 raise HTTPForbidden()
1804
1808
1805 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1809 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1806 super_admin = h.HasPermissionAny('hg.admin')()
1810 super_admin = h.HasPermissionAny('hg.admin')()
1807 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1811 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1808 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1812 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1809 comment_repo_admin = is_repo_admin and is_repo_comment
1813 comment_repo_admin = is_repo_admin and is_repo_comment
1810
1814
1811 if super_admin or comment_owner or comment_repo_admin:
1815 if super_admin or comment_owner or comment_repo_admin:
1812 text = self.request.POST.get('text')
1816 text = self.request.POST.get('text')
1813 version = self.request.POST.get('version')
1817 version = self.request.POST.get('version')
1814 if text == comment.text:
1818 if text == comment.text:
1815 log.warning(
1819 log.warning(
1816 'Comment(PR): '
1820 'Comment(PR): '
1817 'Trying to create new version '
1821 'Trying to create new version '
1818 'with the same comment body {}'.format(
1822 'with the same comment body {}'.format(
1819 comment_id,
1823 comment_id,
1820 )
1824 )
1821 )
1825 )
1822 raise HTTPNotFound()
1826 raise HTTPNotFound()
1823
1827
1824 if version.isdigit():
1828 if version.isdigit():
1825 version = int(version)
1829 version = int(version)
1826 else:
1830 else:
1827 log.warning(
1831 log.warning(
1828 'Comment(PR): Wrong version type {} {} '
1832 'Comment(PR): Wrong version type {} {} '
1829 'for comment {}'.format(
1833 'for comment {}'.format(
1830 version,
1834 version,
1831 type(version),
1835 type(version),
1832 comment_id,
1836 comment_id,
1833 )
1837 )
1834 )
1838 )
1835 raise HTTPNotFound()
1839 raise HTTPNotFound()
1836
1840
1837 try:
1841 try:
1838 comment_history = CommentsModel().edit(
1842 comment_history = CommentsModel().edit(
1839 comment_id=comment_id,
1843 comment_id=comment_id,
1840 text=text,
1844 text=text,
1841 auth_user=self._rhodecode_user,
1845 auth_user=self._rhodecode_user,
1842 version=version,
1846 version=version,
1843 )
1847 )
1844 except CommentVersionMismatch:
1848 except CommentVersionMismatch:
1845 raise HTTPConflict()
1849 raise HTTPConflict()
1846
1850
1847 if not comment_history:
1851 if not comment_history:
1848 raise HTTPNotFound()
1852 raise HTTPNotFound()
1849
1853
1850 Session().commit()
1854 Session().commit()
1851 if not comment.draft:
1855 if not comment.draft:
1852 PullRequestModel().trigger_pull_request_hook(
1856 PullRequestModel().trigger_pull_request_hook(
1853 pull_request, self._rhodecode_user, 'comment_edit',
1857 pull_request, self._rhodecode_user, 'comment_edit',
1854 data={'comment': comment})
1858 data={'comment': comment})
1855
1859
1856 return {
1860 return {
1857 'comment_history_id': comment_history.comment_history_id,
1861 'comment_history_id': comment_history.comment_history_id,
1858 'comment_id': comment.comment_id,
1862 'comment_id': comment.comment_id,
1859 'comment_version': comment_history.version,
1863 'comment_version': comment_history.version,
1860 'comment_author_username': comment_history.author.username,
1864 'comment_author_username': comment_history.author.username,
1861 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1865 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1862 'comment_created_on': h.age_component(comment_history.created_on,
1866 'comment_created_on': h.age_component(comment_history.created_on,
1863 time_is_local=True),
1867 time_is_local=True),
1864 }
1868 }
1865 else:
1869 else:
1866 log.warning('No permissions for user %s to edit comment_id: %s',
1870 log.warning('No permissions for user %s to edit comment_id: %s',
1867 self._rhodecode_db_user, comment_id)
1871 self._rhodecode_db_user, comment_id)
1868 raise HTTPNotFound()
1872 raise HTTPNotFound()
@@ -1,1074 +1,1148 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 """
22 """
23 Some simple helper functions
23 Some simple helper functions
24 """
24 """
25
25
26 import collections
26 import collections
27 import datetime
27 import datetime
28 import dateutil.relativedelta
28 import dateutil.relativedelta
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import re
31 import re
32 import sys
32 import sys
33 import time
33 import time
34 import urllib
34 import urllib
35 import urlobject
35 import urlobject
36 import uuid
36 import uuid
37 import getpass
37 import getpass
38 from functools import update_wrapper, partial
38 from functools import update_wrapper, partial, wraps
39
39
40 import pygments.lexers
40 import pygments.lexers
41 import sqlalchemy
41 import sqlalchemy
42 import sqlalchemy.engine.url
42 import sqlalchemy.engine.url
43 import sqlalchemy.exc
43 import sqlalchemy.exc
44 import sqlalchemy.sql
44 import sqlalchemy.sql
45 import webob
45 import webob
46 import pyramid.threadlocal
46 import pyramid.threadlocal
47 from pyramid import compat
47 from pyramid import compat
48 from pyramid.settings import asbool
48 from pyramid.settings import asbool
49
49
50 import rhodecode
50 import rhodecode
51 from rhodecode.translation import _, _pluralize
51 from rhodecode.translation import _, _pluralize
52
52
53
53
54 def md5(s):
54 def md5(s):
55 return hashlib.md5(s).hexdigest()
55 return hashlib.md5(s).hexdigest()
56
56
57
57
58 def md5_safe(s):
58 def md5_safe(s):
59 return md5(safe_str(s))
59 return md5(safe_str(s))
60
60
61
61
62 def sha1(s):
62 def sha1(s):
63 return hashlib.sha1(s).hexdigest()
63 return hashlib.sha1(s).hexdigest()
64
64
65
65
66 def sha1_safe(s):
66 def sha1_safe(s):
67 return sha1(safe_str(s))
67 return sha1(safe_str(s))
68
68
69
69
70 def __get_lem(extra_mapping=None):
70 def __get_lem(extra_mapping=None):
71 """
71 """
72 Get language extension map based on what's inside pygments lexers
72 Get language extension map based on what's inside pygments lexers
73 """
73 """
74 d = collections.defaultdict(lambda: [])
74 d = collections.defaultdict(lambda: [])
75
75
76 def __clean(s):
76 def __clean(s):
77 s = s.lstrip('*')
77 s = s.lstrip('*')
78 s = s.lstrip('.')
78 s = s.lstrip('.')
79
79
80 if s.find('[') != -1:
80 if s.find('[') != -1:
81 exts = []
81 exts = []
82 start, stop = s.find('['), s.find(']')
82 start, stop = s.find('['), s.find(']')
83
83
84 for suffix in s[start + 1:stop]:
84 for suffix in s[start + 1:stop]:
85 exts.append(s[:s.find('[')] + suffix)
85 exts.append(s[:s.find('[')] + suffix)
86 return [e.lower() for e in exts]
86 return [e.lower() for e in exts]
87 else:
87 else:
88 return [s.lower()]
88 return [s.lower()]
89
89
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
90 for lx, t in sorted(pygments.lexers.LEXERS.items()):
91 m = map(__clean, t[-2])
91 m = map(__clean, t[-2])
92 if m:
92 if m:
93 m = reduce(lambda x, y: x + y, m)
93 m = reduce(lambda x, y: x + y, m)
94 for ext in m:
94 for ext in m:
95 desc = lx.replace('Lexer', '')
95 desc = lx.replace('Lexer', '')
96 d[ext].append(desc)
96 d[ext].append(desc)
97
97
98 data = dict(d)
98 data = dict(d)
99
99
100 extra_mapping = extra_mapping or {}
100 extra_mapping = extra_mapping or {}
101 if extra_mapping:
101 if extra_mapping:
102 for k, v in extra_mapping.items():
102 for k, v in extra_mapping.items():
103 if k not in data:
103 if k not in data:
104 # register new mapping2lexer
104 # register new mapping2lexer
105 data[k] = [v]
105 data[k] = [v]
106
106
107 return data
107 return data
108
108
109
109
110 def str2bool(_str):
110 def str2bool(_str):
111 """
111 """
112 returns True/False value from given string, it tries to translate the
112 returns True/False value from given string, it tries to translate the
113 string into boolean
113 string into boolean
114
114
115 :param _str: string value to translate into boolean
115 :param _str: string value to translate into boolean
116 :rtype: boolean
116 :rtype: boolean
117 :returns: boolean from given string
117 :returns: boolean from given string
118 """
118 """
119 if _str is None:
119 if _str is None:
120 return False
120 return False
121 if _str in (True, False):
121 if _str in (True, False):
122 return _str
122 return _str
123 _str = str(_str).strip().lower()
123 _str = str(_str).strip().lower()
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
124 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
125
125
126
126
127 def aslist(obj, sep=None, strip=True):
127 def aslist(obj, sep=None, strip=True):
128 """
128 """
129 Returns given string separated by sep as list
129 Returns given string separated by sep as list
130
130
131 :param obj:
131 :param obj:
132 :param sep:
132 :param sep:
133 :param strip:
133 :param strip:
134 """
134 """
135 if isinstance(obj, (basestring,)):
135 if isinstance(obj, (basestring,)):
136 lst = obj.split(sep)
136 lst = obj.split(sep)
137 if strip:
137 if strip:
138 lst = [v.strip() for v in lst]
138 lst = [v.strip() for v in lst]
139 return lst
139 return lst
140 elif isinstance(obj, (list, tuple)):
140 elif isinstance(obj, (list, tuple)):
141 return obj
141 return obj
142 elif obj is None:
142 elif obj is None:
143 return []
143 return []
144 else:
144 else:
145 return [obj]
145 return [obj]
146
146
147
147
148 def convert_line_endings(line, mode):
148 def convert_line_endings(line, mode):
149 """
149 """
150 Converts a given line "line end" accordingly to given mode
150 Converts a given line "line end" accordingly to given mode
151
151
152 Available modes are::
152 Available modes are::
153 0 - Unix
153 0 - Unix
154 1 - Mac
154 1 - Mac
155 2 - DOS
155 2 - DOS
156
156
157 :param line: given line to convert
157 :param line: given line to convert
158 :param mode: mode to convert to
158 :param mode: mode to convert to
159 :rtype: str
159 :rtype: str
160 :return: converted line according to mode
160 :return: converted line according to mode
161 """
161 """
162 if mode == 0:
162 if mode == 0:
163 line = line.replace('\r\n', '\n')
163 line = line.replace('\r\n', '\n')
164 line = line.replace('\r', '\n')
164 line = line.replace('\r', '\n')
165 elif mode == 1:
165 elif mode == 1:
166 line = line.replace('\r\n', '\r')
166 line = line.replace('\r\n', '\r')
167 line = line.replace('\n', '\r')
167 line = line.replace('\n', '\r')
168 elif mode == 2:
168 elif mode == 2:
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
169 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
170 return line
170 return line
171
171
172
172
173 def detect_mode(line, default):
173 def detect_mode(line, default):
174 """
174 """
175 Detects line break for given line, if line break couldn't be found
175 Detects line break for given line, if line break couldn't be found
176 given default value is returned
176 given default value is returned
177
177
178 :param line: str line
178 :param line: str line
179 :param default: default
179 :param default: default
180 :rtype: int
180 :rtype: int
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
181 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
182 """
182 """
183 if line.endswith('\r\n'):
183 if line.endswith('\r\n'):
184 return 2
184 return 2
185 elif line.endswith('\n'):
185 elif line.endswith('\n'):
186 return 0
186 return 0
187 elif line.endswith('\r'):
187 elif line.endswith('\r'):
188 return 1
188 return 1
189 else:
189 else:
190 return default
190 return default
191
191
192
192
193 def safe_int(val, default=None):
193 def safe_int(val, default=None):
194 """
194 """
195 Returns int() of val if val is not convertable to int use default
195 Returns int() of val if val is not convertable to int use default
196 instead
196 instead
197
197
198 :param val:
198 :param val:
199 :param default:
199 :param default:
200 """
200 """
201
201
202 try:
202 try:
203 val = int(val)
203 val = int(val)
204 except (ValueError, TypeError):
204 except (ValueError, TypeError):
205 val = default
205 val = default
206
206
207 return val
207 return val
208
208
209
209
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
210 def safe_unicode(str_, from_encoding=None, use_chardet=False):
211 """
211 """
212 safe unicode function. Does few trick to turn str_ into unicode
212 safe unicode function. Does few trick to turn str_ into unicode
213
213
214 In case of UnicodeDecode error, we try to return it with encoding detected
214 In case of UnicodeDecode error, we try to return it with encoding detected
215 by chardet library if it fails fallback to unicode with errors replaced
215 by chardet library if it fails fallback to unicode with errors replaced
216
216
217 :param str_: string to decode
217 :param str_: string to decode
218 :rtype: unicode
218 :rtype: unicode
219 :returns: unicode object
219 :returns: unicode object
220 """
220 """
221 if isinstance(str_, unicode):
221 if isinstance(str_, unicode):
222 return str_
222 return str_
223
223
224 if not from_encoding:
224 if not from_encoding:
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
225 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
226 'utf8'), sep=',')
226 'utf8'), sep=',')
227 from_encoding = DEFAULT_ENCODINGS
227 from_encoding = DEFAULT_ENCODINGS
228
228
229 if not isinstance(from_encoding, (list, tuple)):
229 if not isinstance(from_encoding, (list, tuple)):
230 from_encoding = [from_encoding]
230 from_encoding = [from_encoding]
231
231
232 try:
232 try:
233 return unicode(str_)
233 return unicode(str_)
234 except UnicodeDecodeError:
234 except UnicodeDecodeError:
235 pass
235 pass
236
236
237 for enc in from_encoding:
237 for enc in from_encoding:
238 try:
238 try:
239 return unicode(str_, enc)
239 return unicode(str_, enc)
240 except UnicodeDecodeError:
240 except UnicodeDecodeError:
241 pass
241 pass
242
242
243 if use_chardet:
243 if use_chardet:
244 try:
244 try:
245 import chardet
245 import chardet
246 encoding = chardet.detect(str_)['encoding']
246 encoding = chardet.detect(str_)['encoding']
247 if encoding is None:
247 if encoding is None:
248 raise Exception()
248 raise Exception()
249 return str_.decode(encoding)
249 return str_.decode(encoding)
250 except (ImportError, UnicodeDecodeError, Exception):
250 except (ImportError, UnicodeDecodeError, Exception):
251 return unicode(str_, from_encoding[0], 'replace')
251 return unicode(str_, from_encoding[0], 'replace')
252 else:
252 else:
253 return unicode(str_, from_encoding[0], 'replace')
253 return unicode(str_, from_encoding[0], 'replace')
254
254
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
255 def safe_str(unicode_, to_encoding=None, use_chardet=False):
256 """
256 """
257 safe str function. Does few trick to turn unicode_ into string
257 safe str function. Does few trick to turn unicode_ into string
258
258
259 In case of UnicodeEncodeError, we try to return it with encoding detected
259 In case of UnicodeEncodeError, we try to return it with encoding detected
260 by chardet library if it fails fallback to string with errors replaced
260 by chardet library if it fails fallback to string with errors replaced
261
261
262 :param unicode_: unicode to encode
262 :param unicode_: unicode to encode
263 :rtype: str
263 :rtype: str
264 :returns: str object
264 :returns: str object
265 """
265 """
266
266
267 # if it's not basestr cast to str
267 # if it's not basestr cast to str
268 if not isinstance(unicode_, compat.string_types):
268 if not isinstance(unicode_, compat.string_types):
269 return str(unicode_)
269 return str(unicode_)
270
270
271 if isinstance(unicode_, str):
271 if isinstance(unicode_, str):
272 return unicode_
272 return unicode_
273
273
274 if not to_encoding:
274 if not to_encoding:
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
275 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
276 'utf8'), sep=',')
276 'utf8'), sep=',')
277 to_encoding = DEFAULT_ENCODINGS
277 to_encoding = DEFAULT_ENCODINGS
278
278
279 if not isinstance(to_encoding, (list, tuple)):
279 if not isinstance(to_encoding, (list, tuple)):
280 to_encoding = [to_encoding]
280 to_encoding = [to_encoding]
281
281
282 for enc in to_encoding:
282 for enc in to_encoding:
283 try:
283 try:
284 return unicode_.encode(enc)
284 return unicode_.encode(enc)
285 except UnicodeEncodeError:
285 except UnicodeEncodeError:
286 pass
286 pass
287
287
288 if use_chardet:
288 if use_chardet:
289 try:
289 try:
290 import chardet
290 import chardet
291 encoding = chardet.detect(unicode_)['encoding']
291 encoding = chardet.detect(unicode_)['encoding']
292 if encoding is None:
292 if encoding is None:
293 raise UnicodeEncodeError()
293 raise UnicodeEncodeError()
294
294
295 return unicode_.encode(encoding)
295 return unicode_.encode(encoding)
296 except (ImportError, UnicodeEncodeError):
296 except (ImportError, UnicodeEncodeError):
297 return unicode_.encode(to_encoding[0], 'replace')
297 return unicode_.encode(to_encoding[0], 'replace')
298 else:
298 else:
299 return unicode_.encode(to_encoding[0], 'replace')
299 return unicode_.encode(to_encoding[0], 'replace')
300
300
301
301
302 def remove_suffix(s, suffix):
302 def remove_suffix(s, suffix):
303 if s.endswith(suffix):
303 if s.endswith(suffix):
304 s = s[:-1 * len(suffix)]
304 s = s[:-1 * len(suffix)]
305 return s
305 return s
306
306
307
307
308 def remove_prefix(s, prefix):
308 def remove_prefix(s, prefix):
309 if s.startswith(prefix):
309 if s.startswith(prefix):
310 s = s[len(prefix):]
310 s = s[len(prefix):]
311 return s
311 return s
312
312
313
313
314 def find_calling_context(ignore_modules=None):
314 def find_calling_context(ignore_modules=None):
315 """
315 """
316 Look through the calling stack and return the frame which called
316 Look through the calling stack and return the frame which called
317 this function and is part of core module ( ie. rhodecode.* )
317 this function and is part of core module ( ie. rhodecode.* )
318
318
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
319 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
320 """
320 """
321
321
322 ignore_modules = ignore_modules or []
322 ignore_modules = ignore_modules or []
323
323
324 f = sys._getframe(2)
324 f = sys._getframe(2)
325 while f.f_back is not None:
325 while f.f_back is not None:
326 name = f.f_globals.get('__name__')
326 name = f.f_globals.get('__name__')
327 if name and name.startswith(__name__.split('.')[0]):
327 if name and name.startswith(__name__.split('.')[0]):
328 if name not in ignore_modules:
328 if name not in ignore_modules:
329 return f
329 return f
330 f = f.f_back
330 f = f.f_back
331 return None
331 return None
332
332
333
333
334 def ping_connection(connection, branch):
334 def ping_connection(connection, branch):
335 if branch:
335 if branch:
336 # "branch" refers to a sub-connection of a connection,
336 # "branch" refers to a sub-connection of a connection,
337 # we don't want to bother pinging on these.
337 # we don't want to bother pinging on these.
338 return
338 return
339
339
340 # turn off "close with result". This flag is only used with
340 # turn off "close with result". This flag is only used with
341 # "connectionless" execution, otherwise will be False in any case
341 # "connectionless" execution, otherwise will be False in any case
342 save_should_close_with_result = connection.should_close_with_result
342 save_should_close_with_result = connection.should_close_with_result
343 connection.should_close_with_result = False
343 connection.should_close_with_result = False
344
344
345 try:
345 try:
346 # run a SELECT 1. use a core select() so that
346 # run a SELECT 1. use a core select() so that
347 # the SELECT of a scalar value without a table is
347 # the SELECT of a scalar value without a table is
348 # appropriately formatted for the backend
348 # appropriately formatted for the backend
349 connection.scalar(sqlalchemy.sql.select([1]))
349 connection.scalar(sqlalchemy.sql.select([1]))
350 except sqlalchemy.exc.DBAPIError as err:
350 except sqlalchemy.exc.DBAPIError as err:
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
351 # catch SQLAlchemy's DBAPIError, which is a wrapper
352 # for the DBAPI's exception. It includes a .connection_invalidated
352 # for the DBAPI's exception. It includes a .connection_invalidated
353 # attribute which specifies if this connection is a "disconnect"
353 # attribute which specifies if this connection is a "disconnect"
354 # condition, which is based on inspection of the original exception
354 # condition, which is based on inspection of the original exception
355 # by the dialect in use.
355 # by the dialect in use.
356 if err.connection_invalidated:
356 if err.connection_invalidated:
357 # run the same SELECT again - the connection will re-validate
357 # run the same SELECT again - the connection will re-validate
358 # itself and establish a new connection. The disconnect detection
358 # itself and establish a new connection. The disconnect detection
359 # here also causes the whole connection pool to be invalidated
359 # here also causes the whole connection pool to be invalidated
360 # so that all stale connections are discarded.
360 # so that all stale connections are discarded.
361 connection.scalar(sqlalchemy.sql.select([1]))
361 connection.scalar(sqlalchemy.sql.select([1]))
362 else:
362 else:
363 raise
363 raise
364 finally:
364 finally:
365 # restore "close with result"
365 # restore "close with result"
366 connection.should_close_with_result = save_should_close_with_result
366 connection.should_close_with_result = save_should_close_with_result
367
367
368
368
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
369 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
370 """Custom engine_from_config functions."""
370 """Custom engine_from_config functions."""
371 log = logging.getLogger('sqlalchemy.engine')
371 log = logging.getLogger('sqlalchemy.engine')
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
372 use_ping_connection = asbool(configuration.pop('sqlalchemy.db1.ping_connection', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
373 debug = asbool(configuration.pop('sqlalchemy.db1.debug_query', None))
374
374
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
375 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
376
376
377 def color_sql(sql):
377 def color_sql(sql):
378 color_seq = '\033[1;33m' # This is yellow: code 33
378 color_seq = '\033[1;33m' # This is yellow: code 33
379 normal = '\x1b[0m'
379 normal = '\x1b[0m'
380 return ''.join([color_seq, sql, normal])
380 return ''.join([color_seq, sql, normal])
381
381
382 if use_ping_connection:
382 if use_ping_connection:
383 log.debug('Adding ping_connection on the engine config.')
383 log.debug('Adding ping_connection on the engine config.')
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
384 sqlalchemy.event.listen(engine, "engine_connect", ping_connection)
385
385
386 if debug:
386 if debug:
387 # attach events only for debug configuration
387 # attach events only for debug configuration
388 def before_cursor_execute(conn, cursor, statement,
388 def before_cursor_execute(conn, cursor, statement,
389 parameters, context, executemany):
389 parameters, context, executemany):
390 setattr(conn, 'query_start_time', time.time())
390 setattr(conn, 'query_start_time', time.time())
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
391 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
392 calling_context = find_calling_context(ignore_modules=[
392 calling_context = find_calling_context(ignore_modules=[
393 'rhodecode.lib.caching_query',
393 'rhodecode.lib.caching_query',
394 'rhodecode.model.settings',
394 'rhodecode.model.settings',
395 ])
395 ])
396 if calling_context:
396 if calling_context:
397 log.info(color_sql('call context %s:%s' % (
397 log.info(color_sql('call context %s:%s' % (
398 calling_context.f_code.co_filename,
398 calling_context.f_code.co_filename,
399 calling_context.f_lineno,
399 calling_context.f_lineno,
400 )))
400 )))
401
401
402 def after_cursor_execute(conn, cursor, statement,
402 def after_cursor_execute(conn, cursor, statement,
403 parameters, context, executemany):
403 parameters, context, executemany):
404 delattr(conn, 'query_start_time')
404 delattr(conn, 'query_start_time')
405
405
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
406 sqlalchemy.event.listen(engine, "before_cursor_execute", before_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
407 sqlalchemy.event.listen(engine, "after_cursor_execute", after_cursor_execute)
408
408
409 return engine
409 return engine
410
410
411
411
412 def get_encryption_key(config):
412 def get_encryption_key(config):
413 secret = config.get('rhodecode.encrypted_values.secret')
413 secret = config.get('rhodecode.encrypted_values.secret')
414 default = config['beaker.session.secret']
414 default = config['beaker.session.secret']
415 return secret or default
415 return secret or default
416
416
417
417
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
418 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
419 short_format=False):
419 short_format=False):
420 """
420 """
421 Turns a datetime into an age string.
421 Turns a datetime into an age string.
422 If show_short_version is True, this generates a shorter string with
422 If show_short_version is True, this generates a shorter string with
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
423 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
424
424
425 * IMPORTANT*
425 * IMPORTANT*
426 Code of this function is written in special way so it's easier to
426 Code of this function is written in special way so it's easier to
427 backport it to javascript. If you mean to update it, please also update
427 backport it to javascript. If you mean to update it, please also update
428 `jquery.timeago-extension.js` file
428 `jquery.timeago-extension.js` file
429
429
430 :param prevdate: datetime object
430 :param prevdate: datetime object
431 :param now: get current time, if not define we use
431 :param now: get current time, if not define we use
432 `datetime.datetime.now()`
432 `datetime.datetime.now()`
433 :param show_short_version: if it should approximate the date and
433 :param show_short_version: if it should approximate the date and
434 return a shorter string
434 return a shorter string
435 :param show_suffix:
435 :param show_suffix:
436 :param short_format: show short format, eg 2D instead of 2 days
436 :param short_format: show short format, eg 2D instead of 2 days
437 :rtype: unicode
437 :rtype: unicode
438 :returns: unicode words describing age
438 :returns: unicode words describing age
439 """
439 """
440
440
441 def _get_relative_delta(now, prevdate):
441 def _get_relative_delta(now, prevdate):
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
442 base = dateutil.relativedelta.relativedelta(now, prevdate)
443 return {
443 return {
444 'year': base.years,
444 'year': base.years,
445 'month': base.months,
445 'month': base.months,
446 'day': base.days,
446 'day': base.days,
447 'hour': base.hours,
447 'hour': base.hours,
448 'minute': base.minutes,
448 'minute': base.minutes,
449 'second': base.seconds,
449 'second': base.seconds,
450 }
450 }
451
451
452 def _is_leap_year(year):
452 def _is_leap_year(year):
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
453 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
454
454
455 def get_month(prevdate):
455 def get_month(prevdate):
456 return prevdate.month
456 return prevdate.month
457
457
458 def get_year(prevdate):
458 def get_year(prevdate):
459 return prevdate.year
459 return prevdate.year
460
460
461 now = now or datetime.datetime.now()
461 now = now or datetime.datetime.now()
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
462 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
463 deltas = {}
463 deltas = {}
464 future = False
464 future = False
465
465
466 if prevdate > now:
466 if prevdate > now:
467 now_old = now
467 now_old = now
468 now = prevdate
468 now = prevdate
469 prevdate = now_old
469 prevdate = now_old
470 future = True
470 future = True
471 if future:
471 if future:
472 prevdate = prevdate.replace(microsecond=0)
472 prevdate = prevdate.replace(microsecond=0)
473 # Get date parts deltas
473 # Get date parts deltas
474 for part in order:
474 for part in order:
475 rel_delta = _get_relative_delta(now, prevdate)
475 rel_delta = _get_relative_delta(now, prevdate)
476 deltas[part] = rel_delta[part]
476 deltas[part] = rel_delta[part]
477
477
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
478 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
479 # not 1 hour, -59 minutes and -59 seconds)
479 # not 1 hour, -59 minutes and -59 seconds)
480 offsets = [[5, 60], [4, 60], [3, 24]]
480 offsets = [[5, 60], [4, 60], [3, 24]]
481 for element in offsets: # seconds, minutes, hours
481 for element in offsets: # seconds, minutes, hours
482 num = element[0]
482 num = element[0]
483 length = element[1]
483 length = element[1]
484
484
485 part = order[num]
485 part = order[num]
486 carry_part = order[num - 1]
486 carry_part = order[num - 1]
487
487
488 if deltas[part] < 0:
488 if deltas[part] < 0:
489 deltas[part] += length
489 deltas[part] += length
490 deltas[carry_part] -= 1
490 deltas[carry_part] -= 1
491
491
492 # Same thing for days except that the increment depends on the (variable)
492 # Same thing for days except that the increment depends on the (variable)
493 # number of days in the month
493 # number of days in the month
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
494 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
495 if deltas['day'] < 0:
495 if deltas['day'] < 0:
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
496 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
497 deltas['day'] += 29
497 deltas['day'] += 29
498 else:
498 else:
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
499 deltas['day'] += month_lengths[get_month(prevdate) - 1]
500
500
501 deltas['month'] -= 1
501 deltas['month'] -= 1
502
502
503 if deltas['month'] < 0:
503 if deltas['month'] < 0:
504 deltas['month'] += 12
504 deltas['month'] += 12
505 deltas['year'] -= 1
505 deltas['year'] -= 1
506
506
507 # Format the result
507 # Format the result
508 if short_format:
508 if short_format:
509 fmt_funcs = {
509 fmt_funcs = {
510 'year': lambda d: u'%dy' % d,
510 'year': lambda d: u'%dy' % d,
511 'month': lambda d: u'%dm' % d,
511 'month': lambda d: u'%dm' % d,
512 'day': lambda d: u'%dd' % d,
512 'day': lambda d: u'%dd' % d,
513 'hour': lambda d: u'%dh' % d,
513 'hour': lambda d: u'%dh' % d,
514 'minute': lambda d: u'%dmin' % d,
514 'minute': lambda d: u'%dmin' % d,
515 'second': lambda d: u'%dsec' % d,
515 'second': lambda d: u'%dsec' % d,
516 }
516 }
517 else:
517 else:
518 fmt_funcs = {
518 fmt_funcs = {
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
519 'year': lambda d: _pluralize(u'${num} year', u'${num} years', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
520 'month': lambda d: _pluralize(u'${num} month', u'${num} months', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
521 'day': lambda d: _pluralize(u'${num} day', u'${num} days', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
522 'hour': lambda d: _pluralize(u'${num} hour', u'${num} hours', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
523 'minute': lambda d: _pluralize(u'${num} minute', u'${num} minutes', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
524 'second': lambda d: _pluralize(u'${num} second', u'${num} seconds', d, mapping={'num': d}).interpolate(),
525 }
525 }
526
526
527 i = 0
527 i = 0
528 for part in order:
528 for part in order:
529 value = deltas[part]
529 value = deltas[part]
530 if value != 0:
530 if value != 0:
531
531
532 if i < 5:
532 if i < 5:
533 sub_part = order[i + 1]
533 sub_part = order[i + 1]
534 sub_value = deltas[sub_part]
534 sub_value = deltas[sub_part]
535 else:
535 else:
536 sub_value = 0
536 sub_value = 0
537
537
538 if sub_value == 0 or show_short_version:
538 if sub_value == 0 or show_short_version:
539 _val = fmt_funcs[part](value)
539 _val = fmt_funcs[part](value)
540 if future:
540 if future:
541 if show_suffix:
541 if show_suffix:
542 return _(u'in ${ago}', mapping={'ago': _val})
542 return _(u'in ${ago}', mapping={'ago': _val})
543 else:
543 else:
544 return _(_val)
544 return _(_val)
545
545
546 else:
546 else:
547 if show_suffix:
547 if show_suffix:
548 return _(u'${ago} ago', mapping={'ago': _val})
548 return _(u'${ago} ago', mapping={'ago': _val})
549 else:
549 else:
550 return _(_val)
550 return _(_val)
551
551
552 val = fmt_funcs[part](value)
552 val = fmt_funcs[part](value)
553 val_detail = fmt_funcs[sub_part](sub_value)
553 val_detail = fmt_funcs[sub_part](sub_value)
554 mapping = {'val': val, 'detail': val_detail}
554 mapping = {'val': val, 'detail': val_detail}
555
555
556 if short_format:
556 if short_format:
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
557 datetime_tmpl = _(u'${val}, ${detail}', mapping=mapping)
558 if show_suffix:
558 if show_suffix:
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
559 datetime_tmpl = _(u'${val}, ${detail} ago', mapping=mapping)
560 if future:
560 if future:
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
561 datetime_tmpl = _(u'in ${val}, ${detail}', mapping=mapping)
562 else:
562 else:
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
563 datetime_tmpl = _(u'${val} and ${detail}', mapping=mapping)
564 if show_suffix:
564 if show_suffix:
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
565 datetime_tmpl = _(u'${val} and ${detail} ago', mapping=mapping)
566 if future:
566 if future:
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
567 datetime_tmpl = _(u'in ${val} and ${detail}', mapping=mapping)
568
568
569 return datetime_tmpl
569 return datetime_tmpl
570 i += 1
570 i += 1
571 return _(u'just now')
571 return _(u'just now')
572
572
573
573
574 def age_from_seconds(seconds):
574 def age_from_seconds(seconds):
575 seconds = safe_int(seconds) or 0
575 seconds = safe_int(seconds) or 0
576 prevdate = time_to_datetime(time.time() + seconds)
576 prevdate = time_to_datetime(time.time() + seconds)
577 return age(prevdate, show_suffix=False, show_short_version=True)
577 return age(prevdate, show_suffix=False, show_short_version=True)
578
578
579
579
580 def cleaned_uri(uri):
580 def cleaned_uri(uri):
581 """
581 """
582 Quotes '[' and ']' from uri if there is only one of them.
582 Quotes '[' and ']' from uri if there is only one of them.
583 according to RFC3986 we cannot use such chars in uri
583 according to RFC3986 we cannot use such chars in uri
584 :param uri:
584 :param uri:
585 :return: uri without this chars
585 :return: uri without this chars
586 """
586 """
587 return urllib.quote(uri, safe='@$:/')
587 return urllib.quote(uri, safe='@$:/')
588
588
589
589
590 def credentials_filter(uri):
590 def credentials_filter(uri):
591 """
591 """
592 Returns a url with removed credentials
592 Returns a url with removed credentials
593
593
594 :param uri:
594 :param uri:
595 """
595 """
596 import urlobject
596 import urlobject
597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
597 if isinstance(uri, rhodecode.lib.encrypt.InvalidDecryptedValue):
598 return 'InvalidDecryptionKey'
598 return 'InvalidDecryptionKey'
599
599
600 url_obj = urlobject.URLObject(cleaned_uri(uri))
600 url_obj = urlobject.URLObject(cleaned_uri(uri))
601 url_obj = url_obj.without_password().without_username()
601 url_obj = url_obj.without_password().without_username()
602
602
603 return url_obj
603 return url_obj
604
604
605
605
606 def get_host_info(request):
606 def get_host_info(request):
607 """
607 """
608 Generate host info, to obtain full url e.g https://server.com
608 Generate host info, to obtain full url e.g https://server.com
609 use this
609 use this
610 `{scheme}://{netloc}`
610 `{scheme}://{netloc}`
611 """
611 """
612 if not request:
612 if not request:
613 return {}
613 return {}
614
614
615 qualified_home_url = request.route_url('home')
615 qualified_home_url = request.route_url('home')
616 parsed_url = urlobject.URLObject(qualified_home_url)
616 parsed_url = urlobject.URLObject(qualified_home_url)
617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
617 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
618
618
619 return {
619 return {
620 'scheme': parsed_url.scheme,
620 'scheme': parsed_url.scheme,
621 'netloc': parsed_url.netloc+decoded_path,
621 'netloc': parsed_url.netloc+decoded_path,
622 'hostname': parsed_url.hostname,
622 'hostname': parsed_url.hostname,
623 }
623 }
624
624
625
625
626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
626 def get_clone_url(request, uri_tmpl, repo_name, repo_id, repo_type, **override):
627 qualified_home_url = request.route_url('home')
627 qualified_home_url = request.route_url('home')
628 parsed_url = urlobject.URLObject(qualified_home_url)
628 parsed_url = urlobject.URLObject(qualified_home_url)
629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
629 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
630
630
631 args = {
631 args = {
632 'scheme': parsed_url.scheme,
632 'scheme': parsed_url.scheme,
633 'user': '',
633 'user': '',
634 'sys_user': getpass.getuser(),
634 'sys_user': getpass.getuser(),
635 # path if we use proxy-prefix
635 # path if we use proxy-prefix
636 'netloc': parsed_url.netloc+decoded_path,
636 'netloc': parsed_url.netloc+decoded_path,
637 'hostname': parsed_url.hostname,
637 'hostname': parsed_url.hostname,
638 'prefix': decoded_path,
638 'prefix': decoded_path,
639 'repo': repo_name,
639 'repo': repo_name,
640 'repoid': str(repo_id),
640 'repoid': str(repo_id),
641 'repo_type': repo_type
641 'repo_type': repo_type
642 }
642 }
643 args.update(override)
643 args.update(override)
644 args['user'] = urllib.quote(safe_str(args['user']))
644 args['user'] = urllib.quote(safe_str(args['user']))
645
645
646 for k, v in args.items():
646 for k, v in args.items():
647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
647 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
648
648
649 # special case for SVN clone url
649 # special case for SVN clone url
650 if repo_type == 'svn':
650 if repo_type == 'svn':
651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
651 uri_tmpl = uri_tmpl.replace('ssh://', 'svn+ssh://')
652
652
653 # remove leading @ sign if it's present. Case of empty user
653 # remove leading @ sign if it's present. Case of empty user
654 url_obj = urlobject.URLObject(uri_tmpl)
654 url_obj = urlobject.URLObject(uri_tmpl)
655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
655 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
656
656
657 return safe_unicode(url)
657 return safe_unicode(url)
658
658
659
659
660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
660 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None,
661 maybe_unreachable=False, reference_obj=None):
661 maybe_unreachable=False, reference_obj=None):
662 """
662 """
663 Safe version of get_commit if this commit doesn't exists for a
663 Safe version of get_commit if this commit doesn't exists for a
664 repository it returns a Dummy one instead
664 repository it returns a Dummy one instead
665
665
666 :param repo: repository instance
666 :param repo: repository instance
667 :param commit_id: commit id as str
667 :param commit_id: commit id as str
668 :param commit_idx: numeric commit index
668 :param commit_idx: numeric commit index
669 :param pre_load: optional list of commit attributes to load
669 :param pre_load: optional list of commit attributes to load
670 :param maybe_unreachable: translate unreachable commits on git repos
670 :param maybe_unreachable: translate unreachable commits on git repos
671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
671 :param reference_obj: explicitly search via a reference obj in git. E.g "branch:123" would mean branch "123"
672 """
672 """
673 # TODO(skreft): remove these circular imports
673 # TODO(skreft): remove these circular imports
674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
674 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
675 from rhodecode.lib.vcs.exceptions import RepositoryError
675 from rhodecode.lib.vcs.exceptions import RepositoryError
676 if not isinstance(repo, BaseRepository):
676 if not isinstance(repo, BaseRepository):
677 raise Exception('You must pass an Repository '
677 raise Exception('You must pass an Repository '
678 'object as first argument got %s', type(repo))
678 'object as first argument got %s', type(repo))
679
679
680 try:
680 try:
681 commit = repo.get_commit(
681 commit = repo.get_commit(
682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
682 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load,
683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
683 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
684 except (RepositoryError, LookupError):
684 except (RepositoryError, LookupError):
685 commit = EmptyCommit()
685 commit = EmptyCommit()
686 return commit
686 return commit
687
687
688
688
689 def datetime_to_time(dt):
689 def datetime_to_time(dt):
690 if dt:
690 if dt:
691 return time.mktime(dt.timetuple())
691 return time.mktime(dt.timetuple())
692
692
693
693
694 def time_to_datetime(tm):
694 def time_to_datetime(tm):
695 if tm:
695 if tm:
696 if isinstance(tm, compat.string_types):
696 if isinstance(tm, compat.string_types):
697 try:
697 try:
698 tm = float(tm)
698 tm = float(tm)
699 except ValueError:
699 except ValueError:
700 return
700 return
701 return datetime.datetime.fromtimestamp(tm)
701 return datetime.datetime.fromtimestamp(tm)
702
702
703
703
704 def time_to_utcdatetime(tm):
704 def time_to_utcdatetime(tm):
705 if tm:
705 if tm:
706 if isinstance(tm, compat.string_types):
706 if isinstance(tm, compat.string_types):
707 try:
707 try:
708 tm = float(tm)
708 tm = float(tm)
709 except ValueError:
709 except ValueError:
710 return
710 return
711 return datetime.datetime.utcfromtimestamp(tm)
711 return datetime.datetime.utcfromtimestamp(tm)
712
712
713
713
714 MENTIONS_REGEX = re.compile(
714 MENTIONS_REGEX = re.compile(
715 # ^@ or @ without any special chars in front
715 # ^@ or @ without any special chars in front
716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
716 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
717 # main body starts with letter, then can be . - _
717 # main body starts with letter, then can be . - _
718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
718 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
719 re.VERBOSE | re.MULTILINE)
719 re.VERBOSE | re.MULTILINE)
720
720
721
721
722 def extract_mentioned_users(s):
722 def extract_mentioned_users(s):
723 """
723 """
724 Returns unique usernames from given string s that have @mention
724 Returns unique usernames from given string s that have @mention
725
725
726 :param s: string to get mentions
726 :param s: string to get mentions
727 """
727 """
728 usrs = set()
728 usrs = set()
729 for username in MENTIONS_REGEX.findall(s):
729 for username in MENTIONS_REGEX.findall(s):
730 usrs.add(username)
730 usrs.add(username)
731
731
732 return sorted(list(usrs), key=lambda k: k.lower())
732 return sorted(list(usrs), key=lambda k: k.lower())
733
733
734
734
735 class AttributeDictBase(dict):
735 class AttributeDictBase(dict):
736 def __getstate__(self):
736 def __getstate__(self):
737 odict = self.__dict__ # get attribute dictionary
737 odict = self.__dict__ # get attribute dictionary
738 return odict
738 return odict
739
739
740 def __setstate__(self, dict):
740 def __setstate__(self, dict):
741 self.__dict__ = dict
741 self.__dict__ = dict
742
742
743 __setattr__ = dict.__setitem__
743 __setattr__ = dict.__setitem__
744 __delattr__ = dict.__delitem__
744 __delattr__ = dict.__delitem__
745
745
746
746
747 class StrictAttributeDict(AttributeDictBase):
747 class StrictAttributeDict(AttributeDictBase):
748 """
748 """
749 Strict Version of Attribute dict which raises an Attribute error when
749 Strict Version of Attribute dict which raises an Attribute error when
750 requested attribute is not set
750 requested attribute is not set
751 """
751 """
752 def __getattr__(self, attr):
752 def __getattr__(self, attr):
753 try:
753 try:
754 return self[attr]
754 return self[attr]
755 except KeyError:
755 except KeyError:
756 raise AttributeError('%s object has no attribute %s' % (
756 raise AttributeError('%s object has no attribute %s' % (
757 self.__class__, attr))
757 self.__class__, attr))
758
758
759
759
760 class AttributeDict(AttributeDictBase):
760 class AttributeDict(AttributeDictBase):
761 def __getattr__(self, attr):
761 def __getattr__(self, attr):
762 return self.get(attr, None)
762 return self.get(attr, None)
763
763
764
764
765
765
766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
766 class OrderedDefaultDict(collections.OrderedDict, collections.defaultdict):
767 def __init__(self, default_factory=None, *args, **kwargs):
767 def __init__(self, default_factory=None, *args, **kwargs):
768 # in python3 you can omit the args to super
768 # in python3 you can omit the args to super
769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
769 super(OrderedDefaultDict, self).__init__(*args, **kwargs)
770 self.default_factory = default_factory
770 self.default_factory = default_factory
771
771
772
772
773 def fix_PATH(os_=None):
773 def fix_PATH(os_=None):
774 """
774 """
775 Get current active python path, and append it to PATH variable to fix
775 Get current active python path, and append it to PATH variable to fix
776 issues of subprocess calls and different python versions
776 issues of subprocess calls and different python versions
777 """
777 """
778 if os_ is None:
778 if os_ is None:
779 import os
779 import os
780 else:
780 else:
781 os = os_
781 os = os_
782
782
783 cur_path = os.path.split(sys.executable)[0]
783 cur_path = os.path.split(sys.executable)[0]
784 if not os.environ['PATH'].startswith(cur_path):
784 if not os.environ['PATH'].startswith(cur_path):
785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
785 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
786
786
787
787
788 def obfuscate_url_pw(engine):
788 def obfuscate_url_pw(engine):
789 _url = engine or ''
789 _url = engine or ''
790 try:
790 try:
791 _url = sqlalchemy.engine.url.make_url(engine)
791 _url = sqlalchemy.engine.url.make_url(engine)
792 if _url.password:
792 if _url.password:
793 _url.password = 'XXXXX'
793 _url.password = 'XXXXX'
794 except Exception:
794 except Exception:
795 pass
795 pass
796 return unicode(_url)
796 return unicode(_url)
797
797
798
798
799 def get_server_url(environ):
799 def get_server_url(environ):
800 req = webob.Request(environ)
800 req = webob.Request(environ)
801 return req.host_url + req.script_name
801 return req.host_url + req.script_name
802
802
803
803
804 def unique_id(hexlen=32):
804 def unique_id(hexlen=32):
805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
805 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
806 return suuid(truncate_to=hexlen, alphabet=alphabet)
806 return suuid(truncate_to=hexlen, alphabet=alphabet)
807
807
808
808
809 def suuid(url=None, truncate_to=22, alphabet=None):
809 def suuid(url=None, truncate_to=22, alphabet=None):
810 """
810 """
811 Generate and return a short URL safe UUID.
811 Generate and return a short URL safe UUID.
812
812
813 If the url parameter is provided, set the namespace to the provided
813 If the url parameter is provided, set the namespace to the provided
814 URL and generate a UUID.
814 URL and generate a UUID.
815
815
816 :param url to get the uuid for
816 :param url to get the uuid for
817 :truncate_to: truncate the basic 22 UUID to shorter version
817 :truncate_to: truncate the basic 22 UUID to shorter version
818
818
819 The IDs won't be universally unique any longer, but the probability of
819 The IDs won't be universally unique any longer, but the probability of
820 a collision will still be very low.
820 a collision will still be very low.
821 """
821 """
822 # Define our alphabet.
822 # Define our alphabet.
823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
823 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
824
824
825 # If no URL is given, generate a random UUID.
825 # If no URL is given, generate a random UUID.
826 if url is None:
826 if url is None:
827 unique_id = uuid.uuid4().int
827 unique_id = uuid.uuid4().int
828 else:
828 else:
829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
829 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
830
830
831 alphabet_length = len(_ALPHABET)
831 alphabet_length = len(_ALPHABET)
832 output = []
832 output = []
833 while unique_id > 0:
833 while unique_id > 0:
834 digit = unique_id % alphabet_length
834 digit = unique_id % alphabet_length
835 output.append(_ALPHABET[digit])
835 output.append(_ALPHABET[digit])
836 unique_id = int(unique_id / alphabet_length)
836 unique_id = int(unique_id / alphabet_length)
837 return "".join(output)[:truncate_to]
837 return "".join(output)[:truncate_to]
838
838
839
839
840 def get_current_rhodecode_user(request=None):
840 def get_current_rhodecode_user(request=None):
841 """
841 """
842 Gets rhodecode user from request
842 Gets rhodecode user from request
843 """
843 """
844 pyramid_request = request or pyramid.threadlocal.get_current_request()
844 pyramid_request = request or pyramid.threadlocal.get_current_request()
845
845
846 # web case
846 # web case
847 if pyramid_request and hasattr(pyramid_request, 'user'):
847 if pyramid_request and hasattr(pyramid_request, 'user'):
848 return pyramid_request.user
848 return pyramid_request.user
849
849
850 # api case
850 # api case
851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
851 if pyramid_request and hasattr(pyramid_request, 'rpc_user'):
852 return pyramid_request.rpc_user
852 return pyramid_request.rpc_user
853
853
854 return None
854 return None
855
855
856
856
857 def action_logger_generic(action, namespace=''):
857 def action_logger_generic(action, namespace=''):
858 """
858 """
859 A generic logger for actions useful to the system overview, tries to find
859 A generic logger for actions useful to the system overview, tries to find
860 an acting user for the context of the call otherwise reports unknown user
860 an acting user for the context of the call otherwise reports unknown user
861
861
862 :param action: logging message eg 'comment 5 deleted'
862 :param action: logging message eg 'comment 5 deleted'
863 :param type: string
863 :param type: string
864
864
865 :param namespace: namespace of the logging message eg. 'repo.comments'
865 :param namespace: namespace of the logging message eg. 'repo.comments'
866 :param type: string
866 :param type: string
867
867
868 """
868 """
869
869
870 logger_name = 'rhodecode.actions'
870 logger_name = 'rhodecode.actions'
871
871
872 if namespace:
872 if namespace:
873 logger_name += '.' + namespace
873 logger_name += '.' + namespace
874
874
875 log = logging.getLogger(logger_name)
875 log = logging.getLogger(logger_name)
876
876
877 # get a user if we can
877 # get a user if we can
878 user = get_current_rhodecode_user()
878 user = get_current_rhodecode_user()
879
879
880 logfunc = log.info
880 logfunc = log.info
881
881
882 if not user:
882 if not user:
883 user = '<unknown user>'
883 user = '<unknown user>'
884 logfunc = log.warning
884 logfunc = log.warning
885
885
886 logfunc('Logging action by {}: {}'.format(user, action))
886 logfunc('Logging action by {}: {}'.format(user, action))
887
887
888
888
889 def escape_split(text, sep=',', maxsplit=-1):
889 def escape_split(text, sep=',', maxsplit=-1):
890 r"""
890 r"""
891 Allows for escaping of the separator: e.g. arg='foo\, bar'
891 Allows for escaping of the separator: e.g. arg='foo\, bar'
892
892
893 It should be noted that the way bash et. al. do command line parsing, those
893 It should be noted that the way bash et. al. do command line parsing, those
894 single quotes are required.
894 single quotes are required.
895 """
895 """
896 escaped_sep = r'\%s' % sep
896 escaped_sep = r'\%s' % sep
897
897
898 if escaped_sep not in text:
898 if escaped_sep not in text:
899 return text.split(sep, maxsplit)
899 return text.split(sep, maxsplit)
900
900
901 before, _mid, after = text.partition(escaped_sep)
901 before, _mid, after = text.partition(escaped_sep)
902 startlist = before.split(sep, maxsplit) # a regular split is fine here
902 startlist = before.split(sep, maxsplit) # a regular split is fine here
903 unfinished = startlist[-1]
903 unfinished = startlist[-1]
904 startlist = startlist[:-1]
904 startlist = startlist[:-1]
905
905
906 # recurse because there may be more escaped separators
906 # recurse because there may be more escaped separators
907 endlist = escape_split(after, sep, maxsplit)
907 endlist = escape_split(after, sep, maxsplit)
908
908
909 # finish building the escaped value. we use endlist[0] becaue the first
909 # finish building the escaped value. we use endlist[0] becaue the first
910 # part of the string sent in recursion is the rest of the escaped value.
910 # part of the string sent in recursion is the rest of the escaped value.
911 unfinished += sep + endlist[0]
911 unfinished += sep + endlist[0]
912
912
913 return startlist + [unfinished] + endlist[1:] # put together all the parts
913 return startlist + [unfinished] + endlist[1:] # put together all the parts
914
914
915
915
916 class OptionalAttr(object):
916 class OptionalAttr(object):
917 """
917 """
918 Special Optional Option that defines other attribute. Example::
918 Special Optional Option that defines other attribute. Example::
919
919
920 def test(apiuser, userid=Optional(OAttr('apiuser')):
920 def test(apiuser, userid=Optional(OAttr('apiuser')):
921 user = Optional.extract(userid)
921 user = Optional.extract(userid)
922 # calls
922 # calls
923
923
924 """
924 """
925
925
926 def __init__(self, attr_name):
926 def __init__(self, attr_name):
927 self.attr_name = attr_name
927 self.attr_name = attr_name
928
928
929 def __repr__(self):
929 def __repr__(self):
930 return '<OptionalAttr:%s>' % self.attr_name
930 return '<OptionalAttr:%s>' % self.attr_name
931
931
932 def __call__(self):
932 def __call__(self):
933 return self
933 return self
934
934
935
935
936 # alias
936 # alias
937 OAttr = OptionalAttr
937 OAttr = OptionalAttr
938
938
939
939
940 class Optional(object):
940 class Optional(object):
941 """
941 """
942 Defines an optional parameter::
942 Defines an optional parameter::
943
943
944 param = param.getval() if isinstance(param, Optional) else param
944 param = param.getval() if isinstance(param, Optional) else param
945 param = param() if isinstance(param, Optional) else param
945 param = param() if isinstance(param, Optional) else param
946
946
947 is equivalent of::
947 is equivalent of::
948
948
949 param = Optional.extract(param)
949 param = Optional.extract(param)
950
950
951 """
951 """
952
952
953 def __init__(self, type_):
953 def __init__(self, type_):
954 self.type_ = type_
954 self.type_ = type_
955
955
956 def __repr__(self):
956 def __repr__(self):
957 return '<Optional:%s>' % self.type_.__repr__()
957 return '<Optional:%s>' % self.type_.__repr__()
958
958
959 def __call__(self):
959 def __call__(self):
960 return self.getval()
960 return self.getval()
961
961
962 def getval(self):
962 def getval(self):
963 """
963 """
964 returns value from this Optional instance
964 returns value from this Optional instance
965 """
965 """
966 if isinstance(self.type_, OAttr):
966 if isinstance(self.type_, OAttr):
967 # use params name
967 # use params name
968 return self.type_.attr_name
968 return self.type_.attr_name
969 return self.type_
969 return self.type_
970
970
971 @classmethod
971 @classmethod
972 def extract(cls, val):
972 def extract(cls, val):
973 """
973 """
974 Extracts value from Optional() instance
974 Extracts value from Optional() instance
975
975
976 :param val:
976 :param val:
977 :return: original value if it's not Optional instance else
977 :return: original value if it's not Optional instance else
978 value of instance
978 value of instance
979 """
979 """
980 if isinstance(val, cls):
980 if isinstance(val, cls):
981 return val.getval()
981 return val.getval()
982 return val
982 return val
983
983
984
984
985 def glob2re(pat):
985 def glob2re(pat):
986 """
986 """
987 Translate a shell PATTERN to a regular expression.
987 Translate a shell PATTERN to a regular expression.
988
988
989 There is no way to quote meta-characters.
989 There is no way to quote meta-characters.
990 """
990 """
991
991
992 i, n = 0, len(pat)
992 i, n = 0, len(pat)
993 res = ''
993 res = ''
994 while i < n:
994 while i < n:
995 c = pat[i]
995 c = pat[i]
996 i = i+1
996 i = i+1
997 if c == '*':
997 if c == '*':
998 #res = res + '.*'
998 #res = res + '.*'
999 res = res + '[^/]*'
999 res = res + '[^/]*'
1000 elif c == '?':
1000 elif c == '?':
1001 #res = res + '.'
1001 #res = res + '.'
1002 res = res + '[^/]'
1002 res = res + '[^/]'
1003 elif c == '[':
1003 elif c == '[':
1004 j = i
1004 j = i
1005 if j < n and pat[j] == '!':
1005 if j < n and pat[j] == '!':
1006 j = j+1
1006 j = j+1
1007 if j < n and pat[j] == ']':
1007 if j < n and pat[j] == ']':
1008 j = j+1
1008 j = j+1
1009 while j < n and pat[j] != ']':
1009 while j < n and pat[j] != ']':
1010 j = j+1
1010 j = j+1
1011 if j >= n:
1011 if j >= n:
1012 res = res + '\\['
1012 res = res + '\\['
1013 else:
1013 else:
1014 stuff = pat[i:j].replace('\\','\\\\')
1014 stuff = pat[i:j].replace('\\','\\\\')
1015 i = j+1
1015 i = j+1
1016 if stuff[0] == '!':
1016 if stuff[0] == '!':
1017 stuff = '^' + stuff[1:]
1017 stuff = '^' + stuff[1:]
1018 elif stuff[0] == '^':
1018 elif stuff[0] == '^':
1019 stuff = '\\' + stuff
1019 stuff = '\\' + stuff
1020 res = '%s[%s]' % (res, stuff)
1020 res = '%s[%s]' % (res, stuff)
1021 else:
1021 else:
1022 res = res + re.escape(c)
1022 res = res + re.escape(c)
1023 return res + '\Z(?ms)'
1023 return res + '\Z(?ms)'
1024
1024
1025
1025
1026 def parse_byte_string(size_str):
1026 def parse_byte_string(size_str):
1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1027 match = re.match(r'(\d+)(MB|KB)', size_str, re.IGNORECASE)
1028 if not match:
1028 if not match:
1029 raise ValueError('Given size:%s is invalid, please make sure '
1029 raise ValueError('Given size:%s is invalid, please make sure '
1030 'to use format of <num>(MB|KB)' % size_str)
1030 'to use format of <num>(MB|KB)' % size_str)
1031
1031
1032 _parts = match.groups()
1032 _parts = match.groups()
1033 num, type_ = _parts
1033 num, type_ = _parts
1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1034 return long(num) * {'mb': 1024*1024, 'kb': 1024}[type_.lower()]
1035
1035
1036
1036
1037 class CachedProperty(object):
1037 class CachedProperty(object):
1038 """
1038 """
1039 Lazy Attributes. With option to invalidate the cache by running a method
1039 Lazy Attributes. With option to invalidate the cache by running a method
1040
1040
1041 class Foo():
1041 >>> class Foo(object):
1042 ...
1043 ... @CachedProperty
1044 ... def heavy_func(self):
1045 ... return 'super-calculation'
1046 ...
1047 ... foo = Foo()
1048 ... foo.heavy_func() # first computation
1049 ... foo.heavy_func() # fetch from cache
1050 ... foo._invalidate_prop_cache('heavy_func')
1042
1051
1043 @CachedProperty
1044 def heavy_func():
1045 return 'super-calculation'
1046
1047 foo = Foo()
1048 foo.heavy_func() # first computions
1049 foo.heavy_func() # fetch from cache
1050 foo._invalidate_prop_cache('heavy_func')
1051 # at this point calling foo.heavy_func() will be re-computed
1052 # at this point calling foo.heavy_func() will be re-computed
1052 """
1053 """
1053
1054
1054 def __init__(self, func, func_name=None):
1055 def __init__(self, func, func_name=None):
1055
1056
1056 if func_name is None:
1057 if func_name is None:
1057 func_name = func.__name__
1058 func_name = func.__name__
1058 self.data = (func, func_name)
1059 self.data = (func, func_name)
1059 update_wrapper(self, func)
1060 update_wrapper(self, func)
1060
1061
1061 def __get__(self, inst, class_):
1062 def __get__(self, inst, class_):
1062 if inst is None:
1063 if inst is None:
1063 return self
1064 return self
1064
1065
1065 func, func_name = self.data
1066 func, func_name = self.data
1066 value = func(inst)
1067 value = func(inst)
1067 inst.__dict__[func_name] = value
1068 inst.__dict__[func_name] = value
1068 if '_invalidate_prop_cache' not in inst.__dict__:
1069 if '_invalidate_prop_cache' not in inst.__dict__:
1069 inst.__dict__['_invalidate_prop_cache'] = partial(
1070 inst.__dict__['_invalidate_prop_cache'] = partial(
1070 self._invalidate_prop_cache, inst)
1071 self._invalidate_prop_cache, inst)
1071 return value
1072 return value
1072
1073
1073 def _invalidate_prop_cache(self, inst, name):
1074 def _invalidate_prop_cache(self, inst, name):
1074 inst.__dict__.pop(name, None)
1075 inst.__dict__.pop(name, None)
1076
1077
1078 def retry(func=None, exception=Exception, n_tries=5, delay=5, backoff=1, logger=True):
1079 """
1080 Retry decorator with exponential backoff.
1081
1082 Parameters
1083 ----------
1084 func : typing.Callable, optional
1085 Callable on which the decorator is applied, by default None
1086 exception : Exception or tuple of Exceptions, optional
1087 Exception(s) that invoke retry, by default Exception
1088 n_tries : int, optional
1089 Number of tries before giving up, by default 5
1090 delay : int, optional
1091 Initial delay between retries in seconds, by default 5
1092 backoff : int, optional
1093 Backoff multiplier e.g. value of 2 will double the delay, by default 1
1094 logger : bool, optional
1095 Option to log or print, by default False
1096
1097 Returns
1098 -------
1099 typing.Callable
1100 Decorated callable that calls itself when exception(s) occur.
1101
1102 Examples
1103 --------
1104 >>> import random
1105 >>> @retry(exception=Exception, n_tries=3)
1106 ... def test_random(text):
1107 ... x = random.random()
1108 ... if x < 0.5:
1109 ... raise Exception("Fail")
1110 ... else:
1111 ... print("Success: ", text)
1112 >>> test_random("It works!")
1113 """
1114
1115 if func is None:
1116 return partial(
1117 retry,
1118 exception=exception,
1119 n_tries=n_tries,
1120 delay=delay,
1121 backoff=backoff,
1122 logger=logger,
1123 )
1124
1125 @wraps(func)
1126 def wrapper(*args, **kwargs):
1127 _n_tries, n_delay = n_tries, delay
1128 log = logging.getLogger('rhodecode.retry')
1129
1130 while _n_tries > 1:
1131 try:
1132 return func(*args, **kwargs)
1133 except exception as e:
1134 e_details = repr(e)
1135 msg = "Exception on calling func {func}: {e}, " \
1136 "Retrying in {n_delay} seconds..."\
1137 .format(func=func, e=e_details, n_delay=n_delay)
1138 if logger:
1139 log.warning(msg)
1140 else:
1141 print(msg)
1142 time.sleep(n_delay)
1143 _n_tries -= 1
1144 n_delay *= backoff
1145
1146 return func(*args, **kwargs)
1147
1148 return wrapper
@@ -1,5836 +1,5836 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case, null
44 from sqlalchemy.sql.expression import true, false, case, null
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, lazyload, joinedload, class_mapper, validates, aliased)
47 relationship, lazyload, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import (
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
61 from rhodecode.lib.utils2 import (
61 from rhodecode.lib.utils2 import (
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
66 JsonRaw
66 JsonRaw
67 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.ext_json import json
68 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.caching_query import FromCache
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
70 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.encrypt2 import Encryptor
71 from rhodecode.lib.exceptions import (
71 from rhodecode.lib.exceptions import (
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
73 from rhodecode.model.meta import Base, Session
73 from rhodecode.model.meta import Base, Session
74
74
75 URL_SEP = '/'
75 URL_SEP = '/'
76 log = logging.getLogger(__name__)
76 log = logging.getLogger(__name__)
77
77
78 # =============================================================================
78 # =============================================================================
79 # BASE CLASSES
79 # BASE CLASSES
80 # =============================================================================
80 # =============================================================================
81
81
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # this is propagated from .ini file rhodecode.encrypted_values.secret or
83 # beaker.session.secret if first is not set.
83 # beaker.session.secret if first is not set.
84 # and initialized at environment.py
84 # and initialized at environment.py
85 ENCRYPTION_KEY = None
85 ENCRYPTION_KEY = None
86
86
87 # used to sort permissions by types, '#' used here is not allowed to be in
87 # used to sort permissions by types, '#' used here is not allowed to be in
88 # usernames, and it's very early in sorted string.printable table.
88 # usernames, and it's very early in sorted string.printable table.
89 PERMISSION_TYPE_SORT = {
89 PERMISSION_TYPE_SORT = {
90 'admin': '####',
90 'admin': '####',
91 'write': '###',
91 'write': '###',
92 'read': '##',
92 'read': '##',
93 'none': '#',
93 'none': '#',
94 }
94 }
95
95
96
96
97 def display_user_sort(obj):
97 def display_user_sort(obj):
98 """
98 """
99 Sort function used to sort permissions in .permissions() function of
99 Sort function used to sort permissions in .permissions() function of
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 Repository, RepoGroup, UserGroup. Also it put the default user in front
101 of all other resources
101 of all other resources
102 """
102 """
103
103
104 if obj.username == User.DEFAULT_USER:
104 if obj.username == User.DEFAULT_USER:
105 return '#####'
105 return '#####'
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
107 extra_sort_num = '1' # default
107 extra_sort_num = '1' # default
108
108
109 # NOTE(dan): inactive duplicates goes last
109 # NOTE(dan): inactive duplicates goes last
110 if getattr(obj, 'duplicate_perm', None):
110 if getattr(obj, 'duplicate_perm', None):
111 extra_sort_num = '9'
111 extra_sort_num = '9'
112 return prefix + extra_sort_num + obj.username
112 return prefix + extra_sort_num + obj.username
113
113
114
114
115 def display_user_group_sort(obj):
115 def display_user_group_sort(obj):
116 """
116 """
117 Sort function used to sort permissions in .permissions() function of
117 Sort function used to sort permissions in .permissions() function of
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 Repository, RepoGroup, UserGroup. Also it put the default user in front
119 of all other resources
119 of all other resources
120 """
120 """
121
121
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
123 return prefix + obj.users_group_name
123 return prefix + obj.users_group_name
124
124
125
125
126 def _hash_key(k):
126 def _hash_key(k):
127 return sha1_safe(k)
127 return sha1_safe(k)
128
128
129
129
130 def in_filter_generator(qry, items, limit=500):
130 def in_filter_generator(qry, items, limit=500):
131 """
131 """
132 Splits IN() into multiple with OR
132 Splits IN() into multiple with OR
133 e.g.::
133 e.g.::
134 cnt = Repository.query().filter(
134 cnt = Repository.query().filter(
135 or_(
135 or_(
136 *in_filter_generator(Repository.repo_id, range(100000))
136 *in_filter_generator(Repository.repo_id, range(100000))
137 )).count()
137 )).count()
138 """
138 """
139 if not items:
139 if not items:
140 # empty list will cause empty query which might cause security issues
140 # empty list will cause empty query which might cause security issues
141 # this can lead to hidden unpleasant results
141 # this can lead to hidden unpleasant results
142 items = [-1]
142 items = [-1]
143
143
144 parts = []
144 parts = []
145 for chunk in xrange(0, len(items), limit):
145 for chunk in xrange(0, len(items), limit):
146 parts.append(
146 parts.append(
147 qry.in_(items[chunk: chunk + limit])
147 qry.in_(items[chunk: chunk + limit])
148 )
148 )
149
149
150 return parts
150 return parts
151
151
152
152
153 base_table_args = {
153 base_table_args = {
154 'extend_existing': True,
154 'extend_existing': True,
155 'mysql_engine': 'InnoDB',
155 'mysql_engine': 'InnoDB',
156 'mysql_charset': 'utf8',
156 'mysql_charset': 'utf8',
157 'sqlite_autoincrement': True
157 'sqlite_autoincrement': True
158 }
158 }
159
159
160
160
161 class EncryptedTextValue(TypeDecorator):
161 class EncryptedTextValue(TypeDecorator):
162 """
162 """
163 Special column for encrypted long text data, use like::
163 Special column for encrypted long text data, use like::
164
164
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165 value = Column("encrypted_value", EncryptedValue(), nullable=False)
166
166
167 This column is intelligent so if value is in unencrypted form it return
167 This column is intelligent so if value is in unencrypted form it return
168 unencrypted form, but on save it always encrypts
168 unencrypted form, but on save it always encrypts
169 """
169 """
170 impl = Text
170 impl = Text
171
171
172 def process_bind_param(self, value, dialect):
172 def process_bind_param(self, value, dialect):
173 """
173 """
174 Setter for storing value
174 Setter for storing value
175 """
175 """
176 import rhodecode
176 import rhodecode
177 if not value:
177 if not value:
178 return value
178 return value
179
179
180 # protect against double encrypting if values is already encrypted
180 # protect against double encrypting if values is already encrypted
181 if value.startswith('enc$aes$') \
181 if value.startswith('enc$aes$') \
182 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc$aes_hmac$') \
183 or value.startswith('enc2$'):
183 or value.startswith('enc2$'):
184 raise ValueError('value needs to be in unencrypted format, '
184 raise ValueError('value needs to be in unencrypted format, '
185 'ie. not starting with enc$ or enc2$')
185 'ie. not starting with enc$ or enc2$')
186
186
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
188 if algo == 'aes':
188 if algo == 'aes':
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
190 elif algo == 'fernet':
190 elif algo == 'fernet':
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 return Encryptor(ENCRYPTION_KEY).encrypt(value)
192 else:
192 else:
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
194
194
195 def process_result_value(self, value, dialect):
195 def process_result_value(self, value, dialect):
196 """
196 """
197 Getter for retrieving value
197 Getter for retrieving value
198 """
198 """
199
199
200 import rhodecode
200 import rhodecode
201 if not value:
201 if not value:
202 return value
202 return value
203
203
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
206 if algo == 'aes':
206 if algo == 'aes':
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
208 elif algo == 'fernet':
208 elif algo == 'fernet':
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 return Encryptor(ENCRYPTION_KEY).decrypt(value)
210 else:
210 else:
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
212 return decrypted_data
212 return decrypted_data
213
213
214
214
215 class BaseModel(object):
215 class BaseModel(object):
216 """
216 """
217 Base Model for all classes
217 Base Model for all classes
218 """
218 """
219
219
220 @classmethod
220 @classmethod
221 def _get_keys(cls):
221 def _get_keys(cls):
222 """return column names for this model """
222 """return column names for this model """
223 return class_mapper(cls).c.keys()
223 return class_mapper(cls).c.keys()
224
224
225 def get_dict(self):
225 def get_dict(self):
226 """
226 """
227 return dict with keys and values corresponding
227 return dict with keys and values corresponding
228 to this model data """
228 to this model data """
229
229
230 d = {}
230 d = {}
231 for k in self._get_keys():
231 for k in self._get_keys():
232 d[k] = getattr(self, k)
232 d[k] = getattr(self, k)
233
233
234 # also use __json__() if present to get additional fields
234 # also use __json__() if present to get additional fields
235 _json_attr = getattr(self, '__json__', None)
235 _json_attr = getattr(self, '__json__', None)
236 if _json_attr:
236 if _json_attr:
237 # update with attributes from __json__
237 # update with attributes from __json__
238 if callable(_json_attr):
238 if callable(_json_attr):
239 _json_attr = _json_attr()
239 _json_attr = _json_attr()
240 for k, val in _json_attr.iteritems():
240 for k, val in _json_attr.iteritems():
241 d[k] = val
241 d[k] = val
242 return d
242 return d
243
243
244 def get_appstruct(self):
244 def get_appstruct(self):
245 """return list with keys and values tuples corresponding
245 """return list with keys and values tuples corresponding
246 to this model data """
246 to this model data """
247
247
248 lst = []
248 lst = []
249 for k in self._get_keys():
249 for k in self._get_keys():
250 lst.append((k, getattr(self, k),))
250 lst.append((k, getattr(self, k),))
251 return lst
251 return lst
252
252
253 def populate_obj(self, populate_dict):
253 def populate_obj(self, populate_dict):
254 """populate model with data from given populate_dict"""
254 """populate model with data from given populate_dict"""
255
255
256 for k in self._get_keys():
256 for k in self._get_keys():
257 if k in populate_dict:
257 if k in populate_dict:
258 setattr(self, k, populate_dict[k])
258 setattr(self, k, populate_dict[k])
259
259
260 @classmethod
260 @classmethod
261 def query(cls):
261 def query(cls):
262 return Session().query(cls)
262 return Session().query(cls)
263
263
264 @classmethod
264 @classmethod
265 def get(cls, id_):
265 def get(cls, id_):
266 if id_:
266 if id_:
267 return cls.query().get(id_)
267 return cls.query().get(id_)
268
268
269 @classmethod
269 @classmethod
270 def get_or_404(cls, id_):
270 def get_or_404(cls, id_):
271 from pyramid.httpexceptions import HTTPNotFound
271 from pyramid.httpexceptions import HTTPNotFound
272
272
273 try:
273 try:
274 id_ = int(id_)
274 id_ = int(id_)
275 except (TypeError, ValueError):
275 except (TypeError, ValueError):
276 raise HTTPNotFound()
276 raise HTTPNotFound()
277
277
278 res = cls.query().get(id_)
278 res = cls.query().get(id_)
279 if not res:
279 if not res:
280 raise HTTPNotFound()
280 raise HTTPNotFound()
281 return res
281 return res
282
282
283 @classmethod
283 @classmethod
284 def getAll(cls):
284 def getAll(cls):
285 # deprecated and left for backward compatibility
285 # deprecated and left for backward compatibility
286 return cls.get_all()
286 return cls.get_all()
287
287
288 @classmethod
288 @classmethod
289 def get_all(cls):
289 def get_all(cls):
290 return cls.query().all()
290 return cls.query().all()
291
291
292 @classmethod
292 @classmethod
293 def delete(cls, id_):
293 def delete(cls, id_):
294 obj = cls.query().get(id_)
294 obj = cls.query().get(id_)
295 Session().delete(obj)
295 Session().delete(obj)
296
296
297 @classmethod
297 @classmethod
298 def identity_cache(cls, session, attr_name, value):
298 def identity_cache(cls, session, attr_name, value):
299 exist_in_session = []
299 exist_in_session = []
300 for (item_cls, pkey), instance in session.identity_map.items():
300 for (item_cls, pkey), instance in session.identity_map.items():
301 if cls == item_cls and getattr(instance, attr_name) == value:
301 if cls == item_cls and getattr(instance, attr_name) == value:
302 exist_in_session.append(instance)
302 exist_in_session.append(instance)
303 if exist_in_session:
303 if exist_in_session:
304 if len(exist_in_session) == 1:
304 if len(exist_in_session) == 1:
305 return exist_in_session[0]
305 return exist_in_session[0]
306 log.exception(
306 log.exception(
307 'multiple objects with attr %s and '
307 'multiple objects with attr %s and '
308 'value %s found with same name: %r',
308 'value %s found with same name: %r',
309 attr_name, value, exist_in_session)
309 attr_name, value, exist_in_session)
310
310
311 def __repr__(self):
311 def __repr__(self):
312 if hasattr(self, '__unicode__'):
312 if hasattr(self, '__unicode__'):
313 # python repr needs to return str
313 # python repr needs to return str
314 try:
314 try:
315 return safe_str(self.__unicode__())
315 return safe_str(self.__unicode__())
316 except UnicodeDecodeError:
316 except UnicodeDecodeError:
317 pass
317 pass
318 return '<DB:%s>' % (self.__class__.__name__)
318 return '<DB:%s>' % (self.__class__.__name__)
319
319
320
320
321 class RhodeCodeSetting(Base, BaseModel):
321 class RhodeCodeSetting(Base, BaseModel):
322 __tablename__ = 'rhodecode_settings'
322 __tablename__ = 'rhodecode_settings'
323 __table_args__ = (
323 __table_args__ = (
324 UniqueConstraint('app_settings_name'),
324 UniqueConstraint('app_settings_name'),
325 base_table_args
325 base_table_args
326 )
326 )
327
327
328 SETTINGS_TYPES = {
328 SETTINGS_TYPES = {
329 'str': safe_str,
329 'str': safe_str,
330 'int': safe_int,
330 'int': safe_int,
331 'unicode': safe_unicode,
331 'unicode': safe_unicode,
332 'bool': str2bool,
332 'bool': str2bool,
333 'list': functools.partial(aslist, sep=',')
333 'list': functools.partial(aslist, sep=',')
334 }
334 }
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
336 GLOBAL_CONF_KEY = 'app_settings'
336 GLOBAL_CONF_KEY = 'app_settings'
337
337
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
342
342
343 def __init__(self, key='', val='', type='unicode'):
343 def __init__(self, key='', val='', type='unicode'):
344 self.app_settings_name = key
344 self.app_settings_name = key
345 self.app_settings_type = type
345 self.app_settings_type = type
346 self.app_settings_value = val
346 self.app_settings_value = val
347
347
348 @validates('_app_settings_value')
348 @validates('_app_settings_value')
349 def validate_settings_value(self, key, val):
349 def validate_settings_value(self, key, val):
350 assert type(val) == unicode
350 assert type(val) == unicode
351 return val
351 return val
352
352
353 @hybrid_property
353 @hybrid_property
354 def app_settings_value(self):
354 def app_settings_value(self):
355 v = self._app_settings_value
355 v = self._app_settings_value
356 _type = self.app_settings_type
356 _type = self.app_settings_type
357 if _type:
357 if _type:
358 _type = self.app_settings_type.split('.')[0]
358 _type = self.app_settings_type.split('.')[0]
359 # decode the encrypted value
359 # decode the encrypted value
360 if 'encrypted' in self.app_settings_type:
360 if 'encrypted' in self.app_settings_type:
361 cipher = EncryptedTextValue()
361 cipher = EncryptedTextValue()
362 v = safe_unicode(cipher.process_result_value(v, None))
362 v = safe_unicode(cipher.process_result_value(v, None))
363
363
364 converter = self.SETTINGS_TYPES.get(_type) or \
364 converter = self.SETTINGS_TYPES.get(_type) or \
365 self.SETTINGS_TYPES['unicode']
365 self.SETTINGS_TYPES['unicode']
366 return converter(v)
366 return converter(v)
367
367
368 @app_settings_value.setter
368 @app_settings_value.setter
369 def app_settings_value(self, val):
369 def app_settings_value(self, val):
370 """
370 """
371 Setter that will always make sure we use unicode in app_settings_value
371 Setter that will always make sure we use unicode in app_settings_value
372
372
373 :param val:
373 :param val:
374 """
374 """
375 val = safe_unicode(val)
375 val = safe_unicode(val)
376 # encode the encrypted value
376 # encode the encrypted value
377 if 'encrypted' in self.app_settings_type:
377 if 'encrypted' in self.app_settings_type:
378 cipher = EncryptedTextValue()
378 cipher = EncryptedTextValue()
379 val = safe_unicode(cipher.process_bind_param(val, None))
379 val = safe_unicode(cipher.process_bind_param(val, None))
380 self._app_settings_value = val
380 self._app_settings_value = val
381
381
382 @hybrid_property
382 @hybrid_property
383 def app_settings_type(self):
383 def app_settings_type(self):
384 return self._app_settings_type
384 return self._app_settings_type
385
385
386 @app_settings_type.setter
386 @app_settings_type.setter
387 def app_settings_type(self, val):
387 def app_settings_type(self, val):
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 if val.split('.')[0] not in self.SETTINGS_TYPES:
389 raise Exception('type must be one of %s got %s'
389 raise Exception('type must be one of %s got %s'
390 % (self.SETTINGS_TYPES.keys(), val))
390 % (self.SETTINGS_TYPES.keys(), val))
391 self._app_settings_type = val
391 self._app_settings_type = val
392
392
393 @classmethod
393 @classmethod
394 def get_by_prefix(cls, prefix):
394 def get_by_prefix(cls, prefix):
395 return RhodeCodeSetting.query()\
395 return RhodeCodeSetting.query()\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
397 .all()
397 .all()
398
398
399 def __unicode__(self):
399 def __unicode__(self):
400 return u"<%s('%s:%s[%s]')>" % (
400 return u"<%s('%s:%s[%s]')>" % (
401 self.__class__.__name__,
401 self.__class__.__name__,
402 self.app_settings_name, self.app_settings_value,
402 self.app_settings_name, self.app_settings_value,
403 self.app_settings_type
403 self.app_settings_type
404 )
404 )
405
405
406
406
407 class RhodeCodeUi(Base, BaseModel):
407 class RhodeCodeUi(Base, BaseModel):
408 __tablename__ = 'rhodecode_ui'
408 __tablename__ = 'rhodecode_ui'
409 __table_args__ = (
409 __table_args__ = (
410 UniqueConstraint('ui_key'),
410 UniqueConstraint('ui_key'),
411 base_table_args
411 base_table_args
412 )
412 )
413
413
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 HOOK_REPO_SIZE = 'changegroup.repo_size'
415 # HG
415 # HG
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
417 HOOK_PULL = 'outgoing.pull_logger'
417 HOOK_PULL = 'outgoing.pull_logger'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
420 HOOK_PUSH = 'changegroup.push_logger'
420 HOOK_PUSH = 'changegroup.push_logger'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
421 HOOK_PUSH_KEY = 'pushkey.key_push'
422
422
423 HOOKS_BUILTIN = [
423 HOOKS_BUILTIN = [
424 HOOK_PRE_PULL,
424 HOOK_PRE_PULL,
425 HOOK_PULL,
425 HOOK_PULL,
426 HOOK_PRE_PUSH,
426 HOOK_PRE_PUSH,
427 HOOK_PRETX_PUSH,
427 HOOK_PRETX_PUSH,
428 HOOK_PUSH,
428 HOOK_PUSH,
429 HOOK_PUSH_KEY,
429 HOOK_PUSH_KEY,
430 ]
430 ]
431
431
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 # TODO: johbo: Unify way how hooks are configured for git and hg,
433 # git part is currently hardcoded.
433 # git part is currently hardcoded.
434
434
435 # SVN PATTERNS
435 # SVN PATTERNS
436 SVN_BRANCH_ID = 'vcs_svn_branch'
436 SVN_BRANCH_ID = 'vcs_svn_branch'
437 SVN_TAG_ID = 'vcs_svn_tag'
437 SVN_TAG_ID = 'vcs_svn_tag'
438
438
439 ui_id = Column(
439 ui_id = Column(
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 "ui_id", Integer(), nullable=False, unique=True, default=None,
441 primary_key=True)
441 primary_key=True)
442 ui_section = Column(
442 ui_section = Column(
443 "ui_section", String(255), nullable=True, unique=None, default=None)
443 "ui_section", String(255), nullable=True, unique=None, default=None)
444 ui_key = Column(
444 ui_key = Column(
445 "ui_key", String(255), nullable=True, unique=None, default=None)
445 "ui_key", String(255), nullable=True, unique=None, default=None)
446 ui_value = Column(
446 ui_value = Column(
447 "ui_value", String(255), nullable=True, unique=None, default=None)
447 "ui_value", String(255), nullable=True, unique=None, default=None)
448 ui_active = Column(
448 ui_active = Column(
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449 "ui_active", Boolean(), nullable=True, unique=None, default=True)
450
450
451 def __repr__(self):
451 def __repr__(self):
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
453 self.ui_key, self.ui_value)
453 self.ui_key, self.ui_value)
454
454
455
455
456 class RepoRhodeCodeSetting(Base, BaseModel):
456 class RepoRhodeCodeSetting(Base, BaseModel):
457 __tablename__ = 'repo_rhodecode_settings'
457 __tablename__ = 'repo_rhodecode_settings'
458 __table_args__ = (
458 __table_args__ = (
459 UniqueConstraint(
459 UniqueConstraint(
460 'app_settings_name', 'repository_id',
460 'app_settings_name', 'repository_id',
461 name='uq_repo_rhodecode_setting_name_repo_id'),
461 name='uq_repo_rhodecode_setting_name_repo_id'),
462 base_table_args
462 base_table_args
463 )
463 )
464
464
465 repository_id = Column(
465 repository_id = Column(
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
467 nullable=False)
467 nullable=False)
468 app_settings_id = Column(
468 app_settings_id = Column(
469 "app_settings_id", Integer(), nullable=False, unique=True,
469 "app_settings_id", Integer(), nullable=False, unique=True,
470 default=None, primary_key=True)
470 default=None, primary_key=True)
471 app_settings_name = Column(
471 app_settings_name = Column(
472 "app_settings_name", String(255), nullable=True, unique=None,
472 "app_settings_name", String(255), nullable=True, unique=None,
473 default=None)
473 default=None)
474 _app_settings_value = Column(
474 _app_settings_value = Column(
475 "app_settings_value", String(4096), nullable=True, unique=None,
475 "app_settings_value", String(4096), nullable=True, unique=None,
476 default=None)
476 default=None)
477 _app_settings_type = Column(
477 _app_settings_type = Column(
478 "app_settings_type", String(255), nullable=True, unique=None,
478 "app_settings_type", String(255), nullable=True, unique=None,
479 default=None)
479 default=None)
480
480
481 repository = relationship('Repository')
481 repository = relationship('Repository')
482
482
483 def __init__(self, repository_id, key='', val='', type='unicode'):
483 def __init__(self, repository_id, key='', val='', type='unicode'):
484 self.repository_id = repository_id
484 self.repository_id = repository_id
485 self.app_settings_name = key
485 self.app_settings_name = key
486 self.app_settings_type = type
486 self.app_settings_type = type
487 self.app_settings_value = val
487 self.app_settings_value = val
488
488
489 @validates('_app_settings_value')
489 @validates('_app_settings_value')
490 def validate_settings_value(self, key, val):
490 def validate_settings_value(self, key, val):
491 assert type(val) == unicode
491 assert type(val) == unicode
492 return val
492 return val
493
493
494 @hybrid_property
494 @hybrid_property
495 def app_settings_value(self):
495 def app_settings_value(self):
496 v = self._app_settings_value
496 v = self._app_settings_value
497 type_ = self.app_settings_type
497 type_ = self.app_settings_type
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
500 return converter(v)
500 return converter(v)
501
501
502 @app_settings_value.setter
502 @app_settings_value.setter
503 def app_settings_value(self, val):
503 def app_settings_value(self, val):
504 """
504 """
505 Setter that will always make sure we use unicode in app_settings_value
505 Setter that will always make sure we use unicode in app_settings_value
506
506
507 :param val:
507 :param val:
508 """
508 """
509 self._app_settings_value = safe_unicode(val)
509 self._app_settings_value = safe_unicode(val)
510
510
511 @hybrid_property
511 @hybrid_property
512 def app_settings_type(self):
512 def app_settings_type(self):
513 return self._app_settings_type
513 return self._app_settings_type
514
514
515 @app_settings_type.setter
515 @app_settings_type.setter
516 def app_settings_type(self, val):
516 def app_settings_type(self, val):
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
518 if val not in SETTINGS_TYPES:
518 if val not in SETTINGS_TYPES:
519 raise Exception('type must be one of %s got %s'
519 raise Exception('type must be one of %s got %s'
520 % (SETTINGS_TYPES.keys(), val))
520 % (SETTINGS_TYPES.keys(), val))
521 self._app_settings_type = val
521 self._app_settings_type = val
522
522
523 def __unicode__(self):
523 def __unicode__(self):
524 return u"<%s('%s:%s:%s[%s]')>" % (
524 return u"<%s('%s:%s:%s[%s]')>" % (
525 self.__class__.__name__, self.repository.repo_name,
525 self.__class__.__name__, self.repository.repo_name,
526 self.app_settings_name, self.app_settings_value,
526 self.app_settings_name, self.app_settings_value,
527 self.app_settings_type
527 self.app_settings_type
528 )
528 )
529
529
530
530
531 class RepoRhodeCodeUi(Base, BaseModel):
531 class RepoRhodeCodeUi(Base, BaseModel):
532 __tablename__ = 'repo_rhodecode_ui'
532 __tablename__ = 'repo_rhodecode_ui'
533 __table_args__ = (
533 __table_args__ = (
534 UniqueConstraint(
534 UniqueConstraint(
535 'repository_id', 'ui_section', 'ui_key',
535 'repository_id', 'ui_section', 'ui_key',
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 name='uq_repo_rhodecode_ui_repository_id_section_key'),
537 base_table_args
537 base_table_args
538 )
538 )
539
539
540 repository_id = Column(
540 repository_id = Column(
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
542 nullable=False)
542 nullable=False)
543 ui_id = Column(
543 ui_id = Column(
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 "ui_id", Integer(), nullable=False, unique=True, default=None,
545 primary_key=True)
545 primary_key=True)
546 ui_section = Column(
546 ui_section = Column(
547 "ui_section", String(255), nullable=True, unique=None, default=None)
547 "ui_section", String(255), nullable=True, unique=None, default=None)
548 ui_key = Column(
548 ui_key = Column(
549 "ui_key", String(255), nullable=True, unique=None, default=None)
549 "ui_key", String(255), nullable=True, unique=None, default=None)
550 ui_value = Column(
550 ui_value = Column(
551 "ui_value", String(255), nullable=True, unique=None, default=None)
551 "ui_value", String(255), nullable=True, unique=None, default=None)
552 ui_active = Column(
552 ui_active = Column(
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553 "ui_active", Boolean(), nullable=True, unique=None, default=True)
554
554
555 repository = relationship('Repository')
555 repository = relationship('Repository')
556
556
557 def __repr__(self):
557 def __repr__(self):
558 return '<%s[%s:%s]%s=>%s]>' % (
558 return '<%s[%s:%s]%s=>%s]>' % (
559 self.__class__.__name__, self.repository.repo_name,
559 self.__class__.__name__, self.repository.repo_name,
560 self.ui_section, self.ui_key, self.ui_value)
560 self.ui_section, self.ui_key, self.ui_value)
561
561
562
562
563 class User(Base, BaseModel):
563 class User(Base, BaseModel):
564 __tablename__ = 'users'
564 __tablename__ = 'users'
565 __table_args__ = (
565 __table_args__ = (
566 UniqueConstraint('username'), UniqueConstraint('email'),
566 UniqueConstraint('username'), UniqueConstraint('email'),
567 Index('u_username_idx', 'username'),
567 Index('u_username_idx', 'username'),
568 Index('u_email_idx', 'email'),
568 Index('u_email_idx', 'email'),
569 base_table_args
569 base_table_args
570 )
570 )
571
571
572 DEFAULT_USER = 'default'
572 DEFAULT_USER = 'default'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
575
575
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 username = Column("username", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 password = Column("password", String(255), nullable=True, unique=None, default=None)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 _email = Column("email", String(255), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
587
587
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
594
594
595 user_log = relationship('UserLog')
595 user_log = relationship('UserLog')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
597
597
598 repositories = relationship('Repository')
598 repositories = relationship('Repository')
599 repository_groups = relationship('RepoGroup')
599 repository_groups = relationship('RepoGroup')
600 user_groups = relationship('UserGroup')
600 user_groups = relationship('UserGroup')
601
601
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
604
604
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
608
608
609 group_member = relationship('UserGroupMember', cascade='all')
609 group_member = relationship('UserGroupMember', cascade='all')
610
610
611 notifications = relationship('UserNotification', cascade='all')
611 notifications = relationship('UserNotification', cascade='all')
612 # notifications assigned to this user
612 # notifications assigned to this user
613 user_created_notifications = relationship('Notification', cascade='all')
613 user_created_notifications = relationship('Notification', cascade='all')
614 # comments created by this user
614 # comments created by this user
615 user_comments = relationship('ChangesetComment', cascade='all')
615 user_comments = relationship('ChangesetComment', cascade='all')
616 # user profile extra info
616 # user profile extra info
617 user_emails = relationship('UserEmailMap', cascade='all')
617 user_emails = relationship('UserEmailMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
618 user_ip_map = relationship('UserIpMap', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 user_auth_tokens = relationship('UserApiKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620 user_ssh_keys = relationship('UserSshKeys', cascade='all')
621
621
622 # gists
622 # gists
623 user_gists = relationship('Gist', cascade='all')
623 user_gists = relationship('Gist', cascade='all')
624 # user pull requests
624 # user pull requests
625 user_pull_requests = relationship('PullRequest', cascade='all')
625 user_pull_requests = relationship('PullRequest', cascade='all')
626
626
627 # external identities
627 # external identities
628 external_identities = relationship(
628 external_identities = relationship(
629 'ExternalIdentity',
629 'ExternalIdentity',
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
631 cascade='all')
631 cascade='all')
632 # review rules
632 # review rules
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
634
634
635 # artifacts owned
635 # artifacts owned
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
637
637
638 # no cascade, set NULL
638 # no cascade, set NULL
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
640
640
641 def __unicode__(self):
641 def __unicode__(self):
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
643 self.user_id, self.username)
643 self.user_id, self.username)
644
644
645 @hybrid_property
645 @hybrid_property
646 def email(self):
646 def email(self):
647 return self._email
647 return self._email
648
648
649 @email.setter
649 @email.setter
650 def email(self, val):
650 def email(self, val):
651 self._email = val.lower() if val else None
651 self._email = val.lower() if val else None
652
652
653 @hybrid_property
653 @hybrid_property
654 def first_name(self):
654 def first_name(self):
655 from rhodecode.lib import helpers as h
655 from rhodecode.lib import helpers as h
656 if self.name:
656 if self.name:
657 return h.escape(self.name)
657 return h.escape(self.name)
658 return self.name
658 return self.name
659
659
660 @hybrid_property
660 @hybrid_property
661 def last_name(self):
661 def last_name(self):
662 from rhodecode.lib import helpers as h
662 from rhodecode.lib import helpers as h
663 if self.lastname:
663 if self.lastname:
664 return h.escape(self.lastname)
664 return h.escape(self.lastname)
665 return self.lastname
665 return self.lastname
666
666
667 @hybrid_property
667 @hybrid_property
668 def api_key(self):
668 def api_key(self):
669 """
669 """
670 Fetch if exist an auth-token with role ALL connected to this user
670 Fetch if exist an auth-token with role ALL connected to this user
671 """
671 """
672 user_auth_token = UserApiKeys.query()\
672 user_auth_token = UserApiKeys.query()\
673 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(UserApiKeys.user_id == self.user_id)\
674 .filter(or_(UserApiKeys.expires == -1,
674 .filter(or_(UserApiKeys.expires == -1,
675 UserApiKeys.expires >= time.time()))\
675 UserApiKeys.expires >= time.time()))\
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
677 if user_auth_token:
677 if user_auth_token:
678 user_auth_token = user_auth_token.api_key
678 user_auth_token = user_auth_token.api_key
679
679
680 return user_auth_token
680 return user_auth_token
681
681
682 @api_key.setter
682 @api_key.setter
683 def api_key(self, val):
683 def api_key(self, val):
684 # don't allow to set API key this is deprecated for now
684 # don't allow to set API key this is deprecated for now
685 self._api_key = None
685 self._api_key = None
686
686
687 @property
687 @property
688 def reviewer_pull_requests(self):
688 def reviewer_pull_requests(self):
689 return PullRequestReviewers.query() \
689 return PullRequestReviewers.query() \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
690 .options(joinedload(PullRequestReviewers.pull_request)) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
691 .filter(PullRequestReviewers.user_id == self.user_id) \
692 .all()
692 .all()
693
693
694 @property
694 @property
695 def firstname(self):
695 def firstname(self):
696 # alias for future
696 # alias for future
697 return self.name
697 return self.name
698
698
699 @property
699 @property
700 def emails(self):
700 def emails(self):
701 other = UserEmailMap.query()\
701 other = UserEmailMap.query()\
702 .filter(UserEmailMap.user == self) \
702 .filter(UserEmailMap.user == self) \
703 .order_by(UserEmailMap.email_id.asc()) \
703 .order_by(UserEmailMap.email_id.asc()) \
704 .all()
704 .all()
705 return [self.email] + [x.email for x in other]
705 return [self.email] + [x.email for x in other]
706
706
707 def emails_cached(self):
707 def emails_cached(self):
708 emails = UserEmailMap.query()\
708 emails = UserEmailMap.query()\
709 .filter(UserEmailMap.user == self) \
709 .filter(UserEmailMap.user == self) \
710 .order_by(UserEmailMap.email_id.asc())
710 .order_by(UserEmailMap.email_id.asc())
711
711
712 emails = emails.options(
712 emails = emails.options(
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
714 )
714 )
715
715
716 return [self.email] + [x.email for x in emails]
716 return [self.email] + [x.email for x in emails]
717
717
718 @property
718 @property
719 def auth_tokens(self):
719 def auth_tokens(self):
720 auth_tokens = self.get_auth_tokens()
720 auth_tokens = self.get_auth_tokens()
721 return [x.api_key for x in auth_tokens]
721 return [x.api_key for x in auth_tokens]
722
722
723 def get_auth_tokens(self):
723 def get_auth_tokens(self):
724 return UserApiKeys.query()\
724 return UserApiKeys.query()\
725 .filter(UserApiKeys.user == self)\
725 .filter(UserApiKeys.user == self)\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
726 .order_by(UserApiKeys.user_api_key_id.asc())\
727 .all()
727 .all()
728
728
729 @LazyProperty
729 @LazyProperty
730 def feed_token(self):
730 def feed_token(self):
731 return self.get_feed_token()
731 return self.get_feed_token()
732
732
733 def get_feed_token(self, cache=True):
733 def get_feed_token(self, cache=True):
734 feed_tokens = UserApiKeys.query()\
734 feed_tokens = UserApiKeys.query()\
735 .filter(UserApiKeys.user == self)\
735 .filter(UserApiKeys.user == self)\
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
737 if cache:
737 if cache:
738 feed_tokens = feed_tokens.options(
738 feed_tokens = feed_tokens.options(
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
740
740
741 feed_tokens = feed_tokens.all()
741 feed_tokens = feed_tokens.all()
742 if feed_tokens:
742 if feed_tokens:
743 return feed_tokens[0].api_key
743 return feed_tokens[0].api_key
744 return 'NO_FEED_TOKEN_AVAILABLE'
744 return 'NO_FEED_TOKEN_AVAILABLE'
745
745
746 @LazyProperty
746 @LazyProperty
747 def artifact_token(self):
747 def artifact_token(self):
748 return self.get_artifact_token()
748 return self.get_artifact_token()
749
749
750 def get_artifact_token(self, cache=True):
750 def get_artifact_token(self, cache=True):
751 artifacts_tokens = UserApiKeys.query()\
751 artifacts_tokens = UserApiKeys.query()\
752 .filter(UserApiKeys.user == self) \
752 .filter(UserApiKeys.user == self) \
753 .filter(or_(UserApiKeys.expires == -1,
753 .filter(or_(UserApiKeys.expires == -1,
754 UserApiKeys.expires >= time.time())) \
754 UserApiKeys.expires >= time.time())) \
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
755 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
756
756
757 if cache:
757 if cache:
758 artifacts_tokens = artifacts_tokens.options(
758 artifacts_tokens = artifacts_tokens.options(
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
759 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
760
760
761 artifacts_tokens = artifacts_tokens.all()
761 artifacts_tokens = artifacts_tokens.all()
762 if artifacts_tokens:
762 if artifacts_tokens:
763 return artifacts_tokens[0].api_key
763 return artifacts_tokens[0].api_key
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
764 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
765
765
766 def get_or_create_artifact_token(self):
766 def get_or_create_artifact_token(self):
767 artifacts_tokens = UserApiKeys.query()\
767 artifacts_tokens = UserApiKeys.query()\
768 .filter(UserApiKeys.user == self) \
768 .filter(UserApiKeys.user == self) \
769 .filter(or_(UserApiKeys.expires == -1,
769 .filter(or_(UserApiKeys.expires == -1,
770 UserApiKeys.expires >= time.time())) \
770 UserApiKeys.expires >= time.time())) \
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
771 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
772
772
773 artifacts_tokens = artifacts_tokens.all()
773 artifacts_tokens = artifacts_tokens.all()
774 if artifacts_tokens:
774 if artifacts_tokens:
775 return artifacts_tokens[0].api_key
775 return artifacts_tokens[0].api_key
776 else:
776 else:
777 from rhodecode.model.auth_token import AuthTokenModel
777 from rhodecode.model.auth_token import AuthTokenModel
778 artifact_token = AuthTokenModel().create(
778 artifact_token = AuthTokenModel().create(
779 self, 'auto-generated-artifact-token',
779 self, 'auto-generated-artifact-token',
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
780 lifetime=-1, role=UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
781 Session.commit()
781 Session.commit()
782 return artifact_token.api_key
782 return artifact_token.api_key
783
783
784 @classmethod
784 @classmethod
785 def get(cls, user_id, cache=False):
785 def get(cls, user_id, cache=False):
786 if not user_id:
786 if not user_id:
787 return
787 return
788
788
789 user = cls.query()
789 user = cls.query()
790 if cache:
790 if cache:
791 user = user.options(
791 user = user.options(
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
792 FromCache("sql_cache_short", "get_users_%s" % user_id))
793 return user.get(user_id)
793 return user.get(user_id)
794
794
795 @classmethod
795 @classmethod
796 def extra_valid_auth_tokens(cls, user, role=None):
796 def extra_valid_auth_tokens(cls, user, role=None):
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
797 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
798 .filter(or_(UserApiKeys.expires == -1,
798 .filter(or_(UserApiKeys.expires == -1,
799 UserApiKeys.expires >= time.time()))
799 UserApiKeys.expires >= time.time()))
800 if role:
800 if role:
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
801 tokens = tokens.filter(or_(UserApiKeys.role == role,
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
802 UserApiKeys.role == UserApiKeys.ROLE_ALL))
803 return tokens.all()
803 return tokens.all()
804
804
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
805 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
806 from rhodecode.lib import auth
806 from rhodecode.lib import auth
807
807
808 log.debug('Trying to authenticate user: %s via auth-token, '
808 log.debug('Trying to authenticate user: %s via auth-token, '
809 'and roles: %s', self, roles)
809 'and roles: %s', self, roles)
810
810
811 if not auth_token:
811 if not auth_token:
812 return False
812 return False
813
813
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
814 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
815 tokens_q = UserApiKeys.query()\
815 tokens_q = UserApiKeys.query()\
816 .filter(UserApiKeys.user_id == self.user_id)\
816 .filter(UserApiKeys.user_id == self.user_id)\
817 .filter(or_(UserApiKeys.expires == -1,
817 .filter(or_(UserApiKeys.expires == -1,
818 UserApiKeys.expires >= time.time()))
818 UserApiKeys.expires >= time.time()))
819
819
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
820 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
821
821
822 crypto_backend = auth.crypto_backend()
822 crypto_backend = auth.crypto_backend()
823 enc_token_map = {}
823 enc_token_map = {}
824 plain_token_map = {}
824 plain_token_map = {}
825 for token in tokens_q:
825 for token in tokens_q:
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
826 if token.api_key.startswith(crypto_backend.ENC_PREF):
827 enc_token_map[token.api_key] = token
827 enc_token_map[token.api_key] = token
828 else:
828 else:
829 plain_token_map[token.api_key] = token
829 plain_token_map[token.api_key] = token
830 log.debug(
830 log.debug(
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
831 'Found %s plain and %s encrypted tokens to check for authentication for this user',
832 len(plain_token_map), len(enc_token_map))
832 len(plain_token_map), len(enc_token_map))
833
833
834 # plain token match comes first
834 # plain token match comes first
835 match = plain_token_map.get(auth_token)
835 match = plain_token_map.get(auth_token)
836
836
837 # check encrypted tokens now
837 # check encrypted tokens now
838 if not match:
838 if not match:
839 for token_hash, token in enc_token_map.items():
839 for token_hash, token in enc_token_map.items():
840 # NOTE(marcink): this is expensive to calculate, but most secure
840 # NOTE(marcink): this is expensive to calculate, but most secure
841 if crypto_backend.hash_check(auth_token, token_hash):
841 if crypto_backend.hash_check(auth_token, token_hash):
842 match = token
842 match = token
843 break
843 break
844
844
845 if match:
845 if match:
846 log.debug('Found matching token %s', match)
846 log.debug('Found matching token %s', match)
847 if match.repo_id:
847 if match.repo_id:
848 log.debug('Found scope, checking for scope match of token %s', match)
848 log.debug('Found scope, checking for scope match of token %s', match)
849 if match.repo_id == scope_repo_id:
849 if match.repo_id == scope_repo_id:
850 return True
850 return True
851 else:
851 else:
852 log.debug(
852 log.debug(
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
853 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
854 'and calling scope is:%s, skipping further checks',
854 'and calling scope is:%s, skipping further checks',
855 match.repo, scope_repo_id)
855 match.repo, scope_repo_id)
856 return False
856 return False
857 else:
857 else:
858 return True
858 return True
859
859
860 return False
860 return False
861
861
862 @property
862 @property
863 def ip_addresses(self):
863 def ip_addresses(self):
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
864 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
865 return [x.ip_addr for x in ret]
865 return [x.ip_addr for x in ret]
866
866
867 @property
867 @property
868 def username_and_name(self):
868 def username_and_name(self):
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
869 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
870
870
871 @property
871 @property
872 def username_or_name_or_email(self):
872 def username_or_name_or_email(self):
873 full_name = self.full_name if self.full_name is not ' ' else None
873 full_name = self.full_name if self.full_name is not ' ' else None
874 return self.username or full_name or self.email
874 return self.username or full_name or self.email
875
875
876 @property
876 @property
877 def full_name(self):
877 def full_name(self):
878 return '%s %s' % (self.first_name, self.last_name)
878 return '%s %s' % (self.first_name, self.last_name)
879
879
880 @property
880 @property
881 def full_name_or_username(self):
881 def full_name_or_username(self):
882 return ('%s %s' % (self.first_name, self.last_name)
882 return ('%s %s' % (self.first_name, self.last_name)
883 if (self.first_name and self.last_name) else self.username)
883 if (self.first_name and self.last_name) else self.username)
884
884
885 @property
885 @property
886 def full_contact(self):
886 def full_contact(self):
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
887 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
888
888
889 @property
889 @property
890 def short_contact(self):
890 def short_contact(self):
891 return '%s %s' % (self.first_name, self.last_name)
891 return '%s %s' % (self.first_name, self.last_name)
892
892
893 @property
893 @property
894 def is_admin(self):
894 def is_admin(self):
895 return self.admin
895 return self.admin
896
896
897 @property
897 @property
898 def language(self):
898 def language(self):
899 return self.user_data.get('language')
899 return self.user_data.get('language')
900
900
901 def AuthUser(self, **kwargs):
901 def AuthUser(self, **kwargs):
902 """
902 """
903 Returns instance of AuthUser for this user
903 Returns instance of AuthUser for this user
904 """
904 """
905 from rhodecode.lib.auth import AuthUser
905 from rhodecode.lib.auth import AuthUser
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
906 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
907
907
908 @hybrid_property
908 @hybrid_property
909 def user_data(self):
909 def user_data(self):
910 if not self._user_data:
910 if not self._user_data:
911 return {}
911 return {}
912
912
913 try:
913 try:
914 return json.loads(self._user_data) or {}
914 return json.loads(self._user_data) or {}
915 except TypeError:
915 except TypeError:
916 return {}
916 return {}
917
917
918 @user_data.setter
918 @user_data.setter
919 def user_data(self, val):
919 def user_data(self, val):
920 if not isinstance(val, dict):
920 if not isinstance(val, dict):
921 raise Exception('user_data must be dict, got %s' % type(val))
921 raise Exception('user_data must be dict, got %s' % type(val))
922 try:
922 try:
923 self._user_data = json.dumps(val)
923 self._user_data = json.dumps(val)
924 except Exception:
924 except Exception:
925 log.error(traceback.format_exc())
925 log.error(traceback.format_exc())
926
926
927 @classmethod
927 @classmethod
928 def get_by_username(cls, username, case_insensitive=False,
928 def get_by_username(cls, username, case_insensitive=False,
929 cache=False, identity_cache=False):
929 cache=False, identity_cache=False):
930 session = Session()
930 session = Session()
931
931
932 if case_insensitive:
932 if case_insensitive:
933 q = cls.query().filter(
933 q = cls.query().filter(
934 func.lower(cls.username) == func.lower(username))
934 func.lower(cls.username) == func.lower(username))
935 else:
935 else:
936 q = cls.query().filter(cls.username == username)
936 q = cls.query().filter(cls.username == username)
937
937
938 if cache:
938 if cache:
939 if identity_cache:
939 if identity_cache:
940 val = cls.identity_cache(session, 'username', username)
940 val = cls.identity_cache(session, 'username', username)
941 if val:
941 if val:
942 return val
942 return val
943 else:
943 else:
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
944 cache_key = "get_user_by_name_%s" % _hash_key(username)
945 q = q.options(
945 q = q.options(
946 FromCache("sql_cache_short", cache_key))
946 FromCache("sql_cache_short", cache_key))
947
947
948 return q.scalar()
948 return q.scalar()
949
949
950 @classmethod
950 @classmethod
951 def get_by_auth_token(cls, auth_token, cache=False):
951 def get_by_auth_token(cls, auth_token, cache=False):
952 q = UserApiKeys.query()\
952 q = UserApiKeys.query()\
953 .filter(UserApiKeys.api_key == auth_token)\
953 .filter(UserApiKeys.api_key == auth_token)\
954 .filter(or_(UserApiKeys.expires == -1,
954 .filter(or_(UserApiKeys.expires == -1,
955 UserApiKeys.expires >= time.time()))
955 UserApiKeys.expires >= time.time()))
956 if cache:
956 if cache:
957 q = q.options(
957 q = q.options(
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
958 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
959
959
960 match = q.first()
960 match = q.first()
961 if match:
961 if match:
962 return match.user
962 return match.user
963
963
964 @classmethod
964 @classmethod
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
965 def get_by_email(cls, email, case_insensitive=False, cache=False):
966
966
967 if case_insensitive:
967 if case_insensitive:
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
968 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
969
969
970 else:
970 else:
971 q = cls.query().filter(cls.email == email)
971 q = cls.query().filter(cls.email == email)
972
972
973 email_key = _hash_key(email)
973 email_key = _hash_key(email)
974 if cache:
974 if cache:
975 q = q.options(
975 q = q.options(
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
976 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
977
977
978 ret = q.scalar()
978 ret = q.scalar()
979 if ret is None:
979 if ret is None:
980 q = UserEmailMap.query()
980 q = UserEmailMap.query()
981 # try fetching in alternate email map
981 # try fetching in alternate email map
982 if case_insensitive:
982 if case_insensitive:
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
983 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
984 else:
984 else:
985 q = q.filter(UserEmailMap.email == email)
985 q = q.filter(UserEmailMap.email == email)
986 q = q.options(joinedload(UserEmailMap.user))
986 q = q.options(joinedload(UserEmailMap.user))
987 if cache:
987 if cache:
988 q = q.options(
988 q = q.options(
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
989 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
990 ret = getattr(q.scalar(), 'user', None)
990 ret = getattr(q.scalar(), 'user', None)
991
991
992 return ret
992 return ret
993
993
994 @classmethod
994 @classmethod
995 def get_from_cs_author(cls, author):
995 def get_from_cs_author(cls, author):
996 """
996 """
997 Tries to get User objects out of commit author string
997 Tries to get User objects out of commit author string
998
998
999 :param author:
999 :param author:
1000 """
1000 """
1001 from rhodecode.lib.helpers import email, author_name
1001 from rhodecode.lib.helpers import email, author_name
1002 # Valid email in the attribute passed, see if they're in the system
1002 # Valid email in the attribute passed, see if they're in the system
1003 _email = email(author)
1003 _email = email(author)
1004 if _email:
1004 if _email:
1005 user = cls.get_by_email(_email, case_insensitive=True)
1005 user = cls.get_by_email(_email, case_insensitive=True)
1006 if user:
1006 if user:
1007 return user
1007 return user
1008 # Maybe we can match by username?
1008 # Maybe we can match by username?
1009 _author = author_name(author)
1009 _author = author_name(author)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1010 user = cls.get_by_username(_author, case_insensitive=True)
1011 if user:
1011 if user:
1012 return user
1012 return user
1013
1013
1014 def update_userdata(self, **kwargs):
1014 def update_userdata(self, **kwargs):
1015 usr = self
1015 usr = self
1016 old = usr.user_data
1016 old = usr.user_data
1017 old.update(**kwargs)
1017 old.update(**kwargs)
1018 usr.user_data = old
1018 usr.user_data = old
1019 Session().add(usr)
1019 Session().add(usr)
1020 log.debug('updated userdata with %s', kwargs)
1020 log.debug('updated userdata with %s', kwargs)
1021
1021
1022 def update_lastlogin(self):
1022 def update_lastlogin(self):
1023 """Update user lastlogin"""
1023 """Update user lastlogin"""
1024 self.last_login = datetime.datetime.now()
1024 self.last_login = datetime.datetime.now()
1025 Session().add(self)
1025 Session().add(self)
1026 log.debug('updated user %s lastlogin', self.username)
1026 log.debug('updated user %s lastlogin', self.username)
1027
1027
1028 def update_password(self, new_password):
1028 def update_password(self, new_password):
1029 from rhodecode.lib.auth import get_crypt_password
1029 from rhodecode.lib.auth import get_crypt_password
1030
1030
1031 self.password = get_crypt_password(new_password)
1031 self.password = get_crypt_password(new_password)
1032 Session().add(self)
1032 Session().add(self)
1033
1033
1034 @classmethod
1034 @classmethod
1035 def get_first_super_admin(cls):
1035 def get_first_super_admin(cls):
1036 user = User.query()\
1036 user = User.query()\
1037 .filter(User.admin == true()) \
1037 .filter(User.admin == true()) \
1038 .order_by(User.user_id.asc()) \
1038 .order_by(User.user_id.asc()) \
1039 .first()
1039 .first()
1040
1040
1041 if user is None:
1041 if user is None:
1042 raise Exception('FATAL: Missing administrative account!')
1042 raise Exception('FATAL: Missing administrative account!')
1043 return user
1043 return user
1044
1044
1045 @classmethod
1045 @classmethod
1046 def get_all_super_admins(cls, only_active=False):
1046 def get_all_super_admins(cls, only_active=False):
1047 """
1047 """
1048 Returns all admin accounts sorted by username
1048 Returns all admin accounts sorted by username
1049 """
1049 """
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1050 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1051 if only_active:
1051 if only_active:
1052 qry = qry.filter(User.active == true())
1052 qry = qry.filter(User.active == true())
1053 return qry.all()
1053 return qry.all()
1054
1054
1055 @classmethod
1055 @classmethod
1056 def get_all_user_ids(cls, only_active=True):
1056 def get_all_user_ids(cls, only_active=True):
1057 """
1057 """
1058 Returns all users IDs
1058 Returns all users IDs
1059 """
1059 """
1060 qry = Session().query(User.user_id)
1060 qry = Session().query(User.user_id)
1061
1061
1062 if only_active:
1062 if only_active:
1063 qry = qry.filter(User.active == true())
1063 qry = qry.filter(User.active == true())
1064 return [x.user_id for x in qry]
1064 return [x.user_id for x in qry]
1065
1065
1066 @classmethod
1066 @classmethod
1067 def get_default_user(cls, cache=False, refresh=False):
1067 def get_default_user(cls, cache=False, refresh=False):
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1068 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1069 if user is None:
1069 if user is None:
1070 raise Exception('FATAL: Missing default account!')
1070 raise Exception('FATAL: Missing default account!')
1071 if refresh:
1071 if refresh:
1072 # The default user might be based on outdated state which
1072 # The default user might be based on outdated state which
1073 # has been loaded from the cache.
1073 # has been loaded from the cache.
1074 # A call to refresh() ensures that the
1074 # A call to refresh() ensures that the
1075 # latest state from the database is used.
1075 # latest state from the database is used.
1076 Session().refresh(user)
1076 Session().refresh(user)
1077 return user
1077 return user
1078
1078
1079 @classmethod
1079 @classmethod
1080 def get_default_user_id(cls):
1080 def get_default_user_id(cls):
1081 import rhodecode
1081 import rhodecode
1082 return rhodecode.CONFIG['default_user_id']
1082 return rhodecode.CONFIG['default_user_id']
1083
1083
1084 def _get_default_perms(self, user, suffix=''):
1084 def _get_default_perms(self, user, suffix=''):
1085 from rhodecode.model.permission import PermissionModel
1085 from rhodecode.model.permission import PermissionModel
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1086 return PermissionModel().get_default_perms(user.user_perms, suffix)
1087
1087
1088 def get_default_perms(self, suffix=''):
1088 def get_default_perms(self, suffix=''):
1089 return self._get_default_perms(self, suffix)
1089 return self._get_default_perms(self, suffix)
1090
1090
1091 def get_api_data(self, include_secrets=False, details='full'):
1091 def get_api_data(self, include_secrets=False, details='full'):
1092 """
1092 """
1093 Common function for generating user related data for API
1093 Common function for generating user related data for API
1094
1094
1095 :param include_secrets: By default secrets in the API data will be replaced
1095 :param include_secrets: By default secrets in the API data will be replaced
1096 by a placeholder value to prevent exposing this data by accident. In case
1096 by a placeholder value to prevent exposing this data by accident. In case
1097 this data shall be exposed, set this flag to ``True``.
1097 this data shall be exposed, set this flag to ``True``.
1098
1098
1099 :param details: details can be 'basic|full' basic gives only a subset of
1099 :param details: details can be 'basic|full' basic gives only a subset of
1100 the available user information that includes user_id, name and emails.
1100 the available user information that includes user_id, name and emails.
1101 """
1101 """
1102 user = self
1102 user = self
1103 user_data = self.user_data
1103 user_data = self.user_data
1104 data = {
1104 data = {
1105 'user_id': user.user_id,
1105 'user_id': user.user_id,
1106 'username': user.username,
1106 'username': user.username,
1107 'firstname': user.name,
1107 'firstname': user.name,
1108 'lastname': user.lastname,
1108 'lastname': user.lastname,
1109 'description': user.description,
1109 'description': user.description,
1110 'email': user.email,
1110 'email': user.email,
1111 'emails': user.emails,
1111 'emails': user.emails,
1112 }
1112 }
1113 if details == 'basic':
1113 if details == 'basic':
1114 return data
1114 return data
1115
1115
1116 auth_token_length = 40
1116 auth_token_length = 40
1117 auth_token_replacement = '*' * auth_token_length
1117 auth_token_replacement = '*' * auth_token_length
1118
1118
1119 extras = {
1119 extras = {
1120 'auth_tokens': [auth_token_replacement],
1120 'auth_tokens': [auth_token_replacement],
1121 'active': user.active,
1121 'active': user.active,
1122 'admin': user.admin,
1122 'admin': user.admin,
1123 'extern_type': user.extern_type,
1123 'extern_type': user.extern_type,
1124 'extern_name': user.extern_name,
1124 'extern_name': user.extern_name,
1125 'last_login': user.last_login,
1125 'last_login': user.last_login,
1126 'last_activity': user.last_activity,
1126 'last_activity': user.last_activity,
1127 'ip_addresses': user.ip_addresses,
1127 'ip_addresses': user.ip_addresses,
1128 'language': user_data.get('language')
1128 'language': user_data.get('language')
1129 }
1129 }
1130 data.update(extras)
1130 data.update(extras)
1131
1131
1132 if include_secrets:
1132 if include_secrets:
1133 data['auth_tokens'] = user.auth_tokens
1133 data['auth_tokens'] = user.auth_tokens
1134 return data
1134 return data
1135
1135
1136 def __json__(self):
1136 def __json__(self):
1137 data = {
1137 data = {
1138 'full_name': self.full_name,
1138 'full_name': self.full_name,
1139 'full_name_or_username': self.full_name_or_username,
1139 'full_name_or_username': self.full_name_or_username,
1140 'short_contact': self.short_contact,
1140 'short_contact': self.short_contact,
1141 'full_contact': self.full_contact,
1141 'full_contact': self.full_contact,
1142 }
1142 }
1143 data.update(self.get_api_data())
1143 data.update(self.get_api_data())
1144 return data
1144 return data
1145
1145
1146
1146
1147 class UserApiKeys(Base, BaseModel):
1147 class UserApiKeys(Base, BaseModel):
1148 __tablename__ = 'user_api_keys'
1148 __tablename__ = 'user_api_keys'
1149 __table_args__ = (
1149 __table_args__ = (
1150 Index('uak_api_key_idx', 'api_key'),
1150 Index('uak_api_key_idx', 'api_key'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1151 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1152 base_table_args
1152 base_table_args
1153 )
1153 )
1154 __mapper_args__ = {}
1154 __mapper_args__ = {}
1155
1155
1156 # ApiKey role
1156 # ApiKey role
1157 ROLE_ALL = 'token_role_all'
1157 ROLE_ALL = 'token_role_all'
1158 ROLE_VCS = 'token_role_vcs'
1158 ROLE_VCS = 'token_role_vcs'
1159 ROLE_API = 'token_role_api'
1159 ROLE_API = 'token_role_api'
1160 ROLE_HTTP = 'token_role_http'
1160 ROLE_HTTP = 'token_role_http'
1161 ROLE_FEED = 'token_role_feed'
1161 ROLE_FEED = 'token_role_feed'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1162 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1163 # The last one is ignored in the list as we only
1163 # The last one is ignored in the list as we only
1164 # use it for one action, and cannot be created by users
1164 # use it for one action, and cannot be created by users
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1165 ROLE_PASSWORD_RESET = 'token_password_reset'
1166
1166
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1167 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1168
1168
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1169 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1170 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1171 api_key = Column("api_key", String(255), nullable=False, unique=True)
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1172 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1173 expires = Column('expires', Float(53), nullable=False)
1173 expires = Column('expires', Float(53), nullable=False)
1174 role = Column('role', String(255), nullable=True)
1174 role = Column('role', String(255), nullable=True)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1175 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1176
1176
1177 # scope columns
1177 # scope columns
1178 repo_id = Column(
1178 repo_id = Column(
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1179 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1180 nullable=True, unique=None, default=None)
1180 nullable=True, unique=None, default=None)
1181 repo = relationship('Repository', lazy='joined')
1181 repo = relationship('Repository', lazy='joined')
1182
1182
1183 repo_group_id = Column(
1183 repo_group_id = Column(
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1184 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1185 nullable=True, unique=None, default=None)
1185 nullable=True, unique=None, default=None)
1186 repo_group = relationship('RepoGroup', lazy='joined')
1186 repo_group = relationship('RepoGroup', lazy='joined')
1187
1187
1188 user = relationship('User', lazy='joined')
1188 user = relationship('User', lazy='joined')
1189
1189
1190 def __unicode__(self):
1190 def __unicode__(self):
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1191 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1192
1192
1193 def __json__(self):
1193 def __json__(self):
1194 data = {
1194 data = {
1195 'auth_token': self.api_key,
1195 'auth_token': self.api_key,
1196 'role': self.role,
1196 'role': self.role,
1197 'scope': self.scope_humanized,
1197 'scope': self.scope_humanized,
1198 'expired': self.expired
1198 'expired': self.expired
1199 }
1199 }
1200 return data
1200 return data
1201
1201
1202 def get_api_data(self, include_secrets=False):
1202 def get_api_data(self, include_secrets=False):
1203 data = self.__json__()
1203 data = self.__json__()
1204 if include_secrets:
1204 if include_secrets:
1205 return data
1205 return data
1206 else:
1206 else:
1207 data['auth_token'] = self.token_obfuscated
1207 data['auth_token'] = self.token_obfuscated
1208 return data
1208 return data
1209
1209
1210 @hybrid_property
1210 @hybrid_property
1211 def description_safe(self):
1211 def description_safe(self):
1212 from rhodecode.lib import helpers as h
1212 from rhodecode.lib import helpers as h
1213 return h.escape(self.description)
1213 return h.escape(self.description)
1214
1214
1215 @property
1215 @property
1216 def expired(self):
1216 def expired(self):
1217 if self.expires == -1:
1217 if self.expires == -1:
1218 return False
1218 return False
1219 return time.time() > self.expires
1219 return time.time() > self.expires
1220
1220
1221 @classmethod
1221 @classmethod
1222 def _get_role_name(cls, role):
1222 def _get_role_name(cls, role):
1223 return {
1223 return {
1224 cls.ROLE_ALL: _('all'),
1224 cls.ROLE_ALL: _('all'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1225 cls.ROLE_HTTP: _('http/web interface'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1226 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1227 cls.ROLE_API: _('api calls'),
1227 cls.ROLE_API: _('api calls'),
1228 cls.ROLE_FEED: _('feed access'),
1228 cls.ROLE_FEED: _('feed access'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1229 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1230 }.get(role, role)
1230 }.get(role, role)
1231
1231
1232 @classmethod
1232 @classmethod
1233 def _get_role_description(cls, role):
1233 def _get_role_description(cls, role):
1234 return {
1234 return {
1235 cls.ROLE_ALL: _('Token for all actions.'),
1235 cls.ROLE_ALL: _('Token for all actions.'),
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1236 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1237 'login using `api_access_controllers_whitelist` functionality.'),
1237 'login using `api_access_controllers_whitelist` functionality.'),
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1238 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1239 'Requires auth_token authentication plugin to be active. <br/>'
1239 'Requires auth_token authentication plugin to be active. <br/>'
1240 'Such Token should be used then instead of a password to '
1240 'Such Token should be used then instead of a password to '
1241 'interact with a repository, and additionally can be '
1241 'interact with a repository, and additionally can be '
1242 'limited to single repository using repo scope.'),
1242 'limited to single repository using repo scope.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1243 cls.ROLE_API: _('Token limited to api calls.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1244 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1245 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1246 }.get(role, role)
1246 }.get(role, role)
1247
1247
1248 @property
1248 @property
1249 def role_humanized(self):
1249 def role_humanized(self):
1250 return self._get_role_name(self.role)
1250 return self._get_role_name(self.role)
1251
1251
1252 def _get_scope(self):
1252 def _get_scope(self):
1253 if self.repo:
1253 if self.repo:
1254 return 'Repository: {}'.format(self.repo.repo_name)
1254 return 'Repository: {}'.format(self.repo.repo_name)
1255 if self.repo_group:
1255 if self.repo_group:
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1256 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1257 return 'Global'
1257 return 'Global'
1258
1258
1259 @property
1259 @property
1260 def scope_humanized(self):
1260 def scope_humanized(self):
1261 return self._get_scope()
1261 return self._get_scope()
1262
1262
1263 @property
1263 @property
1264 def token_obfuscated(self):
1264 def token_obfuscated(self):
1265 if self.api_key:
1265 if self.api_key:
1266 return self.api_key[:4] + "****"
1266 return self.api_key[:4] + "****"
1267
1267
1268
1268
1269 class UserEmailMap(Base, BaseModel):
1269 class UserEmailMap(Base, BaseModel):
1270 __tablename__ = 'user_email_map'
1270 __tablename__ = 'user_email_map'
1271 __table_args__ = (
1271 __table_args__ = (
1272 Index('uem_email_idx', 'email'),
1272 Index('uem_email_idx', 'email'),
1273 UniqueConstraint('email'),
1273 UniqueConstraint('email'),
1274 base_table_args
1274 base_table_args
1275 )
1275 )
1276 __mapper_args__ = {}
1276 __mapper_args__ = {}
1277
1277
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1278 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1279 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1280 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1281 user = relationship('User', lazy='joined')
1281 user = relationship('User', lazy='joined')
1282
1282
1283 @validates('_email')
1283 @validates('_email')
1284 def validate_email(self, key, email):
1284 def validate_email(self, key, email):
1285 # check if this email is not main one
1285 # check if this email is not main one
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1286 main_email = Session().query(User).filter(User.email == email).scalar()
1287 if main_email is not None:
1287 if main_email is not None:
1288 raise AttributeError('email %s is present is user table' % email)
1288 raise AttributeError('email %s is present is user table' % email)
1289 return email
1289 return email
1290
1290
1291 @hybrid_property
1291 @hybrid_property
1292 def email(self):
1292 def email(self):
1293 return self._email
1293 return self._email
1294
1294
1295 @email.setter
1295 @email.setter
1296 def email(self, val):
1296 def email(self, val):
1297 self._email = val.lower() if val else None
1297 self._email = val.lower() if val else None
1298
1298
1299
1299
1300 class UserIpMap(Base, BaseModel):
1300 class UserIpMap(Base, BaseModel):
1301 __tablename__ = 'user_ip_map'
1301 __tablename__ = 'user_ip_map'
1302 __table_args__ = (
1302 __table_args__ = (
1303 UniqueConstraint('user_id', 'ip_addr'),
1303 UniqueConstraint('user_id', 'ip_addr'),
1304 base_table_args
1304 base_table_args
1305 )
1305 )
1306 __mapper_args__ = {}
1306 __mapper_args__ = {}
1307
1307
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1308 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1309 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1310 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1311 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1312 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1313 user = relationship('User', lazy='joined')
1313 user = relationship('User', lazy='joined')
1314
1314
1315 @hybrid_property
1315 @hybrid_property
1316 def description_safe(self):
1316 def description_safe(self):
1317 from rhodecode.lib import helpers as h
1317 from rhodecode.lib import helpers as h
1318 return h.escape(self.description)
1318 return h.escape(self.description)
1319
1319
1320 @classmethod
1320 @classmethod
1321 def _get_ip_range(cls, ip_addr):
1321 def _get_ip_range(cls, ip_addr):
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1322 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1323 return [str(net.network_address), str(net.broadcast_address)]
1323 return [str(net.network_address), str(net.broadcast_address)]
1324
1324
1325 def __json__(self):
1325 def __json__(self):
1326 return {
1326 return {
1327 'ip_addr': self.ip_addr,
1327 'ip_addr': self.ip_addr,
1328 'ip_range': self._get_ip_range(self.ip_addr),
1328 'ip_range': self._get_ip_range(self.ip_addr),
1329 }
1329 }
1330
1330
1331 def __unicode__(self):
1331 def __unicode__(self):
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1332 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1333 self.user_id, self.ip_addr)
1333 self.user_id, self.ip_addr)
1334
1334
1335
1335
1336 class UserSshKeys(Base, BaseModel):
1336 class UserSshKeys(Base, BaseModel):
1337 __tablename__ = 'user_ssh_keys'
1337 __tablename__ = 'user_ssh_keys'
1338 __table_args__ = (
1338 __table_args__ = (
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1339 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1340
1340
1341 UniqueConstraint('ssh_key_fingerprint'),
1341 UniqueConstraint('ssh_key_fingerprint'),
1342
1342
1343 base_table_args
1343 base_table_args
1344 )
1344 )
1345 __mapper_args__ = {}
1345 __mapper_args__ = {}
1346
1346
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1347 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1348 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1349 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1350
1350
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1351 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1352
1352
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1353 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1354 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1355 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1356
1356
1357 user = relationship('User', lazy='joined')
1357 user = relationship('User', lazy='joined')
1358
1358
1359 def __json__(self):
1359 def __json__(self):
1360 data = {
1360 data = {
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1361 'ssh_fingerprint': self.ssh_key_fingerprint,
1362 'description': self.description,
1362 'description': self.description,
1363 'created_on': self.created_on
1363 'created_on': self.created_on
1364 }
1364 }
1365 return data
1365 return data
1366
1366
1367 def get_api_data(self):
1367 def get_api_data(self):
1368 data = self.__json__()
1368 data = self.__json__()
1369 return data
1369 return data
1370
1370
1371
1371
1372 class UserLog(Base, BaseModel):
1372 class UserLog(Base, BaseModel):
1373 __tablename__ = 'user_logs'
1373 __tablename__ = 'user_logs'
1374 __table_args__ = (
1374 __table_args__ = (
1375 base_table_args,
1375 base_table_args,
1376 )
1376 )
1377
1377
1378 VERSION_1 = 'v1'
1378 VERSION_1 = 'v1'
1379 VERSION_2 = 'v2'
1379 VERSION_2 = 'v2'
1380 VERSIONS = [VERSION_1, VERSION_2]
1380 VERSIONS = [VERSION_1, VERSION_2]
1381
1381
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1382 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1383 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1384 username = Column("username", String(255), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1385 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1386 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1387 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1388 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1389 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1390
1390
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1391 version = Column("version", String(255), nullable=True, default=VERSION_1)
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1392 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1393 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1394
1394
1395 def __unicode__(self):
1395 def __unicode__(self):
1396 return u"<%s('id:%s:%s')>" % (
1396 return u"<%s('id:%s:%s')>" % (
1397 self.__class__.__name__, self.repository_name, self.action)
1397 self.__class__.__name__, self.repository_name, self.action)
1398
1398
1399 def __json__(self):
1399 def __json__(self):
1400 return {
1400 return {
1401 'user_id': self.user_id,
1401 'user_id': self.user_id,
1402 'username': self.username,
1402 'username': self.username,
1403 'repository_id': self.repository_id,
1403 'repository_id': self.repository_id,
1404 'repository_name': self.repository_name,
1404 'repository_name': self.repository_name,
1405 'user_ip': self.user_ip,
1405 'user_ip': self.user_ip,
1406 'action_date': self.action_date,
1406 'action_date': self.action_date,
1407 'action': self.action,
1407 'action': self.action,
1408 }
1408 }
1409
1409
1410 @hybrid_property
1410 @hybrid_property
1411 def entry_id(self):
1411 def entry_id(self):
1412 return self.user_log_id
1412 return self.user_log_id
1413
1413
1414 @property
1414 @property
1415 def action_as_day(self):
1415 def action_as_day(self):
1416 return datetime.date(*self.action_date.timetuple()[:3])
1416 return datetime.date(*self.action_date.timetuple()[:3])
1417
1417
1418 user = relationship('User')
1418 user = relationship('User')
1419 repository = relationship('Repository', cascade='')
1419 repository = relationship('Repository', cascade='')
1420
1420
1421
1421
1422 class UserGroup(Base, BaseModel):
1422 class UserGroup(Base, BaseModel):
1423 __tablename__ = 'users_groups'
1423 __tablename__ = 'users_groups'
1424 __table_args__ = (
1424 __table_args__ = (
1425 base_table_args,
1425 base_table_args,
1426 )
1426 )
1427
1427
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1428 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1429 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1430 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1431 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1432 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1433 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1434 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1435 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1436
1436
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1437 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1438 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1439 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1440 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1441 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1442 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1443
1443
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1444 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1445 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1446
1446
1447 @classmethod
1447 @classmethod
1448 def _load_group_data(cls, column):
1448 def _load_group_data(cls, column):
1449 if not column:
1449 if not column:
1450 return {}
1450 return {}
1451
1451
1452 try:
1452 try:
1453 return json.loads(column) or {}
1453 return json.loads(column) or {}
1454 except TypeError:
1454 except TypeError:
1455 return {}
1455 return {}
1456
1456
1457 @hybrid_property
1457 @hybrid_property
1458 def description_safe(self):
1458 def description_safe(self):
1459 from rhodecode.lib import helpers as h
1459 from rhodecode.lib import helpers as h
1460 return h.escape(self.user_group_description)
1460 return h.escape(self.user_group_description)
1461
1461
1462 @hybrid_property
1462 @hybrid_property
1463 def group_data(self):
1463 def group_data(self):
1464 return self._load_group_data(self._group_data)
1464 return self._load_group_data(self._group_data)
1465
1465
1466 @group_data.expression
1466 @group_data.expression
1467 def group_data(self, **kwargs):
1467 def group_data(self, **kwargs):
1468 return self._group_data
1468 return self._group_data
1469
1469
1470 @group_data.setter
1470 @group_data.setter
1471 def group_data(self, val):
1471 def group_data(self, val):
1472 try:
1472 try:
1473 self._group_data = json.dumps(val)
1473 self._group_data = json.dumps(val)
1474 except Exception:
1474 except Exception:
1475 log.error(traceback.format_exc())
1475 log.error(traceback.format_exc())
1476
1476
1477 @classmethod
1477 @classmethod
1478 def _load_sync(cls, group_data):
1478 def _load_sync(cls, group_data):
1479 if group_data:
1479 if group_data:
1480 return group_data.get('extern_type')
1480 return group_data.get('extern_type')
1481
1481
1482 @property
1482 @property
1483 def sync(self):
1483 def sync(self):
1484 return self._load_sync(self.group_data)
1484 return self._load_sync(self.group_data)
1485
1485
1486 def __unicode__(self):
1486 def __unicode__(self):
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1487 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1488 self.users_group_id,
1488 self.users_group_id,
1489 self.users_group_name)
1489 self.users_group_name)
1490
1490
1491 @classmethod
1491 @classmethod
1492 def get_by_group_name(cls, group_name, cache=False,
1492 def get_by_group_name(cls, group_name, cache=False,
1493 case_insensitive=False):
1493 case_insensitive=False):
1494 if case_insensitive:
1494 if case_insensitive:
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1495 q = cls.query().filter(func.lower(cls.users_group_name) ==
1496 func.lower(group_name))
1496 func.lower(group_name))
1497
1497
1498 else:
1498 else:
1499 q = cls.query().filter(cls.users_group_name == group_name)
1499 q = cls.query().filter(cls.users_group_name == group_name)
1500 if cache:
1500 if cache:
1501 q = q.options(
1501 q = q.options(
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1502 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1503 return q.scalar()
1503 return q.scalar()
1504
1504
1505 @classmethod
1505 @classmethod
1506 def get(cls, user_group_id, cache=False):
1506 def get(cls, user_group_id, cache=False):
1507 if not user_group_id:
1507 if not user_group_id:
1508 return
1508 return
1509
1509
1510 user_group = cls.query()
1510 user_group = cls.query()
1511 if cache:
1511 if cache:
1512 user_group = user_group.options(
1512 user_group = user_group.options(
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1513 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1514 return user_group.get(user_group_id)
1514 return user_group.get(user_group_id)
1515
1515
1516 def permissions(self, with_admins=True, with_owner=True,
1516 def permissions(self, with_admins=True, with_owner=True,
1517 expand_from_user_groups=False):
1517 expand_from_user_groups=False):
1518 """
1518 """
1519 Permissions for user groups
1519 Permissions for user groups
1520 """
1520 """
1521 _admin_perm = 'usergroup.admin'
1521 _admin_perm = 'usergroup.admin'
1522
1522
1523 owner_row = []
1523 owner_row = []
1524 if with_owner:
1524 if with_owner:
1525 usr = AttributeDict(self.user.get_dict())
1525 usr = AttributeDict(self.user.get_dict())
1526 usr.owner_row = True
1526 usr.owner_row = True
1527 usr.permission = _admin_perm
1527 usr.permission = _admin_perm
1528 owner_row.append(usr)
1528 owner_row.append(usr)
1529
1529
1530 super_admin_ids = []
1530 super_admin_ids = []
1531 super_admin_rows = []
1531 super_admin_rows = []
1532 if with_admins:
1532 if with_admins:
1533 for usr in User.get_all_super_admins():
1533 for usr in User.get_all_super_admins():
1534 super_admin_ids.append(usr.user_id)
1534 super_admin_ids.append(usr.user_id)
1535 # if this admin is also owner, don't double the record
1535 # if this admin is also owner, don't double the record
1536 if usr.user_id == owner_row[0].user_id:
1536 if usr.user_id == owner_row[0].user_id:
1537 owner_row[0].admin_row = True
1537 owner_row[0].admin_row = True
1538 else:
1538 else:
1539 usr = AttributeDict(usr.get_dict())
1539 usr = AttributeDict(usr.get_dict())
1540 usr.admin_row = True
1540 usr.admin_row = True
1541 usr.permission = _admin_perm
1541 usr.permission = _admin_perm
1542 super_admin_rows.append(usr)
1542 super_admin_rows.append(usr)
1543
1543
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1544 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1545 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1546 joinedload(UserUserGroupToPerm.user),
1546 joinedload(UserUserGroupToPerm.user),
1547 joinedload(UserUserGroupToPerm.permission),)
1547 joinedload(UserUserGroupToPerm.permission),)
1548
1548
1549 # get owners and admins and permissions. We do a trick of re-writing
1549 # get owners and admins and permissions. We do a trick of re-writing
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1550 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1551 # has a global reference and changing one object propagates to all
1551 # has a global reference and changing one object propagates to all
1552 # others. This means if admin is also an owner admin_row that change
1552 # others. This means if admin is also an owner admin_row that change
1553 # would propagate to both objects
1553 # would propagate to both objects
1554 perm_rows = []
1554 perm_rows = []
1555 for _usr in q.all():
1555 for _usr in q.all():
1556 usr = AttributeDict(_usr.user.get_dict())
1556 usr = AttributeDict(_usr.user.get_dict())
1557 # if this user is also owner/admin, mark as duplicate record
1557 # if this user is also owner/admin, mark as duplicate record
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1558 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1559 usr.duplicate_perm = True
1559 usr.duplicate_perm = True
1560 usr.permission = _usr.permission.permission_name
1560 usr.permission = _usr.permission.permission_name
1561 perm_rows.append(usr)
1561 perm_rows.append(usr)
1562
1562
1563 # filter the perm rows by 'default' first and then sort them by
1563 # filter the perm rows by 'default' first and then sort them by
1564 # admin,write,read,none permissions sorted again alphabetically in
1564 # admin,write,read,none permissions sorted again alphabetically in
1565 # each group
1565 # each group
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1566 perm_rows = sorted(perm_rows, key=display_user_sort)
1567
1567
1568 user_groups_rows = []
1568 user_groups_rows = []
1569 if expand_from_user_groups:
1569 if expand_from_user_groups:
1570 for ug in self.permission_user_groups(with_members=True):
1570 for ug in self.permission_user_groups(with_members=True):
1571 for user_data in ug.members:
1571 for user_data in ug.members:
1572 user_groups_rows.append(user_data)
1572 user_groups_rows.append(user_data)
1573
1573
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1574 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1575
1575
1576 def permission_user_groups(self, with_members=False):
1576 def permission_user_groups(self, with_members=False):
1577 q = UserGroupUserGroupToPerm.query()\
1577 q = UserGroupUserGroupToPerm.query()\
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1578 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1579 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1580 joinedload(UserGroupUserGroupToPerm.target_user_group),
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1581 joinedload(UserGroupUserGroupToPerm.permission),)
1582
1582
1583 perm_rows = []
1583 perm_rows = []
1584 for _user_group in q.all():
1584 for _user_group in q.all():
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1585 entry = AttributeDict(_user_group.user_group.get_dict())
1586 entry.permission = _user_group.permission.permission_name
1586 entry.permission = _user_group.permission.permission_name
1587 if with_members:
1587 if with_members:
1588 entry.members = [x.user.get_dict()
1588 entry.members = [x.user.get_dict()
1589 for x in _user_group.user_group.members]
1589 for x in _user_group.user_group.members]
1590 perm_rows.append(entry)
1590 perm_rows.append(entry)
1591
1591
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1592 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1593 return perm_rows
1593 return perm_rows
1594
1594
1595 def _get_default_perms(self, user_group, suffix=''):
1595 def _get_default_perms(self, user_group, suffix=''):
1596 from rhodecode.model.permission import PermissionModel
1596 from rhodecode.model.permission import PermissionModel
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1597 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1598
1598
1599 def get_default_perms(self, suffix=''):
1599 def get_default_perms(self, suffix=''):
1600 return self._get_default_perms(self, suffix)
1600 return self._get_default_perms(self, suffix)
1601
1601
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1602 def get_api_data(self, with_group_members=True, include_secrets=False):
1603 """
1603 """
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1604 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1605 basically forwarded.
1605 basically forwarded.
1606
1606
1607 """
1607 """
1608 user_group = self
1608 user_group = self
1609 data = {
1609 data = {
1610 'users_group_id': user_group.users_group_id,
1610 'users_group_id': user_group.users_group_id,
1611 'group_name': user_group.users_group_name,
1611 'group_name': user_group.users_group_name,
1612 'group_description': user_group.user_group_description,
1612 'group_description': user_group.user_group_description,
1613 'active': user_group.users_group_active,
1613 'active': user_group.users_group_active,
1614 'owner': user_group.user.username,
1614 'owner': user_group.user.username,
1615 'sync': user_group.sync,
1615 'sync': user_group.sync,
1616 'owner_email': user_group.user.email,
1616 'owner_email': user_group.user.email,
1617 }
1617 }
1618
1618
1619 if with_group_members:
1619 if with_group_members:
1620 users = []
1620 users = []
1621 for user in user_group.members:
1621 for user in user_group.members:
1622 user = user.user
1622 user = user.user
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1623 users.append(user.get_api_data(include_secrets=include_secrets))
1624 data['users'] = users
1624 data['users'] = users
1625
1625
1626 return data
1626 return data
1627
1627
1628
1628
1629 class UserGroupMember(Base, BaseModel):
1629 class UserGroupMember(Base, BaseModel):
1630 __tablename__ = 'users_groups_members'
1630 __tablename__ = 'users_groups_members'
1631 __table_args__ = (
1631 __table_args__ = (
1632 base_table_args,
1632 base_table_args,
1633 )
1633 )
1634
1634
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1636 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1638
1638
1639 user = relationship('User', lazy='joined')
1639 user = relationship('User', lazy='joined')
1640 users_group = relationship('UserGroup')
1640 users_group = relationship('UserGroup')
1641
1641
1642 def __init__(self, gr_id='', u_id=''):
1642 def __init__(self, gr_id='', u_id=''):
1643 self.users_group_id = gr_id
1643 self.users_group_id = gr_id
1644 self.user_id = u_id
1644 self.user_id = u_id
1645
1645
1646
1646
1647 class RepositoryField(Base, BaseModel):
1647 class RepositoryField(Base, BaseModel):
1648 __tablename__ = 'repositories_fields'
1648 __tablename__ = 'repositories_fields'
1649 __table_args__ = (
1649 __table_args__ = (
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1650 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1651 base_table_args,
1651 base_table_args,
1652 )
1652 )
1653
1653
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1654 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1655
1655
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1656 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1657 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1658 field_key = Column("field_key", String(250))
1658 field_key = Column("field_key", String(250))
1659 field_label = Column("field_label", String(1024), nullable=False)
1659 field_label = Column("field_label", String(1024), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1660 field_value = Column("field_value", String(10000), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1661 field_desc = Column("field_desc", String(1024), nullable=False)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1662 field_type = Column("field_type", String(255), nullable=False, unique=None)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1663 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1664
1664
1665 repository = relationship('Repository')
1665 repository = relationship('Repository')
1666
1666
1667 @property
1667 @property
1668 def field_key_prefixed(self):
1668 def field_key_prefixed(self):
1669 return 'ex_%s' % self.field_key
1669 return 'ex_%s' % self.field_key
1670
1670
1671 @classmethod
1671 @classmethod
1672 def un_prefix_key(cls, key):
1672 def un_prefix_key(cls, key):
1673 if key.startswith(cls.PREFIX):
1673 if key.startswith(cls.PREFIX):
1674 return key[len(cls.PREFIX):]
1674 return key[len(cls.PREFIX):]
1675 return key
1675 return key
1676
1676
1677 @classmethod
1677 @classmethod
1678 def get_by_key_name(cls, key, repo):
1678 def get_by_key_name(cls, key, repo):
1679 row = cls.query()\
1679 row = cls.query()\
1680 .filter(cls.repository == repo)\
1680 .filter(cls.repository == repo)\
1681 .filter(cls.field_key == key).scalar()
1681 .filter(cls.field_key == key).scalar()
1682 return row
1682 return row
1683
1683
1684
1684
1685 class Repository(Base, BaseModel):
1685 class Repository(Base, BaseModel):
1686 __tablename__ = 'repositories'
1686 __tablename__ = 'repositories'
1687 __table_args__ = (
1687 __table_args__ = (
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1688 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1689 base_table_args,
1689 base_table_args,
1690 )
1690 )
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1691 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1692 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1693 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1694
1694
1695 STATE_CREATED = 'repo_state_created'
1695 STATE_CREATED = 'repo_state_created'
1696 STATE_PENDING = 'repo_state_pending'
1696 STATE_PENDING = 'repo_state_pending'
1697 STATE_ERROR = 'repo_state_error'
1697 STATE_ERROR = 'repo_state_error'
1698
1698
1699 LOCK_AUTOMATIC = 'lock_auto'
1699 LOCK_AUTOMATIC = 'lock_auto'
1700 LOCK_API = 'lock_api'
1700 LOCK_API = 'lock_api'
1701 LOCK_WEB = 'lock_web'
1701 LOCK_WEB = 'lock_web'
1702 LOCK_PULL = 'lock_pull'
1702 LOCK_PULL = 'lock_pull'
1703
1703
1704 NAME_SEP = URL_SEP
1704 NAME_SEP = URL_SEP
1705
1705
1706 repo_id = Column(
1706 repo_id = Column(
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1707 "repo_id", Integer(), nullable=False, unique=True, default=None,
1708 primary_key=True)
1708 primary_key=True)
1709 _repo_name = Column(
1709 _repo_name = Column(
1710 "repo_name", Text(), nullable=False, default=None)
1710 "repo_name", Text(), nullable=False, default=None)
1711 repo_name_hash = Column(
1711 repo_name_hash = Column(
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1712 "repo_name_hash", String(255), nullable=False, unique=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1713 repo_state = Column("repo_state", String(255), nullable=True)
1714
1714
1715 clone_uri = Column(
1715 clone_uri = Column(
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1716 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1717 default=None)
1717 default=None)
1718 push_uri = Column(
1718 push_uri = Column(
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1719 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1720 default=None)
1720 default=None)
1721 repo_type = Column(
1721 repo_type = Column(
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1722 "repo_type", String(255), nullable=False, unique=False, default=None)
1723 user_id = Column(
1723 user_id = Column(
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1724 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1725 unique=False, default=None)
1725 unique=False, default=None)
1726 private = Column(
1726 private = Column(
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1727 "private", Boolean(), nullable=True, unique=None, default=None)
1728 archived = Column(
1728 archived = Column(
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1729 "archived", Boolean(), nullable=True, unique=None, default=None)
1730 enable_statistics = Column(
1730 enable_statistics = Column(
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1731 "statistics", Boolean(), nullable=True, unique=None, default=True)
1732 enable_downloads = Column(
1732 enable_downloads = Column(
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1733 "downloads", Boolean(), nullable=True, unique=None, default=True)
1734 description = Column(
1734 description = Column(
1735 "description", String(10000), nullable=True, unique=None, default=None)
1735 "description", String(10000), nullable=True, unique=None, default=None)
1736 created_on = Column(
1736 created_on = Column(
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1737 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1738 default=datetime.datetime.now)
1738 default=datetime.datetime.now)
1739 updated_on = Column(
1739 updated_on = Column(
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1740 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1741 default=datetime.datetime.now)
1741 default=datetime.datetime.now)
1742 _landing_revision = Column(
1742 _landing_revision = Column(
1743 "landing_revision", String(255), nullable=False, unique=False,
1743 "landing_revision", String(255), nullable=False, unique=False,
1744 default=None)
1744 default=None)
1745 enable_locking = Column(
1745 enable_locking = Column(
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1746 "enable_locking", Boolean(), nullable=False, unique=None,
1747 default=False)
1747 default=False)
1748 _locked = Column(
1748 _locked = Column(
1749 "locked", String(255), nullable=True, unique=False, default=None)
1749 "locked", String(255), nullable=True, unique=False, default=None)
1750 _changeset_cache = Column(
1750 _changeset_cache = Column(
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1751 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1752
1752
1753 fork_id = Column(
1753 fork_id = Column(
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1754 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1755 nullable=True, unique=False, default=None)
1755 nullable=True, unique=False, default=None)
1756 group_id = Column(
1756 group_id = Column(
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1757 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1758 unique=False, default=None)
1758 unique=False, default=None)
1759
1759
1760 user = relationship('User', lazy='joined')
1760 user = relationship('User', lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1761 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1762 group = relationship('RepoGroup', lazy='joined')
1763 repo_to_perm = relationship(
1763 repo_to_perm = relationship(
1764 'UserRepoToPerm', cascade='all',
1764 'UserRepoToPerm', cascade='all',
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1765 order_by='UserRepoToPerm.repo_to_perm_id')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1766 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1767 stats = relationship('Statistics', cascade='all', uselist=False)
1768
1768
1769 followers = relationship(
1769 followers = relationship(
1770 'UserFollowing',
1770 'UserFollowing',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1771 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1772 cascade='all')
1772 cascade='all')
1773 extra_fields = relationship(
1773 extra_fields = relationship(
1774 'RepositoryField', cascade="all, delete-orphan")
1774 'RepositoryField', cascade="all, delete-orphan")
1775 logs = relationship('UserLog')
1775 logs = relationship('UserLog')
1776 comments = relationship(
1776 comments = relationship(
1777 'ChangesetComment', cascade="all, delete-orphan")
1777 'ChangesetComment', cascade="all, delete-orphan")
1778 pull_requests_source = relationship(
1778 pull_requests_source = relationship(
1779 'PullRequest',
1779 'PullRequest',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1780 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1781 cascade="all, delete-orphan")
1781 cascade="all, delete-orphan")
1782 pull_requests_target = relationship(
1782 pull_requests_target = relationship(
1783 'PullRequest',
1783 'PullRequest',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1784 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1785 cascade="all, delete-orphan")
1785 cascade="all, delete-orphan")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1786 ui = relationship('RepoRhodeCodeUi', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1787 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1788 integrations = relationship('Integration', cascade="all, delete-orphan")
1789
1789
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1790 scoped_tokens = relationship('UserApiKeys', cascade="all")
1791
1791
1792 # no cascade, set NULL
1792 # no cascade, set NULL
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1793 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1794
1794
1795 def __unicode__(self):
1795 def __unicode__(self):
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1796 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1797 safe_unicode(self.repo_name))
1797 safe_unicode(self.repo_name))
1798
1798
1799 @hybrid_property
1799 @hybrid_property
1800 def description_safe(self):
1800 def description_safe(self):
1801 from rhodecode.lib import helpers as h
1801 from rhodecode.lib import helpers as h
1802 return h.escape(self.description)
1802 return h.escape(self.description)
1803
1803
1804 @hybrid_property
1804 @hybrid_property
1805 def landing_rev(self):
1805 def landing_rev(self):
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1806 # always should return [rev_type, rev], e.g ['branch', 'master']
1807 if self._landing_revision:
1807 if self._landing_revision:
1808 _rev_info = self._landing_revision.split(':')
1808 _rev_info = self._landing_revision.split(':')
1809 if len(_rev_info) < 2:
1809 if len(_rev_info) < 2:
1810 _rev_info.insert(0, 'rev')
1810 _rev_info.insert(0, 'rev')
1811 return [_rev_info[0], _rev_info[1]]
1811 return [_rev_info[0], _rev_info[1]]
1812 return [None, None]
1812 return [None, None]
1813
1813
1814 @property
1814 @property
1815 def landing_ref_type(self):
1815 def landing_ref_type(self):
1816 return self.landing_rev[0]
1816 return self.landing_rev[0]
1817
1817
1818 @property
1818 @property
1819 def landing_ref_name(self):
1819 def landing_ref_name(self):
1820 return self.landing_rev[1]
1820 return self.landing_rev[1]
1821
1821
1822 @landing_rev.setter
1822 @landing_rev.setter
1823 def landing_rev(self, val):
1823 def landing_rev(self, val):
1824 if ':' not in val:
1824 if ':' not in val:
1825 raise ValueError('value must be delimited with `:` and consist '
1825 raise ValueError('value must be delimited with `:` and consist '
1826 'of <rev_type>:<rev>, got %s instead' % val)
1826 'of <rev_type>:<rev>, got %s instead' % val)
1827 self._landing_revision = val
1827 self._landing_revision = val
1828
1828
1829 @hybrid_property
1829 @hybrid_property
1830 def locked(self):
1830 def locked(self):
1831 if self._locked:
1831 if self._locked:
1832 user_id, timelocked, reason = self._locked.split(':')
1832 user_id, timelocked, reason = self._locked.split(':')
1833 lock_values = int(user_id), timelocked, reason
1833 lock_values = int(user_id), timelocked, reason
1834 else:
1834 else:
1835 lock_values = [None, None, None]
1835 lock_values = [None, None, None]
1836 return lock_values
1836 return lock_values
1837
1837
1838 @locked.setter
1838 @locked.setter
1839 def locked(self, val):
1839 def locked(self, val):
1840 if val and isinstance(val, (list, tuple)):
1840 if val and isinstance(val, (list, tuple)):
1841 self._locked = ':'.join(map(str, val))
1841 self._locked = ':'.join(map(str, val))
1842 else:
1842 else:
1843 self._locked = None
1843 self._locked = None
1844
1844
1845 @classmethod
1845 @classmethod
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1846 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1847 from rhodecode.lib.vcs.backends.base import EmptyCommit
1848 dummy = EmptyCommit().__json__()
1848 dummy = EmptyCommit().__json__()
1849 if not changeset_cache_raw:
1849 if not changeset_cache_raw:
1850 dummy['source_repo_id'] = repo_id
1850 dummy['source_repo_id'] = repo_id
1851 return json.loads(json.dumps(dummy))
1851 return json.loads(json.dumps(dummy))
1852
1852
1853 try:
1853 try:
1854 return json.loads(changeset_cache_raw)
1854 return json.loads(changeset_cache_raw)
1855 except TypeError:
1855 except TypeError:
1856 return dummy
1856 return dummy
1857 except Exception:
1857 except Exception:
1858 log.error(traceback.format_exc())
1858 log.error(traceback.format_exc())
1859 return dummy
1859 return dummy
1860
1860
1861 @hybrid_property
1861 @hybrid_property
1862 def changeset_cache(self):
1862 def changeset_cache(self):
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1863 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1864
1864
1865 @changeset_cache.setter
1865 @changeset_cache.setter
1866 def changeset_cache(self, val):
1866 def changeset_cache(self, val):
1867 try:
1867 try:
1868 self._changeset_cache = json.dumps(val)
1868 self._changeset_cache = json.dumps(val)
1869 except Exception:
1869 except Exception:
1870 log.error(traceback.format_exc())
1870 log.error(traceback.format_exc())
1871
1871
1872 @hybrid_property
1872 @hybrid_property
1873 def repo_name(self):
1873 def repo_name(self):
1874 return self._repo_name
1874 return self._repo_name
1875
1875
1876 @repo_name.setter
1876 @repo_name.setter
1877 def repo_name(self, value):
1877 def repo_name(self, value):
1878 self._repo_name = value
1878 self._repo_name = value
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1879 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1880
1880
1881 @classmethod
1881 @classmethod
1882 def normalize_repo_name(cls, repo_name):
1882 def normalize_repo_name(cls, repo_name):
1883 """
1883 """
1884 Normalizes os specific repo_name to the format internally stored inside
1884 Normalizes os specific repo_name to the format internally stored inside
1885 database using URL_SEP
1885 database using URL_SEP
1886
1886
1887 :param cls:
1887 :param cls:
1888 :param repo_name:
1888 :param repo_name:
1889 """
1889 """
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1890 return cls.NAME_SEP.join(repo_name.split(os.sep))
1891
1891
1892 @classmethod
1892 @classmethod
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1893 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1894 session = Session()
1894 session = Session()
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1895 q = session.query(cls).filter(cls.repo_name == repo_name)
1896
1896
1897 if cache:
1897 if cache:
1898 if identity_cache:
1898 if identity_cache:
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1899 val = cls.identity_cache(session, 'repo_name', repo_name)
1900 if val:
1900 if val:
1901 return val
1901 return val
1902 else:
1902 else:
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1903 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1904 q = q.options(
1904 q = q.options(
1905 FromCache("sql_cache_short", cache_key))
1905 FromCache("sql_cache_short", cache_key))
1906
1906
1907 return q.scalar()
1907 return q.scalar()
1908
1908
1909 @classmethod
1909 @classmethod
1910 def get_by_id_or_repo_name(cls, repoid):
1910 def get_by_id_or_repo_name(cls, repoid):
1911 if isinstance(repoid, (int, long)):
1911 if isinstance(repoid, (int, long)):
1912 try:
1912 try:
1913 repo = cls.get(repoid)
1913 repo = cls.get(repoid)
1914 except ValueError:
1914 except ValueError:
1915 repo = None
1915 repo = None
1916 else:
1916 else:
1917 repo = cls.get_by_repo_name(repoid)
1917 repo = cls.get_by_repo_name(repoid)
1918 return repo
1918 return repo
1919
1919
1920 @classmethod
1920 @classmethod
1921 def get_by_full_path(cls, repo_full_path):
1921 def get_by_full_path(cls, repo_full_path):
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1922 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1923 repo_name = cls.normalize_repo_name(repo_name)
1923 repo_name = cls.normalize_repo_name(repo_name)
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1924 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1925
1925
1926 @classmethod
1926 @classmethod
1927 def get_repo_forks(cls, repo_id):
1927 def get_repo_forks(cls, repo_id):
1928 return cls.query().filter(Repository.fork_id == repo_id)
1928 return cls.query().filter(Repository.fork_id == repo_id)
1929
1929
1930 @classmethod
1930 @classmethod
1931 def base_path(cls):
1931 def base_path(cls):
1932 """
1932 """
1933 Returns base path when all repos are stored
1933 Returns base path when all repos are stored
1934
1934
1935 :param cls:
1935 :param cls:
1936 """
1936 """
1937 q = Session().query(RhodeCodeUi)\
1937 q = Session().query(RhodeCodeUi)\
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1938 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1939 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1940 return q.one().ui_value
1940 return q.one().ui_value
1941
1941
1942 @classmethod
1942 @classmethod
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1943 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1944 case_insensitive=True, archived=False):
1944 case_insensitive=True, archived=False):
1945 q = Repository.query()
1945 q = Repository.query()
1946
1946
1947 if not archived:
1947 if not archived:
1948 q = q.filter(Repository.archived.isnot(true()))
1948 q = q.filter(Repository.archived.isnot(true()))
1949
1949
1950 if not isinstance(user_id, Optional):
1950 if not isinstance(user_id, Optional):
1951 q = q.filter(Repository.user_id == user_id)
1951 q = q.filter(Repository.user_id == user_id)
1952
1952
1953 if not isinstance(group_id, Optional):
1953 if not isinstance(group_id, Optional):
1954 q = q.filter(Repository.group_id == group_id)
1954 q = q.filter(Repository.group_id == group_id)
1955
1955
1956 if case_insensitive:
1956 if case_insensitive:
1957 q = q.order_by(func.lower(Repository.repo_name))
1957 q = q.order_by(func.lower(Repository.repo_name))
1958 else:
1958 else:
1959 q = q.order_by(Repository.repo_name)
1959 q = q.order_by(Repository.repo_name)
1960
1960
1961 return q.all()
1961 return q.all()
1962
1962
1963 @property
1963 @property
1964 def repo_uid(self):
1964 def repo_uid(self):
1965 return '_{}'.format(self.repo_id)
1965 return '_{}'.format(self.repo_id)
1966
1966
1967 @property
1967 @property
1968 def forks(self):
1968 def forks(self):
1969 """
1969 """
1970 Return forks of this repo
1970 Return forks of this repo
1971 """
1971 """
1972 return Repository.get_repo_forks(self.repo_id)
1972 return Repository.get_repo_forks(self.repo_id)
1973
1973
1974 @property
1974 @property
1975 def parent(self):
1975 def parent(self):
1976 """
1976 """
1977 Returns fork parent
1977 Returns fork parent
1978 """
1978 """
1979 return self.fork
1979 return self.fork
1980
1980
1981 @property
1981 @property
1982 def just_name(self):
1982 def just_name(self):
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1983 return self.repo_name.split(self.NAME_SEP)[-1]
1984
1984
1985 @property
1985 @property
1986 def groups_with_parents(self):
1986 def groups_with_parents(self):
1987 groups = []
1987 groups = []
1988 if self.group is None:
1988 if self.group is None:
1989 return groups
1989 return groups
1990
1990
1991 cur_gr = self.group
1991 cur_gr = self.group
1992 groups.insert(0, cur_gr)
1992 groups.insert(0, cur_gr)
1993 while 1:
1993 while 1:
1994 gr = getattr(cur_gr, 'parent_group', None)
1994 gr = getattr(cur_gr, 'parent_group', None)
1995 cur_gr = cur_gr.parent_group
1995 cur_gr = cur_gr.parent_group
1996 if gr is None:
1996 if gr is None:
1997 break
1997 break
1998 groups.insert(0, gr)
1998 groups.insert(0, gr)
1999
1999
2000 return groups
2000 return groups
2001
2001
2002 @property
2002 @property
2003 def groups_and_repo(self):
2003 def groups_and_repo(self):
2004 return self.groups_with_parents, self
2004 return self.groups_with_parents, self
2005
2005
2006 @LazyProperty
2006 @LazyProperty
2007 def repo_path(self):
2007 def repo_path(self):
2008 """
2008 """
2009 Returns base full path for that repository means where it actually
2009 Returns base full path for that repository means where it actually
2010 exists on a filesystem
2010 exists on a filesystem
2011 """
2011 """
2012 q = Session().query(RhodeCodeUi).filter(
2012 q = Session().query(RhodeCodeUi).filter(
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2013 RhodeCodeUi.ui_key == self.NAME_SEP)
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2014 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2015 return q.one().ui_value
2015 return q.one().ui_value
2016
2016
2017 @property
2017 @property
2018 def repo_full_path(self):
2018 def repo_full_path(self):
2019 p = [self.repo_path]
2019 p = [self.repo_path]
2020 # we need to split the name by / since this is how we store the
2020 # we need to split the name by / since this is how we store the
2021 # names in the database, but that eventually needs to be converted
2021 # names in the database, but that eventually needs to be converted
2022 # into a valid system path
2022 # into a valid system path
2023 p += self.repo_name.split(self.NAME_SEP)
2023 p += self.repo_name.split(self.NAME_SEP)
2024 return os.path.join(*map(safe_unicode, p))
2024 return os.path.join(*map(safe_unicode, p))
2025
2025
2026 @property
2026 @property
2027 def cache_keys(self):
2027 def cache_keys(self):
2028 """
2028 """
2029 Returns associated cache keys for that repo
2029 Returns associated cache keys for that repo
2030 """
2030 """
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2031 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2032 repo_id=self.repo_id)
2032 repo_id=self.repo_id)
2033 return CacheKey.query()\
2033 return CacheKey.query()\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2034 .filter(CacheKey.cache_args == invalidation_namespace)\
2035 .order_by(CacheKey.cache_key)\
2035 .order_by(CacheKey.cache_key)\
2036 .all()
2036 .all()
2037
2037
2038 @property
2038 @property
2039 def cached_diffs_relative_dir(self):
2039 def cached_diffs_relative_dir(self):
2040 """
2040 """
2041 Return a relative to the repository store path of cached diffs
2041 Return a relative to the repository store path of cached diffs
2042 used for safe display for users, who shouldn't know the absolute store
2042 used for safe display for users, who shouldn't know the absolute store
2043 path
2043 path
2044 """
2044 """
2045 return os.path.join(
2045 return os.path.join(
2046 os.path.dirname(self.repo_name),
2046 os.path.dirname(self.repo_name),
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2047 self.cached_diffs_dir.split(os.path.sep)[-1])
2048
2048
2049 @property
2049 @property
2050 def cached_diffs_dir(self):
2050 def cached_diffs_dir(self):
2051 path = self.repo_full_path
2051 path = self.repo_full_path
2052 return os.path.join(
2052 return os.path.join(
2053 os.path.dirname(path),
2053 os.path.dirname(path),
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2054 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2055
2055
2056 def cached_diffs(self):
2056 def cached_diffs(self):
2057 diff_cache_dir = self.cached_diffs_dir
2057 diff_cache_dir = self.cached_diffs_dir
2058 if os.path.isdir(diff_cache_dir):
2058 if os.path.isdir(diff_cache_dir):
2059 return os.listdir(diff_cache_dir)
2059 return os.listdir(diff_cache_dir)
2060 return []
2060 return []
2061
2061
2062 def shadow_repos(self):
2062 def shadow_repos(self):
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2063 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2064 return [
2064 return [
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2065 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2066 if x.startswith(shadow_repos_pattern)]
2066 if x.startswith(shadow_repos_pattern)]
2067
2067
2068 def get_new_name(self, repo_name):
2068 def get_new_name(self, repo_name):
2069 """
2069 """
2070 returns new full repository name based on assigned group and new new
2070 returns new full repository name based on assigned group and new new
2071
2071
2072 :param group_name:
2072 :param group_name:
2073 """
2073 """
2074 path_prefix = self.group.full_path_splitted if self.group else []
2074 path_prefix = self.group.full_path_splitted if self.group else []
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2075 return self.NAME_SEP.join(path_prefix + [repo_name])
2076
2076
2077 @property
2077 @property
2078 def _config(self):
2078 def _config(self):
2079 """
2079 """
2080 Returns db based config object.
2080 Returns db based config object.
2081 """
2081 """
2082 from rhodecode.lib.utils import make_db_config
2082 from rhodecode.lib.utils import make_db_config
2083 return make_db_config(clear_session=False, repo=self)
2083 return make_db_config(clear_session=False, repo=self)
2084
2084
2085 def permissions(self, with_admins=True, with_owner=True,
2085 def permissions(self, with_admins=True, with_owner=True,
2086 expand_from_user_groups=False):
2086 expand_from_user_groups=False):
2087 """
2087 """
2088 Permissions for repositories
2088 Permissions for repositories
2089 """
2089 """
2090 _admin_perm = 'repository.admin'
2090 _admin_perm = 'repository.admin'
2091
2091
2092 owner_row = []
2092 owner_row = []
2093 if with_owner:
2093 if with_owner:
2094 usr = AttributeDict(self.user.get_dict())
2094 usr = AttributeDict(self.user.get_dict())
2095 usr.owner_row = True
2095 usr.owner_row = True
2096 usr.permission = _admin_perm
2096 usr.permission = _admin_perm
2097 usr.permission_id = None
2097 usr.permission_id = None
2098 owner_row.append(usr)
2098 owner_row.append(usr)
2099
2099
2100 super_admin_ids = []
2100 super_admin_ids = []
2101 super_admin_rows = []
2101 super_admin_rows = []
2102 if with_admins:
2102 if with_admins:
2103 for usr in User.get_all_super_admins():
2103 for usr in User.get_all_super_admins():
2104 super_admin_ids.append(usr.user_id)
2104 super_admin_ids.append(usr.user_id)
2105 # if this admin is also owner, don't double the record
2105 # if this admin is also owner, don't double the record
2106 if usr.user_id == owner_row[0].user_id:
2106 if usr.user_id == owner_row[0].user_id:
2107 owner_row[0].admin_row = True
2107 owner_row[0].admin_row = True
2108 else:
2108 else:
2109 usr = AttributeDict(usr.get_dict())
2109 usr = AttributeDict(usr.get_dict())
2110 usr.admin_row = True
2110 usr.admin_row = True
2111 usr.permission = _admin_perm
2111 usr.permission = _admin_perm
2112 usr.permission_id = None
2112 usr.permission_id = None
2113 super_admin_rows.append(usr)
2113 super_admin_rows.append(usr)
2114
2114
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2115 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2116 q = q.options(joinedload(UserRepoToPerm.repository),
2117 joinedload(UserRepoToPerm.user),
2117 joinedload(UserRepoToPerm.user),
2118 joinedload(UserRepoToPerm.permission),)
2118 joinedload(UserRepoToPerm.permission),)
2119
2119
2120 # get owners and admins and permissions. We do a trick of re-writing
2120 # get owners and admins and permissions. We do a trick of re-writing
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2121 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2122 # has a global reference and changing one object propagates to all
2122 # has a global reference and changing one object propagates to all
2123 # others. This means if admin is also an owner admin_row that change
2123 # others. This means if admin is also an owner admin_row that change
2124 # would propagate to both objects
2124 # would propagate to both objects
2125 perm_rows = []
2125 perm_rows = []
2126 for _usr in q.all():
2126 for _usr in q.all():
2127 usr = AttributeDict(_usr.user.get_dict())
2127 usr = AttributeDict(_usr.user.get_dict())
2128 # if this user is also owner/admin, mark as duplicate record
2128 # if this user is also owner/admin, mark as duplicate record
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2129 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2130 usr.duplicate_perm = True
2130 usr.duplicate_perm = True
2131 # also check if this permission is maybe used by branch_permissions
2131 # also check if this permission is maybe used by branch_permissions
2132 if _usr.branch_perm_entry:
2132 if _usr.branch_perm_entry:
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2133 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2134
2134
2135 usr.permission = _usr.permission.permission_name
2135 usr.permission = _usr.permission.permission_name
2136 usr.permission_id = _usr.repo_to_perm_id
2136 usr.permission_id = _usr.repo_to_perm_id
2137 perm_rows.append(usr)
2137 perm_rows.append(usr)
2138
2138
2139 # filter the perm rows by 'default' first and then sort them by
2139 # filter the perm rows by 'default' first and then sort them by
2140 # admin,write,read,none permissions sorted again alphabetically in
2140 # admin,write,read,none permissions sorted again alphabetically in
2141 # each group
2141 # each group
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2142 perm_rows = sorted(perm_rows, key=display_user_sort)
2143
2143
2144 user_groups_rows = []
2144 user_groups_rows = []
2145 if expand_from_user_groups:
2145 if expand_from_user_groups:
2146 for ug in self.permission_user_groups(with_members=True):
2146 for ug in self.permission_user_groups(with_members=True):
2147 for user_data in ug.members:
2147 for user_data in ug.members:
2148 user_groups_rows.append(user_data)
2148 user_groups_rows.append(user_data)
2149
2149
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2150 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2151
2151
2152 def permission_user_groups(self, with_members=True):
2152 def permission_user_groups(self, with_members=True):
2153 q = UserGroupRepoToPerm.query()\
2153 q = UserGroupRepoToPerm.query()\
2154 .filter(UserGroupRepoToPerm.repository == self)
2154 .filter(UserGroupRepoToPerm.repository == self)
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2155 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2156 joinedload(UserGroupRepoToPerm.users_group),
2156 joinedload(UserGroupRepoToPerm.users_group),
2157 joinedload(UserGroupRepoToPerm.permission),)
2157 joinedload(UserGroupRepoToPerm.permission),)
2158
2158
2159 perm_rows = []
2159 perm_rows = []
2160 for _user_group in q.all():
2160 for _user_group in q.all():
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2161 entry = AttributeDict(_user_group.users_group.get_dict())
2162 entry.permission = _user_group.permission.permission_name
2162 entry.permission = _user_group.permission.permission_name
2163 if with_members:
2163 if with_members:
2164 entry.members = [x.user.get_dict()
2164 entry.members = [x.user.get_dict()
2165 for x in _user_group.users_group.members]
2165 for x in _user_group.users_group.members]
2166 perm_rows.append(entry)
2166 perm_rows.append(entry)
2167
2167
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2168 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2169 return perm_rows
2169 return perm_rows
2170
2170
2171 def get_api_data(self, include_secrets=False):
2171 def get_api_data(self, include_secrets=False):
2172 """
2172 """
2173 Common function for generating repo api data
2173 Common function for generating repo api data
2174
2174
2175 :param include_secrets: See :meth:`User.get_api_data`.
2175 :param include_secrets: See :meth:`User.get_api_data`.
2176
2176
2177 """
2177 """
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2178 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2179 # move this methods on models level.
2179 # move this methods on models level.
2180 from rhodecode.model.settings import SettingsModel
2180 from rhodecode.model.settings import SettingsModel
2181 from rhodecode.model.repo import RepoModel
2181 from rhodecode.model.repo import RepoModel
2182
2182
2183 repo = self
2183 repo = self
2184 _user_id, _time, _reason = self.locked
2184 _user_id, _time, _reason = self.locked
2185
2185
2186 data = {
2186 data = {
2187 'repo_id': repo.repo_id,
2187 'repo_id': repo.repo_id,
2188 'repo_name': repo.repo_name,
2188 'repo_name': repo.repo_name,
2189 'repo_type': repo.repo_type,
2189 'repo_type': repo.repo_type,
2190 'clone_uri': repo.clone_uri or '',
2190 'clone_uri': repo.clone_uri or '',
2191 'push_uri': repo.push_uri or '',
2191 'push_uri': repo.push_uri or '',
2192 'url': RepoModel().get_url(self),
2192 'url': RepoModel().get_url(self),
2193 'private': repo.private,
2193 'private': repo.private,
2194 'created_on': repo.created_on,
2194 'created_on': repo.created_on,
2195 'description': repo.description_safe,
2195 'description': repo.description_safe,
2196 'landing_rev': repo.landing_rev,
2196 'landing_rev': repo.landing_rev,
2197 'owner': repo.user.username,
2197 'owner': repo.user.username,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2198 'fork_of': repo.fork.repo_name if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2199 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2200 'enable_statistics': repo.enable_statistics,
2200 'enable_statistics': repo.enable_statistics,
2201 'enable_locking': repo.enable_locking,
2201 'enable_locking': repo.enable_locking,
2202 'enable_downloads': repo.enable_downloads,
2202 'enable_downloads': repo.enable_downloads,
2203 'last_changeset': repo.changeset_cache,
2203 'last_changeset': repo.changeset_cache,
2204 'locked_by': User.get(_user_id).get_api_data(
2204 'locked_by': User.get(_user_id).get_api_data(
2205 include_secrets=include_secrets) if _user_id else None,
2205 include_secrets=include_secrets) if _user_id else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2206 'locked_date': time_to_datetime(_time) if _time else None,
2207 'lock_reason': _reason if _reason else None,
2207 'lock_reason': _reason if _reason else None,
2208 }
2208 }
2209
2209
2210 # TODO: mikhail: should be per-repo settings here
2210 # TODO: mikhail: should be per-repo settings here
2211 rc_config = SettingsModel().get_all_settings()
2211 rc_config = SettingsModel().get_all_settings()
2212 repository_fields = str2bool(
2212 repository_fields = str2bool(
2213 rc_config.get('rhodecode_repository_fields'))
2213 rc_config.get('rhodecode_repository_fields'))
2214 if repository_fields:
2214 if repository_fields:
2215 for f in self.extra_fields:
2215 for f in self.extra_fields:
2216 data[f.field_key_prefixed] = f.field_value
2216 data[f.field_key_prefixed] = f.field_value
2217
2217
2218 return data
2218 return data
2219
2219
2220 @classmethod
2220 @classmethod
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2221 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2222 if not lock_time:
2222 if not lock_time:
2223 lock_time = time.time()
2223 lock_time = time.time()
2224 if not lock_reason:
2224 if not lock_reason:
2225 lock_reason = cls.LOCK_AUTOMATIC
2225 lock_reason = cls.LOCK_AUTOMATIC
2226 repo.locked = [user_id, lock_time, lock_reason]
2226 repo.locked = [user_id, lock_time, lock_reason]
2227 Session().add(repo)
2227 Session().add(repo)
2228 Session().commit()
2228 Session().commit()
2229
2229
2230 @classmethod
2230 @classmethod
2231 def unlock(cls, repo):
2231 def unlock(cls, repo):
2232 repo.locked = None
2232 repo.locked = None
2233 Session().add(repo)
2233 Session().add(repo)
2234 Session().commit()
2234 Session().commit()
2235
2235
2236 @classmethod
2236 @classmethod
2237 def getlock(cls, repo):
2237 def getlock(cls, repo):
2238 return repo.locked
2238 return repo.locked
2239
2239
2240 def is_user_lock(self, user_id):
2240 def is_user_lock(self, user_id):
2241 if self.lock[0]:
2241 if self.lock[0]:
2242 lock_user_id = safe_int(self.lock[0])
2242 lock_user_id = safe_int(self.lock[0])
2243 user_id = safe_int(user_id)
2243 user_id = safe_int(user_id)
2244 # both are ints, and they are equal
2244 # both are ints, and they are equal
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2245 return all([lock_user_id, user_id]) and lock_user_id == user_id
2246
2246
2247 return False
2247 return False
2248
2248
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2249 def get_locking_state(self, action, user_id, only_when_enabled=True):
2250 """
2250 """
2251 Checks locking on this repository, if locking is enabled and lock is
2251 Checks locking on this repository, if locking is enabled and lock is
2252 present returns a tuple of make_lock, locked, locked_by.
2252 present returns a tuple of make_lock, locked, locked_by.
2253 make_lock can have 3 states None (do nothing) True, make lock
2253 make_lock can have 3 states None (do nothing) True, make lock
2254 False release lock, This value is later propagated to hooks, which
2254 False release lock, This value is later propagated to hooks, which
2255 do the locking. Think about this as signals passed to hooks what to do.
2255 do the locking. Think about this as signals passed to hooks what to do.
2256
2256
2257 """
2257 """
2258 # TODO: johbo: This is part of the business logic and should be moved
2258 # TODO: johbo: This is part of the business logic and should be moved
2259 # into the RepositoryModel.
2259 # into the RepositoryModel.
2260
2260
2261 if action not in ('push', 'pull'):
2261 if action not in ('push', 'pull'):
2262 raise ValueError("Invalid action value: %s" % repr(action))
2262 raise ValueError("Invalid action value: %s" % repr(action))
2263
2263
2264 # defines if locked error should be thrown to user
2264 # defines if locked error should be thrown to user
2265 currently_locked = False
2265 currently_locked = False
2266 # defines if new lock should be made, tri-state
2266 # defines if new lock should be made, tri-state
2267 make_lock = None
2267 make_lock = None
2268 repo = self
2268 repo = self
2269 user = User.get(user_id)
2269 user = User.get(user_id)
2270
2270
2271 lock_info = repo.locked
2271 lock_info = repo.locked
2272
2272
2273 if repo and (repo.enable_locking or not only_when_enabled):
2273 if repo and (repo.enable_locking or not only_when_enabled):
2274 if action == 'push':
2274 if action == 'push':
2275 # check if it's already locked !, if it is compare users
2275 # check if it's already locked !, if it is compare users
2276 locked_by_user_id = lock_info[0]
2276 locked_by_user_id = lock_info[0]
2277 if user.user_id == locked_by_user_id:
2277 if user.user_id == locked_by_user_id:
2278 log.debug(
2278 log.debug(
2279 'Got `push` action from user %s, now unlocking', user)
2279 'Got `push` action from user %s, now unlocking', user)
2280 # unlock if we have push from user who locked
2280 # unlock if we have push from user who locked
2281 make_lock = False
2281 make_lock = False
2282 else:
2282 else:
2283 # we're not the same user who locked, ban with
2283 # we're not the same user who locked, ban with
2284 # code defined in settings (default is 423 HTTP Locked) !
2284 # code defined in settings (default is 423 HTTP Locked) !
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2285 log.debug('Repo %s is currently locked by %s', repo, user)
2286 currently_locked = True
2286 currently_locked = True
2287 elif action == 'pull':
2287 elif action == 'pull':
2288 # [0] user [1] date
2288 # [0] user [1] date
2289 if lock_info[0] and lock_info[1]:
2289 if lock_info[0] and lock_info[1]:
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2290 log.debug('Repo %s is currently locked by %s', repo, user)
2291 currently_locked = True
2291 currently_locked = True
2292 else:
2292 else:
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2293 log.debug('Setting lock on repo %s by %s', repo, user)
2294 make_lock = True
2294 make_lock = True
2295
2295
2296 else:
2296 else:
2297 log.debug('Repository %s do not have locking enabled', repo)
2297 log.debug('Repository %s do not have locking enabled', repo)
2298
2298
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2299 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2300 make_lock, currently_locked, lock_info)
2300 make_lock, currently_locked, lock_info)
2301
2301
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2302 from rhodecode.lib.auth import HasRepoPermissionAny
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2303 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2304 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2305 # if we don't have at least write permission we cannot make a lock
2305 # if we don't have at least write permission we cannot make a lock
2306 log.debug('lock state reset back to FALSE due to lack '
2306 log.debug('lock state reset back to FALSE due to lack '
2307 'of at least read permission')
2307 'of at least read permission')
2308 make_lock = False
2308 make_lock = False
2309
2309
2310 return make_lock, currently_locked, lock_info
2310 return make_lock, currently_locked, lock_info
2311
2311
2312 @property
2312 @property
2313 def last_commit_cache_update_diff(self):
2313 def last_commit_cache_update_diff(self):
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2314 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2315
2315
2316 @classmethod
2316 @classmethod
2317 def _load_commit_change(cls, last_commit_cache):
2317 def _load_commit_change(cls, last_commit_cache):
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2318 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2319 empty_date = datetime.datetime.fromtimestamp(0)
2319 empty_date = datetime.datetime.fromtimestamp(0)
2320 date_latest = last_commit_cache.get('date', empty_date)
2320 date_latest = last_commit_cache.get('date', empty_date)
2321 try:
2321 try:
2322 return parse_datetime(date_latest)
2322 return parse_datetime(date_latest)
2323 except Exception:
2323 except Exception:
2324 return empty_date
2324 return empty_date
2325
2325
2326 @property
2326 @property
2327 def last_commit_change(self):
2327 def last_commit_change(self):
2328 return self._load_commit_change(self.changeset_cache)
2328 return self._load_commit_change(self.changeset_cache)
2329
2329
2330 @property
2330 @property
2331 def last_db_change(self):
2331 def last_db_change(self):
2332 return self.updated_on
2332 return self.updated_on
2333
2333
2334 @property
2334 @property
2335 def clone_uri_hidden(self):
2335 def clone_uri_hidden(self):
2336 clone_uri = self.clone_uri
2336 clone_uri = self.clone_uri
2337 if clone_uri:
2337 if clone_uri:
2338 import urlobject
2338 import urlobject
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2339 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2340 if url_obj.password:
2340 if url_obj.password:
2341 clone_uri = url_obj.with_password('*****')
2341 clone_uri = url_obj.with_password('*****')
2342 return clone_uri
2342 return clone_uri
2343
2343
2344 @property
2344 @property
2345 def push_uri_hidden(self):
2345 def push_uri_hidden(self):
2346 push_uri = self.push_uri
2346 push_uri = self.push_uri
2347 if push_uri:
2347 if push_uri:
2348 import urlobject
2348 import urlobject
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2349 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2350 if url_obj.password:
2350 if url_obj.password:
2351 push_uri = url_obj.with_password('*****')
2351 push_uri = url_obj.with_password('*****')
2352 return push_uri
2352 return push_uri
2353
2353
2354 def clone_url(self, **override):
2354 def clone_url(self, **override):
2355 from rhodecode.model.settings import SettingsModel
2355 from rhodecode.model.settings import SettingsModel
2356
2356
2357 uri_tmpl = None
2357 uri_tmpl = None
2358 if 'with_id' in override:
2358 if 'with_id' in override:
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2359 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2360 del override['with_id']
2360 del override['with_id']
2361
2361
2362 if 'uri_tmpl' in override:
2362 if 'uri_tmpl' in override:
2363 uri_tmpl = override['uri_tmpl']
2363 uri_tmpl = override['uri_tmpl']
2364 del override['uri_tmpl']
2364 del override['uri_tmpl']
2365
2365
2366 ssh = False
2366 ssh = False
2367 if 'ssh' in override:
2367 if 'ssh' in override:
2368 ssh = True
2368 ssh = True
2369 del override['ssh']
2369 del override['ssh']
2370
2370
2371 # we didn't override our tmpl from **overrides
2371 # we didn't override our tmpl from **overrides
2372 request = get_current_request()
2372 request = get_current_request()
2373 if not uri_tmpl:
2373 if not uri_tmpl:
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2374 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2375 rc_config = request.call_context.rc_config
2375 rc_config = request.call_context.rc_config
2376 else:
2376 else:
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2377 rc_config = SettingsModel().get_all_settings(cache=True)
2378
2378
2379 if ssh:
2379 if ssh:
2380 uri_tmpl = rc_config.get(
2380 uri_tmpl = rc_config.get(
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2381 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2382
2382
2383 else:
2383 else:
2384 uri_tmpl = rc_config.get(
2384 uri_tmpl = rc_config.get(
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2385 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2386
2386
2387 return get_clone_url(request=request,
2387 return get_clone_url(request=request,
2388 uri_tmpl=uri_tmpl,
2388 uri_tmpl=uri_tmpl,
2389 repo_name=self.repo_name,
2389 repo_name=self.repo_name,
2390 repo_id=self.repo_id,
2390 repo_id=self.repo_id,
2391 repo_type=self.repo_type,
2391 repo_type=self.repo_type,
2392 **override)
2392 **override)
2393
2393
2394 def set_state(self, state):
2394 def set_state(self, state):
2395 self.repo_state = state
2395 self.repo_state = state
2396 Session().add(self)
2396 Session().add(self)
2397 #==========================================================================
2397 #==========================================================================
2398 # SCM PROPERTIES
2398 # SCM PROPERTIES
2399 #==========================================================================
2399 #==========================================================================
2400
2400
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2401 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False, reference_obj=None):
2402 return get_commit_safe(
2402 return get_commit_safe(
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2403 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2404 maybe_unreachable=maybe_unreachable, reference_obj=reference_obj)
2405
2405
2406 def get_changeset(self, rev=None, pre_load=None):
2406 def get_changeset(self, rev=None, pre_load=None):
2407 warnings.warn("Use get_commit", DeprecationWarning)
2407 warnings.warn("Use get_commit", DeprecationWarning)
2408 commit_id = None
2408 commit_id = None
2409 commit_idx = None
2409 commit_idx = None
2410 if isinstance(rev, compat.string_types):
2410 if isinstance(rev, compat.string_types):
2411 commit_id = rev
2411 commit_id = rev
2412 else:
2412 else:
2413 commit_idx = rev
2413 commit_idx = rev
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2414 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2415 pre_load=pre_load)
2415 pre_load=pre_load)
2416
2416
2417 def get_landing_commit(self):
2417 def get_landing_commit(self):
2418 """
2418 """
2419 Returns landing commit, or if that doesn't exist returns the tip
2419 Returns landing commit, or if that doesn't exist returns the tip
2420 """
2420 """
2421 _rev_type, _rev = self.landing_rev
2421 _rev_type, _rev = self.landing_rev
2422 commit = self.get_commit(_rev)
2422 commit = self.get_commit(_rev)
2423 if isinstance(commit, EmptyCommit):
2423 if isinstance(commit, EmptyCommit):
2424 return self.get_commit()
2424 return self.get_commit()
2425 return commit
2425 return commit
2426
2426
2427 def flush_commit_cache(self):
2427 def flush_commit_cache(self):
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2428 self.update_commit_cache(cs_cache={'raw_id':'0'})
2429 self.update_commit_cache()
2429 self.update_commit_cache()
2430
2430
2431 def update_commit_cache(self, cs_cache=None, config=None):
2431 def update_commit_cache(self, cs_cache=None, config=None):
2432 """
2432 """
2433 Update cache of last commit for repository
2433 Update cache of last commit for repository
2434 cache_keys should be::
2434 cache_keys should be::
2435
2435
2436 source_repo_id
2436 source_repo_id
2437 short_id
2437 short_id
2438 raw_id
2438 raw_id
2439 revision
2439 revision
2440 parents
2440 parents
2441 message
2441 message
2442 date
2442 date
2443 author
2443 author
2444 updated_on
2444 updated_on
2445
2445
2446 """
2446 """
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2447 from rhodecode.lib.vcs.backends.base import BaseChangeset
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2448 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2449 empty_date = datetime.datetime.fromtimestamp(0)
2449 empty_date = datetime.datetime.fromtimestamp(0)
2450
2450
2451 if cs_cache is None:
2451 if cs_cache is None:
2452 # use no-cache version here
2452 # use no-cache version here
2453 try:
2453 try:
2454 scm_repo = self.scm_instance(cache=False, config=config)
2454 scm_repo = self.scm_instance(cache=False, config=config)
2455 except VCSError:
2455 except VCSError:
2456 scm_repo = None
2456 scm_repo = None
2457 empty = scm_repo is None or scm_repo.is_empty()
2457 empty = scm_repo is None or scm_repo.is_empty()
2458
2458
2459 if not empty:
2459 if not empty:
2460 cs_cache = scm_repo.get_commit(
2460 cs_cache = scm_repo.get_commit(
2461 pre_load=["author", "date", "message", "parents", "branch"])
2461 pre_load=["author", "date", "message", "parents", "branch"])
2462 else:
2462 else:
2463 cs_cache = EmptyCommit()
2463 cs_cache = EmptyCommit()
2464
2464
2465 if isinstance(cs_cache, BaseChangeset):
2465 if isinstance(cs_cache, BaseChangeset):
2466 cs_cache = cs_cache.__json__()
2466 cs_cache = cs_cache.__json__()
2467
2467
2468 def is_outdated(new_cs_cache):
2468 def is_outdated(new_cs_cache):
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2469 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2470 new_cs_cache['revision'] != self.changeset_cache['revision']):
2471 return True
2471 return True
2472 return False
2472 return False
2473
2473
2474 # check if we have maybe already latest cached revision
2474 # check if we have maybe already latest cached revision
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2475 if is_outdated(cs_cache) or not self.changeset_cache:
2476 _current_datetime = datetime.datetime.utcnow()
2476 _current_datetime = datetime.datetime.utcnow()
2477 last_change = cs_cache.get('date') or _current_datetime
2477 last_change = cs_cache.get('date') or _current_datetime
2478 # we check if last update is newer than the new value
2478 # we check if last update is newer than the new value
2479 # if yes, we use the current timestamp instead. Imagine you get
2479 # if yes, we use the current timestamp instead. Imagine you get
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2480 # old commit pushed 1y ago, we'd set last update 1y to ago.
2481 last_change_timestamp = datetime_to_time(last_change)
2481 last_change_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2482 current_timestamp = datetime_to_time(last_change)
2483 if last_change_timestamp > current_timestamp and not empty:
2483 if last_change_timestamp > current_timestamp and not empty:
2484 cs_cache['date'] = _current_datetime
2484 cs_cache['date'] = _current_datetime
2485
2485
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2486 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2487 cs_cache['updated_on'] = time.time()
2487 cs_cache['updated_on'] = time.time()
2488 self.changeset_cache = cs_cache
2488 self.changeset_cache = cs_cache
2489 self.updated_on = last_change
2489 self.updated_on = last_change
2490 Session().add(self)
2490 Session().add(self)
2491 Session().commit()
2491 Session().commit()
2492
2492
2493 else:
2493 else:
2494 if empty:
2494 if empty:
2495 cs_cache = EmptyCommit().__json__()
2495 cs_cache = EmptyCommit().__json__()
2496 else:
2496 else:
2497 cs_cache = self.changeset_cache
2497 cs_cache = self.changeset_cache
2498
2498
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2499 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2500
2500
2501 cs_cache['updated_on'] = time.time()
2501 cs_cache['updated_on'] = time.time()
2502 self.changeset_cache = cs_cache
2502 self.changeset_cache = cs_cache
2503 self.updated_on = _date_latest
2503 self.updated_on = _date_latest
2504 Session().add(self)
2504 Session().add(self)
2505 Session().commit()
2505 Session().commit()
2506
2506
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2507 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2508 self.repo_name, cs_cache, _date_latest)
2508 self.repo_name, cs_cache, _date_latest)
2509
2509
2510 @property
2510 @property
2511 def tip(self):
2511 def tip(self):
2512 return self.get_commit('tip')
2512 return self.get_commit('tip')
2513
2513
2514 @property
2514 @property
2515 def author(self):
2515 def author(self):
2516 return self.tip.author
2516 return self.tip.author
2517
2517
2518 @property
2518 @property
2519 def last_change(self):
2519 def last_change(self):
2520 return self.scm_instance().last_change
2520 return self.scm_instance().last_change
2521
2521
2522 def get_comments(self, revisions=None):
2522 def get_comments(self, revisions=None):
2523 """
2523 """
2524 Returns comments for this repository grouped by revisions
2524 Returns comments for this repository grouped by revisions
2525
2525
2526 :param revisions: filter query by revisions only
2526 :param revisions: filter query by revisions only
2527 """
2527 """
2528 cmts = ChangesetComment.query()\
2528 cmts = ChangesetComment.query()\
2529 .filter(ChangesetComment.repo == self)
2529 .filter(ChangesetComment.repo == self)
2530 if revisions:
2530 if revisions:
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2531 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2532 grouped = collections.defaultdict(list)
2532 grouped = collections.defaultdict(list)
2533 for cmt in cmts.all():
2533 for cmt in cmts.all():
2534 grouped[cmt.revision].append(cmt)
2534 grouped[cmt.revision].append(cmt)
2535 return grouped
2535 return grouped
2536
2536
2537 def statuses(self, revisions=None):
2537 def statuses(self, revisions=None):
2538 """
2538 """
2539 Returns statuses for this repository
2539 Returns statuses for this repository
2540
2540
2541 :param revisions: list of revisions to get statuses for
2541 :param revisions: list of revisions to get statuses for
2542 """
2542 """
2543 statuses = ChangesetStatus.query()\
2543 statuses = ChangesetStatus.query()\
2544 .filter(ChangesetStatus.repo == self)\
2544 .filter(ChangesetStatus.repo == self)\
2545 .filter(ChangesetStatus.version == 0)
2545 .filter(ChangesetStatus.version == 0)
2546
2546
2547 if revisions:
2547 if revisions:
2548 # Try doing the filtering in chunks to avoid hitting limits
2548 # Try doing the filtering in chunks to avoid hitting limits
2549 size = 500
2549 size = 500
2550 status_results = []
2550 status_results = []
2551 for chunk in xrange(0, len(revisions), size):
2551 for chunk in xrange(0, len(revisions), size):
2552 status_results += statuses.filter(
2552 status_results += statuses.filter(
2553 ChangesetStatus.revision.in_(
2553 ChangesetStatus.revision.in_(
2554 revisions[chunk: chunk+size])
2554 revisions[chunk: chunk+size])
2555 ).all()
2555 ).all()
2556 else:
2556 else:
2557 status_results = statuses.all()
2557 status_results = statuses.all()
2558
2558
2559 grouped = {}
2559 grouped = {}
2560
2560
2561 # maybe we have open new pullrequest without a status?
2561 # maybe we have open new pullrequest without a status?
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2562 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2563 status_lbl = ChangesetStatus.get_status_lbl(stat)
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2564 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2565 for rev in pr.revisions:
2565 for rev in pr.revisions:
2566 pr_id = pr.pull_request_id
2566 pr_id = pr.pull_request_id
2567 pr_repo = pr.target_repo.repo_name
2567 pr_repo = pr.target_repo.repo_name
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2568 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2569
2569
2570 for stat in status_results:
2570 for stat in status_results:
2571 pr_id = pr_repo = None
2571 pr_id = pr_repo = None
2572 if stat.pull_request:
2572 if stat.pull_request:
2573 pr_id = stat.pull_request.pull_request_id
2573 pr_id = stat.pull_request.pull_request_id
2574 pr_repo = stat.pull_request.target_repo.repo_name
2574 pr_repo = stat.pull_request.target_repo.repo_name
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2575 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2576 pr_id, pr_repo]
2576 pr_id, pr_repo]
2577 return grouped
2577 return grouped
2578
2578
2579 # ==========================================================================
2579 # ==========================================================================
2580 # SCM CACHE INSTANCE
2580 # SCM CACHE INSTANCE
2581 # ==========================================================================
2581 # ==========================================================================
2582
2582
2583 def scm_instance(self, **kwargs):
2583 def scm_instance(self, **kwargs):
2584 import rhodecode
2584 import rhodecode
2585
2585
2586 # Passing a config will not hit the cache currently only used
2586 # Passing a config will not hit the cache currently only used
2587 # for repo2dbmapper
2587 # for repo2dbmapper
2588 config = kwargs.pop('config', None)
2588 config = kwargs.pop('config', None)
2589 cache = kwargs.pop('cache', None)
2589 cache = kwargs.pop('cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2590 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2591 if vcs_full_cache is not None:
2591 if vcs_full_cache is not None:
2592 # allows override global config
2592 # allows override global config
2593 full_cache = vcs_full_cache
2593 full_cache = vcs_full_cache
2594 else:
2594 else:
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2595 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2596 # if cache is NOT defined use default global, else we have a full
2596 # if cache is NOT defined use default global, else we have a full
2597 # control over cache behaviour
2597 # control over cache behaviour
2598 if cache is None and full_cache and not config:
2598 if cache is None and full_cache and not config:
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2599 log.debug('Initializing pure cached instance for %s', self.repo_path)
2600 return self._get_instance_cached()
2600 return self._get_instance_cached()
2601
2601
2602 # cache here is sent to the "vcs server"
2602 # cache here is sent to the "vcs server"
2603 return self._get_instance(cache=bool(cache), config=config)
2603 return self._get_instance(cache=bool(cache), config=config)
2604
2604
2605 def _get_instance_cached(self):
2605 def _get_instance_cached(self):
2606 from rhodecode.lib import rc_cache
2606 from rhodecode.lib import rc_cache
2607
2607
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2608 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2609 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2610 repo_id=self.repo_id)
2610 repo_id=self.repo_id)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2611 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2612
2612
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2613 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2614 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2615 return self._get_instance(repo_state_uid=_cache_state_uid)
2616
2616
2617 # we must use thread scoped cache here,
2617 # we must use thread scoped cache here,
2618 # because each thread of gevent needs it's own not shared connection and cache
2618 # because each thread of gevent needs it's own not shared connection and cache
2619 # we also alter `args` so the cache key is individual for every green thread.
2619 # we also alter `args` so the cache key is individual for every green thread.
2620 inv_context_manager = rc_cache.InvalidationContext(
2620 inv_context_manager = rc_cache.InvalidationContext(
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2621 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2622 thread_scoped=True)
2622 thread_scoped=True)
2623 with inv_context_manager as invalidation_context:
2623 with inv_context_manager as invalidation_context:
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2624 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2625 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2626
2626
2627 # re-compute and store cache if we get invalidate signal
2627 # re-compute and store cache if we get invalidate signal
2628 if invalidation_context.should_invalidate():
2628 if invalidation_context.should_invalidate():
2629 instance = get_instance_cached.refresh(*args)
2629 instance = get_instance_cached.refresh(*args)
2630 else:
2630 else:
2631 instance = get_instance_cached(*args)
2631 instance = get_instance_cached(*args)
2632
2632
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2633 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2634 return instance
2634 return instance
2635
2635
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2636 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2637 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2638 self.repo_type, self.repo_path, cache)
2638 self.repo_type, self.repo_path, cache)
2639 config = config or self._config
2639 config = config or self._config
2640 custom_wire = {
2640 custom_wire = {
2641 'cache': cache, # controls the vcs.remote cache
2641 'cache': cache, # controls the vcs.remote cache
2642 'repo_state_uid': repo_state_uid
2642 'repo_state_uid': repo_state_uid
2643 }
2643 }
2644 repo = get_vcs_instance(
2644 repo = get_vcs_instance(
2645 repo_path=safe_str(self.repo_full_path),
2645 repo_path=safe_str(self.repo_full_path),
2646 config=config,
2646 config=config,
2647 with_wire=custom_wire,
2647 with_wire=custom_wire,
2648 create=False,
2648 create=False,
2649 _vcs_alias=self.repo_type)
2649 _vcs_alias=self.repo_type)
2650 if repo is not None:
2650 if repo is not None:
2651 repo.count() # cache rebuild
2651 repo.count() # cache rebuild
2652 return repo
2652 return repo
2653
2653
2654 def get_shadow_repository_path(self, workspace_id):
2654 def get_shadow_repository_path(self, workspace_id):
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2655 from rhodecode.lib.vcs.backends.base import BaseRepository
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2656 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2657 self.repo_full_path, self.repo_id, workspace_id)
2657 self.repo_full_path, self.repo_id, workspace_id)
2658 return shadow_repo_path
2658 return shadow_repo_path
2659
2659
2660 def __json__(self):
2660 def __json__(self):
2661 return {'landing_rev': self.landing_rev}
2661 return {'landing_rev': self.landing_rev}
2662
2662
2663 def get_dict(self):
2663 def get_dict(self):
2664
2664
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2665 # Since we transformed `repo_name` to a hybrid property, we need to
2666 # keep compatibility with the code which uses `repo_name` field.
2666 # keep compatibility with the code which uses `repo_name` field.
2667
2667
2668 result = super(Repository, self).get_dict()
2668 result = super(Repository, self).get_dict()
2669 result['repo_name'] = result.pop('_repo_name', None)
2669 result['repo_name'] = result.pop('_repo_name', None)
2670 return result
2670 return result
2671
2671
2672
2672
2673 class RepoGroup(Base, BaseModel):
2673 class RepoGroup(Base, BaseModel):
2674 __tablename__ = 'groups'
2674 __tablename__ = 'groups'
2675 __table_args__ = (
2675 __table_args__ = (
2676 UniqueConstraint('group_name', 'group_parent_id'),
2676 UniqueConstraint('group_name', 'group_parent_id'),
2677 base_table_args,
2677 base_table_args,
2678 )
2678 )
2679 __mapper_args__ = {'order_by': 'group_name'}
2679 __mapper_args__ = {'order_by': 'group_name'}
2680
2680
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2681 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2682
2682
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2683 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2684 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2685 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2686 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2687 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2688 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2689 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2690 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2691 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2692 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2693 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2694
2694
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2695 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2696 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2697 parent_group = relationship('RepoGroup', remote_side=group_id)
2698 user = relationship('User')
2698 user = relationship('User')
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2699 integrations = relationship('Integration', cascade="all, delete-orphan")
2700
2700
2701 # no cascade, set NULL
2701 # no cascade, set NULL
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2702 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2703
2703
2704 def __init__(self, group_name='', parent_group=None):
2704 def __init__(self, group_name='', parent_group=None):
2705 self.group_name = group_name
2705 self.group_name = group_name
2706 self.parent_group = parent_group
2706 self.parent_group = parent_group
2707
2707
2708 def __unicode__(self):
2708 def __unicode__(self):
2709 return u"<%s('id:%s:%s')>" % (
2709 return u"<%s('id:%s:%s')>" % (
2710 self.__class__.__name__, self.group_id, self.group_name)
2710 self.__class__.__name__, self.group_id, self.group_name)
2711
2711
2712 @hybrid_property
2712 @hybrid_property
2713 def group_name(self):
2713 def group_name(self):
2714 return self._group_name
2714 return self._group_name
2715
2715
2716 @group_name.setter
2716 @group_name.setter
2717 def group_name(self, value):
2717 def group_name(self, value):
2718 self._group_name = value
2718 self._group_name = value
2719 self.group_name_hash = self.hash_repo_group_name(value)
2719 self.group_name_hash = self.hash_repo_group_name(value)
2720
2720
2721 @classmethod
2721 @classmethod
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2722 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2723 from rhodecode.lib.vcs.backends.base import EmptyCommit
2724 dummy = EmptyCommit().__json__()
2724 dummy = EmptyCommit().__json__()
2725 if not changeset_cache_raw:
2725 if not changeset_cache_raw:
2726 dummy['source_repo_id'] = repo_id
2726 dummy['source_repo_id'] = repo_id
2727 return json.loads(json.dumps(dummy))
2727 return json.loads(json.dumps(dummy))
2728
2728
2729 try:
2729 try:
2730 return json.loads(changeset_cache_raw)
2730 return json.loads(changeset_cache_raw)
2731 except TypeError:
2731 except TypeError:
2732 return dummy
2732 return dummy
2733 except Exception:
2733 except Exception:
2734 log.error(traceback.format_exc())
2734 log.error(traceback.format_exc())
2735 return dummy
2735 return dummy
2736
2736
2737 @hybrid_property
2737 @hybrid_property
2738 def changeset_cache(self):
2738 def changeset_cache(self):
2739 return self._load_changeset_cache('', self._changeset_cache)
2739 return self._load_changeset_cache('', self._changeset_cache)
2740
2740
2741 @changeset_cache.setter
2741 @changeset_cache.setter
2742 def changeset_cache(self, val):
2742 def changeset_cache(self, val):
2743 try:
2743 try:
2744 self._changeset_cache = json.dumps(val)
2744 self._changeset_cache = json.dumps(val)
2745 except Exception:
2745 except Exception:
2746 log.error(traceback.format_exc())
2746 log.error(traceback.format_exc())
2747
2747
2748 @validates('group_parent_id')
2748 @validates('group_parent_id')
2749 def validate_group_parent_id(self, key, val):
2749 def validate_group_parent_id(self, key, val):
2750 """
2750 """
2751 Check cycle references for a parent group to self
2751 Check cycle references for a parent group to self
2752 """
2752 """
2753 if self.group_id and val:
2753 if self.group_id and val:
2754 assert val != self.group_id
2754 assert val != self.group_id
2755
2755
2756 return val
2756 return val
2757
2757
2758 @hybrid_property
2758 @hybrid_property
2759 def description_safe(self):
2759 def description_safe(self):
2760 from rhodecode.lib import helpers as h
2760 from rhodecode.lib import helpers as h
2761 return h.escape(self.group_description)
2761 return h.escape(self.group_description)
2762
2762
2763 @classmethod
2763 @classmethod
2764 def hash_repo_group_name(cls, repo_group_name):
2764 def hash_repo_group_name(cls, repo_group_name):
2765 val = remove_formatting(repo_group_name)
2765 val = remove_formatting(repo_group_name)
2766 val = safe_str(val).lower()
2766 val = safe_str(val).lower()
2767 chars = []
2767 chars = []
2768 for c in val:
2768 for c in val:
2769 if c not in string.ascii_letters:
2769 if c not in string.ascii_letters:
2770 c = str(ord(c))
2770 c = str(ord(c))
2771 chars.append(c)
2771 chars.append(c)
2772
2772
2773 return ''.join(chars)
2773 return ''.join(chars)
2774
2774
2775 @classmethod
2775 @classmethod
2776 def _generate_choice(cls, repo_group):
2776 def _generate_choice(cls, repo_group):
2777 from webhelpers2.html import literal as _literal
2777 from webhelpers2.html import literal as _literal
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2778 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2779 return repo_group.group_id, _name(repo_group.full_path_splitted)
2780
2780
2781 @classmethod
2781 @classmethod
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2782 def groups_choices(cls, groups=None, show_empty_group=True):
2783 if not groups:
2783 if not groups:
2784 groups = cls.query().all()
2784 groups = cls.query().all()
2785
2785
2786 repo_groups = []
2786 repo_groups = []
2787 if show_empty_group:
2787 if show_empty_group:
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2788 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2789
2789
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2790 repo_groups.extend([cls._generate_choice(x) for x in groups])
2791
2791
2792 repo_groups = sorted(
2792 repo_groups = sorted(
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2793 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2794 return repo_groups
2794 return repo_groups
2795
2795
2796 @classmethod
2796 @classmethod
2797 def url_sep(cls):
2797 def url_sep(cls):
2798 return URL_SEP
2798 return URL_SEP
2799
2799
2800 @classmethod
2800 @classmethod
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2801 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2802 if case_insensitive:
2802 if case_insensitive:
2803 gr = cls.query().filter(func.lower(cls.group_name)
2803 gr = cls.query().filter(func.lower(cls.group_name)
2804 == func.lower(group_name))
2804 == func.lower(group_name))
2805 else:
2805 else:
2806 gr = cls.query().filter(cls.group_name == group_name)
2806 gr = cls.query().filter(cls.group_name == group_name)
2807 if cache:
2807 if cache:
2808 name_key = _hash_key(group_name)
2808 name_key = _hash_key(group_name)
2809 gr = gr.options(
2809 gr = gr.options(
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2810 FromCache("sql_cache_short", "get_group_%s" % name_key))
2811 return gr.scalar()
2811 return gr.scalar()
2812
2812
2813 @classmethod
2813 @classmethod
2814 def get_user_personal_repo_group(cls, user_id):
2814 def get_user_personal_repo_group(cls, user_id):
2815 user = User.get(user_id)
2815 user = User.get(user_id)
2816 if user.username == User.DEFAULT_USER:
2816 if user.username == User.DEFAULT_USER:
2817 return None
2817 return None
2818
2818
2819 return cls.query()\
2819 return cls.query()\
2820 .filter(cls.personal == true()) \
2820 .filter(cls.personal == true()) \
2821 .filter(cls.user == user) \
2821 .filter(cls.user == user) \
2822 .order_by(cls.group_id.asc()) \
2822 .order_by(cls.group_id.asc()) \
2823 .first()
2823 .first()
2824
2824
2825 @classmethod
2825 @classmethod
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2826 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2827 case_insensitive=True):
2827 case_insensitive=True):
2828 q = RepoGroup.query()
2828 q = RepoGroup.query()
2829
2829
2830 if not isinstance(user_id, Optional):
2830 if not isinstance(user_id, Optional):
2831 q = q.filter(RepoGroup.user_id == user_id)
2831 q = q.filter(RepoGroup.user_id == user_id)
2832
2832
2833 if not isinstance(group_id, Optional):
2833 if not isinstance(group_id, Optional):
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2834 q = q.filter(RepoGroup.group_parent_id == group_id)
2835
2835
2836 if case_insensitive:
2836 if case_insensitive:
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2837 q = q.order_by(func.lower(RepoGroup.group_name))
2838 else:
2838 else:
2839 q = q.order_by(RepoGroup.group_name)
2839 q = q.order_by(RepoGroup.group_name)
2840 return q.all()
2840 return q.all()
2841
2841
2842 @property
2842 @property
2843 def parents(self, parents_recursion_limit=10):
2843 def parents(self, parents_recursion_limit=10):
2844 groups = []
2844 groups = []
2845 if self.parent_group is None:
2845 if self.parent_group is None:
2846 return groups
2846 return groups
2847 cur_gr = self.parent_group
2847 cur_gr = self.parent_group
2848 groups.insert(0, cur_gr)
2848 groups.insert(0, cur_gr)
2849 cnt = 0
2849 cnt = 0
2850 while 1:
2850 while 1:
2851 cnt += 1
2851 cnt += 1
2852 gr = getattr(cur_gr, 'parent_group', None)
2852 gr = getattr(cur_gr, 'parent_group', None)
2853 cur_gr = cur_gr.parent_group
2853 cur_gr = cur_gr.parent_group
2854 if gr is None:
2854 if gr is None:
2855 break
2855 break
2856 if cnt == parents_recursion_limit:
2856 if cnt == parents_recursion_limit:
2857 # this will prevent accidental infinit loops
2857 # this will prevent accidental infinit loops
2858 log.error('more than %s parents found for group %s, stopping '
2858 log.error('more than %s parents found for group %s, stopping '
2859 'recursive parent fetching', parents_recursion_limit, self)
2859 'recursive parent fetching', parents_recursion_limit, self)
2860 break
2860 break
2861
2861
2862 groups.insert(0, gr)
2862 groups.insert(0, gr)
2863 return groups
2863 return groups
2864
2864
2865 @property
2865 @property
2866 def last_commit_cache_update_diff(self):
2866 def last_commit_cache_update_diff(self):
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2867 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2868
2868
2869 @classmethod
2869 @classmethod
2870 def _load_commit_change(cls, last_commit_cache):
2870 def _load_commit_change(cls, last_commit_cache):
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2871 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2872 empty_date = datetime.datetime.fromtimestamp(0)
2872 empty_date = datetime.datetime.fromtimestamp(0)
2873 date_latest = last_commit_cache.get('date', empty_date)
2873 date_latest = last_commit_cache.get('date', empty_date)
2874 try:
2874 try:
2875 return parse_datetime(date_latest)
2875 return parse_datetime(date_latest)
2876 except Exception:
2876 except Exception:
2877 return empty_date
2877 return empty_date
2878
2878
2879 @property
2879 @property
2880 def last_commit_change(self):
2880 def last_commit_change(self):
2881 return self._load_commit_change(self.changeset_cache)
2881 return self._load_commit_change(self.changeset_cache)
2882
2882
2883 @property
2883 @property
2884 def last_db_change(self):
2884 def last_db_change(self):
2885 return self.updated_on
2885 return self.updated_on
2886
2886
2887 @property
2887 @property
2888 def children(self):
2888 def children(self):
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2889 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2890
2890
2891 @property
2891 @property
2892 def name(self):
2892 def name(self):
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2893 return self.group_name.split(RepoGroup.url_sep())[-1]
2894
2894
2895 @property
2895 @property
2896 def full_path(self):
2896 def full_path(self):
2897 return self.group_name
2897 return self.group_name
2898
2898
2899 @property
2899 @property
2900 def full_path_splitted(self):
2900 def full_path_splitted(self):
2901 return self.group_name.split(RepoGroup.url_sep())
2901 return self.group_name.split(RepoGroup.url_sep())
2902
2902
2903 @property
2903 @property
2904 def repositories(self):
2904 def repositories(self):
2905 return Repository.query()\
2905 return Repository.query()\
2906 .filter(Repository.group == self)\
2906 .filter(Repository.group == self)\
2907 .order_by(Repository.repo_name)
2907 .order_by(Repository.repo_name)
2908
2908
2909 @property
2909 @property
2910 def repositories_recursive_count(self):
2910 def repositories_recursive_count(self):
2911 cnt = self.repositories.count()
2911 cnt = self.repositories.count()
2912
2912
2913 def children_count(group):
2913 def children_count(group):
2914 cnt = 0
2914 cnt = 0
2915 for child in group.children:
2915 for child in group.children:
2916 cnt += child.repositories.count()
2916 cnt += child.repositories.count()
2917 cnt += children_count(child)
2917 cnt += children_count(child)
2918 return cnt
2918 return cnt
2919
2919
2920 return cnt + children_count(self)
2920 return cnt + children_count(self)
2921
2921
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2922 def _recursive_objects(self, include_repos=True, include_groups=True):
2923 all_ = []
2923 all_ = []
2924
2924
2925 def _get_members(root_gr):
2925 def _get_members(root_gr):
2926 if include_repos:
2926 if include_repos:
2927 for r in root_gr.repositories:
2927 for r in root_gr.repositories:
2928 all_.append(r)
2928 all_.append(r)
2929 childs = root_gr.children.all()
2929 childs = root_gr.children.all()
2930 if childs:
2930 if childs:
2931 for gr in childs:
2931 for gr in childs:
2932 if include_groups:
2932 if include_groups:
2933 all_.append(gr)
2933 all_.append(gr)
2934 _get_members(gr)
2934 _get_members(gr)
2935
2935
2936 root_group = []
2936 root_group = []
2937 if include_groups:
2937 if include_groups:
2938 root_group = [self]
2938 root_group = [self]
2939
2939
2940 _get_members(self)
2940 _get_members(self)
2941 return root_group + all_
2941 return root_group + all_
2942
2942
2943 def recursive_groups_and_repos(self):
2943 def recursive_groups_and_repos(self):
2944 """
2944 """
2945 Recursive return all groups, with repositories in those groups
2945 Recursive return all groups, with repositories in those groups
2946 """
2946 """
2947 return self._recursive_objects()
2947 return self._recursive_objects()
2948
2948
2949 def recursive_groups(self):
2949 def recursive_groups(self):
2950 """
2950 """
2951 Returns all children groups for this group including children of children
2951 Returns all children groups for this group including children of children
2952 """
2952 """
2953 return self._recursive_objects(include_repos=False)
2953 return self._recursive_objects(include_repos=False)
2954
2954
2955 def recursive_repos(self):
2955 def recursive_repos(self):
2956 """
2956 """
2957 Returns all children repositories for this group
2957 Returns all children repositories for this group
2958 """
2958 """
2959 return self._recursive_objects(include_groups=False)
2959 return self._recursive_objects(include_groups=False)
2960
2960
2961 def get_new_name(self, group_name):
2961 def get_new_name(self, group_name):
2962 """
2962 """
2963 returns new full group name based on parent and new name
2963 returns new full group name based on parent and new name
2964
2964
2965 :param group_name:
2965 :param group_name:
2966 """
2966 """
2967 path_prefix = (self.parent_group.full_path_splitted if
2967 path_prefix = (self.parent_group.full_path_splitted if
2968 self.parent_group else [])
2968 self.parent_group else [])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2969 return RepoGroup.url_sep().join(path_prefix + [group_name])
2970
2970
2971 def update_commit_cache(self, config=None):
2971 def update_commit_cache(self, config=None):
2972 """
2972 """
2973 Update cache of last commit for newest repository inside this repository group.
2973 Update cache of last commit for newest repository inside this repository group.
2974 cache_keys should be::
2974 cache_keys should be::
2975
2975
2976 source_repo_id
2976 source_repo_id
2977 short_id
2977 short_id
2978 raw_id
2978 raw_id
2979 revision
2979 revision
2980 parents
2980 parents
2981 message
2981 message
2982 date
2982 date
2983 author
2983 author
2984
2984
2985 """
2985 """
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2986 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2987 empty_date = datetime.datetime.fromtimestamp(0)
2987 empty_date = datetime.datetime.fromtimestamp(0)
2988
2988
2989 def repo_groups_and_repos(root_gr):
2989 def repo_groups_and_repos(root_gr):
2990 for _repo in root_gr.repositories:
2990 for _repo in root_gr.repositories:
2991 yield _repo
2991 yield _repo
2992 for child_group in root_gr.children.all():
2992 for child_group in root_gr.children.all():
2993 yield child_group
2993 yield child_group
2994
2994
2995 latest_repo_cs_cache = {}
2995 latest_repo_cs_cache = {}
2996 for obj in repo_groups_and_repos(self):
2996 for obj in repo_groups_and_repos(self):
2997 repo_cs_cache = obj.changeset_cache
2997 repo_cs_cache = obj.changeset_cache
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2998 date_latest = latest_repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
2999 date_current = repo_cs_cache.get('date', empty_date)
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3000 current_timestamp = datetime_to_time(parse_datetime(date_latest))
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3001 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
3002 latest_repo_cs_cache = repo_cs_cache
3002 latest_repo_cs_cache = repo_cs_cache
3003 if hasattr(obj, 'repo_id'):
3003 if hasattr(obj, 'repo_id'):
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3004 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
3005 else:
3005 else:
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3006 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
3007
3007
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3008 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
3009
3009
3010 latest_repo_cs_cache['updated_on'] = time.time()
3010 latest_repo_cs_cache['updated_on'] = time.time()
3011 self.changeset_cache = latest_repo_cs_cache
3011 self.changeset_cache = latest_repo_cs_cache
3012 self.updated_on = _date_latest
3012 self.updated_on = _date_latest
3013 Session().add(self)
3013 Session().add(self)
3014 Session().commit()
3014 Session().commit()
3015
3015
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3016 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3017 self.group_name, latest_repo_cs_cache, _date_latest)
3018
3018
3019 def permissions(self, with_admins=True, with_owner=True,
3019 def permissions(self, with_admins=True, with_owner=True,
3020 expand_from_user_groups=False):
3020 expand_from_user_groups=False):
3021 """
3021 """
3022 Permissions for repository groups
3022 Permissions for repository groups
3023 """
3023 """
3024 _admin_perm = 'group.admin'
3024 _admin_perm = 'group.admin'
3025
3025
3026 owner_row = []
3026 owner_row = []
3027 if with_owner:
3027 if with_owner:
3028 usr = AttributeDict(self.user.get_dict())
3028 usr = AttributeDict(self.user.get_dict())
3029 usr.owner_row = True
3029 usr.owner_row = True
3030 usr.permission = _admin_perm
3030 usr.permission = _admin_perm
3031 owner_row.append(usr)
3031 owner_row.append(usr)
3032
3032
3033 super_admin_ids = []
3033 super_admin_ids = []
3034 super_admin_rows = []
3034 super_admin_rows = []
3035 if with_admins:
3035 if with_admins:
3036 for usr in User.get_all_super_admins():
3036 for usr in User.get_all_super_admins():
3037 super_admin_ids.append(usr.user_id)
3037 super_admin_ids.append(usr.user_id)
3038 # if this admin is also owner, don't double the record
3038 # if this admin is also owner, don't double the record
3039 if usr.user_id == owner_row[0].user_id:
3039 if usr.user_id == owner_row[0].user_id:
3040 owner_row[0].admin_row = True
3040 owner_row[0].admin_row = True
3041 else:
3041 else:
3042 usr = AttributeDict(usr.get_dict())
3042 usr = AttributeDict(usr.get_dict())
3043 usr.admin_row = True
3043 usr.admin_row = True
3044 usr.permission = _admin_perm
3044 usr.permission = _admin_perm
3045 super_admin_rows.append(usr)
3045 super_admin_rows.append(usr)
3046
3046
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3047 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3048 q = q.options(joinedload(UserRepoGroupToPerm.group),
3049 joinedload(UserRepoGroupToPerm.user),
3049 joinedload(UserRepoGroupToPerm.user),
3050 joinedload(UserRepoGroupToPerm.permission),)
3050 joinedload(UserRepoGroupToPerm.permission),)
3051
3051
3052 # get owners and admins and permissions. We do a trick of re-writing
3052 # get owners and admins and permissions. We do a trick of re-writing
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3053 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3054 # has a global reference and changing one object propagates to all
3054 # has a global reference and changing one object propagates to all
3055 # others. This means if admin is also an owner admin_row that change
3055 # others. This means if admin is also an owner admin_row that change
3056 # would propagate to both objects
3056 # would propagate to both objects
3057 perm_rows = []
3057 perm_rows = []
3058 for _usr in q.all():
3058 for _usr in q.all():
3059 usr = AttributeDict(_usr.user.get_dict())
3059 usr = AttributeDict(_usr.user.get_dict())
3060 # if this user is also owner/admin, mark as duplicate record
3060 # if this user is also owner/admin, mark as duplicate record
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3061 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3062 usr.duplicate_perm = True
3062 usr.duplicate_perm = True
3063 usr.permission = _usr.permission.permission_name
3063 usr.permission = _usr.permission.permission_name
3064 perm_rows.append(usr)
3064 perm_rows.append(usr)
3065
3065
3066 # filter the perm rows by 'default' first and then sort them by
3066 # filter the perm rows by 'default' first and then sort them by
3067 # admin,write,read,none permissions sorted again alphabetically in
3067 # admin,write,read,none permissions sorted again alphabetically in
3068 # each group
3068 # each group
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3069 perm_rows = sorted(perm_rows, key=display_user_sort)
3070
3070
3071 user_groups_rows = []
3071 user_groups_rows = []
3072 if expand_from_user_groups:
3072 if expand_from_user_groups:
3073 for ug in self.permission_user_groups(with_members=True):
3073 for ug in self.permission_user_groups(with_members=True):
3074 for user_data in ug.members:
3074 for user_data in ug.members:
3075 user_groups_rows.append(user_data)
3075 user_groups_rows.append(user_data)
3076
3076
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3077 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3078
3078
3079 def permission_user_groups(self, with_members=False):
3079 def permission_user_groups(self, with_members=False):
3080 q = UserGroupRepoGroupToPerm.query()\
3080 q = UserGroupRepoGroupToPerm.query()\
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3081 .filter(UserGroupRepoGroupToPerm.group == self)
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3082 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3083 joinedload(UserGroupRepoGroupToPerm.users_group),
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3084 joinedload(UserGroupRepoGroupToPerm.permission),)
3085
3085
3086 perm_rows = []
3086 perm_rows = []
3087 for _user_group in q.all():
3087 for _user_group in q.all():
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3088 entry = AttributeDict(_user_group.users_group.get_dict())
3089 entry.permission = _user_group.permission.permission_name
3089 entry.permission = _user_group.permission.permission_name
3090 if with_members:
3090 if with_members:
3091 entry.members = [x.user.get_dict()
3091 entry.members = [x.user.get_dict()
3092 for x in _user_group.users_group.members]
3092 for x in _user_group.users_group.members]
3093 perm_rows.append(entry)
3093 perm_rows.append(entry)
3094
3094
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3095 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3096 return perm_rows
3096 return perm_rows
3097
3097
3098 def get_api_data(self):
3098 def get_api_data(self):
3099 """
3099 """
3100 Common function for generating api data
3100 Common function for generating api data
3101
3101
3102 """
3102 """
3103 group = self
3103 group = self
3104 data = {
3104 data = {
3105 'group_id': group.group_id,
3105 'group_id': group.group_id,
3106 'group_name': group.group_name,
3106 'group_name': group.group_name,
3107 'group_description': group.description_safe,
3107 'group_description': group.description_safe,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3108 'parent_group': group.parent_group.group_name if group.parent_group else None,
3109 'repositories': [x.repo_name for x in group.repositories],
3109 'repositories': [x.repo_name for x in group.repositories],
3110 'owner': group.user.username,
3110 'owner': group.user.username,
3111 }
3111 }
3112 return data
3112 return data
3113
3113
3114 def get_dict(self):
3114 def get_dict(self):
3115 # Since we transformed `group_name` to a hybrid property, we need to
3115 # Since we transformed `group_name` to a hybrid property, we need to
3116 # keep compatibility with the code which uses `group_name` field.
3116 # keep compatibility with the code which uses `group_name` field.
3117 result = super(RepoGroup, self).get_dict()
3117 result = super(RepoGroup, self).get_dict()
3118 result['group_name'] = result.pop('_group_name', None)
3118 result['group_name'] = result.pop('_group_name', None)
3119 return result
3119 return result
3120
3120
3121
3121
3122 class Permission(Base, BaseModel):
3122 class Permission(Base, BaseModel):
3123 __tablename__ = 'permissions'
3123 __tablename__ = 'permissions'
3124 __table_args__ = (
3124 __table_args__ = (
3125 Index('p_perm_name_idx', 'permission_name'),
3125 Index('p_perm_name_idx', 'permission_name'),
3126 base_table_args,
3126 base_table_args,
3127 )
3127 )
3128
3128
3129 PERMS = [
3129 PERMS = [
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3130 ('hg.admin', _('RhodeCode Super Administrator')),
3131
3131
3132 ('repository.none', _('Repository no access')),
3132 ('repository.none', _('Repository no access')),
3133 ('repository.read', _('Repository read access')),
3133 ('repository.read', _('Repository read access')),
3134 ('repository.write', _('Repository write access')),
3134 ('repository.write', _('Repository write access')),
3135 ('repository.admin', _('Repository admin access')),
3135 ('repository.admin', _('Repository admin access')),
3136
3136
3137 ('group.none', _('Repository group no access')),
3137 ('group.none', _('Repository group no access')),
3138 ('group.read', _('Repository group read access')),
3138 ('group.read', _('Repository group read access')),
3139 ('group.write', _('Repository group write access')),
3139 ('group.write', _('Repository group write access')),
3140 ('group.admin', _('Repository group admin access')),
3140 ('group.admin', _('Repository group admin access')),
3141
3141
3142 ('usergroup.none', _('User group no access')),
3142 ('usergroup.none', _('User group no access')),
3143 ('usergroup.read', _('User group read access')),
3143 ('usergroup.read', _('User group read access')),
3144 ('usergroup.write', _('User group write access')),
3144 ('usergroup.write', _('User group write access')),
3145 ('usergroup.admin', _('User group admin access')),
3145 ('usergroup.admin', _('User group admin access')),
3146
3146
3147 ('branch.none', _('Branch no permissions')),
3147 ('branch.none', _('Branch no permissions')),
3148 ('branch.merge', _('Branch access by web merge')),
3148 ('branch.merge', _('Branch access by web merge')),
3149 ('branch.push', _('Branch access by push')),
3149 ('branch.push', _('Branch access by push')),
3150 ('branch.push_force', _('Branch access by push with force')),
3150 ('branch.push_force', _('Branch access by push with force')),
3151
3151
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3152 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3153 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3154
3154
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3155 ('hg.usergroup.create.false', _('User Group creation disabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3156 ('hg.usergroup.create.true', _('User Group creation enabled')),
3157
3157
3158 ('hg.create.none', _('Repository creation disabled')),
3158 ('hg.create.none', _('Repository creation disabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3159 ('hg.create.repository', _('Repository creation enabled')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3160 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3161 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3162
3162
3163 ('hg.fork.none', _('Repository forking disabled')),
3163 ('hg.fork.none', _('Repository forking disabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3164 ('hg.fork.repository', _('Repository forking enabled')),
3165
3165
3166 ('hg.register.none', _('Registration disabled')),
3166 ('hg.register.none', _('Registration disabled')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3167 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3168 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3169
3169
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3170 ('hg.password_reset.enabled', _('Password reset enabled')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3171 ('hg.password_reset.hidden', _('Password reset hidden')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3172 ('hg.password_reset.disabled', _('Password reset disabled')),
3173
3173
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3174 ('hg.extern_activate.manual', _('Manual activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3175 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3176
3176
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3177 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3178 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3179 ]
3179 ]
3180
3180
3181 # definition of system default permissions for DEFAULT user, created on
3181 # definition of system default permissions for DEFAULT user, created on
3182 # system setup
3182 # system setup
3183 DEFAULT_USER_PERMISSIONS = [
3183 DEFAULT_USER_PERMISSIONS = [
3184 # object perms
3184 # object perms
3185 'repository.read',
3185 'repository.read',
3186 'group.read',
3186 'group.read',
3187 'usergroup.read',
3187 'usergroup.read',
3188 # branch, for backward compat we need same value as before so forced pushed
3188 # branch, for backward compat we need same value as before so forced pushed
3189 'branch.push_force',
3189 'branch.push_force',
3190 # global
3190 # global
3191 'hg.create.repository',
3191 'hg.create.repository',
3192 'hg.repogroup.create.false',
3192 'hg.repogroup.create.false',
3193 'hg.usergroup.create.false',
3193 'hg.usergroup.create.false',
3194 'hg.create.write_on_repogroup.true',
3194 'hg.create.write_on_repogroup.true',
3195 'hg.fork.repository',
3195 'hg.fork.repository',
3196 'hg.register.manual_activate',
3196 'hg.register.manual_activate',
3197 'hg.password_reset.enabled',
3197 'hg.password_reset.enabled',
3198 'hg.extern_activate.auto',
3198 'hg.extern_activate.auto',
3199 'hg.inherit_default_perms.true',
3199 'hg.inherit_default_perms.true',
3200 ]
3200 ]
3201
3201
3202 # defines which permissions are more important higher the more important
3202 # defines which permissions are more important higher the more important
3203 # Weight defines which permissions are more important.
3203 # Weight defines which permissions are more important.
3204 # The higher number the more important.
3204 # The higher number the more important.
3205 PERM_WEIGHTS = {
3205 PERM_WEIGHTS = {
3206 'repository.none': 0,
3206 'repository.none': 0,
3207 'repository.read': 1,
3207 'repository.read': 1,
3208 'repository.write': 3,
3208 'repository.write': 3,
3209 'repository.admin': 4,
3209 'repository.admin': 4,
3210
3210
3211 'group.none': 0,
3211 'group.none': 0,
3212 'group.read': 1,
3212 'group.read': 1,
3213 'group.write': 3,
3213 'group.write': 3,
3214 'group.admin': 4,
3214 'group.admin': 4,
3215
3215
3216 'usergroup.none': 0,
3216 'usergroup.none': 0,
3217 'usergroup.read': 1,
3217 'usergroup.read': 1,
3218 'usergroup.write': 3,
3218 'usergroup.write': 3,
3219 'usergroup.admin': 4,
3219 'usergroup.admin': 4,
3220
3220
3221 'branch.none': 0,
3221 'branch.none': 0,
3222 'branch.merge': 1,
3222 'branch.merge': 1,
3223 'branch.push': 3,
3223 'branch.push': 3,
3224 'branch.push_force': 4,
3224 'branch.push_force': 4,
3225
3225
3226 'hg.repogroup.create.false': 0,
3226 'hg.repogroup.create.false': 0,
3227 'hg.repogroup.create.true': 1,
3227 'hg.repogroup.create.true': 1,
3228
3228
3229 'hg.usergroup.create.false': 0,
3229 'hg.usergroup.create.false': 0,
3230 'hg.usergroup.create.true': 1,
3230 'hg.usergroup.create.true': 1,
3231
3231
3232 'hg.fork.none': 0,
3232 'hg.fork.none': 0,
3233 'hg.fork.repository': 1,
3233 'hg.fork.repository': 1,
3234 'hg.create.none': 0,
3234 'hg.create.none': 0,
3235 'hg.create.repository': 1
3235 'hg.create.repository': 1
3236 }
3236 }
3237
3237
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3238 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3239 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3240 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3241
3241
3242 def __unicode__(self):
3242 def __unicode__(self):
3243 return u"<%s('%s:%s')>" % (
3243 return u"<%s('%s:%s')>" % (
3244 self.__class__.__name__, self.permission_id, self.permission_name
3244 self.__class__.__name__, self.permission_id, self.permission_name
3245 )
3245 )
3246
3246
3247 @classmethod
3247 @classmethod
3248 def get_by_key(cls, key):
3248 def get_by_key(cls, key):
3249 return cls.query().filter(cls.permission_name == key).scalar()
3249 return cls.query().filter(cls.permission_name == key).scalar()
3250
3250
3251 @classmethod
3251 @classmethod
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3252 def get_default_repo_perms(cls, user_id, repo_id=None):
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3253 q = Session().query(UserRepoToPerm, Repository, Permission)\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3254 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3255 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3256 .filter(UserRepoToPerm.user_id == user_id)
3256 .filter(UserRepoToPerm.user_id == user_id)
3257 if repo_id:
3257 if repo_id:
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3258 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3259 return q.all()
3259 return q.all()
3260
3260
3261 @classmethod
3261 @classmethod
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3262 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3263 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3264 .join(
3264 .join(
3265 Permission,
3265 Permission,
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3266 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3267 .join(
3267 .join(
3268 UserRepoToPerm,
3268 UserRepoToPerm,
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3269 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3270 .filter(UserRepoToPerm.user_id == user_id)
3270 .filter(UserRepoToPerm.user_id == user_id)
3271
3271
3272 if repo_id:
3272 if repo_id:
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3273 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3274 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3275
3275
3276 @classmethod
3276 @classmethod
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3277 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3278 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3279 .join(
3279 .join(
3280 Permission,
3280 Permission,
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3281 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3282 .join(
3282 .join(
3283 Repository,
3283 Repository,
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3284 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3285 .join(
3285 .join(
3286 UserGroup,
3286 UserGroup,
3287 UserGroupRepoToPerm.users_group_id ==
3287 UserGroupRepoToPerm.users_group_id ==
3288 UserGroup.users_group_id)\
3288 UserGroup.users_group_id)\
3289 .join(
3289 .join(
3290 UserGroupMember,
3290 UserGroupMember,
3291 UserGroupRepoToPerm.users_group_id ==
3291 UserGroupRepoToPerm.users_group_id ==
3292 UserGroupMember.users_group_id)\
3292 UserGroupMember.users_group_id)\
3293 .filter(
3293 .filter(
3294 UserGroupMember.user_id == user_id,
3294 UserGroupMember.user_id == user_id,
3295 UserGroup.users_group_active == true())
3295 UserGroup.users_group_active == true())
3296 if repo_id:
3296 if repo_id:
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3297 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3298 return q.all()
3298 return q.all()
3299
3299
3300 @classmethod
3300 @classmethod
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3301 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3302 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3303 .join(
3303 .join(
3304 Permission,
3304 Permission,
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3305 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3306 .join(
3306 .join(
3307 UserGroupRepoToPerm,
3307 UserGroupRepoToPerm,
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3308 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3309 .join(
3309 .join(
3310 UserGroup,
3310 UserGroup,
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3311 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3312 .join(
3312 .join(
3313 UserGroupMember,
3313 UserGroupMember,
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3314 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3315 .filter(
3315 .filter(
3316 UserGroupMember.user_id == user_id,
3316 UserGroupMember.user_id == user_id,
3317 UserGroup.users_group_active == true())
3317 UserGroup.users_group_active == true())
3318
3318
3319 if repo_id:
3319 if repo_id:
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3320 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3321 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3322
3322
3323 @classmethod
3323 @classmethod
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3324 def get_default_group_perms(cls, user_id, repo_group_id=None):
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3325 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3326 .join(
3326 .join(
3327 Permission,
3327 Permission,
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3328 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3329 .join(
3329 .join(
3330 RepoGroup,
3330 RepoGroup,
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3331 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3332 .filter(UserRepoGroupToPerm.user_id == user_id)
3333 if repo_group_id:
3333 if repo_group_id:
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3334 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3335 return q.all()
3335 return q.all()
3336
3336
3337 @classmethod
3337 @classmethod
3338 def get_default_group_perms_from_user_group(
3338 def get_default_group_perms_from_user_group(
3339 cls, user_id, repo_group_id=None):
3339 cls, user_id, repo_group_id=None):
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3340 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3341 .join(
3341 .join(
3342 Permission,
3342 Permission,
3343 UserGroupRepoGroupToPerm.permission_id ==
3343 UserGroupRepoGroupToPerm.permission_id ==
3344 Permission.permission_id)\
3344 Permission.permission_id)\
3345 .join(
3345 .join(
3346 RepoGroup,
3346 RepoGroup,
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3347 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3348 .join(
3348 .join(
3349 UserGroup,
3349 UserGroup,
3350 UserGroupRepoGroupToPerm.users_group_id ==
3350 UserGroupRepoGroupToPerm.users_group_id ==
3351 UserGroup.users_group_id)\
3351 UserGroup.users_group_id)\
3352 .join(
3352 .join(
3353 UserGroupMember,
3353 UserGroupMember,
3354 UserGroupRepoGroupToPerm.users_group_id ==
3354 UserGroupRepoGroupToPerm.users_group_id ==
3355 UserGroupMember.users_group_id)\
3355 UserGroupMember.users_group_id)\
3356 .filter(
3356 .filter(
3357 UserGroupMember.user_id == user_id,
3357 UserGroupMember.user_id == user_id,
3358 UserGroup.users_group_active == true())
3358 UserGroup.users_group_active == true())
3359 if repo_group_id:
3359 if repo_group_id:
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3360 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3361 return q.all()
3361 return q.all()
3362
3362
3363 @classmethod
3363 @classmethod
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3364 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3365 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3366 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3367 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3368 .filter(UserUserGroupToPerm.user_id == user_id)
3369 if user_group_id:
3369 if user_group_id:
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3370 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3371 return q.all()
3371 return q.all()
3372
3372
3373 @classmethod
3373 @classmethod
3374 def get_default_user_group_perms_from_user_group(
3374 def get_default_user_group_perms_from_user_group(
3375 cls, user_id, user_group_id=None):
3375 cls, user_id, user_group_id=None):
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3376 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3377 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3378 .join(
3378 .join(
3379 Permission,
3379 Permission,
3380 UserGroupUserGroupToPerm.permission_id ==
3380 UserGroupUserGroupToPerm.permission_id ==
3381 Permission.permission_id)\
3381 Permission.permission_id)\
3382 .join(
3382 .join(
3383 TargetUserGroup,
3383 TargetUserGroup,
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3384 UserGroupUserGroupToPerm.target_user_group_id ==
3385 TargetUserGroup.users_group_id)\
3385 TargetUserGroup.users_group_id)\
3386 .join(
3386 .join(
3387 UserGroup,
3387 UserGroup,
3388 UserGroupUserGroupToPerm.user_group_id ==
3388 UserGroupUserGroupToPerm.user_group_id ==
3389 UserGroup.users_group_id)\
3389 UserGroup.users_group_id)\
3390 .join(
3390 .join(
3391 UserGroupMember,
3391 UserGroupMember,
3392 UserGroupUserGroupToPerm.user_group_id ==
3392 UserGroupUserGroupToPerm.user_group_id ==
3393 UserGroupMember.users_group_id)\
3393 UserGroupMember.users_group_id)\
3394 .filter(
3394 .filter(
3395 UserGroupMember.user_id == user_id,
3395 UserGroupMember.user_id == user_id,
3396 UserGroup.users_group_active == true())
3396 UserGroup.users_group_active == true())
3397 if user_group_id:
3397 if user_group_id:
3398 q = q.filter(
3398 q = q.filter(
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3399 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3400
3400
3401 return q.all()
3401 return q.all()
3402
3402
3403
3403
3404 class UserRepoToPerm(Base, BaseModel):
3404 class UserRepoToPerm(Base, BaseModel):
3405 __tablename__ = 'repo_to_perm'
3405 __tablename__ = 'repo_to_perm'
3406 __table_args__ = (
3406 __table_args__ = (
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3407 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3408 base_table_args
3408 base_table_args
3409 )
3409 )
3410
3410
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3411 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3412 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3413 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3415
3415
3416 user = relationship('User')
3416 user = relationship('User')
3417 repository = relationship('Repository')
3417 repository = relationship('Repository')
3418 permission = relationship('Permission')
3418 permission = relationship('Permission')
3419
3419
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3420 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3421
3421
3422 @classmethod
3422 @classmethod
3423 def create(cls, user, repository, permission):
3423 def create(cls, user, repository, permission):
3424 n = cls()
3424 n = cls()
3425 n.user = user
3425 n.user = user
3426 n.repository = repository
3426 n.repository = repository
3427 n.permission = permission
3427 n.permission = permission
3428 Session().add(n)
3428 Session().add(n)
3429 return n
3429 return n
3430
3430
3431 def __unicode__(self):
3431 def __unicode__(self):
3432 return u'<%s => %s >' % (self.user, self.repository)
3432 return u'<%s => %s >' % (self.user, self.repository)
3433
3433
3434
3434
3435 class UserUserGroupToPerm(Base, BaseModel):
3435 class UserUserGroupToPerm(Base, BaseModel):
3436 __tablename__ = 'user_user_group_to_perm'
3436 __tablename__ = 'user_user_group_to_perm'
3437 __table_args__ = (
3437 __table_args__ = (
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3438 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3439 base_table_args
3439 base_table_args
3440 )
3440 )
3441
3441
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3442 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3443 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3444 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3445 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3446
3446
3447 user = relationship('User')
3447 user = relationship('User')
3448 user_group = relationship('UserGroup')
3448 user_group = relationship('UserGroup')
3449 permission = relationship('Permission')
3449 permission = relationship('Permission')
3450
3450
3451 @classmethod
3451 @classmethod
3452 def create(cls, user, user_group, permission):
3452 def create(cls, user, user_group, permission):
3453 n = cls()
3453 n = cls()
3454 n.user = user
3454 n.user = user
3455 n.user_group = user_group
3455 n.user_group = user_group
3456 n.permission = permission
3456 n.permission = permission
3457 Session().add(n)
3457 Session().add(n)
3458 return n
3458 return n
3459
3459
3460 def __unicode__(self):
3460 def __unicode__(self):
3461 return u'<%s => %s >' % (self.user, self.user_group)
3461 return u'<%s => %s >' % (self.user, self.user_group)
3462
3462
3463
3463
3464 class UserToPerm(Base, BaseModel):
3464 class UserToPerm(Base, BaseModel):
3465 __tablename__ = 'user_to_perm'
3465 __tablename__ = 'user_to_perm'
3466 __table_args__ = (
3466 __table_args__ = (
3467 UniqueConstraint('user_id', 'permission_id'),
3467 UniqueConstraint('user_id', 'permission_id'),
3468 base_table_args
3468 base_table_args
3469 )
3469 )
3470
3470
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3471 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3472 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3473 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3474
3474
3475 user = relationship('User')
3475 user = relationship('User')
3476 permission = relationship('Permission', lazy='joined')
3476 permission = relationship('Permission', lazy='joined')
3477
3477
3478 def __unicode__(self):
3478 def __unicode__(self):
3479 return u'<%s => %s >' % (self.user, self.permission)
3479 return u'<%s => %s >' % (self.user, self.permission)
3480
3480
3481
3481
3482 class UserGroupRepoToPerm(Base, BaseModel):
3482 class UserGroupRepoToPerm(Base, BaseModel):
3483 __tablename__ = 'users_group_repo_to_perm'
3483 __tablename__ = 'users_group_repo_to_perm'
3484 __table_args__ = (
3484 __table_args__ = (
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3485 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3486 base_table_args
3486 base_table_args
3487 )
3487 )
3488
3488
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3489 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3490 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3491 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3492 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3493
3493
3494 users_group = relationship('UserGroup')
3494 users_group = relationship('UserGroup')
3495 permission = relationship('Permission')
3495 permission = relationship('Permission')
3496 repository = relationship('Repository')
3496 repository = relationship('Repository')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3497 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3498
3498
3499 @classmethod
3499 @classmethod
3500 def create(cls, users_group, repository, permission):
3500 def create(cls, users_group, repository, permission):
3501 n = cls()
3501 n = cls()
3502 n.users_group = users_group
3502 n.users_group = users_group
3503 n.repository = repository
3503 n.repository = repository
3504 n.permission = permission
3504 n.permission = permission
3505 Session().add(n)
3505 Session().add(n)
3506 return n
3506 return n
3507
3507
3508 def __unicode__(self):
3508 def __unicode__(self):
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3509 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3510
3510
3511
3511
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3512 class UserGroupUserGroupToPerm(Base, BaseModel):
3513 __tablename__ = 'user_group_user_group_to_perm'
3513 __tablename__ = 'user_group_user_group_to_perm'
3514 __table_args__ = (
3514 __table_args__ = (
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3515 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3516 CheckConstraint('target_user_group_id != user_group_id'),
3517 base_table_args
3517 base_table_args
3518 )
3518 )
3519
3519
3520 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3520 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3521 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3523 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3524
3524
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3525 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3526 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3527 permission = relationship('Permission')
3527 permission = relationship('Permission')
3528
3528
3529 @classmethod
3529 @classmethod
3530 def create(cls, target_user_group, user_group, permission):
3530 def create(cls, target_user_group, user_group, permission):
3531 n = cls()
3531 n = cls()
3532 n.target_user_group = target_user_group
3532 n.target_user_group = target_user_group
3533 n.user_group = user_group
3533 n.user_group = user_group
3534 n.permission = permission
3534 n.permission = permission
3535 Session().add(n)
3535 Session().add(n)
3536 return n
3536 return n
3537
3537
3538 def __unicode__(self):
3538 def __unicode__(self):
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3539 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3540
3540
3541
3541
3542 class UserGroupToPerm(Base, BaseModel):
3542 class UserGroupToPerm(Base, BaseModel):
3543 __tablename__ = 'users_group_to_perm'
3543 __tablename__ = 'users_group_to_perm'
3544 __table_args__ = (
3544 __table_args__ = (
3545 UniqueConstraint('users_group_id', 'permission_id',),
3545 UniqueConstraint('users_group_id', 'permission_id',),
3546 base_table_args
3546 base_table_args
3547 )
3547 )
3548
3548
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3549 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3550 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3552
3552
3553 users_group = relationship('UserGroup')
3553 users_group = relationship('UserGroup')
3554 permission = relationship('Permission')
3554 permission = relationship('Permission')
3555
3555
3556
3556
3557 class UserRepoGroupToPerm(Base, BaseModel):
3557 class UserRepoGroupToPerm(Base, BaseModel):
3558 __tablename__ = 'user_repo_group_to_perm'
3558 __tablename__ = 'user_repo_group_to_perm'
3559 __table_args__ = (
3559 __table_args__ = (
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3560 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3561 base_table_args
3561 base_table_args
3562 )
3562 )
3563
3563
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3564 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3566 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3567 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3568
3568
3569 user = relationship('User')
3569 user = relationship('User')
3570 group = relationship('RepoGroup')
3570 group = relationship('RepoGroup')
3571 permission = relationship('Permission')
3571 permission = relationship('Permission')
3572
3572
3573 @classmethod
3573 @classmethod
3574 def create(cls, user, repository_group, permission):
3574 def create(cls, user, repository_group, permission):
3575 n = cls()
3575 n = cls()
3576 n.user = user
3576 n.user = user
3577 n.group = repository_group
3577 n.group = repository_group
3578 n.permission = permission
3578 n.permission = permission
3579 Session().add(n)
3579 Session().add(n)
3580 return n
3580 return n
3581
3581
3582
3582
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3583 class UserGroupRepoGroupToPerm(Base, BaseModel):
3584 __tablename__ = 'users_group_repo_group_to_perm'
3584 __tablename__ = 'users_group_repo_group_to_perm'
3585 __table_args__ = (
3585 __table_args__ = (
3586 UniqueConstraint('users_group_id', 'group_id'),
3586 UniqueConstraint('users_group_id', 'group_id'),
3587 base_table_args
3587 base_table_args
3588 )
3588 )
3589
3589
3590 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3590 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3591 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3592 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3594
3594
3595 users_group = relationship('UserGroup')
3595 users_group = relationship('UserGroup')
3596 permission = relationship('Permission')
3596 permission = relationship('Permission')
3597 group = relationship('RepoGroup')
3597 group = relationship('RepoGroup')
3598
3598
3599 @classmethod
3599 @classmethod
3600 def create(cls, user_group, repository_group, permission):
3600 def create(cls, user_group, repository_group, permission):
3601 n = cls()
3601 n = cls()
3602 n.users_group = user_group
3602 n.users_group = user_group
3603 n.group = repository_group
3603 n.group = repository_group
3604 n.permission = permission
3604 n.permission = permission
3605 Session().add(n)
3605 Session().add(n)
3606 return n
3606 return n
3607
3607
3608 def __unicode__(self):
3608 def __unicode__(self):
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3609 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3610
3610
3611
3611
3612 class Statistics(Base, BaseModel):
3612 class Statistics(Base, BaseModel):
3613 __tablename__ = 'statistics'
3613 __tablename__ = 'statistics'
3614 __table_args__ = (
3614 __table_args__ = (
3615 base_table_args
3615 base_table_args
3616 )
3616 )
3617
3617
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3618 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3619 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3620 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3621 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3622 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3623 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3624
3624
3625 repository = relationship('Repository', single_parent=True)
3625 repository = relationship('Repository', single_parent=True)
3626
3626
3627
3627
3628 class UserFollowing(Base, BaseModel):
3628 class UserFollowing(Base, BaseModel):
3629 __tablename__ = 'user_followings'
3629 __tablename__ = 'user_followings'
3630 __table_args__ = (
3630 __table_args__ = (
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3631 UniqueConstraint('user_id', 'follows_repository_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3632 UniqueConstraint('user_id', 'follows_user_id'),
3633 base_table_args
3633 base_table_args
3634 )
3634 )
3635
3635
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3636 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3637 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3638 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3639 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3640 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3641
3641
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3642 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3643
3643
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3644 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3645 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3646
3646
3647 @classmethod
3647 @classmethod
3648 def get_repo_followers(cls, repo_id):
3648 def get_repo_followers(cls, repo_id):
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3649 return cls.query().filter(cls.follows_repo_id == repo_id)
3650
3650
3651
3651
3652 class CacheKey(Base, BaseModel):
3652 class CacheKey(Base, BaseModel):
3653 __tablename__ = 'cache_invalidation'
3653 __tablename__ = 'cache_invalidation'
3654 __table_args__ = (
3654 __table_args__ = (
3655 UniqueConstraint('cache_key'),
3655 UniqueConstraint('cache_key'),
3656 Index('key_idx', 'cache_key'),
3656 Index('key_idx', 'cache_key'),
3657 base_table_args,
3657 base_table_args,
3658 )
3658 )
3659
3659
3660 CACHE_TYPE_FEED = 'FEED'
3660 CACHE_TYPE_FEED = 'FEED'
3661
3661
3662 # namespaces used to register process/thread aware caches
3662 # namespaces used to register process/thread aware caches
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3663 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3664 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3665
3665
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3666 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3667 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3668 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3669 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3670 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3671
3671
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3672 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3673 self.cache_key = cache_key
3673 self.cache_key = cache_key
3674 self.cache_args = cache_args
3674 self.cache_args = cache_args
3675 self.cache_active = False
3675 self.cache_active = False
3676 # first key should be same for all entries, since all workers should share it
3676 # first key should be same for all entries, since all workers should share it
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3677 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3678
3678
3679 def __unicode__(self):
3679 def __unicode__(self):
3680 return u"<%s('%s:%s[%s]')>" % (
3680 return u"<%s('%s:%s[%s]')>" % (
3681 self.__class__.__name__,
3681 self.__class__.__name__,
3682 self.cache_id, self.cache_key, self.cache_active)
3682 self.cache_id, self.cache_key, self.cache_active)
3683
3683
3684 def _cache_key_partition(self):
3684 def _cache_key_partition(self):
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3685 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3686 return prefix, repo_name, suffix
3686 return prefix, repo_name, suffix
3687
3687
3688 def get_prefix(self):
3688 def get_prefix(self):
3689 """
3689 """
3690 Try to extract prefix from existing cache key. The key could consist
3690 Try to extract prefix from existing cache key. The key could consist
3691 of prefix, repo_name, suffix
3691 of prefix, repo_name, suffix
3692 """
3692 """
3693 # this returns prefix, repo_name, suffix
3693 # this returns prefix, repo_name, suffix
3694 return self._cache_key_partition()[0]
3694 return self._cache_key_partition()[0]
3695
3695
3696 def get_suffix(self):
3696 def get_suffix(self):
3697 """
3697 """
3698 get suffix that might have been used in _get_cache_key to
3698 get suffix that might have been used in _get_cache_key to
3699 generate self.cache_key. Only used for informational purposes
3699 generate self.cache_key. Only used for informational purposes
3700 in repo_edit.mako.
3700 in repo_edit.mako.
3701 """
3701 """
3702 # prefix, repo_name, suffix
3702 # prefix, repo_name, suffix
3703 return self._cache_key_partition()[2]
3703 return self._cache_key_partition()[2]
3704
3704
3705 @classmethod
3705 @classmethod
3706 def generate_new_state_uid(cls, based_on=None):
3706 def generate_new_state_uid(cls, based_on=None):
3707 if based_on:
3707 if based_on:
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3708 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3709 else:
3709 else:
3710 return str(uuid.uuid4())
3710 return str(uuid.uuid4())
3711
3711
3712 @classmethod
3712 @classmethod
3713 def delete_all_cache(cls):
3713 def delete_all_cache(cls):
3714 """
3714 """
3715 Delete all cache keys from database.
3715 Delete all cache keys from database.
3716 Should only be run when all instances are down and all entries
3716 Should only be run when all instances are down and all entries
3717 thus stale.
3717 thus stale.
3718 """
3718 """
3719 cls.query().delete()
3719 cls.query().delete()
3720 Session().commit()
3720 Session().commit()
3721
3721
3722 @classmethod
3722 @classmethod
3723 def set_invalidate(cls, cache_uid, delete=False):
3723 def set_invalidate(cls, cache_uid, delete=False):
3724 """
3724 """
3725 Mark all caches of a repo as invalid in the database.
3725 Mark all caches of a repo as invalid in the database.
3726 """
3726 """
3727
3727
3728 try:
3728 try:
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3729 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3730 if delete:
3730 if delete:
3731 qry.delete()
3731 qry.delete()
3732 log.debug('cache objects deleted for cache args %s',
3732 log.debug('cache objects deleted for cache args %s',
3733 safe_str(cache_uid))
3733 safe_str(cache_uid))
3734 else:
3734 else:
3735 qry.update({"cache_active": False,
3735 qry.update({"cache_active": False,
3736 "cache_state_uid": cls.generate_new_state_uid()})
3736 "cache_state_uid": cls.generate_new_state_uid()})
3737 log.debug('cache objects marked as invalid for cache args %s',
3737 log.debug('cache objects marked as invalid for cache args %s',
3738 safe_str(cache_uid))
3738 safe_str(cache_uid))
3739
3739
3740 Session().commit()
3740 Session().commit()
3741 except Exception:
3741 except Exception:
3742 log.exception(
3742 log.exception(
3743 'Cache key invalidation failed for cache args %s',
3743 'Cache key invalidation failed for cache args %s',
3744 safe_str(cache_uid))
3744 safe_str(cache_uid))
3745 Session().rollback()
3745 Session().rollback()
3746
3746
3747 @classmethod
3747 @classmethod
3748 def get_active_cache(cls, cache_key):
3748 def get_active_cache(cls, cache_key):
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3749 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3750 if inv_obj:
3750 if inv_obj:
3751 return inv_obj
3751 return inv_obj
3752 return None
3752 return None
3753
3753
3754 @classmethod
3754 @classmethod
3755 def get_namespace_map(cls, namespace):
3755 def get_namespace_map(cls, namespace):
3756 return {
3756 return {
3757 x.cache_key: x
3757 x.cache_key: x
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3758 for x in cls.query().filter(cls.cache_args == namespace)}
3759
3759
3760
3760
3761 class ChangesetComment(Base, BaseModel):
3761 class ChangesetComment(Base, BaseModel):
3762 __tablename__ = 'changeset_comments'
3762 __tablename__ = 'changeset_comments'
3763 __table_args__ = (
3763 __table_args__ = (
3764 Index('cc_revision_idx', 'revision'),
3764 Index('cc_revision_idx', 'revision'),
3765 base_table_args,
3765 base_table_args,
3766 )
3766 )
3767
3767
3768 COMMENT_OUTDATED = u'comment_outdated'
3768 COMMENT_OUTDATED = u'comment_outdated'
3769 COMMENT_TYPE_NOTE = u'note'
3769 COMMENT_TYPE_NOTE = u'note'
3770 COMMENT_TYPE_TODO = u'todo'
3770 COMMENT_TYPE_TODO = u'todo'
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3771 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3772
3772
3773 OP_IMMUTABLE = u'immutable'
3773 OP_IMMUTABLE = u'immutable'
3774 OP_CHANGEABLE = u'changeable'
3774 OP_CHANGEABLE = u'changeable'
3775
3775
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3776 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3777 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3778 revision = Column('revision', String(40), nullable=True)
3778 revision = Column('revision', String(40), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3779 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3780 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3781 line_no = Column('line_no', Unicode(10), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3782 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3783 f_path = Column('f_path', Unicode(1000), nullable=True)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3784 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3785 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3786 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3787 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3788 renderer = Column('renderer', Unicode(64), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3789 display_state = Column('display_state', Unicode(128), nullable=True)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3790 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3791 draft = Column('draft', Boolean(), nullable=True, default=False)
3792
3792
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3793 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3794 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3795
3795
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3796 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3797 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3798
3798
3799 author = relationship('User', lazy='select')
3799 author = relationship('User', lazy='select')
3800 repo = relationship('Repository')
3800 repo = relationship('Repository')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3801 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3802 pull_request = relationship('PullRequest', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3803 pull_request_version = relationship('PullRequestVersion', lazy='select')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3804 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3805
3805
3806 @classmethod
3806 @classmethod
3807 def get_users(cls, revision=None, pull_request_id=None):
3807 def get_users(cls, revision=None, pull_request_id=None):
3808 """
3808 """
3809 Returns user associated with this ChangesetComment. ie those
3809 Returns user associated with this ChangesetComment. ie those
3810 who actually commented
3810 who actually commented
3811
3811
3812 :param cls:
3812 :param cls:
3813 :param revision:
3813 :param revision:
3814 """
3814 """
3815 q = Session().query(User)\
3815 q = Session().query(User)\
3816 .join(ChangesetComment.author)
3816 .join(ChangesetComment.author)
3817 if revision:
3817 if revision:
3818 q = q.filter(cls.revision == revision)
3818 q = q.filter(cls.revision == revision)
3819 elif pull_request_id:
3819 elif pull_request_id:
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3820 q = q.filter(cls.pull_request_id == pull_request_id)
3821 return q.all()
3821 return q.all()
3822
3822
3823 @classmethod
3823 @classmethod
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3824 def get_index_from_version(cls, pr_version, versions=None, num_versions=None):
3825
3825
3826 if versions is not None:
3826 if versions is not None:
3827 num_versions = [x.pull_request_version_id for x in versions]
3827 num_versions = [x.pull_request_version_id for x in versions]
3828
3828
3829 num_versions = num_versions or []
3829 num_versions = num_versions or []
3830 try:
3830 try:
3831 return num_versions.index(pr_version) + 1
3831 return num_versions.index(pr_version) + 1
3832 except (IndexError, ValueError):
3832 except (IndexError, ValueError):
3833 return
3833 return
3834
3834
3835 @property
3835 @property
3836 def outdated(self):
3836 def outdated(self):
3837 return self.display_state == self.COMMENT_OUTDATED
3837 return self.display_state == self.COMMENT_OUTDATED
3838
3838
3839 @property
3839 @property
3840 def outdated_js(self):
3840 def outdated_js(self):
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3841 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3842
3842
3843 @property
3843 @property
3844 def immutable(self):
3844 def immutable(self):
3845 return self.immutable_state == self.OP_IMMUTABLE
3845 return self.immutable_state == self.OP_IMMUTABLE
3846
3846
3847 def outdated_at_version(self, version):
3847 def outdated_at_version(self, version):
3848 """
3848 """
3849 Checks if comment is outdated for given pull request version
3849 Checks if comment is outdated for given pull request version
3850 """
3850 """
3851 def version_check():
3851 def version_check():
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3852 return self.pull_request_version_id and self.pull_request_version_id != version
3853
3853
3854 if self.is_inline:
3854 if self.is_inline:
3855 return self.outdated and version_check()
3855 return self.outdated and version_check()
3856 else:
3856 else:
3857 # general comments don't have .outdated set, also latest don't have a version
3857 # general comments don't have .outdated set, also latest don't have a version
3858 return version_check()
3858 return version_check()
3859
3859
3860 def outdated_at_version_js(self, version):
3860 def outdated_at_version_js(self, version):
3861 """
3861 """
3862 Checks if comment is outdated for given pull request version
3862 Checks if comment is outdated for given pull request version
3863 """
3863 """
3864 return json.dumps(self.outdated_at_version(version))
3864 return json.dumps(self.outdated_at_version(version))
3865
3865
3866 def older_than_version(self, version):
3866 def older_than_version(self, version):
3867 """
3867 """
3868 Checks if comment is made from previous version than given
3868 Checks if comment is made from previous version than given
3869 """
3869 """
3870 if version is None:
3870 if version is None:
3871 return self.pull_request_version != version
3871 return self.pull_request_version != version
3872
3872
3873 return self.pull_request_version < version
3873 return self.pull_request_version < version
3874
3874
3875 def older_than_version_js(self, version):
3875 def older_than_version_js(self, version):
3876 """
3876 """
3877 Checks if comment is made from previous version than given
3877 Checks if comment is made from previous version than given
3878 """
3878 """
3879 return json.dumps(self.older_than_version(version))
3879 return json.dumps(self.older_than_version(version))
3880
3880
3881 @property
3881 @property
3882 def commit_id(self):
3882 def commit_id(self):
3883 """New style naming to stop using .revision"""
3883 """New style naming to stop using .revision"""
3884 return self.revision
3884 return self.revision
3885
3885
3886 @property
3886 @property
3887 def resolved(self):
3887 def resolved(self):
3888 return self.resolved_by[0] if self.resolved_by else None
3888 return self.resolved_by[0] if self.resolved_by else None
3889
3889
3890 @property
3890 @property
3891 def is_todo(self):
3891 def is_todo(self):
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3892 return self.comment_type == self.COMMENT_TYPE_TODO
3893
3893
3894 @property
3894 @property
3895 def is_inline(self):
3895 def is_inline(self):
3896 if self.line_no and self.f_path:
3896 if self.line_no and self.f_path:
3897 return True
3897 return True
3898 return False
3898 return False
3899
3899
3900 @property
3900 @property
3901 def last_version(self):
3901 def last_version(self):
3902 version = 0
3902 version = 0
3903 if self.history:
3903 if self.history:
3904 version = self.history[-1].version
3904 version = self.history[-1].version
3905 return version
3905 return version
3906
3906
3907 def get_index_version(self, versions):
3907 def get_index_version(self, versions):
3908 return self.get_index_from_version(
3908 return self.get_index_from_version(
3909 self.pull_request_version_id, versions)
3909 self.pull_request_version_id, versions)
3910
3910
3911 @property
3911 @property
3912 def review_status(self):
3912 def review_status(self):
3913 if self.status_change:
3913 if self.status_change:
3914 return self.status_change[0].status
3914 return self.status_change[0].status
3915
3915
3916 @property
3916 @property
3917 def review_status_lbl(self):
3917 def review_status_lbl(self):
3918 if self.status_change:
3918 if self.status_change:
3919 return self.status_change[0].status_lbl
3919 return self.status_change[0].status_lbl
3920
3920
3921 def __repr__(self):
3921 def __repr__(self):
3922 if self.comment_id:
3922 if self.comment_id:
3923 return '<DB:Comment #%s>' % self.comment_id
3923 return '<DB:Comment #%s>' % self.comment_id
3924 else:
3924 else:
3925 return '<DB:Comment at %#x>' % id(self)
3925 return '<DB:Comment at %#x>' % id(self)
3926
3926
3927 def get_api_data(self):
3927 def get_api_data(self):
3928 comment = self
3928 comment = self
3929
3929
3930 data = {
3930 data = {
3931 'comment_id': comment.comment_id,
3931 'comment_id': comment.comment_id,
3932 'comment_type': comment.comment_type,
3932 'comment_type': comment.comment_type,
3933 'comment_text': comment.text,
3933 'comment_text': comment.text,
3934 'comment_status': comment.status_change,
3934 'comment_status': comment.status_change,
3935 'comment_f_path': comment.f_path,
3935 'comment_f_path': comment.f_path,
3936 'comment_lineno': comment.line_no,
3936 'comment_lineno': comment.line_no,
3937 'comment_author': comment.author,
3937 'comment_author': comment.author,
3938 'comment_created_on': comment.created_on,
3938 'comment_created_on': comment.created_on,
3939 'comment_resolved_by': self.resolved,
3939 'comment_resolved_by': self.resolved,
3940 'comment_commit_id': comment.revision,
3940 'comment_commit_id': comment.revision,
3941 'comment_pull_request_id': comment.pull_request_id,
3941 'comment_pull_request_id': comment.pull_request_id,
3942 'comment_last_version': self.last_version
3942 'comment_last_version': self.last_version
3943 }
3943 }
3944 return data
3944 return data
3945
3945
3946 def __json__(self):
3946 def __json__(self):
3947 data = dict()
3947 data = dict()
3948 data.update(self.get_api_data())
3948 data.update(self.get_api_data())
3949 return data
3949 return data
3950
3950
3951
3951
3952 class ChangesetCommentHistory(Base, BaseModel):
3952 class ChangesetCommentHistory(Base, BaseModel):
3953 __tablename__ = 'changeset_comments_history'
3953 __tablename__ = 'changeset_comments_history'
3954 __table_args__ = (
3954 __table_args__ = (
3955 Index('cch_comment_id_idx', 'comment_id'),
3955 Index('cch_comment_id_idx', 'comment_id'),
3956 base_table_args,
3956 base_table_args,
3957 )
3957 )
3958
3958
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3959 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3960 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3961 version = Column("version", Integer(), nullable=False, default=0)
3961 version = Column("version", Integer(), nullable=False, default=0)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3962 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3963 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3964 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3965 deleted = Column('deleted', Boolean(), default=False)
3965 deleted = Column('deleted', Boolean(), default=False)
3966
3966
3967 author = relationship('User', lazy='joined')
3967 author = relationship('User', lazy='joined')
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3968 comment = relationship('ChangesetComment', cascade="all, delete")
3969
3969
3970 @classmethod
3970 @classmethod
3971 def get_version(cls, comment_id):
3971 def get_version(cls, comment_id):
3972 q = Session().query(ChangesetCommentHistory).filter(
3972 q = Session().query(ChangesetCommentHistory).filter(
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3973 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3974 if q.count() == 0:
3974 if q.count() == 0:
3975 return 1
3975 return 1
3976 elif q.count() >= q[0].version:
3976 elif q.count() >= q[0].version:
3977 return q.count() + 1
3977 return q.count() + 1
3978 else:
3978 else:
3979 return q[0].version + 1
3979 return q[0].version + 1
3980
3980
3981
3981
3982 class ChangesetStatus(Base, BaseModel):
3982 class ChangesetStatus(Base, BaseModel):
3983 __tablename__ = 'changeset_statuses'
3983 __tablename__ = 'changeset_statuses'
3984 __table_args__ = (
3984 __table_args__ = (
3985 Index('cs_revision_idx', 'revision'),
3985 Index('cs_revision_idx', 'revision'),
3986 Index('cs_version_idx', 'version'),
3986 Index('cs_version_idx', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3987 UniqueConstraint('repo_id', 'revision', 'version'),
3988 base_table_args
3988 base_table_args
3989 )
3989 )
3990
3990
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3991 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3992 STATUS_APPROVED = 'approved'
3992 STATUS_APPROVED = 'approved'
3993 STATUS_REJECTED = 'rejected'
3993 STATUS_REJECTED = 'rejected'
3994 STATUS_UNDER_REVIEW = 'under_review'
3994 STATUS_UNDER_REVIEW = 'under_review'
3995 CheckConstraint,
3995 CheckConstraint,
3996 STATUSES = [
3996 STATUSES = [
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3997 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3998 (STATUS_APPROVED, _("Approved")),
3998 (STATUS_APPROVED, _("Approved")),
3999 (STATUS_REJECTED, _("Rejected")),
3999 (STATUS_REJECTED, _("Rejected")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4000 (STATUS_UNDER_REVIEW, _("Under Review")),
4001 ]
4001 ]
4002
4002
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4003 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4004 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4005 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
4006 revision = Column('revision', String(40), nullable=False)
4006 revision = Column('revision', String(40), nullable=False)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4007 status = Column('status', String(128), nullable=False, default=DEFAULT)
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4008 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4009 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
4010 version = Column('version', Integer(), nullable=False, default=0)
4010 version = Column('version', Integer(), nullable=False, default=0)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4011 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
4012
4012
4013 author = relationship('User', lazy='select')
4013 author = relationship('User', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4014 repo = relationship('Repository', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4015 comment = relationship('ChangesetComment', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4016 pull_request = relationship('PullRequest', lazy='select')
4017
4017
4018 def __unicode__(self):
4018 def __unicode__(self):
4019 return u"<%s('%s[v%s]:%s')>" % (
4019 return u"<%s('%s[v%s]:%s')>" % (
4020 self.__class__.__name__,
4020 self.__class__.__name__,
4021 self.status, self.version, self.author
4021 self.status, self.version, self.author
4022 )
4022 )
4023
4023
4024 @classmethod
4024 @classmethod
4025 def get_status_lbl(cls, value):
4025 def get_status_lbl(cls, value):
4026 return dict(cls.STATUSES).get(value)
4026 return dict(cls.STATUSES).get(value)
4027
4027
4028 @property
4028 @property
4029 def status_lbl(self):
4029 def status_lbl(self):
4030 return ChangesetStatus.get_status_lbl(self.status)
4030 return ChangesetStatus.get_status_lbl(self.status)
4031
4031
4032 def get_api_data(self):
4032 def get_api_data(self):
4033 status = self
4033 status = self
4034 data = {
4034 data = {
4035 'status_id': status.changeset_status_id,
4035 'status_id': status.changeset_status_id,
4036 'status': status.status,
4036 'status': status.status,
4037 }
4037 }
4038 return data
4038 return data
4039
4039
4040 def __json__(self):
4040 def __json__(self):
4041 data = dict()
4041 data = dict()
4042 data.update(self.get_api_data())
4042 data.update(self.get_api_data())
4043 return data
4043 return data
4044
4044
4045
4045
4046 class _SetState(object):
4046 class _SetState(object):
4047 """
4047 """
4048 Context processor allowing changing state for sensitive operation such as
4048 Context processor allowing changing state for sensitive operation such as
4049 pull request update or merge
4049 pull request update or merge
4050 """
4050 """
4051
4051
4052 def __init__(self, pull_request, pr_state, back_state=None):
4052 def __init__(self, pull_request, pr_state, back_state=None):
4053 self._pr = pull_request
4053 self._pr = pull_request
4054 self._org_state = back_state or pull_request.pull_request_state
4054 self._org_state = back_state or pull_request.pull_request_state
4055 self._pr_state = pr_state
4055 self._pr_state = pr_state
4056 self._current_state = None
4056 self._current_state = None
4057
4057
4058 def __enter__(self):
4058 def __enter__(self):
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4059 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4060 self._pr, self._pr_state)
4060 self._pr, self._pr_state)
4061 self.set_pr_state(self._pr_state)
4061 self.set_pr_state(self._pr_state)
4062 return self
4062 return self
4063
4063
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4064 def __exit__(self, exc_type, exc_val, exc_tb):
4065 if exc_val is not None:
4065 if exc_val is not None or exc_type is not None:
4066 log.error(traceback.format_exc(exc_tb))
4066 log.error(traceback.format_exc(exc_tb))
4067 return None
4067 return None
4068
4068
4069 self.set_pr_state(self._org_state)
4069 self.set_pr_state(self._org_state)
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4070 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4071 self._pr, self._org_state)
4071 self._pr, self._org_state)
4072
4072
4073 @property
4073 @property
4074 def state(self):
4074 def state(self):
4075 return self._current_state
4075 return self._current_state
4076
4076
4077 def set_pr_state(self, pr_state):
4077 def set_pr_state(self, pr_state):
4078 try:
4078 try:
4079 self._pr.pull_request_state = pr_state
4079 self._pr.pull_request_state = pr_state
4080 Session().add(self._pr)
4080 Session().add(self._pr)
4081 Session().commit()
4081 Session().commit()
4082 self._current_state = pr_state
4082 self._current_state = pr_state
4083 except Exception:
4083 except Exception:
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4084 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4085 raise
4085 raise
4086
4086
4087
4087
4088 class _PullRequestBase(BaseModel):
4088 class _PullRequestBase(BaseModel):
4089 """
4089 """
4090 Common attributes of pull request and version entries.
4090 Common attributes of pull request and version entries.
4091 """
4091 """
4092
4092
4093 # .status values
4093 # .status values
4094 STATUS_NEW = u'new'
4094 STATUS_NEW = u'new'
4095 STATUS_OPEN = u'open'
4095 STATUS_OPEN = u'open'
4096 STATUS_CLOSED = u'closed'
4096 STATUS_CLOSED = u'closed'
4097
4097
4098 # available states
4098 # available states
4099 STATE_CREATING = u'creating'
4099 STATE_CREATING = u'creating'
4100 STATE_UPDATING = u'updating'
4100 STATE_UPDATING = u'updating'
4101 STATE_MERGING = u'merging'
4101 STATE_MERGING = u'merging'
4102 STATE_CREATED = u'created'
4102 STATE_CREATED = u'created'
4103
4103
4104 title = Column('title', Unicode(255), nullable=True)
4104 title = Column('title', Unicode(255), nullable=True)
4105 description = Column(
4105 description = Column(
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4106 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4107 nullable=True)
4107 nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4108 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4109
4109
4110 # new/open/closed status of pull request (not approve/reject/etc)
4110 # new/open/closed status of pull request (not approve/reject/etc)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4111 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4112 created_on = Column(
4112 created_on = Column(
4113 'created_on', DateTime(timezone=False), nullable=False,
4113 'created_on', DateTime(timezone=False), nullable=False,
4114 default=datetime.datetime.now)
4114 default=datetime.datetime.now)
4115 updated_on = Column(
4115 updated_on = Column(
4116 'updated_on', DateTime(timezone=False), nullable=False,
4116 'updated_on', DateTime(timezone=False), nullable=False,
4117 default=datetime.datetime.now)
4117 default=datetime.datetime.now)
4118
4118
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4119 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4120
4120
4121 @declared_attr
4121 @declared_attr
4122 def user_id(cls):
4122 def user_id(cls):
4123 return Column(
4123 return Column(
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4124 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4125 unique=None)
4125 unique=None)
4126
4126
4127 # 500 revisions max
4127 # 500 revisions max
4128 _revisions = Column(
4128 _revisions = Column(
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4129 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4130
4130
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4131 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4132
4132
4133 @declared_attr
4133 @declared_attr
4134 def source_repo_id(cls):
4134 def source_repo_id(cls):
4135 # TODO: dan: rename column to source_repo_id
4135 # TODO: dan: rename column to source_repo_id
4136 return Column(
4136 return Column(
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4137 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4138 nullable=False)
4138 nullable=False)
4139
4139
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4140 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4141
4141
4142 @hybrid_property
4142 @hybrid_property
4143 def source_ref(self):
4143 def source_ref(self):
4144 return self._source_ref
4144 return self._source_ref
4145
4145
4146 @source_ref.setter
4146 @source_ref.setter
4147 def source_ref(self, val):
4147 def source_ref(self, val):
4148 parts = (val or '').split(':')
4148 parts = (val or '').split(':')
4149 if len(parts) != 3:
4149 if len(parts) != 3:
4150 raise ValueError(
4150 raise ValueError(
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4151 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4152 self._source_ref = safe_unicode(val)
4152 self._source_ref = safe_unicode(val)
4153
4153
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4154 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4155
4155
4156 @hybrid_property
4156 @hybrid_property
4157 def target_ref(self):
4157 def target_ref(self):
4158 return self._target_ref
4158 return self._target_ref
4159
4159
4160 @target_ref.setter
4160 @target_ref.setter
4161 def target_ref(self, val):
4161 def target_ref(self, val):
4162 parts = (val or '').split(':')
4162 parts = (val or '').split(':')
4163 if len(parts) != 3:
4163 if len(parts) != 3:
4164 raise ValueError(
4164 raise ValueError(
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4165 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4166 self._target_ref = safe_unicode(val)
4166 self._target_ref = safe_unicode(val)
4167
4167
4168 @declared_attr
4168 @declared_attr
4169 def target_repo_id(cls):
4169 def target_repo_id(cls):
4170 # TODO: dan: rename column to target_repo_id
4170 # TODO: dan: rename column to target_repo_id
4171 return Column(
4171 return Column(
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4172 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4173 nullable=False)
4173 nullable=False)
4174
4174
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4175 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4176
4176
4177 # TODO: dan: rename column to last_merge_source_rev
4177 # TODO: dan: rename column to last_merge_source_rev
4178 _last_merge_source_rev = Column(
4178 _last_merge_source_rev = Column(
4179 'last_merge_org_rev', String(40), nullable=True)
4179 'last_merge_org_rev', String(40), nullable=True)
4180 # TODO: dan: rename column to last_merge_target_rev
4180 # TODO: dan: rename column to last_merge_target_rev
4181 _last_merge_target_rev = Column(
4181 _last_merge_target_rev = Column(
4182 'last_merge_other_rev', String(40), nullable=True)
4182 'last_merge_other_rev', String(40), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4183 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4184 last_merge_metadata = Column(
4184 last_merge_metadata = Column(
4185 'last_merge_metadata', MutationObj.as_mutable(
4185 'last_merge_metadata', MutationObj.as_mutable(
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4186 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4187
4187
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4188 merge_rev = Column('merge_rev', String(40), nullable=True)
4189
4189
4190 reviewer_data = Column(
4190 reviewer_data = Column(
4191 'reviewer_data_json', MutationObj.as_mutable(
4191 'reviewer_data_json', MutationObj.as_mutable(
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4192 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4193
4193
4194 @property
4194 @property
4195 def reviewer_data_json(self):
4195 def reviewer_data_json(self):
4196 return json.dumps(self.reviewer_data)
4196 return json.dumps(self.reviewer_data)
4197
4197
4198 @property
4198 @property
4199 def last_merge_metadata_parsed(self):
4199 def last_merge_metadata_parsed(self):
4200 metadata = {}
4200 metadata = {}
4201 if not self.last_merge_metadata:
4201 if not self.last_merge_metadata:
4202 return metadata
4202 return metadata
4203
4203
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4204 if hasattr(self.last_merge_metadata, 'de_coerce'):
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4205 for k, v in self.last_merge_metadata.de_coerce().items():
4206 if k in ['target_ref', 'source_ref']:
4206 if k in ['target_ref', 'source_ref']:
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4207 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4208 else:
4208 else:
4209 if hasattr(v, 'de_coerce'):
4209 if hasattr(v, 'de_coerce'):
4210 metadata[k] = v.de_coerce()
4210 metadata[k] = v.de_coerce()
4211 else:
4211 else:
4212 metadata[k] = v
4212 metadata[k] = v
4213 return metadata
4213 return metadata
4214
4214
4215 @property
4215 @property
4216 def work_in_progress(self):
4216 def work_in_progress(self):
4217 """checks if pull request is work in progress by checking the title"""
4217 """checks if pull request is work in progress by checking the title"""
4218 title = self.title.upper()
4218 title = self.title.upper()
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4219 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4220 return True
4220 return True
4221 return False
4221 return False
4222
4222
4223 @property
4223 @property
4224 def title_safe(self):
4224 def title_safe(self):
4225 return self.title\
4225 return self.title\
4226 .replace('{', '{{')\
4226 .replace('{', '{{')\
4227 .replace('}', '}}')
4227 .replace('}', '}}')
4228
4228
4229 @hybrid_property
4229 @hybrid_property
4230 def description_safe(self):
4230 def description_safe(self):
4231 from rhodecode.lib import helpers as h
4231 from rhodecode.lib import helpers as h
4232 return h.escape(self.description)
4232 return h.escape(self.description)
4233
4233
4234 @hybrid_property
4234 @hybrid_property
4235 def revisions(self):
4235 def revisions(self):
4236 return self._revisions.split(':') if self._revisions else []
4236 return self._revisions.split(':') if self._revisions else []
4237
4237
4238 @revisions.setter
4238 @revisions.setter
4239 def revisions(self, val):
4239 def revisions(self, val):
4240 self._revisions = u':'.join(val)
4240 self._revisions = u':'.join(val)
4241
4241
4242 @hybrid_property
4242 @hybrid_property
4243 def last_merge_status(self):
4243 def last_merge_status(self):
4244 return safe_int(self._last_merge_status)
4244 return safe_int(self._last_merge_status)
4245
4245
4246 @last_merge_status.setter
4246 @last_merge_status.setter
4247 def last_merge_status(self, val):
4247 def last_merge_status(self, val):
4248 self._last_merge_status = val
4248 self._last_merge_status = val
4249
4249
4250 @declared_attr
4250 @declared_attr
4251 def author(cls):
4251 def author(cls):
4252 return relationship('User', lazy='joined')
4252 return relationship('User', lazy='joined')
4253
4253
4254 @declared_attr
4254 @declared_attr
4255 def source_repo(cls):
4255 def source_repo(cls):
4256 return relationship(
4256 return relationship(
4257 'Repository',
4257 'Repository',
4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4258 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4259
4259
4260 @property
4260 @property
4261 def source_ref_parts(self):
4261 def source_ref_parts(self):
4262 return self.unicode_to_reference(self.source_ref)
4262 return self.unicode_to_reference(self.source_ref)
4263
4263
4264 @declared_attr
4264 @declared_attr
4265 def target_repo(cls):
4265 def target_repo(cls):
4266 return relationship(
4266 return relationship(
4267 'Repository',
4267 'Repository',
4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4268 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4269
4269
4270 @property
4270 @property
4271 def target_ref_parts(self):
4271 def target_ref_parts(self):
4272 return self.unicode_to_reference(self.target_ref)
4272 return self.unicode_to_reference(self.target_ref)
4273
4273
4274 @property
4274 @property
4275 def shadow_merge_ref(self):
4275 def shadow_merge_ref(self):
4276 return self.unicode_to_reference(self._shadow_merge_ref)
4276 return self.unicode_to_reference(self._shadow_merge_ref)
4277
4277
4278 @shadow_merge_ref.setter
4278 @shadow_merge_ref.setter
4279 def shadow_merge_ref(self, ref):
4279 def shadow_merge_ref(self, ref):
4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4280 self._shadow_merge_ref = self.reference_to_unicode(ref)
4281
4281
4282 @staticmethod
4282 @staticmethod
4283 def unicode_to_reference(raw):
4283 def unicode_to_reference(raw):
4284 return unicode_to_reference(raw)
4284 return unicode_to_reference(raw)
4285
4285
4286 @staticmethod
4286 @staticmethod
4287 def reference_to_unicode(ref):
4287 def reference_to_unicode(ref):
4288 return reference_to_unicode(ref)
4288 return reference_to_unicode(ref)
4289
4289
4290 def get_api_data(self, with_merge_state=True):
4290 def get_api_data(self, with_merge_state=True):
4291 from rhodecode.model.pull_request import PullRequestModel
4291 from rhodecode.model.pull_request import PullRequestModel
4292
4292
4293 pull_request = self
4293 pull_request = self
4294 if with_merge_state:
4294 if with_merge_state:
4295 merge_response, merge_status, msg = \
4295 merge_response, merge_status, msg = \
4296 PullRequestModel().merge_status(pull_request)
4296 PullRequestModel().merge_status(pull_request)
4297 merge_state = {
4297 merge_state = {
4298 'status': merge_status,
4298 'status': merge_status,
4299 'message': safe_unicode(msg),
4299 'message': safe_unicode(msg),
4300 }
4300 }
4301 else:
4301 else:
4302 merge_state = {'status': 'not_available',
4302 merge_state = {'status': 'not_available',
4303 'message': 'not_available'}
4303 'message': 'not_available'}
4304
4304
4305 merge_data = {
4305 merge_data = {
4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4306 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4307 'reference': (
4307 'reference': (
4308 pull_request.shadow_merge_ref._asdict()
4308 pull_request.shadow_merge_ref._asdict()
4309 if pull_request.shadow_merge_ref else None),
4309 if pull_request.shadow_merge_ref else None),
4310 }
4310 }
4311
4311
4312 data = {
4312 data = {
4313 'pull_request_id': pull_request.pull_request_id,
4313 'pull_request_id': pull_request.pull_request_id,
4314 'url': PullRequestModel().get_url(pull_request),
4314 'url': PullRequestModel().get_url(pull_request),
4315 'title': pull_request.title,
4315 'title': pull_request.title,
4316 'description': pull_request.description,
4316 'description': pull_request.description,
4317 'status': pull_request.status,
4317 'status': pull_request.status,
4318 'state': pull_request.pull_request_state,
4318 'state': pull_request.pull_request_state,
4319 'created_on': pull_request.created_on,
4319 'created_on': pull_request.created_on,
4320 'updated_on': pull_request.updated_on,
4320 'updated_on': pull_request.updated_on,
4321 'commit_ids': pull_request.revisions,
4321 'commit_ids': pull_request.revisions,
4322 'review_status': pull_request.calculated_review_status(),
4322 'review_status': pull_request.calculated_review_status(),
4323 'mergeable': merge_state,
4323 'mergeable': merge_state,
4324 'source': {
4324 'source': {
4325 'clone_url': pull_request.source_repo.clone_url(),
4325 'clone_url': pull_request.source_repo.clone_url(),
4326 'repository': pull_request.source_repo.repo_name,
4326 'repository': pull_request.source_repo.repo_name,
4327 'reference': {
4327 'reference': {
4328 'name': pull_request.source_ref_parts.name,
4328 'name': pull_request.source_ref_parts.name,
4329 'type': pull_request.source_ref_parts.type,
4329 'type': pull_request.source_ref_parts.type,
4330 'commit_id': pull_request.source_ref_parts.commit_id,
4330 'commit_id': pull_request.source_ref_parts.commit_id,
4331 },
4331 },
4332 },
4332 },
4333 'target': {
4333 'target': {
4334 'clone_url': pull_request.target_repo.clone_url(),
4334 'clone_url': pull_request.target_repo.clone_url(),
4335 'repository': pull_request.target_repo.repo_name,
4335 'repository': pull_request.target_repo.repo_name,
4336 'reference': {
4336 'reference': {
4337 'name': pull_request.target_ref_parts.name,
4337 'name': pull_request.target_ref_parts.name,
4338 'type': pull_request.target_ref_parts.type,
4338 'type': pull_request.target_ref_parts.type,
4339 'commit_id': pull_request.target_ref_parts.commit_id,
4339 'commit_id': pull_request.target_ref_parts.commit_id,
4340 },
4340 },
4341 },
4341 },
4342 'merge': merge_data,
4342 'merge': merge_data,
4343 'author': pull_request.author.get_api_data(include_secrets=False,
4343 'author': pull_request.author.get_api_data(include_secrets=False,
4344 details='basic'),
4344 details='basic'),
4345 'reviewers': [
4345 'reviewers': [
4346 {
4346 {
4347 'user': reviewer.get_api_data(include_secrets=False,
4347 'user': reviewer.get_api_data(include_secrets=False,
4348 details='basic'),
4348 details='basic'),
4349 'reasons': reasons,
4349 'reasons': reasons,
4350 'review_status': st[0][1].status if st else 'not_reviewed',
4350 'review_status': st[0][1].status if st else 'not_reviewed',
4351 }
4351 }
4352 for obj, reviewer, reasons, mandatory, st in
4352 for obj, reviewer, reasons, mandatory, st in
4353 pull_request.reviewers_statuses()
4353 pull_request.reviewers_statuses()
4354 ]
4354 ]
4355 }
4355 }
4356
4356
4357 return data
4357 return data
4358
4358
4359 def set_state(self, pull_request_state, final_state=None):
4359 def set_state(self, pull_request_state, final_state=None):
4360 """
4360 """
4361 # goes from initial state to updating to initial state.
4361 # goes from initial state to updating to initial state.
4362 # initial state can be changed by specifying back_state=
4362 # initial state can be changed by specifying back_state=
4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4363 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4364 pull_request.merge()
4364 pull_request.merge()
4365
4365
4366 :param pull_request_state:
4366 :param pull_request_state:
4367 :param final_state:
4367 :param final_state:
4368
4368
4369 """
4369 """
4370
4370
4371 return _SetState(self, pull_request_state, back_state=final_state)
4371 return _SetState(self, pull_request_state, back_state=final_state)
4372
4372
4373
4373
4374 class PullRequest(Base, _PullRequestBase):
4374 class PullRequest(Base, _PullRequestBase):
4375 __tablename__ = 'pull_requests'
4375 __tablename__ = 'pull_requests'
4376 __table_args__ = (
4376 __table_args__ = (
4377 base_table_args,
4377 base_table_args,
4378 )
4378 )
4379 LATEST_VER = 'latest'
4379 LATEST_VER = 'latest'
4380
4380
4381 pull_request_id = Column(
4381 pull_request_id = Column(
4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4382 'pull_request_id', Integer(), nullable=False, primary_key=True)
4383
4383
4384 def __repr__(self):
4384 def __repr__(self):
4385 if self.pull_request_id:
4385 if self.pull_request_id:
4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4386 return '<DB:PullRequest #%s>' % self.pull_request_id
4387 else:
4387 else:
4388 return '<DB:PullRequest at %#x>' % id(self)
4388 return '<DB:PullRequest at %#x>' % id(self)
4389
4389
4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4390 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4391 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4392 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4393 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4394 lazy='dynamic')
4394 lazy='dynamic')
4395
4395
4396 @classmethod
4396 @classmethod
4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4397 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4398 internal_methods=None):
4398 internal_methods=None):
4399
4399
4400 class PullRequestDisplay(object):
4400 class PullRequestDisplay(object):
4401 """
4401 """
4402 Special object wrapper for showing PullRequest data via Versions
4402 Special object wrapper for showing PullRequest data via Versions
4403 It mimics PR object as close as possible. This is read only object
4403 It mimics PR object as close as possible. This is read only object
4404 just for display
4404 just for display
4405 """
4405 """
4406
4406
4407 def __init__(self, attrs, internal=None):
4407 def __init__(self, attrs, internal=None):
4408 self.attrs = attrs
4408 self.attrs = attrs
4409 # internal have priority over the given ones via attrs
4409 # internal have priority over the given ones via attrs
4410 self.internal = internal or ['versions']
4410 self.internal = internal or ['versions']
4411
4411
4412 def __getattr__(self, item):
4412 def __getattr__(self, item):
4413 if item in self.internal:
4413 if item in self.internal:
4414 return getattr(self, item)
4414 return getattr(self, item)
4415 try:
4415 try:
4416 return self.attrs[item]
4416 return self.attrs[item]
4417 except KeyError:
4417 except KeyError:
4418 raise AttributeError(
4418 raise AttributeError(
4419 '%s object has no attribute %s' % (self, item))
4419 '%s object has no attribute %s' % (self, item))
4420
4420
4421 def __repr__(self):
4421 def __repr__(self):
4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4422 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4423
4423
4424 def versions(self):
4424 def versions(self):
4425 return pull_request_obj.versions.order_by(
4425 return pull_request_obj.versions.order_by(
4426 PullRequestVersion.pull_request_version_id).all()
4426 PullRequestVersion.pull_request_version_id).all()
4427
4427
4428 def is_closed(self):
4428 def is_closed(self):
4429 return pull_request_obj.is_closed()
4429 return pull_request_obj.is_closed()
4430
4430
4431 def is_state_changing(self):
4431 def is_state_changing(self):
4432 return pull_request_obj.is_state_changing()
4432 return pull_request_obj.is_state_changing()
4433
4433
4434 @property
4434 @property
4435 def pull_request_version_id(self):
4435 def pull_request_version_id(self):
4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4436 return getattr(pull_request_obj, 'pull_request_version_id', None)
4437
4437
4438 @property
4438 @property
4439 def pull_request_last_version(self):
4439 def pull_request_last_version(self):
4440 return pull_request_obj.pull_request_last_version
4440 return pull_request_obj.pull_request_last_version
4441
4441
4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4442 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4443
4443
4444 attrs.author = StrictAttributeDict(
4444 attrs.author = StrictAttributeDict(
4445 pull_request_obj.author.get_api_data())
4445 pull_request_obj.author.get_api_data())
4446 if pull_request_obj.target_repo:
4446 if pull_request_obj.target_repo:
4447 attrs.target_repo = StrictAttributeDict(
4447 attrs.target_repo = StrictAttributeDict(
4448 pull_request_obj.target_repo.get_api_data())
4448 pull_request_obj.target_repo.get_api_data())
4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4449 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4450
4450
4451 if pull_request_obj.source_repo:
4451 if pull_request_obj.source_repo:
4452 attrs.source_repo = StrictAttributeDict(
4452 attrs.source_repo = StrictAttributeDict(
4453 pull_request_obj.source_repo.get_api_data())
4453 pull_request_obj.source_repo.get_api_data())
4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4454 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4455
4455
4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4456 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4457 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4458 attrs.revisions = pull_request_obj.revisions
4458 attrs.revisions = pull_request_obj.revisions
4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4459 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4460 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4461 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4462 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4463
4463
4464 return PullRequestDisplay(attrs, internal=internal_methods)
4464 return PullRequestDisplay(attrs, internal=internal_methods)
4465
4465
4466 def is_closed(self):
4466 def is_closed(self):
4467 return self.status == self.STATUS_CLOSED
4467 return self.status == self.STATUS_CLOSED
4468
4468
4469 def is_state_changing(self):
4469 def is_state_changing(self):
4470 return self.pull_request_state != PullRequest.STATE_CREATED
4470 return self.pull_request_state != PullRequest.STATE_CREATED
4471
4471
4472 def __json__(self):
4472 def __json__(self):
4473 return {
4473 return {
4474 'revisions': self.revisions,
4474 'revisions': self.revisions,
4475 'versions': self.versions_count
4475 'versions': self.versions_count
4476 }
4476 }
4477
4477
4478 def calculated_review_status(self):
4478 def calculated_review_status(self):
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4479 from rhodecode.model.changeset_status import ChangesetStatusModel
4480 return ChangesetStatusModel().calculated_review_status(self)
4480 return ChangesetStatusModel().calculated_review_status(self)
4481
4481
4482 def reviewers_statuses(self, user=None):
4482 def reviewers_statuses(self, user=None):
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4483 from rhodecode.model.changeset_status import ChangesetStatusModel
4484 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4484 return ChangesetStatusModel().reviewers_statuses(self, user=user)
4485
4485
4486 def get_pull_request_reviewers(self, role=None):
4486 def get_pull_request_reviewers(self, role=None):
4487 qry = PullRequestReviewers.query()\
4487 qry = PullRequestReviewers.query()\
4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4488 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4489 if role:
4489 if role:
4490 qry = qry.filter(PullRequestReviewers.role == role)
4490 qry = qry.filter(PullRequestReviewers.role == role)
4491
4491
4492 return qry.all()
4492 return qry.all()
4493
4493
4494 @property
4494 @property
4495 def reviewers_count(self):
4495 def reviewers_count(self):
4496 qry = PullRequestReviewers.query()\
4496 qry = PullRequestReviewers.query()\
4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4497 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4498 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4499 return qry.count()
4499 return qry.count()
4500
4500
4501 @property
4501 @property
4502 def observers_count(self):
4502 def observers_count(self):
4503 qry = PullRequestReviewers.query()\
4503 qry = PullRequestReviewers.query()\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4504 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4505 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4506 return qry.count()
4506 return qry.count()
4507
4507
4508 def observers(self):
4508 def observers(self):
4509 qry = PullRequestReviewers.query()\
4509 qry = PullRequestReviewers.query()\
4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4510 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4511 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4512 .all()
4512 .all()
4513
4513
4514 for entry in qry:
4514 for entry in qry:
4515 yield entry, entry.user
4515 yield entry, entry.user
4516
4516
4517 @property
4517 @property
4518 def workspace_id(self):
4518 def workspace_id(self):
4519 from rhodecode.model.pull_request import PullRequestModel
4519 from rhodecode.model.pull_request import PullRequestModel
4520 return PullRequestModel()._workspace_id(self)
4520 return PullRequestModel()._workspace_id(self)
4521
4521
4522 def get_shadow_repo(self):
4522 def get_shadow_repo(self):
4523 workspace_id = self.workspace_id
4523 workspace_id = self.workspace_id
4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4524 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4525 if os.path.isdir(shadow_repository_path):
4525 if os.path.isdir(shadow_repository_path):
4526 vcs_obj = self.target_repo.scm_instance()
4526 vcs_obj = self.target_repo.scm_instance()
4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4527 return vcs_obj.get_shadow_instance(shadow_repository_path)
4528
4528
4529 @property
4529 @property
4530 def versions_count(self):
4530 def versions_count(self):
4531 """
4531 """
4532 return number of versions this PR have, e.g a PR that once been
4532 return number of versions this PR have, e.g a PR that once been
4533 updated will have 2 versions
4533 updated will have 2 versions
4534 """
4534 """
4535 return self.versions.count() + 1
4535 return self.versions.count() + 1
4536
4536
4537 @property
4537 @property
4538 def pull_request_last_version(self):
4538 def pull_request_last_version(self):
4539 return self.versions_count
4539 return self.versions_count
4540
4540
4541
4541
4542 class PullRequestVersion(Base, _PullRequestBase):
4542 class PullRequestVersion(Base, _PullRequestBase):
4543 __tablename__ = 'pull_request_versions'
4543 __tablename__ = 'pull_request_versions'
4544 __table_args__ = (
4544 __table_args__ = (
4545 base_table_args,
4545 base_table_args,
4546 )
4546 )
4547
4547
4548 pull_request_version_id = Column(
4548 pull_request_version_id = Column(
4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4549 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4550 pull_request_id = Column(
4550 pull_request_id = Column(
4551 'pull_request_id', Integer(),
4551 'pull_request_id', Integer(),
4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4552 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4553 pull_request = relationship('PullRequest')
4553 pull_request = relationship('PullRequest')
4554
4554
4555 def __repr__(self):
4555 def __repr__(self):
4556 if self.pull_request_version_id:
4556 if self.pull_request_version_id:
4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4557 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4558 else:
4558 else:
4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4559 return '<DB:PullRequestVersion at %#x>' % id(self)
4560
4560
4561 @property
4561 @property
4562 def reviewers(self):
4562 def reviewers(self):
4563 return self.pull_request.reviewers
4563 return self.pull_request.reviewers
4564 @property
4564 @property
4565 def reviewers(self):
4565 def reviewers(self):
4566 return self.pull_request.reviewers
4566 return self.pull_request.reviewers
4567
4567
4568 @property
4568 @property
4569 def versions(self):
4569 def versions(self):
4570 return self.pull_request.versions
4570 return self.pull_request.versions
4571
4571
4572 def is_closed(self):
4572 def is_closed(self):
4573 # calculate from original
4573 # calculate from original
4574 return self.pull_request.status == self.STATUS_CLOSED
4574 return self.pull_request.status == self.STATUS_CLOSED
4575
4575
4576 def is_state_changing(self):
4576 def is_state_changing(self):
4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4577 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4578
4578
4579 def calculated_review_status(self):
4579 def calculated_review_status(self):
4580 return self.pull_request.calculated_review_status()
4580 return self.pull_request.calculated_review_status()
4581
4581
4582 def reviewers_statuses(self):
4582 def reviewers_statuses(self):
4583 return self.pull_request.reviewers_statuses()
4583 return self.pull_request.reviewers_statuses()
4584
4584
4585 def observers(self):
4585 def observers(self):
4586 return self.pull_request.observers()
4586 return self.pull_request.observers()
4587
4587
4588
4588
4589 class PullRequestReviewers(Base, BaseModel):
4589 class PullRequestReviewers(Base, BaseModel):
4590 __tablename__ = 'pull_request_reviewers'
4590 __tablename__ = 'pull_request_reviewers'
4591 __table_args__ = (
4591 __table_args__ = (
4592 base_table_args,
4592 base_table_args,
4593 )
4593 )
4594 ROLE_REVIEWER = u'reviewer'
4594 ROLE_REVIEWER = u'reviewer'
4595 ROLE_OBSERVER = u'observer'
4595 ROLE_OBSERVER = u'observer'
4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4596 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4597
4597
4598 @hybrid_property
4598 @hybrid_property
4599 def reasons(self):
4599 def reasons(self):
4600 if not self._reasons:
4600 if not self._reasons:
4601 return []
4601 return []
4602 return self._reasons
4602 return self._reasons
4603
4603
4604 @reasons.setter
4604 @reasons.setter
4605 def reasons(self, val):
4605 def reasons(self, val):
4606 val = val or []
4606 val = val or []
4607 if any(not isinstance(x, compat.string_types) for x in val):
4607 if any(not isinstance(x, compat.string_types) for x in val):
4608 raise Exception('invalid reasons type, must be list of strings')
4608 raise Exception('invalid reasons type, must be list of strings')
4609 self._reasons = val
4609 self._reasons = val
4610
4610
4611 pull_requests_reviewers_id = Column(
4611 pull_requests_reviewers_id = Column(
4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4612 'pull_requests_reviewers_id', Integer(), nullable=False,
4613 primary_key=True)
4613 primary_key=True)
4614 pull_request_id = Column(
4614 pull_request_id = Column(
4615 "pull_request_id", Integer(),
4615 "pull_request_id", Integer(),
4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4616 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4617 user_id = Column(
4617 user_id = Column(
4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4618 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4619 _reasons = Column(
4619 _reasons = Column(
4620 'reason', MutationList.as_mutable(
4620 'reason', MutationList.as_mutable(
4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4621 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4622
4622
4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4623 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4624 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4625
4625
4626 user = relationship('User')
4626 user = relationship('User')
4627 pull_request = relationship('PullRequest')
4627 pull_request = relationship('PullRequest')
4628
4628
4629 rule_data = Column(
4629 rule_data = Column(
4630 'rule_data_json',
4630 'rule_data_json',
4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4631 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4632
4632
4633 def rule_user_group_data(self):
4633 def rule_user_group_data(self):
4634 """
4634 """
4635 Returns the voting user group rule data for this reviewer
4635 Returns the voting user group rule data for this reviewer
4636 """
4636 """
4637
4637
4638 if self.rule_data and 'vote_rule' in self.rule_data:
4638 if self.rule_data and 'vote_rule' in self.rule_data:
4639 user_group_data = {}
4639 user_group_data = {}
4640 if 'rule_user_group_entry_id' in self.rule_data:
4640 if 'rule_user_group_entry_id' in self.rule_data:
4641 # means a group with voting rules !
4641 # means a group with voting rules !
4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4642 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4643 user_group_data['name'] = self.rule_data['rule_name']
4643 user_group_data['name'] = self.rule_data['rule_name']
4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4644 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4645
4645
4646 return user_group_data
4646 return user_group_data
4647
4647
4648 @classmethod
4648 @classmethod
4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4649 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4650 qry = PullRequestReviewers.query()\
4650 qry = PullRequestReviewers.query()\
4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4651 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4652 if role:
4652 if role:
4653 qry = qry.filter(PullRequestReviewers.role == role)
4653 qry = qry.filter(PullRequestReviewers.role == role)
4654
4654
4655 return qry.all()
4655 return qry.all()
4656
4656
4657 def __unicode__(self):
4657 def __unicode__(self):
4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4658 return u"<%s('id:%s')>" % (self.__class__.__name__,
4659 self.pull_requests_reviewers_id)
4659 self.pull_requests_reviewers_id)
4660
4660
4661
4661
4662 class Notification(Base, BaseModel):
4662 class Notification(Base, BaseModel):
4663 __tablename__ = 'notifications'
4663 __tablename__ = 'notifications'
4664 __table_args__ = (
4664 __table_args__ = (
4665 Index('notification_type_idx', 'type'),
4665 Index('notification_type_idx', 'type'),
4666 base_table_args,
4666 base_table_args,
4667 )
4667 )
4668
4668
4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4669 TYPE_CHANGESET_COMMENT = u'cs_comment'
4670 TYPE_MESSAGE = u'message'
4670 TYPE_MESSAGE = u'message'
4671 TYPE_MENTION = u'mention'
4671 TYPE_MENTION = u'mention'
4672 TYPE_REGISTRATION = u'registration'
4672 TYPE_REGISTRATION = u'registration'
4673 TYPE_PULL_REQUEST = u'pull_request'
4673 TYPE_PULL_REQUEST = u'pull_request'
4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4674 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4675 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4676
4676
4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4677 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4678 subject = Column('subject', Unicode(512), nullable=True)
4678 subject = Column('subject', Unicode(512), nullable=True)
4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4679 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4680 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4681 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4682 type_ = Column('type', Unicode(255))
4682 type_ = Column('type', Unicode(255))
4683
4683
4684 created_by_user = relationship('User')
4684 created_by_user = relationship('User')
4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4685 notifications_to_users = relationship('UserNotification', lazy='joined',
4686 cascade="all, delete-orphan")
4686 cascade="all, delete-orphan")
4687
4687
4688 @property
4688 @property
4689 def recipients(self):
4689 def recipients(self):
4690 return [x.user for x in UserNotification.query()\
4690 return [x.user for x in UserNotification.query()\
4691 .filter(UserNotification.notification == self)\
4691 .filter(UserNotification.notification == self)\
4692 .order_by(UserNotification.user_id.asc()).all()]
4692 .order_by(UserNotification.user_id.asc()).all()]
4693
4693
4694 @classmethod
4694 @classmethod
4695 def create(cls, created_by, subject, body, recipients, type_=None):
4695 def create(cls, created_by, subject, body, recipients, type_=None):
4696 if type_ is None:
4696 if type_ is None:
4697 type_ = Notification.TYPE_MESSAGE
4697 type_ = Notification.TYPE_MESSAGE
4698
4698
4699 notification = cls()
4699 notification = cls()
4700 notification.created_by_user = created_by
4700 notification.created_by_user = created_by
4701 notification.subject = subject
4701 notification.subject = subject
4702 notification.body = body
4702 notification.body = body
4703 notification.type_ = type_
4703 notification.type_ = type_
4704 notification.created_on = datetime.datetime.now()
4704 notification.created_on = datetime.datetime.now()
4705
4705
4706 # For each recipient link the created notification to his account
4706 # For each recipient link the created notification to his account
4707 for u in recipients:
4707 for u in recipients:
4708 assoc = UserNotification()
4708 assoc = UserNotification()
4709 assoc.user_id = u.user_id
4709 assoc.user_id = u.user_id
4710 assoc.notification = notification
4710 assoc.notification = notification
4711
4711
4712 # if created_by is inside recipients mark his notification
4712 # if created_by is inside recipients mark his notification
4713 # as read
4713 # as read
4714 if u.user_id == created_by.user_id:
4714 if u.user_id == created_by.user_id:
4715 assoc.read = True
4715 assoc.read = True
4716 Session().add(assoc)
4716 Session().add(assoc)
4717
4717
4718 Session().add(notification)
4718 Session().add(notification)
4719
4719
4720 return notification
4720 return notification
4721
4721
4722
4722
4723 class UserNotification(Base, BaseModel):
4723 class UserNotification(Base, BaseModel):
4724 __tablename__ = 'user_to_notification'
4724 __tablename__ = 'user_to_notification'
4725 __table_args__ = (
4725 __table_args__ = (
4726 UniqueConstraint('user_id', 'notification_id'),
4726 UniqueConstraint('user_id', 'notification_id'),
4727 base_table_args
4727 base_table_args
4728 )
4728 )
4729
4729
4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4730 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4731 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4732 read = Column('read', Boolean, default=False)
4732 read = Column('read', Boolean, default=False)
4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4733 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4734
4734
4735 user = relationship('User', lazy="joined")
4735 user = relationship('User', lazy="joined")
4736 notification = relationship('Notification', lazy="joined",
4736 notification = relationship('Notification', lazy="joined",
4737 order_by=lambda: Notification.created_on.desc(),)
4737 order_by=lambda: Notification.created_on.desc(),)
4738
4738
4739 def mark_as_read(self):
4739 def mark_as_read(self):
4740 self.read = True
4740 self.read = True
4741 Session().add(self)
4741 Session().add(self)
4742
4742
4743
4743
4744 class UserNotice(Base, BaseModel):
4744 class UserNotice(Base, BaseModel):
4745 __tablename__ = 'user_notices'
4745 __tablename__ = 'user_notices'
4746 __table_args__ = (
4746 __table_args__ = (
4747 base_table_args
4747 base_table_args
4748 )
4748 )
4749
4749
4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4750 NOTIFICATION_TYPE_MESSAGE = 'message'
4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4751 NOTIFICATION_TYPE_NOTICE = 'notice'
4752
4752
4753 NOTIFICATION_LEVEL_INFO = 'info'
4753 NOTIFICATION_LEVEL_INFO = 'info'
4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4754 NOTIFICATION_LEVEL_WARNING = 'warning'
4755 NOTIFICATION_LEVEL_ERROR = 'error'
4755 NOTIFICATION_LEVEL_ERROR = 'error'
4756
4756
4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4757 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4758
4758
4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4759 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4760 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4761
4761
4762 notice_read = Column('notice_read', Boolean, default=False)
4762 notice_read = Column('notice_read', Boolean, default=False)
4763
4763
4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4764 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4765 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4766
4766
4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4767 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4768 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4769
4769
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4770 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4771 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4772
4772
4773 @classmethod
4773 @classmethod
4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4774 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4775
4775
4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4776 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4777 cls.NOTIFICATION_LEVEL_WARNING,
4777 cls.NOTIFICATION_LEVEL_WARNING,
4778 cls.NOTIFICATION_LEVEL_INFO]:
4778 cls.NOTIFICATION_LEVEL_INFO]:
4779 return
4779 return
4780
4780
4781 from rhodecode.model.user import UserModel
4781 from rhodecode.model.user import UserModel
4782 user = UserModel().get_user(user)
4782 user = UserModel().get_user(user)
4783
4783
4784 new_notice = UserNotice()
4784 new_notice = UserNotice()
4785 if not allow_duplicate:
4785 if not allow_duplicate:
4786 existing_msg = UserNotice().query() \
4786 existing_msg = UserNotice().query() \
4787 .filter(UserNotice.user == user) \
4787 .filter(UserNotice.user == user) \
4788 .filter(UserNotice.notice_body == body) \
4788 .filter(UserNotice.notice_body == body) \
4789 .filter(UserNotice.notice_read == false()) \
4789 .filter(UserNotice.notice_read == false()) \
4790 .scalar()
4790 .scalar()
4791 if existing_msg:
4791 if existing_msg:
4792 log.warning('Ignoring duplicate notice for user %s', user)
4792 log.warning('Ignoring duplicate notice for user %s', user)
4793 return
4793 return
4794
4794
4795 new_notice.user = user
4795 new_notice.user = user
4796 new_notice.notice_subject = subject
4796 new_notice.notice_subject = subject
4797 new_notice.notice_body = body
4797 new_notice.notice_body = body
4798 new_notice.notification_level = notice_level
4798 new_notice.notification_level = notice_level
4799 Session().add(new_notice)
4799 Session().add(new_notice)
4800 Session().commit()
4800 Session().commit()
4801
4801
4802
4802
4803 class Gist(Base, BaseModel):
4803 class Gist(Base, BaseModel):
4804 __tablename__ = 'gists'
4804 __tablename__ = 'gists'
4805 __table_args__ = (
4805 __table_args__ = (
4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4806 Index('g_gist_access_id_idx', 'gist_access_id'),
4807 Index('g_created_on_idx', 'created_on'),
4807 Index('g_created_on_idx', 'created_on'),
4808 base_table_args
4808 base_table_args
4809 )
4809 )
4810
4810
4811 GIST_PUBLIC = u'public'
4811 GIST_PUBLIC = u'public'
4812 GIST_PRIVATE = u'private'
4812 GIST_PRIVATE = u'private'
4813 DEFAULT_FILENAME = u'gistfile1.txt'
4813 DEFAULT_FILENAME = u'gistfile1.txt'
4814
4814
4815 ACL_LEVEL_PUBLIC = u'acl_public'
4815 ACL_LEVEL_PUBLIC = u'acl_public'
4816 ACL_LEVEL_PRIVATE = u'acl_private'
4816 ACL_LEVEL_PRIVATE = u'acl_private'
4817
4817
4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4818 gist_id = Column('gist_id', Integer(), primary_key=True)
4819 gist_access_id = Column('gist_access_id', Unicode(250))
4819 gist_access_id = Column('gist_access_id', Unicode(250))
4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4820 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4821 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4822 gist_expires = Column('gist_expires', Float(53), nullable=False)
4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4823 gist_type = Column('gist_type', Unicode(128), nullable=False)
4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4824 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4825 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4826 acl_level = Column('acl_level', Unicode(128), nullable=True)
4827
4827
4828 owner = relationship('User')
4828 owner = relationship('User')
4829
4829
4830 def __repr__(self):
4830 def __repr__(self):
4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4831 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4832
4832
4833 @hybrid_property
4833 @hybrid_property
4834 def description_safe(self):
4834 def description_safe(self):
4835 from rhodecode.lib import helpers as h
4835 from rhodecode.lib import helpers as h
4836 return h.escape(self.gist_description)
4836 return h.escape(self.gist_description)
4837
4837
4838 @classmethod
4838 @classmethod
4839 def get_or_404(cls, id_):
4839 def get_or_404(cls, id_):
4840 from pyramid.httpexceptions import HTTPNotFound
4840 from pyramid.httpexceptions import HTTPNotFound
4841
4841
4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4842 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4843 if not res:
4843 if not res:
4844 log.debug('WARN: No DB entry with id %s', id_)
4844 log.debug('WARN: No DB entry with id %s', id_)
4845 raise HTTPNotFound()
4845 raise HTTPNotFound()
4846 return res
4846 return res
4847
4847
4848 @classmethod
4848 @classmethod
4849 def get_by_access_id(cls, gist_access_id):
4849 def get_by_access_id(cls, gist_access_id):
4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4850 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4851
4851
4852 def gist_url(self):
4852 def gist_url(self):
4853 from rhodecode.model.gist import GistModel
4853 from rhodecode.model.gist import GistModel
4854 return GistModel().get_url(self)
4854 return GistModel().get_url(self)
4855
4855
4856 @classmethod
4856 @classmethod
4857 def base_path(cls):
4857 def base_path(cls):
4858 """
4858 """
4859 Returns base path when all gists are stored
4859 Returns base path when all gists are stored
4860
4860
4861 :param cls:
4861 :param cls:
4862 """
4862 """
4863 from rhodecode.model.gist import GIST_STORE_LOC
4863 from rhodecode.model.gist import GIST_STORE_LOC
4864 q = Session().query(RhodeCodeUi)\
4864 q = Session().query(RhodeCodeUi)\
4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4865 .filter(RhodeCodeUi.ui_key == URL_SEP)
4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4866 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4867 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4868
4868
4869 def get_api_data(self):
4869 def get_api_data(self):
4870 """
4870 """
4871 Common function for generating gist related data for API
4871 Common function for generating gist related data for API
4872 """
4872 """
4873 gist = self
4873 gist = self
4874 data = {
4874 data = {
4875 'gist_id': gist.gist_id,
4875 'gist_id': gist.gist_id,
4876 'type': gist.gist_type,
4876 'type': gist.gist_type,
4877 'access_id': gist.gist_access_id,
4877 'access_id': gist.gist_access_id,
4878 'description': gist.gist_description,
4878 'description': gist.gist_description,
4879 'url': gist.gist_url(),
4879 'url': gist.gist_url(),
4880 'expires': gist.gist_expires,
4880 'expires': gist.gist_expires,
4881 'created_on': gist.created_on,
4881 'created_on': gist.created_on,
4882 'modified_at': gist.modified_at,
4882 'modified_at': gist.modified_at,
4883 'content': None,
4883 'content': None,
4884 'acl_level': gist.acl_level,
4884 'acl_level': gist.acl_level,
4885 }
4885 }
4886 return data
4886 return data
4887
4887
4888 def __json__(self):
4888 def __json__(self):
4889 data = dict(
4889 data = dict(
4890 )
4890 )
4891 data.update(self.get_api_data())
4891 data.update(self.get_api_data())
4892 return data
4892 return data
4893 # SCM functions
4893 # SCM functions
4894
4894
4895 def scm_instance(self, **kwargs):
4895 def scm_instance(self, **kwargs):
4896 """
4896 """
4897 Get an instance of VCS Repository
4897 Get an instance of VCS Repository
4898
4898
4899 :param kwargs:
4899 :param kwargs:
4900 """
4900 """
4901 from rhodecode.model.gist import GistModel
4901 from rhodecode.model.gist import GistModel
4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4902 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4903 return get_vcs_instance(
4903 return get_vcs_instance(
4904 repo_path=safe_str(full_repo_path), create=False,
4904 repo_path=safe_str(full_repo_path), create=False,
4905 _vcs_alias=GistModel.vcs_backend)
4905 _vcs_alias=GistModel.vcs_backend)
4906
4906
4907
4907
4908 class ExternalIdentity(Base, BaseModel):
4908 class ExternalIdentity(Base, BaseModel):
4909 __tablename__ = 'external_identities'
4909 __tablename__ = 'external_identities'
4910 __table_args__ = (
4910 __table_args__ = (
4911 Index('local_user_id_idx', 'local_user_id'),
4911 Index('local_user_id_idx', 'local_user_id'),
4912 Index('external_id_idx', 'external_id'),
4912 Index('external_id_idx', 'external_id'),
4913 base_table_args
4913 base_table_args
4914 )
4914 )
4915
4915
4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4916 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4917 external_username = Column('external_username', Unicode(1024), default=u'')
4917 external_username = Column('external_username', Unicode(1024), default=u'')
4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4918 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4919 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4920 access_token = Column('access_token', String(1024), default=u'')
4920 access_token = Column('access_token', String(1024), default=u'')
4921 alt_token = Column('alt_token', String(1024), default=u'')
4921 alt_token = Column('alt_token', String(1024), default=u'')
4922 token_secret = Column('token_secret', String(1024), default=u'')
4922 token_secret = Column('token_secret', String(1024), default=u'')
4923
4923
4924 @classmethod
4924 @classmethod
4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4925 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4926 """
4926 """
4927 Returns ExternalIdentity instance based on search params
4927 Returns ExternalIdentity instance based on search params
4928
4928
4929 :param external_id:
4929 :param external_id:
4930 :param provider_name:
4930 :param provider_name:
4931 :return: ExternalIdentity
4931 :return: ExternalIdentity
4932 """
4932 """
4933 query = cls.query()
4933 query = cls.query()
4934 query = query.filter(cls.external_id == external_id)
4934 query = query.filter(cls.external_id == external_id)
4935 query = query.filter(cls.provider_name == provider_name)
4935 query = query.filter(cls.provider_name == provider_name)
4936 if local_user_id:
4936 if local_user_id:
4937 query = query.filter(cls.local_user_id == local_user_id)
4937 query = query.filter(cls.local_user_id == local_user_id)
4938 return query.first()
4938 return query.first()
4939
4939
4940 @classmethod
4940 @classmethod
4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4941 def user_by_external_id_and_provider(cls, external_id, provider_name):
4942 """
4942 """
4943 Returns User instance based on search params
4943 Returns User instance based on search params
4944
4944
4945 :param external_id:
4945 :param external_id:
4946 :param provider_name:
4946 :param provider_name:
4947 :return: User
4947 :return: User
4948 """
4948 """
4949 query = User.query()
4949 query = User.query()
4950 query = query.filter(cls.external_id == external_id)
4950 query = query.filter(cls.external_id == external_id)
4951 query = query.filter(cls.provider_name == provider_name)
4951 query = query.filter(cls.provider_name == provider_name)
4952 query = query.filter(User.user_id == cls.local_user_id)
4952 query = query.filter(User.user_id == cls.local_user_id)
4953 return query.first()
4953 return query.first()
4954
4954
4955 @classmethod
4955 @classmethod
4956 def by_local_user_id(cls, local_user_id):
4956 def by_local_user_id(cls, local_user_id):
4957 """
4957 """
4958 Returns all tokens for user
4958 Returns all tokens for user
4959
4959
4960 :param local_user_id:
4960 :param local_user_id:
4961 :return: ExternalIdentity
4961 :return: ExternalIdentity
4962 """
4962 """
4963 query = cls.query()
4963 query = cls.query()
4964 query = query.filter(cls.local_user_id == local_user_id)
4964 query = query.filter(cls.local_user_id == local_user_id)
4965 return query
4965 return query
4966
4966
4967 @classmethod
4967 @classmethod
4968 def load_provider_plugin(cls, plugin_id):
4968 def load_provider_plugin(cls, plugin_id):
4969 from rhodecode.authentication.base import loadplugin
4969 from rhodecode.authentication.base import loadplugin
4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4970 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4971 auth_plugin = loadplugin(_plugin_id)
4971 auth_plugin = loadplugin(_plugin_id)
4972 return auth_plugin
4972 return auth_plugin
4973
4973
4974
4974
4975 class Integration(Base, BaseModel):
4975 class Integration(Base, BaseModel):
4976 __tablename__ = 'integrations'
4976 __tablename__ = 'integrations'
4977 __table_args__ = (
4977 __table_args__ = (
4978 base_table_args
4978 base_table_args
4979 )
4979 )
4980
4980
4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4981 integration_id = Column('integration_id', Integer(), primary_key=True)
4982 integration_type = Column('integration_type', String(255))
4982 integration_type = Column('integration_type', String(255))
4983 enabled = Column('enabled', Boolean(), nullable=False)
4983 enabled = Column('enabled', Boolean(), nullable=False)
4984 name = Column('name', String(255), nullable=False)
4984 name = Column('name', String(255), nullable=False)
4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4985 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4986 default=False)
4986 default=False)
4987
4987
4988 settings = Column(
4988 settings = Column(
4989 'settings_json', MutationObj.as_mutable(
4989 'settings_json', MutationObj.as_mutable(
4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4990 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4991 repo_id = Column(
4991 repo_id = Column(
4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4992 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4993 nullable=True, unique=None, default=None)
4993 nullable=True, unique=None, default=None)
4994 repo = relationship('Repository', lazy='joined')
4994 repo = relationship('Repository', lazy='joined')
4995
4995
4996 repo_group_id = Column(
4996 repo_group_id = Column(
4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4997 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4998 nullable=True, unique=None, default=None)
4998 nullable=True, unique=None, default=None)
4999 repo_group = relationship('RepoGroup', lazy='joined')
4999 repo_group = relationship('RepoGroup', lazy='joined')
5000
5000
5001 @property
5001 @property
5002 def scope(self):
5002 def scope(self):
5003 if self.repo:
5003 if self.repo:
5004 return repr(self.repo)
5004 return repr(self.repo)
5005 if self.repo_group:
5005 if self.repo_group:
5006 if self.child_repos_only:
5006 if self.child_repos_only:
5007 return repr(self.repo_group) + ' (child repos only)'
5007 return repr(self.repo_group) + ' (child repos only)'
5008 else:
5008 else:
5009 return repr(self.repo_group) + ' (recursive)'
5009 return repr(self.repo_group) + ' (recursive)'
5010 if self.child_repos_only:
5010 if self.child_repos_only:
5011 return 'root_repos'
5011 return 'root_repos'
5012 return 'global'
5012 return 'global'
5013
5013
5014 def __repr__(self):
5014 def __repr__(self):
5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5015 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
5016
5016
5017
5017
5018 class RepoReviewRuleUser(Base, BaseModel):
5018 class RepoReviewRuleUser(Base, BaseModel):
5019 __tablename__ = 'repo_review_rules_users'
5019 __tablename__ = 'repo_review_rules_users'
5020 __table_args__ = (
5020 __table_args__ = (
5021 base_table_args
5021 base_table_args
5022 )
5022 )
5023 ROLE_REVIEWER = u'reviewer'
5023 ROLE_REVIEWER = u'reviewer'
5024 ROLE_OBSERVER = u'observer'
5024 ROLE_OBSERVER = u'observer'
5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5025 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5026
5026
5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5027 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5028 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5029 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5030 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5031 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5032 user = relationship('User')
5032 user = relationship('User')
5033
5033
5034 def rule_data(self):
5034 def rule_data(self):
5035 return {
5035 return {
5036 'mandatory': self.mandatory,
5036 'mandatory': self.mandatory,
5037 'role': self.role,
5037 'role': self.role,
5038 }
5038 }
5039
5039
5040
5040
5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5041 class RepoReviewRuleUserGroup(Base, BaseModel):
5042 __tablename__ = 'repo_review_rules_users_groups'
5042 __tablename__ = 'repo_review_rules_users_groups'
5043 __table_args__ = (
5043 __table_args__ = (
5044 base_table_args
5044 base_table_args
5045 )
5045 )
5046
5046
5047 VOTE_RULE_ALL = -1
5047 VOTE_RULE_ALL = -1
5048 ROLE_REVIEWER = u'reviewer'
5048 ROLE_REVIEWER = u'reviewer'
5049 ROLE_OBSERVER = u'observer'
5049 ROLE_OBSERVER = u'observer'
5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5050 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
5051
5051
5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5052 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5053 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5054 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5055 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5056 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5057 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5058 users_group = relationship('UserGroup')
5058 users_group = relationship('UserGroup')
5059
5059
5060 def rule_data(self):
5060 def rule_data(self):
5061 return {
5061 return {
5062 'mandatory': self.mandatory,
5062 'mandatory': self.mandatory,
5063 'role': self.role,
5063 'role': self.role,
5064 'vote_rule': self.vote_rule
5064 'vote_rule': self.vote_rule
5065 }
5065 }
5066
5066
5067 @property
5067 @property
5068 def vote_rule_label(self):
5068 def vote_rule_label(self):
5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5069 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
5070 return 'all must vote'
5070 return 'all must vote'
5071 else:
5071 else:
5072 return 'min. vote {}'.format(self.vote_rule)
5072 return 'min. vote {}'.format(self.vote_rule)
5073
5073
5074
5074
5075 class RepoReviewRule(Base, BaseModel):
5075 class RepoReviewRule(Base, BaseModel):
5076 __tablename__ = 'repo_review_rules'
5076 __tablename__ = 'repo_review_rules'
5077 __table_args__ = (
5077 __table_args__ = (
5078 base_table_args
5078 base_table_args
5079 )
5079 )
5080
5080
5081 repo_review_rule_id = Column(
5081 repo_review_rule_id = Column(
5082 'repo_review_rule_id', Integer(), primary_key=True)
5082 'repo_review_rule_id', Integer(), primary_key=True)
5083 repo_id = Column(
5083 repo_id = Column(
5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5084 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
5085 repo = relationship('Repository', backref='review_rules')
5085 repo = relationship('Repository', backref='review_rules')
5086
5086
5087 review_rule_name = Column('review_rule_name', String(255))
5087 review_rule_name = Column('review_rule_name', String(255))
5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5088 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5089 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5090 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5091
5091
5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5092 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5093
5093
5094 # Legacy fields, just for backward compat
5094 # Legacy fields, just for backward compat
5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5095 _forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5096 _forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5097
5097
5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5098 pr_author = Column("pr_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5099 commit_author = Column("commit_author", UnicodeText().with_variant(UnicodeText(255), 'mysql'), nullable=True)
5100
5100
5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5101 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5102
5102
5103 rule_users = relationship('RepoReviewRuleUser')
5103 rule_users = relationship('RepoReviewRuleUser')
5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5104 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5105
5105
5106 def _validate_pattern(self, value):
5106 def _validate_pattern(self, value):
5107 re.compile('^' + glob2re(value) + '$')
5107 re.compile('^' + glob2re(value) + '$')
5108
5108
5109 @hybrid_property
5109 @hybrid_property
5110 def source_branch_pattern(self):
5110 def source_branch_pattern(self):
5111 return self._branch_pattern or '*'
5111 return self._branch_pattern or '*'
5112
5112
5113 @source_branch_pattern.setter
5113 @source_branch_pattern.setter
5114 def source_branch_pattern(self, value):
5114 def source_branch_pattern(self, value):
5115 self._validate_pattern(value)
5115 self._validate_pattern(value)
5116 self._branch_pattern = value or '*'
5116 self._branch_pattern = value or '*'
5117
5117
5118 @hybrid_property
5118 @hybrid_property
5119 def target_branch_pattern(self):
5119 def target_branch_pattern(self):
5120 return self._target_branch_pattern or '*'
5120 return self._target_branch_pattern or '*'
5121
5121
5122 @target_branch_pattern.setter
5122 @target_branch_pattern.setter
5123 def target_branch_pattern(self, value):
5123 def target_branch_pattern(self, value):
5124 self._validate_pattern(value)
5124 self._validate_pattern(value)
5125 self._target_branch_pattern = value or '*'
5125 self._target_branch_pattern = value or '*'
5126
5126
5127 @hybrid_property
5127 @hybrid_property
5128 def file_pattern(self):
5128 def file_pattern(self):
5129 return self._file_pattern or '*'
5129 return self._file_pattern or '*'
5130
5130
5131 @file_pattern.setter
5131 @file_pattern.setter
5132 def file_pattern(self, value):
5132 def file_pattern(self, value):
5133 self._validate_pattern(value)
5133 self._validate_pattern(value)
5134 self._file_pattern = value or '*'
5134 self._file_pattern = value or '*'
5135
5135
5136 @hybrid_property
5136 @hybrid_property
5137 def forbid_pr_author_to_review(self):
5137 def forbid_pr_author_to_review(self):
5138 return self.pr_author == 'forbid_pr_author'
5138 return self.pr_author == 'forbid_pr_author'
5139
5139
5140 @hybrid_property
5140 @hybrid_property
5141 def include_pr_author_to_review(self):
5141 def include_pr_author_to_review(self):
5142 return self.pr_author == 'include_pr_author'
5142 return self.pr_author == 'include_pr_author'
5143
5143
5144 @hybrid_property
5144 @hybrid_property
5145 def forbid_commit_author_to_review(self):
5145 def forbid_commit_author_to_review(self):
5146 return self.commit_author == 'forbid_commit_author'
5146 return self.commit_author == 'forbid_commit_author'
5147
5147
5148 @hybrid_property
5148 @hybrid_property
5149 def include_commit_author_to_review(self):
5149 def include_commit_author_to_review(self):
5150 return self.commit_author == 'include_commit_author'
5150 return self.commit_author == 'include_commit_author'
5151
5151
5152 def matches(self, source_branch, target_branch, files_changed):
5152 def matches(self, source_branch, target_branch, files_changed):
5153 """
5153 """
5154 Check if this review rule matches a branch/files in a pull request
5154 Check if this review rule matches a branch/files in a pull request
5155
5155
5156 :param source_branch: source branch name for the commit
5156 :param source_branch: source branch name for the commit
5157 :param target_branch: target branch name for the commit
5157 :param target_branch: target branch name for the commit
5158 :param files_changed: list of file paths changed in the pull request
5158 :param files_changed: list of file paths changed in the pull request
5159 """
5159 """
5160
5160
5161 source_branch = source_branch or ''
5161 source_branch = source_branch or ''
5162 target_branch = target_branch or ''
5162 target_branch = target_branch or ''
5163 files_changed = files_changed or []
5163 files_changed = files_changed or []
5164
5164
5165 branch_matches = True
5165 branch_matches = True
5166 if source_branch or target_branch:
5166 if source_branch or target_branch:
5167 if self.source_branch_pattern == '*':
5167 if self.source_branch_pattern == '*':
5168 source_branch_match = True
5168 source_branch_match = True
5169 else:
5169 else:
5170 if self.source_branch_pattern.startswith('re:'):
5170 if self.source_branch_pattern.startswith('re:'):
5171 source_pattern = self.source_branch_pattern[3:]
5171 source_pattern = self.source_branch_pattern[3:]
5172 else:
5172 else:
5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5173 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5174 source_branch_regex = re.compile(source_pattern)
5174 source_branch_regex = re.compile(source_pattern)
5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5175 source_branch_match = bool(source_branch_regex.search(source_branch))
5176 if self.target_branch_pattern == '*':
5176 if self.target_branch_pattern == '*':
5177 target_branch_match = True
5177 target_branch_match = True
5178 else:
5178 else:
5179 if self.target_branch_pattern.startswith('re:'):
5179 if self.target_branch_pattern.startswith('re:'):
5180 target_pattern = self.target_branch_pattern[3:]
5180 target_pattern = self.target_branch_pattern[3:]
5181 else:
5181 else:
5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5182 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5183 target_branch_regex = re.compile(target_pattern)
5183 target_branch_regex = re.compile(target_pattern)
5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5184 target_branch_match = bool(target_branch_regex.search(target_branch))
5185
5185
5186 branch_matches = source_branch_match and target_branch_match
5186 branch_matches = source_branch_match and target_branch_match
5187
5187
5188 files_matches = True
5188 files_matches = True
5189 if self.file_pattern != '*':
5189 if self.file_pattern != '*':
5190 files_matches = False
5190 files_matches = False
5191 if self.file_pattern.startswith('re:'):
5191 if self.file_pattern.startswith('re:'):
5192 file_pattern = self.file_pattern[3:]
5192 file_pattern = self.file_pattern[3:]
5193 else:
5193 else:
5194 file_pattern = glob2re(self.file_pattern)
5194 file_pattern = glob2re(self.file_pattern)
5195 file_regex = re.compile(file_pattern)
5195 file_regex = re.compile(file_pattern)
5196 for file_data in files_changed:
5196 for file_data in files_changed:
5197 filename = file_data.get('filename')
5197 filename = file_data.get('filename')
5198
5198
5199 if file_regex.search(filename):
5199 if file_regex.search(filename):
5200 files_matches = True
5200 files_matches = True
5201 break
5201 break
5202
5202
5203 return branch_matches and files_matches
5203 return branch_matches and files_matches
5204
5204
5205 @property
5205 @property
5206 def review_users(self):
5206 def review_users(self):
5207 """ Returns the users which this rule applies to """
5207 """ Returns the users which this rule applies to """
5208
5208
5209 users = collections.OrderedDict()
5209 users = collections.OrderedDict()
5210
5210
5211 for rule_user in self.rule_users:
5211 for rule_user in self.rule_users:
5212 if rule_user.user.active:
5212 if rule_user.user.active:
5213 if rule_user.user not in users:
5213 if rule_user.user not in users:
5214 users[rule_user.user.username] = {
5214 users[rule_user.user.username] = {
5215 'user': rule_user.user,
5215 'user': rule_user.user,
5216 'source': 'user',
5216 'source': 'user',
5217 'source_data': {},
5217 'source_data': {},
5218 'data': rule_user.rule_data()
5218 'data': rule_user.rule_data()
5219 }
5219 }
5220
5220
5221 for rule_user_group in self.rule_user_groups:
5221 for rule_user_group in self.rule_user_groups:
5222 source_data = {
5222 source_data = {
5223 'user_group_id': rule_user_group.users_group.users_group_id,
5223 'user_group_id': rule_user_group.users_group.users_group_id,
5224 'name': rule_user_group.users_group.users_group_name,
5224 'name': rule_user_group.users_group.users_group_name,
5225 'members': len(rule_user_group.users_group.members)
5225 'members': len(rule_user_group.users_group.members)
5226 }
5226 }
5227 for member in rule_user_group.users_group.members:
5227 for member in rule_user_group.users_group.members:
5228 if member.user.active:
5228 if member.user.active:
5229 key = member.user.username
5229 key = member.user.username
5230 if key in users:
5230 if key in users:
5231 # skip this member as we have him already
5231 # skip this member as we have him already
5232 # this prevents from override the "first" matched
5232 # this prevents from override the "first" matched
5233 # users with duplicates in multiple groups
5233 # users with duplicates in multiple groups
5234 continue
5234 continue
5235
5235
5236 users[key] = {
5236 users[key] = {
5237 'user': member.user,
5237 'user': member.user,
5238 'source': 'user_group',
5238 'source': 'user_group',
5239 'source_data': source_data,
5239 'source_data': source_data,
5240 'data': rule_user_group.rule_data()
5240 'data': rule_user_group.rule_data()
5241 }
5241 }
5242
5242
5243 return users
5243 return users
5244
5244
5245 def user_group_vote_rule(self, user_id):
5245 def user_group_vote_rule(self, user_id):
5246
5246
5247 rules = []
5247 rules = []
5248 if not self.rule_user_groups:
5248 if not self.rule_user_groups:
5249 return rules
5249 return rules
5250
5250
5251 for user_group in self.rule_user_groups:
5251 for user_group in self.rule_user_groups:
5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5252 user_group_members = [x.user_id for x in user_group.users_group.members]
5253 if user_id in user_group_members:
5253 if user_id in user_group_members:
5254 rules.append(user_group)
5254 rules.append(user_group)
5255 return rules
5255 return rules
5256
5256
5257 def __repr__(self):
5257 def __repr__(self):
5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5258 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5259 self.repo_review_rule_id, self.repo)
5259 self.repo_review_rule_id, self.repo)
5260
5260
5261
5261
5262 class ScheduleEntry(Base, BaseModel):
5262 class ScheduleEntry(Base, BaseModel):
5263 __tablename__ = 'schedule_entries'
5263 __tablename__ = 'schedule_entries'
5264 __table_args__ = (
5264 __table_args__ = (
5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5265 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5266 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5267 base_table_args,
5267 base_table_args,
5268 )
5268 )
5269
5269
5270 schedule_types = ['crontab', 'timedelta', 'integer']
5270 schedule_types = ['crontab', 'timedelta', 'integer']
5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5271 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5272
5272
5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5273 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5274 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5275 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5276
5276
5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5277 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5278 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5279
5279
5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5280 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5281 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5282
5282
5283 # task
5283 # task
5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5284 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5285 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5286 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5287 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5288
5288
5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5289 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5290 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5291
5291
5292 @hybrid_property
5292 @hybrid_property
5293 def schedule_type(self):
5293 def schedule_type(self):
5294 return self._schedule_type
5294 return self._schedule_type
5295
5295
5296 @schedule_type.setter
5296 @schedule_type.setter
5297 def schedule_type(self, val):
5297 def schedule_type(self, val):
5298 if val not in self.schedule_types:
5298 if val not in self.schedule_types:
5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5299 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5300 val, self.schedule_type))
5300 val, self.schedule_type))
5301
5301
5302 self._schedule_type = val
5302 self._schedule_type = val
5303
5303
5304 @classmethod
5304 @classmethod
5305 def get_uid(cls, obj):
5305 def get_uid(cls, obj):
5306 args = obj.task_args
5306 args = obj.task_args
5307 kwargs = obj.task_kwargs
5307 kwargs = obj.task_kwargs
5308 if isinstance(args, JsonRaw):
5308 if isinstance(args, JsonRaw):
5309 try:
5309 try:
5310 args = json.loads(args)
5310 args = json.loads(args)
5311 except ValueError:
5311 except ValueError:
5312 args = tuple()
5312 args = tuple()
5313
5313
5314 if isinstance(kwargs, JsonRaw):
5314 if isinstance(kwargs, JsonRaw):
5315 try:
5315 try:
5316 kwargs = json.loads(kwargs)
5316 kwargs = json.loads(kwargs)
5317 except ValueError:
5317 except ValueError:
5318 kwargs = dict()
5318 kwargs = dict()
5319
5319
5320 dot_notation = obj.task_dot_notation
5320 dot_notation = obj.task_dot_notation
5321 val = '.'.join(map(safe_str, [
5321 val = '.'.join(map(safe_str, [
5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5322 sorted(dot_notation), args, sorted(kwargs.items())]))
5323 return hashlib.sha1(val).hexdigest()
5323 return hashlib.sha1(val).hexdigest()
5324
5324
5325 @classmethod
5325 @classmethod
5326 def get_by_schedule_name(cls, schedule_name):
5326 def get_by_schedule_name(cls, schedule_name):
5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5327 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5328
5328
5329 @classmethod
5329 @classmethod
5330 def get_by_schedule_id(cls, schedule_id):
5330 def get_by_schedule_id(cls, schedule_id):
5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5331 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5332
5332
5333 @property
5333 @property
5334 def task(self):
5334 def task(self):
5335 return self.task_dot_notation
5335 return self.task_dot_notation
5336
5336
5337 @property
5337 @property
5338 def schedule(self):
5338 def schedule(self):
5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5339 from rhodecode.lib.celerylib.utils import raw_2_schedule
5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5340 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5341 return schedule
5341 return schedule
5342
5342
5343 @property
5343 @property
5344 def args(self):
5344 def args(self):
5345 try:
5345 try:
5346 return list(self.task_args or [])
5346 return list(self.task_args or [])
5347 except ValueError:
5347 except ValueError:
5348 return list()
5348 return list()
5349
5349
5350 @property
5350 @property
5351 def kwargs(self):
5351 def kwargs(self):
5352 try:
5352 try:
5353 return dict(self.task_kwargs or {})
5353 return dict(self.task_kwargs or {})
5354 except ValueError:
5354 except ValueError:
5355 return dict()
5355 return dict()
5356
5356
5357 def _as_raw(self, val, indent=None):
5357 def _as_raw(self, val, indent=None):
5358 if hasattr(val, 'de_coerce'):
5358 if hasattr(val, 'de_coerce'):
5359 val = val.de_coerce()
5359 val = val.de_coerce()
5360 if val:
5360 if val:
5361 val = json.dumps(val, indent=indent, sort_keys=True)
5361 val = json.dumps(val, indent=indent, sort_keys=True)
5362
5362
5363 return val
5363 return val
5364
5364
5365 @property
5365 @property
5366 def schedule_definition_raw(self):
5366 def schedule_definition_raw(self):
5367 return self._as_raw(self.schedule_definition)
5367 return self._as_raw(self.schedule_definition)
5368
5368
5369 def args_raw(self, indent=None):
5369 def args_raw(self, indent=None):
5370 return self._as_raw(self.task_args, indent)
5370 return self._as_raw(self.task_args, indent)
5371
5371
5372 def kwargs_raw(self, indent=None):
5372 def kwargs_raw(self, indent=None):
5373 return self._as_raw(self.task_kwargs, indent)
5373 return self._as_raw(self.task_kwargs, indent)
5374
5374
5375 def __repr__(self):
5375 def __repr__(self):
5376 return '<DB:ScheduleEntry({}:{})>'.format(
5376 return '<DB:ScheduleEntry({}:{})>'.format(
5377 self.schedule_entry_id, self.schedule_name)
5377 self.schedule_entry_id, self.schedule_name)
5378
5378
5379
5379
5380 @event.listens_for(ScheduleEntry, 'before_update')
5380 @event.listens_for(ScheduleEntry, 'before_update')
5381 def update_task_uid(mapper, connection, target):
5381 def update_task_uid(mapper, connection, target):
5382 target.task_uid = ScheduleEntry.get_uid(target)
5382 target.task_uid = ScheduleEntry.get_uid(target)
5383
5383
5384
5384
5385 @event.listens_for(ScheduleEntry, 'before_insert')
5385 @event.listens_for(ScheduleEntry, 'before_insert')
5386 def set_task_uid(mapper, connection, target):
5386 def set_task_uid(mapper, connection, target):
5387 target.task_uid = ScheduleEntry.get_uid(target)
5387 target.task_uid = ScheduleEntry.get_uid(target)
5388
5388
5389
5389
5390 class _BaseBranchPerms(BaseModel):
5390 class _BaseBranchPerms(BaseModel):
5391 @classmethod
5391 @classmethod
5392 def compute_hash(cls, value):
5392 def compute_hash(cls, value):
5393 return sha1_safe(value)
5393 return sha1_safe(value)
5394
5394
5395 @hybrid_property
5395 @hybrid_property
5396 def branch_pattern(self):
5396 def branch_pattern(self):
5397 return self._branch_pattern or '*'
5397 return self._branch_pattern or '*'
5398
5398
5399 @hybrid_property
5399 @hybrid_property
5400 def branch_hash(self):
5400 def branch_hash(self):
5401 return self._branch_hash
5401 return self._branch_hash
5402
5402
5403 def _validate_glob(self, value):
5403 def _validate_glob(self, value):
5404 re.compile('^' + glob2re(value) + '$')
5404 re.compile('^' + glob2re(value) + '$')
5405
5405
5406 @branch_pattern.setter
5406 @branch_pattern.setter
5407 def branch_pattern(self, value):
5407 def branch_pattern(self, value):
5408 self._validate_glob(value)
5408 self._validate_glob(value)
5409 self._branch_pattern = value or '*'
5409 self._branch_pattern = value or '*'
5410 # set the Hash when setting the branch pattern
5410 # set the Hash when setting the branch pattern
5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5411 self._branch_hash = self.compute_hash(self._branch_pattern)
5412
5412
5413 def matches(self, branch):
5413 def matches(self, branch):
5414 """
5414 """
5415 Check if this the branch matches entry
5415 Check if this the branch matches entry
5416
5416
5417 :param branch: branch name for the commit
5417 :param branch: branch name for the commit
5418 """
5418 """
5419
5419
5420 branch = branch or ''
5420 branch = branch or ''
5421
5421
5422 branch_matches = True
5422 branch_matches = True
5423 if branch:
5423 if branch:
5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5424 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5425 branch_matches = bool(branch_regex.search(branch))
5425 branch_matches = bool(branch_regex.search(branch))
5426
5426
5427 return branch_matches
5427 return branch_matches
5428
5428
5429
5429
5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5430 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5431 __tablename__ = 'user_to_repo_branch_permissions'
5431 __tablename__ = 'user_to_repo_branch_permissions'
5432 __table_args__ = (
5432 __table_args__ = (
5433 base_table_args
5433 base_table_args
5434 )
5434 )
5435
5435
5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5436 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5437
5437
5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5438 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5439 repo = relationship('Repository', backref='user_branch_perms')
5439 repo = relationship('Repository', backref='user_branch_perms')
5440
5440
5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5441 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5442 permission = relationship('Permission')
5442 permission = relationship('Permission')
5443
5443
5444 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5444 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5445 user_repo_to_perm = relationship('UserRepoToPerm')
5445 user_repo_to_perm = relationship('UserRepoToPerm')
5446
5446
5447 rule_order = Column('rule_order', Integer(), nullable=False)
5447 rule_order = Column('rule_order', Integer(), nullable=False)
5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5448 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5449 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5450
5450
5451 def __unicode__(self):
5451 def __unicode__(self):
5452 return u'<UserBranchPermission(%s => %r)>' % (
5452 return u'<UserBranchPermission(%s => %r)>' % (
5453 self.user_repo_to_perm, self.branch_pattern)
5453 self.user_repo_to_perm, self.branch_pattern)
5454
5454
5455
5455
5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5456 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5457 __tablename__ = 'user_group_to_repo_branch_permissions'
5458 __table_args__ = (
5458 __table_args__ = (
5459 base_table_args
5459 base_table_args
5460 )
5460 )
5461
5461
5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5462 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5463
5463
5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5464 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5465 repo = relationship('Repository', backref='user_group_branch_perms')
5465 repo = relationship('Repository', backref='user_group_branch_perms')
5466
5466
5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5467 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5468 permission = relationship('Permission')
5468 permission = relationship('Permission')
5469
5469
5470 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5470 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5471 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5472
5472
5473 rule_order = Column('rule_order', Integer(), nullable=False)
5473 rule_order = Column('rule_order', Integer(), nullable=False)
5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5474 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5475 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5476
5476
5477 def __unicode__(self):
5477 def __unicode__(self):
5478 return u'<UserBranchPermission(%s => %r)>' % (
5478 return u'<UserBranchPermission(%s => %r)>' % (
5479 self.user_group_repo_to_perm, self.branch_pattern)
5479 self.user_group_repo_to_perm, self.branch_pattern)
5480
5480
5481
5481
5482 class UserBookmark(Base, BaseModel):
5482 class UserBookmark(Base, BaseModel):
5483 __tablename__ = 'user_bookmarks'
5483 __tablename__ = 'user_bookmarks'
5484 __table_args__ = (
5484 __table_args__ = (
5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5485 UniqueConstraint('user_id', 'bookmark_repo_id'),
5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5486 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5487 UniqueConstraint('user_id', 'bookmark_position'),
5487 UniqueConstraint('user_id', 'bookmark_position'),
5488 base_table_args
5488 base_table_args
5489 )
5489 )
5490
5490
5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5491 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5493 position = Column("bookmark_position", Integer(), nullable=False)
5493 position = Column("bookmark_position", Integer(), nullable=False)
5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5494 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5495 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5496 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5497
5497
5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5498 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5499 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5500
5500
5501 user = relationship("User")
5501 user = relationship("User")
5502
5502
5503 repository = relationship("Repository")
5503 repository = relationship("Repository")
5504 repository_group = relationship("RepoGroup")
5504 repository_group = relationship("RepoGroup")
5505
5505
5506 @classmethod
5506 @classmethod
5507 def get_by_position_for_user(cls, position, user_id):
5507 def get_by_position_for_user(cls, position, user_id):
5508 return cls.query() \
5508 return cls.query() \
5509 .filter(UserBookmark.user_id == user_id) \
5509 .filter(UserBookmark.user_id == user_id) \
5510 .filter(UserBookmark.position == position).scalar()
5510 .filter(UserBookmark.position == position).scalar()
5511
5511
5512 @classmethod
5512 @classmethod
5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5513 def get_bookmarks_for_user(cls, user_id, cache=True):
5514 bookmarks = cls.query() \
5514 bookmarks = cls.query() \
5515 .filter(UserBookmark.user_id == user_id) \
5515 .filter(UserBookmark.user_id == user_id) \
5516 .options(joinedload(UserBookmark.repository)) \
5516 .options(joinedload(UserBookmark.repository)) \
5517 .options(joinedload(UserBookmark.repository_group)) \
5517 .options(joinedload(UserBookmark.repository_group)) \
5518 .order_by(UserBookmark.position.asc())
5518 .order_by(UserBookmark.position.asc())
5519
5519
5520 if cache:
5520 if cache:
5521 bookmarks = bookmarks.options(
5521 bookmarks = bookmarks.options(
5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5522 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5523 )
5523 )
5524
5524
5525 return bookmarks.all()
5525 return bookmarks.all()
5526
5526
5527 def __unicode__(self):
5527 def __unicode__(self):
5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5528 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5529
5529
5530
5530
5531 class FileStore(Base, BaseModel):
5531 class FileStore(Base, BaseModel):
5532 __tablename__ = 'file_store'
5532 __tablename__ = 'file_store'
5533 __table_args__ = (
5533 __table_args__ = (
5534 base_table_args
5534 base_table_args
5535 )
5535 )
5536
5536
5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5537 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5538 file_uid = Column('file_uid', String(1024), nullable=False)
5538 file_uid = Column('file_uid', String(1024), nullable=False)
5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5539 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5540 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5541 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5542
5542
5543 # sha256 hash
5543 # sha256 hash
5544 file_hash = Column('file_hash', String(512), nullable=False)
5544 file_hash = Column('file_hash', String(512), nullable=False)
5545 file_size = Column('file_size', BigInteger(), nullable=False)
5545 file_size = Column('file_size', BigInteger(), nullable=False)
5546
5546
5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5547 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5548 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5549 accessed_count = Column('accessed_count', Integer(), default=0)
5549 accessed_count = Column('accessed_count', Integer(), default=0)
5550
5550
5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5551 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5552
5552
5553 # if repo/repo_group reference is set, check for permissions
5553 # if repo/repo_group reference is set, check for permissions
5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5554 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5555
5555
5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5556 # hidden defines an attachment that should be hidden from showing in artifact listing
5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5557 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5558
5558
5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5559 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5560 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5561
5561
5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5562 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5563
5563
5564 # scope limited to user, which requester have access to
5564 # scope limited to user, which requester have access to
5565 scope_user_id = Column(
5565 scope_user_id = Column(
5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5566 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5567 nullable=True, unique=None, default=None)
5567 nullable=True, unique=None, default=None)
5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5568 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5569
5569
5570 # scope limited to user group, which requester have access to
5570 # scope limited to user group, which requester have access to
5571 scope_user_group_id = Column(
5571 scope_user_group_id = Column(
5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5572 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5573 nullable=True, unique=None, default=None)
5573 nullable=True, unique=None, default=None)
5574 user_group = relationship('UserGroup', lazy='joined')
5574 user_group = relationship('UserGroup', lazy='joined')
5575
5575
5576 # scope limited to repo, which requester have access to
5576 # scope limited to repo, which requester have access to
5577 scope_repo_id = Column(
5577 scope_repo_id = Column(
5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5578 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5579 nullable=True, unique=None, default=None)
5579 nullable=True, unique=None, default=None)
5580 repo = relationship('Repository', lazy='joined')
5580 repo = relationship('Repository', lazy='joined')
5581
5581
5582 # scope limited to repo group, which requester have access to
5582 # scope limited to repo group, which requester have access to
5583 scope_repo_group_id = Column(
5583 scope_repo_group_id = Column(
5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5584 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5585 nullable=True, unique=None, default=None)
5585 nullable=True, unique=None, default=None)
5586 repo_group = relationship('RepoGroup', lazy='joined')
5586 repo_group = relationship('RepoGroup', lazy='joined')
5587
5587
5588 @classmethod
5588 @classmethod
5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5589 def get_by_store_uid(cls, file_store_uid, safe=False):
5590 if safe:
5590 if safe:
5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5591 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5592 else:
5592 else:
5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5593 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5594
5594
5595 @classmethod
5595 @classmethod
5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5596 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5597 file_description='', enabled=True, hidden=False, check_acl=True,
5597 file_description='', enabled=True, hidden=False, check_acl=True,
5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5598 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5599
5599
5600 store_entry = FileStore()
5600 store_entry = FileStore()
5601 store_entry.file_uid = file_uid
5601 store_entry.file_uid = file_uid
5602 store_entry.file_display_name = file_display_name
5602 store_entry.file_display_name = file_display_name
5603 store_entry.file_org_name = filename
5603 store_entry.file_org_name = filename
5604 store_entry.file_size = file_size
5604 store_entry.file_size = file_size
5605 store_entry.file_hash = file_hash
5605 store_entry.file_hash = file_hash
5606 store_entry.file_description = file_description
5606 store_entry.file_description = file_description
5607
5607
5608 store_entry.check_acl = check_acl
5608 store_entry.check_acl = check_acl
5609 store_entry.enabled = enabled
5609 store_entry.enabled = enabled
5610 store_entry.hidden = hidden
5610 store_entry.hidden = hidden
5611
5611
5612 store_entry.user_id = user_id
5612 store_entry.user_id = user_id
5613 store_entry.scope_user_id = scope_user_id
5613 store_entry.scope_user_id = scope_user_id
5614 store_entry.scope_repo_id = scope_repo_id
5614 store_entry.scope_repo_id = scope_repo_id
5615 store_entry.scope_repo_group_id = scope_repo_group_id
5615 store_entry.scope_repo_group_id = scope_repo_group_id
5616
5616
5617 return store_entry
5617 return store_entry
5618
5618
5619 @classmethod
5619 @classmethod
5620 def store_metadata(cls, file_store_id, args, commit=True):
5620 def store_metadata(cls, file_store_id, args, commit=True):
5621 file_store = FileStore.get(file_store_id)
5621 file_store = FileStore.get(file_store_id)
5622 if file_store is None:
5622 if file_store is None:
5623 return
5623 return
5624
5624
5625 for section, key, value, value_type in args:
5625 for section, key, value, value_type in args:
5626 has_key = FileStoreMetadata().query() \
5626 has_key = FileStoreMetadata().query() \
5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5627 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5628 .filter(FileStoreMetadata.file_store_meta_section == section) \
5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5629 .filter(FileStoreMetadata.file_store_meta_key == key) \
5630 .scalar()
5630 .scalar()
5631 if has_key:
5631 if has_key:
5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5632 msg = 'key `{}` already defined under section `{}` for this file.'\
5633 .format(key, section)
5633 .format(key, section)
5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5634 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5635
5635
5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5636 # NOTE(marcink): raises ArtifactMetadataBadValueType
5637 FileStoreMetadata.valid_value_type(value_type)
5637 FileStoreMetadata.valid_value_type(value_type)
5638
5638
5639 meta_entry = FileStoreMetadata()
5639 meta_entry = FileStoreMetadata()
5640 meta_entry.file_store = file_store
5640 meta_entry.file_store = file_store
5641 meta_entry.file_store_meta_section = section
5641 meta_entry.file_store_meta_section = section
5642 meta_entry.file_store_meta_key = key
5642 meta_entry.file_store_meta_key = key
5643 meta_entry.file_store_meta_value_type = value_type
5643 meta_entry.file_store_meta_value_type = value_type
5644 meta_entry.file_store_meta_value = value
5644 meta_entry.file_store_meta_value = value
5645
5645
5646 Session().add(meta_entry)
5646 Session().add(meta_entry)
5647
5647
5648 try:
5648 try:
5649 if commit:
5649 if commit:
5650 Session().commit()
5650 Session().commit()
5651 except IntegrityError:
5651 except IntegrityError:
5652 Session().rollback()
5652 Session().rollback()
5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5653 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5654
5654
5655 @classmethod
5655 @classmethod
5656 def bump_access_counter(cls, file_uid, commit=True):
5656 def bump_access_counter(cls, file_uid, commit=True):
5657 FileStore().query()\
5657 FileStore().query()\
5658 .filter(FileStore.file_uid == file_uid)\
5658 .filter(FileStore.file_uid == file_uid)\
5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5659 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5660 FileStore.accessed_on: datetime.datetime.now()})
5660 FileStore.accessed_on: datetime.datetime.now()})
5661 if commit:
5661 if commit:
5662 Session().commit()
5662 Session().commit()
5663
5663
5664 def __json__(self):
5664 def __json__(self):
5665 data = {
5665 data = {
5666 'filename': self.file_display_name,
5666 'filename': self.file_display_name,
5667 'filename_org': self.file_org_name,
5667 'filename_org': self.file_org_name,
5668 'file_uid': self.file_uid,
5668 'file_uid': self.file_uid,
5669 'description': self.file_description,
5669 'description': self.file_description,
5670 'hidden': self.hidden,
5670 'hidden': self.hidden,
5671 'size': self.file_size,
5671 'size': self.file_size,
5672 'created_on': self.created_on,
5672 'created_on': self.created_on,
5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5673 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5674 'downloaded_times': self.accessed_count,
5674 'downloaded_times': self.accessed_count,
5675 'sha256': self.file_hash,
5675 'sha256': self.file_hash,
5676 'metadata': self.file_metadata,
5676 'metadata': self.file_metadata,
5677 }
5677 }
5678
5678
5679 return data
5679 return data
5680
5680
5681 def __repr__(self):
5681 def __repr__(self):
5682 return '<FileStore({})>'.format(self.file_store_id)
5682 return '<FileStore({})>'.format(self.file_store_id)
5683
5683
5684
5684
5685 class FileStoreMetadata(Base, BaseModel):
5685 class FileStoreMetadata(Base, BaseModel):
5686 __tablename__ = 'file_store_metadata'
5686 __tablename__ = 'file_store_metadata'
5687 __table_args__ = (
5687 __table_args__ = (
5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5688 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5689 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5690 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5691 base_table_args
5691 base_table_args
5692 )
5692 )
5693 SETTINGS_TYPES = {
5693 SETTINGS_TYPES = {
5694 'str': safe_str,
5694 'str': safe_str,
5695 'int': safe_int,
5695 'int': safe_int,
5696 'unicode': safe_unicode,
5696 'unicode': safe_unicode,
5697 'bool': str2bool,
5697 'bool': str2bool,
5698 'list': functools.partial(aslist, sep=',')
5698 'list': functools.partial(aslist, sep=',')
5699 }
5699 }
5700
5700
5701 file_store_meta_id = Column(
5701 file_store_meta_id = Column(
5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5702 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5703 primary_key=True)
5703 primary_key=True)
5704 _file_store_meta_section = Column(
5704 _file_store_meta_section = Column(
5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5705 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5706 nullable=True, unique=None, default=None)
5706 nullable=True, unique=None, default=None)
5707 _file_store_meta_section_hash = Column(
5707 _file_store_meta_section_hash = Column(
5708 "file_store_meta_section_hash", String(255),
5708 "file_store_meta_section_hash", String(255),
5709 nullable=True, unique=None, default=None)
5709 nullable=True, unique=None, default=None)
5710 _file_store_meta_key = Column(
5710 _file_store_meta_key = Column(
5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5711 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5712 nullable=True, unique=None, default=None)
5712 nullable=True, unique=None, default=None)
5713 _file_store_meta_key_hash = Column(
5713 _file_store_meta_key_hash = Column(
5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5714 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5715 _file_store_meta_value = Column(
5715 _file_store_meta_value = Column(
5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5716 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5717 nullable=True, unique=None, default=None)
5717 nullable=True, unique=None, default=None)
5718 _file_store_meta_value_type = Column(
5718 _file_store_meta_value_type = Column(
5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5719 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5720 default='unicode')
5720 default='unicode')
5721
5721
5722 file_store_id = Column(
5722 file_store_id = Column(
5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5723 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5724 nullable=True, unique=None, default=None)
5724 nullable=True, unique=None, default=None)
5725
5725
5726 file_store = relationship('FileStore', lazy='joined')
5726 file_store = relationship('FileStore', lazy='joined')
5727
5727
5728 @classmethod
5728 @classmethod
5729 def valid_value_type(cls, value):
5729 def valid_value_type(cls, value):
5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5730 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5731 raise ArtifactMetadataBadValueType(
5731 raise ArtifactMetadataBadValueType(
5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5732 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5733
5733
5734 @hybrid_property
5734 @hybrid_property
5735 def file_store_meta_section(self):
5735 def file_store_meta_section(self):
5736 return self._file_store_meta_section
5736 return self._file_store_meta_section
5737
5737
5738 @file_store_meta_section.setter
5738 @file_store_meta_section.setter
5739 def file_store_meta_section(self, value):
5739 def file_store_meta_section(self, value):
5740 self._file_store_meta_section = value
5740 self._file_store_meta_section = value
5741 self._file_store_meta_section_hash = _hash_key(value)
5741 self._file_store_meta_section_hash = _hash_key(value)
5742
5742
5743 @hybrid_property
5743 @hybrid_property
5744 def file_store_meta_key(self):
5744 def file_store_meta_key(self):
5745 return self._file_store_meta_key
5745 return self._file_store_meta_key
5746
5746
5747 @file_store_meta_key.setter
5747 @file_store_meta_key.setter
5748 def file_store_meta_key(self, value):
5748 def file_store_meta_key(self, value):
5749 self._file_store_meta_key = value
5749 self._file_store_meta_key = value
5750 self._file_store_meta_key_hash = _hash_key(value)
5750 self._file_store_meta_key_hash = _hash_key(value)
5751
5751
5752 @hybrid_property
5752 @hybrid_property
5753 def file_store_meta_value(self):
5753 def file_store_meta_value(self):
5754 val = self._file_store_meta_value
5754 val = self._file_store_meta_value
5755
5755
5756 if self._file_store_meta_value_type:
5756 if self._file_store_meta_value_type:
5757 # e.g unicode.encrypted == unicode
5757 # e.g unicode.encrypted == unicode
5758 _type = self._file_store_meta_value_type.split('.')[0]
5758 _type = self._file_store_meta_value_type.split('.')[0]
5759 # decode the encrypted value if it's encrypted field type
5759 # decode the encrypted value if it's encrypted field type
5760 if '.encrypted' in self._file_store_meta_value_type:
5760 if '.encrypted' in self._file_store_meta_value_type:
5761 cipher = EncryptedTextValue()
5761 cipher = EncryptedTextValue()
5762 val = safe_unicode(cipher.process_result_value(val, None))
5762 val = safe_unicode(cipher.process_result_value(val, None))
5763 # do final type conversion
5763 # do final type conversion
5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5764 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5765 val = converter(val)
5765 val = converter(val)
5766
5766
5767 return val
5767 return val
5768
5768
5769 @file_store_meta_value.setter
5769 @file_store_meta_value.setter
5770 def file_store_meta_value(self, val):
5770 def file_store_meta_value(self, val):
5771 val = safe_unicode(val)
5771 val = safe_unicode(val)
5772 # encode the encrypted value
5772 # encode the encrypted value
5773 if '.encrypted' in self.file_store_meta_value_type:
5773 if '.encrypted' in self.file_store_meta_value_type:
5774 cipher = EncryptedTextValue()
5774 cipher = EncryptedTextValue()
5775 val = safe_unicode(cipher.process_bind_param(val, None))
5775 val = safe_unicode(cipher.process_bind_param(val, None))
5776 self._file_store_meta_value = val
5776 self._file_store_meta_value = val
5777
5777
5778 @hybrid_property
5778 @hybrid_property
5779 def file_store_meta_value_type(self):
5779 def file_store_meta_value_type(self):
5780 return self._file_store_meta_value_type
5780 return self._file_store_meta_value_type
5781
5781
5782 @file_store_meta_value_type.setter
5782 @file_store_meta_value_type.setter
5783 def file_store_meta_value_type(self, val):
5783 def file_store_meta_value_type(self, val):
5784 # e.g unicode.encrypted
5784 # e.g unicode.encrypted
5785 self.valid_value_type(val)
5785 self.valid_value_type(val)
5786 self._file_store_meta_value_type = val
5786 self._file_store_meta_value_type = val
5787
5787
5788 def __json__(self):
5788 def __json__(self):
5789 data = {
5789 data = {
5790 'artifact': self.file_store.file_uid,
5790 'artifact': self.file_store.file_uid,
5791 'section': self.file_store_meta_section,
5791 'section': self.file_store_meta_section,
5792 'key': self.file_store_meta_key,
5792 'key': self.file_store_meta_key,
5793 'value': self.file_store_meta_value,
5793 'value': self.file_store_meta_value,
5794 }
5794 }
5795
5795
5796 return data
5796 return data
5797
5797
5798 def __repr__(self):
5798 def __repr__(self):
5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5799 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5800 self.file_store_meta_key, self.file_store_meta_value)
5800 self.file_store_meta_key, self.file_store_meta_value)
5801
5801
5802
5802
5803 class DbMigrateVersion(Base, BaseModel):
5803 class DbMigrateVersion(Base, BaseModel):
5804 __tablename__ = 'db_migrate_version'
5804 __tablename__ = 'db_migrate_version'
5805 __table_args__ = (
5805 __table_args__ = (
5806 base_table_args,
5806 base_table_args,
5807 )
5807 )
5808
5808
5809 repository_id = Column('repository_id', String(250), primary_key=True)
5809 repository_id = Column('repository_id', String(250), primary_key=True)
5810 repository_path = Column('repository_path', Text)
5810 repository_path = Column('repository_path', Text)
5811 version = Column('version', Integer)
5811 version = Column('version', Integer)
5812
5812
5813 @classmethod
5813 @classmethod
5814 def set_version(cls, version):
5814 def set_version(cls, version):
5815 """
5815 """
5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5816 Helper for forcing a different version, usually for debugging purposes via ishell.
5817 """
5817 """
5818 ver = DbMigrateVersion.query().first()
5818 ver = DbMigrateVersion.query().first()
5819 ver.version = version
5819 ver.version = version
5820 Session().commit()
5820 Session().commit()
5821
5821
5822
5822
5823 class DbSession(Base, BaseModel):
5823 class DbSession(Base, BaseModel):
5824 __tablename__ = 'db_session'
5824 __tablename__ = 'db_session'
5825 __table_args__ = (
5825 __table_args__ = (
5826 base_table_args,
5826 base_table_args,
5827 )
5827 )
5828
5828
5829 def __repr__(self):
5829 def __repr__(self):
5830 return '<DB:DbSession({})>'.format(self.id)
5830 return '<DB:DbSession({})>'.format(self.id)
5831
5831
5832 id = Column('id', Integer())
5832 id = Column('id', Integer())
5833 namespace = Column('namespace', String(255), primary_key=True)
5833 namespace = Column('namespace', String(255), primary_key=True)
5834 accessed = Column('accessed', DateTime, nullable=False)
5834 accessed = Column('accessed', DateTime, nullable=False)
5835 created = Column('created', DateTime, nullable=False)
5835 created = Column('created', DateTime, nullable=False)
5836 data = Column('data', PickleType, nullable=False)
5836 data = Column('data', PickleType, nullable=False)
General Comments 0
You need to be logged in to leave comments. Login now