##// END OF EJS Templates
pull-request: introduced new merge-checks....
marcink -
r1334:68703a99 default
parent child Browse files
Show More
@@ -0,0 +1,37 b''
1
2 <div class="pull-request-wrap">
3
4 <ul>
5 % for pr_check_type, pr_check_msg in c.pr_merge_checks:
6 <li>
7 <span class="merge-message ${pr_check_type}" data-role="merge-message">
8 % if pr_check_type in ['success']:
9 <i class="icon-true"></i>
10 % else:
11 <i class="icon-false"></i>
12 % endif
13 ${pr_check_msg}
14 </span>
15 </li>
16 % endfor
17 </ul>
18
19 <div class="pull-request-merge-actions">
20 % if c.allowed_to_merge:
21 <div class="pull-right">
22 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
23 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
24 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
25 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
26 ${h.end_form()}
27 </div>
28 % elif c.rhodecode_user.username != h.DEFAULT_USER:
29 <a class="btn" href="#" onclick="refreshMergeChecks(); return false;">${_('refresh checks')}</a>
30 <input type="submit" value="${_('Merge Pull Request')}" class="btn disabled" disabled="disabled" title="${_('You are not allowed to merge this pull request.')}">
31 % else:
32 <input type="submit" value="${_('Login to Merge this Pull Request')}" class="btn disabled" disabled="disabled">
33 % endif
34 </div>
35
36 </div>
37
@@ -1,1018 +1,1046 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 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 pull requests controller for rhodecode for initializing pull requests
22 pull requests controller for rhodecode for initializing pull requests
23 """
23 """
24 import types
24 import types
25
25
26 import peppercorn
26 import peppercorn
27 import formencode
27 import formencode
28 import logging
28 import logging
29 import collections
29 import collections
30
30
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
31 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPBadRequest
32 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from pyramid.threadlocal import get_current_registry
35 from pyramid.threadlocal import get_current_registry
36 from sqlalchemy.sql import func
36 from sqlalchemy.sql import func
37 from sqlalchemy.sql.expression import or_
37 from sqlalchemy.sql.expression import or_
38
38
39 from rhodecode import events
39 from rhodecode import events
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
40 from rhodecode.lib import auth, diffs, helpers as h, codeblocks
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.base import (
42 from rhodecode.lib.base import (
43 BaseRepoController, render, vcs_operation_context)
43 BaseRepoController, render, vcs_operation_context)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
45 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous,
46 HasAcceptedRepoType, XHRRequired)
46 HasAcceptedRepoType, XHRRequired)
47 from rhodecode.lib.channelstream import channelstream_request
47 from rhodecode.lib.channelstream import channelstream_request
48 from rhodecode.lib.utils import jsonify
48 from rhodecode.lib.utils import jsonify
49 from rhodecode.lib.utils2 import (
49 from rhodecode.lib.utils2 import (
50 safe_int, safe_str, str2bool, safe_unicode)
50 safe_int, safe_str, str2bool, safe_unicode)
51 from rhodecode.lib.vcs.backends.base import (
51 from rhodecode.lib.vcs.backends.base import (
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
52 EmptyCommit, UpdateFailureReason, EmptyRepository)
53 from rhodecode.lib.vcs.exceptions import (
53 from rhodecode.lib.vcs.exceptions import (
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
54 EmptyRepositoryError, CommitDoesNotExistError, RepositoryRequirementError,
55 NodeDoesNotExistError)
55 NodeDoesNotExistError)
56
56
57 from rhodecode.model.changeset_status import ChangesetStatusModel
57 from rhodecode.model.changeset_status import ChangesetStatusModel
58 from rhodecode.model.comment import CommentsModel
58 from rhodecode.model.comment import CommentsModel
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
59 from rhodecode.model.db import (PullRequest, ChangesetStatus, ChangesetComment,
60 Repository, PullRequestVersion)
60 Repository, PullRequestVersion)
61 from rhodecode.model.forms import PullRequestForm
61 from rhodecode.model.forms import PullRequestForm
62 from rhodecode.model.meta import Session
62 from rhodecode.model.meta import Session
63 from rhodecode.model.pull_request import PullRequestModel
63 from rhodecode.model.pull_request import PullRequestModel
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69 def __before__(self):
69 def __before__(self):
70 super(PullrequestsController, self).__before__()
70 super(PullrequestsController, self).__before__()
71
71
72 def _load_compare_data(self, pull_request, inline_comments):
72 def _load_compare_data(self, pull_request, inline_comments):
73 """
73 """
74 Load context data needed for generating compare diff
74 Load context data needed for generating compare diff
75
75
76 :param pull_request: object related to the request
76 :param pull_request: object related to the request
77 :param enable_comments: flag to determine if comments are included
77 :param enable_comments: flag to determine if comments are included
78 """
78 """
79 source_repo = pull_request.source_repo
79 source_repo = pull_request.source_repo
80 source_ref_id = pull_request.source_ref_parts.commit_id
80 source_ref_id = pull_request.source_ref_parts.commit_id
81
81
82 target_repo = pull_request.target_repo
82 target_repo = pull_request.target_repo
83 target_ref_id = pull_request.target_ref_parts.commit_id
83 target_ref_id = pull_request.target_ref_parts.commit_id
84
84
85 # despite opening commits for bookmarks/branches/tags, we always
85 # despite opening commits for bookmarks/branches/tags, we always
86 # convert this to rev to prevent changes after bookmark or branch change
86 # convert this to rev to prevent changes after bookmark or branch change
87 c.source_ref_type = 'rev'
87 c.source_ref_type = 'rev'
88 c.source_ref = source_ref_id
88 c.source_ref = source_ref_id
89
89
90 c.target_ref_type = 'rev'
90 c.target_ref_type = 'rev'
91 c.target_ref = target_ref_id
91 c.target_ref = target_ref_id
92
92
93 c.source_repo = source_repo
93 c.source_repo = source_repo
94 c.target_repo = target_repo
94 c.target_repo = target_repo
95
95
96 c.fulldiff = bool(request.GET.get('fulldiff'))
96 c.fulldiff = bool(request.GET.get('fulldiff'))
97
97
98 # diff_limit is the old behavior, will cut off the whole diff
98 # diff_limit is the old behavior, will cut off the whole diff
99 # if the limit is applied otherwise will just hide the
99 # if the limit is applied otherwise will just hide the
100 # big files from the front-end
100 # big files from the front-end
101 diff_limit = self.cut_off_limit_diff
101 diff_limit = self.cut_off_limit_diff
102 file_limit = self.cut_off_limit_file
102 file_limit = self.cut_off_limit_file
103
103
104 pre_load = ["author", "branch", "date", "message"]
104 pre_load = ["author", "branch", "date", "message"]
105
105
106 c.commit_ranges = []
106 c.commit_ranges = []
107 source_commit = EmptyCommit()
107 source_commit = EmptyCommit()
108 target_commit = EmptyCommit()
108 target_commit = EmptyCommit()
109 c.missing_requirements = False
109 c.missing_requirements = False
110 try:
110 try:
111 c.commit_ranges = [
111 c.commit_ranges = [
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
112 source_repo.get_commit(commit_id=rev, pre_load=pre_load)
113 for rev in pull_request.revisions]
113 for rev in pull_request.revisions]
114
114
115 c.statuses = source_repo.statuses(
115 c.statuses = source_repo.statuses(
116 [x.raw_id for x in c.commit_ranges])
116 [x.raw_id for x in c.commit_ranges])
117
117
118 target_commit = source_repo.get_commit(
118 target_commit = source_repo.get_commit(
119 commit_id=safe_str(target_ref_id))
119 commit_id=safe_str(target_ref_id))
120 source_commit = source_repo.get_commit(
120 source_commit = source_repo.get_commit(
121 commit_id=safe_str(source_ref_id))
121 commit_id=safe_str(source_ref_id))
122 except RepositoryRequirementError:
122 except RepositoryRequirementError:
123 c.missing_requirements = True
123 c.missing_requirements = True
124
124
125 # auto collapse if we have more than limit
125 # auto collapse if we have more than limit
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
126 collapse_limit = diffs.DiffProcessor._collapse_commits_over
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
127 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
128
128
129 c.changes = {}
129 c.changes = {}
130 c.missing_commits = False
130 c.missing_commits = False
131 if (c.missing_requirements or
131 if (c.missing_requirements or
132 isinstance(source_commit, EmptyCommit) or
132 isinstance(source_commit, EmptyCommit) or
133 source_commit == target_commit):
133 source_commit == target_commit):
134 _parsed = []
134 _parsed = []
135 c.missing_commits = True
135 c.missing_commits = True
136 else:
136 else:
137 vcs_diff = PullRequestModel().get_diff(pull_request)
137 vcs_diff = PullRequestModel().get_diff(pull_request)
138 diff_processor = diffs.DiffProcessor(
138 diff_processor = diffs.DiffProcessor(
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
139 vcs_diff, format='newdiff', diff_limit=diff_limit,
140 file_limit=file_limit, show_full_diff=c.fulldiff)
140 file_limit=file_limit, show_full_diff=c.fulldiff)
141
141
142 _parsed = diff_processor.prepare()
142 _parsed = diff_processor.prepare()
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
143 c.limited_diff = isinstance(_parsed, diffs.LimitedDiffContainer)
144
144
145 included_files = {}
145 included_files = {}
146 for f in _parsed:
146 for f in _parsed:
147 included_files[f['filename']] = f['stats']
147 included_files[f['filename']] = f['stats']
148
148
149 c.deleted_files = [fname for fname in inline_comments if
149 c.deleted_files = [fname for fname in inline_comments if
150 fname not in included_files]
150 fname not in included_files]
151
151
152 c.deleted_files_comments = collections.defaultdict(dict)
152 c.deleted_files_comments = collections.defaultdict(dict)
153 for fname, per_line_comments in inline_comments.items():
153 for fname, per_line_comments in inline_comments.items():
154 if fname in c.deleted_files:
154 if fname in c.deleted_files:
155 c.deleted_files_comments[fname]['stats'] = 0
155 c.deleted_files_comments[fname]['stats'] = 0
156 c.deleted_files_comments[fname]['comments'] = list()
156 c.deleted_files_comments[fname]['comments'] = list()
157 for lno, comments in per_line_comments.items():
157 for lno, comments in per_line_comments.items():
158 c.deleted_files_comments[fname]['comments'].extend(comments)
158 c.deleted_files_comments[fname]['comments'].extend(comments)
159
159
160 def _node_getter(commit):
160 def _node_getter(commit):
161 def get_node(fname):
161 def get_node(fname):
162 try:
162 try:
163 return commit.get_node(fname)
163 return commit.get_node(fname)
164 except NodeDoesNotExistError:
164 except NodeDoesNotExistError:
165 return None
165 return None
166 return get_node
166 return get_node
167
167
168 c.diffset = codeblocks.DiffSet(
168 c.diffset = codeblocks.DiffSet(
169 repo_name=c.repo_name,
169 repo_name=c.repo_name,
170 source_repo_name=c.source_repo.repo_name,
170 source_repo_name=c.source_repo.repo_name,
171 source_node_getter=_node_getter(target_commit),
171 source_node_getter=_node_getter(target_commit),
172 target_node_getter=_node_getter(source_commit),
172 target_node_getter=_node_getter(source_commit),
173 comments=inline_comments
173 comments=inline_comments
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
174 ).render_patchset(_parsed, target_commit.raw_id, source_commit.raw_id)
175
175
176 def _extract_ordering(self, request):
176 def _extract_ordering(self, request):
177 column_index = safe_int(request.GET.get('order[0][column]'))
177 column_index = safe_int(request.GET.get('order[0][column]'))
178 order_dir = request.GET.get('order[0][dir]', 'desc')
178 order_dir = request.GET.get('order[0][dir]', 'desc')
179 order_by = request.GET.get(
179 order_by = request.GET.get(
180 'columns[%s][data][sort]' % column_index, 'name_raw')
180 'columns[%s][data][sort]' % column_index, 'name_raw')
181 return order_by, order_dir
181 return order_by, order_dir
182
182
183 @LoginRequired()
183 @LoginRequired()
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
184 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
185 'repository.admin')
185 'repository.admin')
186 @HasAcceptedRepoType('git', 'hg')
186 @HasAcceptedRepoType('git', 'hg')
187 def show_all(self, repo_name):
187 def show_all(self, repo_name):
188 # filter types
188 # filter types
189 c.active = 'open'
189 c.active = 'open'
190 c.source = str2bool(request.GET.get('source'))
190 c.source = str2bool(request.GET.get('source'))
191 c.closed = str2bool(request.GET.get('closed'))
191 c.closed = str2bool(request.GET.get('closed'))
192 c.my = str2bool(request.GET.get('my'))
192 c.my = str2bool(request.GET.get('my'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
193 c.awaiting_review = str2bool(request.GET.get('awaiting_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
194 c.awaiting_my_review = str2bool(request.GET.get('awaiting_my_review'))
195 c.repo_name = repo_name
195 c.repo_name = repo_name
196
196
197 opened_by = None
197 opened_by = None
198 if c.my:
198 if c.my:
199 c.active = 'my'
199 c.active = 'my'
200 opened_by = [c.rhodecode_user.user_id]
200 opened_by = [c.rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if c.closed:
203 if c.closed:
204 c.active = 'closed'
204 c.active = 'closed'
205 statuses = [PullRequest.STATUS_CLOSED]
205 statuses = [PullRequest.STATUS_CLOSED]
206
206
207 if c.awaiting_review and not c.source:
207 if c.awaiting_review and not c.source:
208 c.active = 'awaiting'
208 c.active = 'awaiting'
209 if c.source and not c.awaiting_review:
209 if c.source and not c.awaiting_review:
210 c.active = 'source'
210 c.active = 'source'
211 if c.awaiting_my_review:
211 if c.awaiting_my_review:
212 c.active = 'awaiting_my'
212 c.active = 'awaiting_my'
213
213
214 data = self._get_pull_requests_list(
214 data = self._get_pull_requests_list(
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
215 repo_name=repo_name, opened_by=opened_by, statuses=statuses)
216 if not request.is_xhr:
216 if not request.is_xhr:
217 c.data = json.dumps(data['data'])
217 c.data = json.dumps(data['data'])
218 c.records_total = data['recordsTotal']
218 c.records_total = data['recordsTotal']
219 return render('/pullrequests/pullrequests.mako')
219 return render('/pullrequests/pullrequests.mako')
220 else:
220 else:
221 return json.dumps(data)
221 return json.dumps(data)
222
222
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
223 def _get_pull_requests_list(self, repo_name, opened_by, statuses):
224 # pagination
224 # pagination
225 start = safe_int(request.GET.get('start'), 0)
225 start = safe_int(request.GET.get('start'), 0)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
226 length = safe_int(request.GET.get('length'), c.visual.dashboard_items)
227 order_by, order_dir = self._extract_ordering(request)
227 order_by, order_dir = self._extract_ordering(request)
228
228
229 if c.awaiting_review:
229 if c.awaiting_review:
230 pull_requests = PullRequestModel().get_awaiting_review(
230 pull_requests = PullRequestModel().get_awaiting_review(
231 repo_name, source=c.source, opened_by=opened_by,
231 repo_name, source=c.source, opened_by=opened_by,
232 statuses=statuses, offset=start, length=length,
232 statuses=statuses, offset=start, length=length,
233 order_by=order_by, order_dir=order_dir)
233 order_by=order_by, order_dir=order_dir)
234 pull_requests_total_count = PullRequestModel(
234 pull_requests_total_count = PullRequestModel(
235 ).count_awaiting_review(
235 ).count_awaiting_review(
236 repo_name, source=c.source, statuses=statuses,
236 repo_name, source=c.source, statuses=statuses,
237 opened_by=opened_by)
237 opened_by=opened_by)
238 elif c.awaiting_my_review:
238 elif c.awaiting_my_review:
239 pull_requests = PullRequestModel().get_awaiting_my_review(
239 pull_requests = PullRequestModel().get_awaiting_my_review(
240 repo_name, source=c.source, opened_by=opened_by,
240 repo_name, source=c.source, opened_by=opened_by,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
241 user_id=c.rhodecode_user.user_id, statuses=statuses,
242 offset=start, length=length, order_by=order_by,
242 offset=start, length=length, order_by=order_by,
243 order_dir=order_dir)
243 order_dir=order_dir)
244 pull_requests_total_count = PullRequestModel(
244 pull_requests_total_count = PullRequestModel(
245 ).count_awaiting_my_review(
245 ).count_awaiting_my_review(
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
246 repo_name, source=c.source, user_id=c.rhodecode_user.user_id,
247 statuses=statuses, opened_by=opened_by)
247 statuses=statuses, opened_by=opened_by)
248 else:
248 else:
249 pull_requests = PullRequestModel().get_all(
249 pull_requests = PullRequestModel().get_all(
250 repo_name, source=c.source, opened_by=opened_by,
250 repo_name, source=c.source, opened_by=opened_by,
251 statuses=statuses, offset=start, length=length,
251 statuses=statuses, offset=start, length=length,
252 order_by=order_by, order_dir=order_dir)
252 order_by=order_by, order_dir=order_dir)
253 pull_requests_total_count = PullRequestModel().count_all(
253 pull_requests_total_count = PullRequestModel().count_all(
254 repo_name, source=c.source, statuses=statuses,
254 repo_name, source=c.source, statuses=statuses,
255 opened_by=opened_by)
255 opened_by=opened_by)
256
256
257 from rhodecode.lib.utils import PartialRenderer
257 from rhodecode.lib.utils import PartialRenderer
258 _render = PartialRenderer('data_table/_dt_elements.mako')
258 _render = PartialRenderer('data_table/_dt_elements.mako')
259 data = []
259 data = []
260 for pr in pull_requests:
260 for pr in pull_requests:
261 comments = CommentsModel().get_all_comments(
261 comments = CommentsModel().get_all_comments(
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
262 c.rhodecode_db_repo.repo_id, pull_request=pr)
263
263
264 data.append({
264 data.append({
265 'name': _render('pullrequest_name',
265 'name': _render('pullrequest_name',
266 pr.pull_request_id, pr.target_repo.repo_name),
266 pr.pull_request_id, pr.target_repo.repo_name),
267 'name_raw': pr.pull_request_id,
267 'name_raw': pr.pull_request_id,
268 'status': _render('pullrequest_status',
268 'status': _render('pullrequest_status',
269 pr.calculated_review_status()),
269 pr.calculated_review_status()),
270 'title': _render(
270 'title': _render(
271 'pullrequest_title', pr.title, pr.description),
271 'pullrequest_title', pr.title, pr.description),
272 'description': h.escape(pr.description),
272 'description': h.escape(pr.description),
273 'updated_on': _render('pullrequest_updated_on',
273 'updated_on': _render('pullrequest_updated_on',
274 h.datetime_to_time(pr.updated_on)),
274 h.datetime_to_time(pr.updated_on)),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
275 'updated_on_raw': h.datetime_to_time(pr.updated_on),
276 'created_on': _render('pullrequest_updated_on',
276 'created_on': _render('pullrequest_updated_on',
277 h.datetime_to_time(pr.created_on)),
277 h.datetime_to_time(pr.created_on)),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
278 'created_on_raw': h.datetime_to_time(pr.created_on),
279 'author': _render('pullrequest_author',
279 'author': _render('pullrequest_author',
280 pr.author.full_contact, ),
280 pr.author.full_contact, ),
281 'author_raw': pr.author.full_name,
281 'author_raw': pr.author.full_name,
282 'comments': _render('pullrequest_comments', len(comments)),
282 'comments': _render('pullrequest_comments', len(comments)),
283 'comments_raw': len(comments),
283 'comments_raw': len(comments),
284 'closed': pr.is_closed(),
284 'closed': pr.is_closed(),
285 })
285 })
286 # json used to render the grid
286 # json used to render the grid
287 data = ({
287 data = ({
288 'data': data,
288 'data': data,
289 'recordsTotal': pull_requests_total_count,
289 'recordsTotal': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
290 'recordsFiltered': pull_requests_total_count,
291 })
291 })
292 return data
292 return data
293
293
294 @LoginRequired()
294 @LoginRequired()
295 @NotAnonymous()
295 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
297 'repository.admin')
297 'repository.admin')
298 @HasAcceptedRepoType('git', 'hg')
298 @HasAcceptedRepoType('git', 'hg')
299 def index(self):
299 def index(self):
300 source_repo = c.rhodecode_db_repo
300 source_repo = c.rhodecode_db_repo
301
301
302 try:
302 try:
303 source_repo.scm_instance().get_commit()
303 source_repo.scm_instance().get_commit()
304 except EmptyRepositoryError:
304 except EmptyRepositoryError:
305 h.flash(h.literal(_('There are no commits yet')),
305 h.flash(h.literal(_('There are no commits yet')),
306 category='warning')
306 category='warning')
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
307 redirect(url('summary_home', repo_name=source_repo.repo_name))
308
308
309 commit_id = request.GET.get('commit')
309 commit_id = request.GET.get('commit')
310 branch_ref = request.GET.get('branch')
310 branch_ref = request.GET.get('branch')
311 bookmark_ref = request.GET.get('bookmark')
311 bookmark_ref = request.GET.get('bookmark')
312
312
313 try:
313 try:
314 source_repo_data = PullRequestModel().generate_repo_data(
314 source_repo_data = PullRequestModel().generate_repo_data(
315 source_repo, commit_id=commit_id,
315 source_repo, commit_id=commit_id,
316 branch=branch_ref, bookmark=bookmark_ref)
316 branch=branch_ref, bookmark=bookmark_ref)
317 except CommitDoesNotExistError as e:
317 except CommitDoesNotExistError as e:
318 log.exception(e)
318 log.exception(e)
319 h.flash(_('Commit does not exist'), 'error')
319 h.flash(_('Commit does not exist'), 'error')
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
320 redirect(url('pullrequest_home', repo_name=source_repo.repo_name))
321
321
322 default_target_repo = source_repo
322 default_target_repo = source_repo
323
323
324 if source_repo.parent:
324 if source_repo.parent:
325 parent_vcs_obj = source_repo.parent.scm_instance()
325 parent_vcs_obj = source_repo.parent.scm_instance()
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
326 if parent_vcs_obj and not parent_vcs_obj.is_empty():
327 # change default if we have a parent repo
327 # change default if we have a parent repo
328 default_target_repo = source_repo.parent
328 default_target_repo = source_repo.parent
329
329
330 target_repo_data = PullRequestModel().generate_repo_data(
330 target_repo_data = PullRequestModel().generate_repo_data(
331 default_target_repo)
331 default_target_repo)
332
332
333 selected_source_ref = source_repo_data['refs']['selected_ref']
333 selected_source_ref = source_repo_data['refs']['selected_ref']
334
334
335 title_source_ref = selected_source_ref.split(':', 2)[1]
335 title_source_ref = selected_source_ref.split(':', 2)[1]
336 c.default_title = PullRequestModel().generate_pullrequest_title(
336 c.default_title = PullRequestModel().generate_pullrequest_title(
337 source=source_repo.repo_name,
337 source=source_repo.repo_name,
338 source_ref=title_source_ref,
338 source_ref=title_source_ref,
339 target=default_target_repo.repo_name
339 target=default_target_repo.repo_name
340 )
340 )
341
341
342 c.default_repo_data = {
342 c.default_repo_data = {
343 'source_repo_name': source_repo.repo_name,
343 'source_repo_name': source_repo.repo_name,
344 'source_refs_json': json.dumps(source_repo_data),
344 'source_refs_json': json.dumps(source_repo_data),
345 'target_repo_name': default_target_repo.repo_name,
345 'target_repo_name': default_target_repo.repo_name,
346 'target_refs_json': json.dumps(target_repo_data),
346 'target_refs_json': json.dumps(target_repo_data),
347 }
347 }
348 c.default_source_ref = selected_source_ref
348 c.default_source_ref = selected_source_ref
349
349
350 return render('/pullrequests/pullrequest.mako')
350 return render('/pullrequests/pullrequest.mako')
351
351
352 @LoginRequired()
352 @LoginRequired()
353 @NotAnonymous()
353 @NotAnonymous()
354 @XHRRequired()
354 @XHRRequired()
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
356 'repository.admin')
356 'repository.admin')
357 @jsonify
357 @jsonify
358 def get_repo_refs(self, repo_name, target_repo_name):
358 def get_repo_refs(self, repo_name, target_repo_name):
359 repo = Repository.get_by_repo_name(target_repo_name)
359 repo = Repository.get_by_repo_name(target_repo_name)
360 if not repo:
360 if not repo:
361 raise HTTPNotFound
361 raise HTTPNotFound
362 return PullRequestModel().generate_repo_data(repo)
362 return PullRequestModel().generate_repo_data(repo)
363
363
364 @LoginRequired()
364 @LoginRequired()
365 @NotAnonymous()
365 @NotAnonymous()
366 @XHRRequired()
366 @XHRRequired()
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
367 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
368 'repository.admin')
368 'repository.admin')
369 @jsonify
369 @jsonify
370 def get_repo_destinations(self, repo_name):
370 def get_repo_destinations(self, repo_name):
371 repo = Repository.get_by_repo_name(repo_name)
371 repo = Repository.get_by_repo_name(repo_name)
372 if not repo:
372 if not repo:
373 raise HTTPNotFound
373 raise HTTPNotFound
374 filter_query = request.GET.get('query')
374 filter_query = request.GET.get('query')
375
375
376 query = Repository.query() \
376 query = Repository.query() \
377 .order_by(func.length(Repository.repo_name)) \
377 .order_by(func.length(Repository.repo_name)) \
378 .filter(or_(
378 .filter(or_(
379 Repository.repo_name == repo.repo_name,
379 Repository.repo_name == repo.repo_name,
380 Repository.fork_id == repo.repo_id))
380 Repository.fork_id == repo.repo_id))
381
381
382 if filter_query:
382 if filter_query:
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
383 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
384 query = query.filter(
384 query = query.filter(
385 Repository.repo_name.ilike(ilike_expression))
385 Repository.repo_name.ilike(ilike_expression))
386
386
387 add_parent = False
387 add_parent = False
388 if repo.parent:
388 if repo.parent:
389 if filter_query in repo.parent.repo_name:
389 if filter_query in repo.parent.repo_name:
390 parent_vcs_obj = repo.parent.scm_instance()
390 parent_vcs_obj = repo.parent.scm_instance()
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
391 if parent_vcs_obj and not parent_vcs_obj.is_empty():
392 add_parent = True
392 add_parent = True
393
393
394 limit = 20 - 1 if add_parent else 20
394 limit = 20 - 1 if add_parent else 20
395 all_repos = query.limit(limit).all()
395 all_repos = query.limit(limit).all()
396 if add_parent:
396 if add_parent:
397 all_repos += [repo.parent]
397 all_repos += [repo.parent]
398
398
399 repos = []
399 repos = []
400 for obj in self.scm_model.get_repos(all_repos):
400 for obj in self.scm_model.get_repos(all_repos):
401 repos.append({
401 repos.append({
402 'id': obj['name'],
402 'id': obj['name'],
403 'text': obj['name'],
403 'text': obj['name'],
404 'type': 'repo',
404 'type': 'repo',
405 'obj': obj['dbrepo']
405 'obj': obj['dbrepo']
406 })
406 })
407
407
408 data = {
408 data = {
409 'more': False,
409 'more': False,
410 'results': [{
410 'results': [{
411 'text': _('Repositories'),
411 'text': _('Repositories'),
412 'children': repos
412 'children': repos
413 }] if repos else []
413 }] if repos else []
414 }
414 }
415 return data
415 return data
416
416
417 @LoginRequired()
417 @LoginRequired()
418 @NotAnonymous()
418 @NotAnonymous()
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
419 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
420 'repository.admin')
420 'repository.admin')
421 @HasAcceptedRepoType('git', 'hg')
421 @HasAcceptedRepoType('git', 'hg')
422 @auth.CSRFRequired()
422 @auth.CSRFRequired()
423 def create(self, repo_name):
423 def create(self, repo_name):
424 repo = Repository.get_by_repo_name(repo_name)
424 repo = Repository.get_by_repo_name(repo_name)
425 if not repo:
425 if not repo:
426 raise HTTPNotFound
426 raise HTTPNotFound
427
427
428 controls = peppercorn.parse(request.POST.items())
428 controls = peppercorn.parse(request.POST.items())
429
429
430 try:
430 try:
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
431 _form = PullRequestForm(repo.repo_id)().to_python(controls)
432 except formencode.Invalid as errors:
432 except formencode.Invalid as errors:
433 if errors.error_dict.get('revisions'):
433 if errors.error_dict.get('revisions'):
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
434 msg = 'Revisions: %s' % errors.error_dict['revisions']
435 elif errors.error_dict.get('pullrequest_title'):
435 elif errors.error_dict.get('pullrequest_title'):
436 msg = _('Pull request requires a title with min. 3 chars')
436 msg = _('Pull request requires a title with min. 3 chars')
437 else:
437 else:
438 msg = _('Error creating pull request: {}').format(errors)
438 msg = _('Error creating pull request: {}').format(errors)
439 log.exception(msg)
439 log.exception(msg)
440 h.flash(msg, 'error')
440 h.flash(msg, 'error')
441
441
442 # would rather just go back to form ...
442 # would rather just go back to form ...
443 return redirect(url('pullrequest_home', repo_name=repo_name))
443 return redirect(url('pullrequest_home', repo_name=repo_name))
444
444
445 source_repo = _form['source_repo']
445 source_repo = _form['source_repo']
446 source_ref = _form['source_ref']
446 source_ref = _form['source_ref']
447 target_repo = _form['target_repo']
447 target_repo = _form['target_repo']
448 target_ref = _form['target_ref']
448 target_ref = _form['target_ref']
449 commit_ids = _form['revisions'][::-1]
449 commit_ids = _form['revisions'][::-1]
450 reviewers = [
450 reviewers = [
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
451 (r['user_id'], r['reasons']) for r in _form['review_members']]
452
452
453 # find the ancestor for this pr
453 # find the ancestor for this pr
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
454 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
455 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
456
456
457 source_scm = source_db_repo.scm_instance()
457 source_scm = source_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
458 target_scm = target_db_repo.scm_instance()
459
459
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
460 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
461 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
462
462
463 ancestor = source_scm.get_common_ancestor(
463 ancestor = source_scm.get_common_ancestor(
464 source_commit.raw_id, target_commit.raw_id, target_scm)
464 source_commit.raw_id, target_commit.raw_id, target_scm)
465
465
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
466 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
467 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
468
468
469 pullrequest_title = _form['pullrequest_title']
469 pullrequest_title = _form['pullrequest_title']
470 title_source_ref = source_ref.split(':', 2)[1]
470 title_source_ref = source_ref.split(':', 2)[1]
471 if not pullrequest_title:
471 if not pullrequest_title:
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
472 pullrequest_title = PullRequestModel().generate_pullrequest_title(
473 source=source_repo,
473 source=source_repo,
474 source_ref=title_source_ref,
474 source_ref=title_source_ref,
475 target=target_repo
475 target=target_repo
476 )
476 )
477
477
478 description = _form['pullrequest_desc']
478 description = _form['pullrequest_desc']
479 try:
479 try:
480 pull_request = PullRequestModel().create(
480 pull_request = PullRequestModel().create(
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
481 c.rhodecode_user.user_id, source_repo, source_ref, target_repo,
482 target_ref, commit_ids, reviewers, pullrequest_title,
482 target_ref, commit_ids, reviewers, pullrequest_title,
483 description
483 description
484 )
484 )
485 Session().commit()
485 Session().commit()
486 h.flash(_('Successfully opened new pull request'),
486 h.flash(_('Successfully opened new pull request'),
487 category='success')
487 category='success')
488 except Exception as e:
488 except Exception as e:
489 msg = _('Error occurred during sending pull request')
489 msg = _('Error occurred during sending pull request')
490 log.exception(msg)
490 log.exception(msg)
491 h.flash(msg, category='error')
491 h.flash(msg, category='error')
492 return redirect(url('pullrequest_home', repo_name=repo_name))
492 return redirect(url('pullrequest_home', repo_name=repo_name))
493
493
494 return redirect(url('pullrequest_show', repo_name=target_repo,
494 return redirect(url('pullrequest_show', repo_name=target_repo,
495 pull_request_id=pull_request.pull_request_id))
495 pull_request_id=pull_request.pull_request_id))
496
496
497 @LoginRequired()
497 @LoginRequired()
498 @NotAnonymous()
498 @NotAnonymous()
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
499 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 'repository.admin')
500 'repository.admin')
501 @auth.CSRFRequired()
501 @auth.CSRFRequired()
502 @jsonify
502 @jsonify
503 def update(self, repo_name, pull_request_id):
503 def update(self, repo_name, pull_request_id):
504 pull_request_id = safe_int(pull_request_id)
504 pull_request_id = safe_int(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
505 pull_request = PullRequest.get_or_404(pull_request_id)
506 # only owner or admin can update it
506 # only owner or admin can update it
507 allowed_to_update = PullRequestModel().check_user_update(
507 allowed_to_update = PullRequestModel().check_user_update(
508 pull_request, c.rhodecode_user)
508 pull_request, c.rhodecode_user)
509 if allowed_to_update:
509 if allowed_to_update:
510 controls = peppercorn.parse(request.POST.items())
510 controls = peppercorn.parse(request.POST.items())
511
511
512 if 'review_members' in controls:
512 if 'review_members' in controls:
513 self._update_reviewers(
513 self._update_reviewers(
514 pull_request_id, controls['review_members'])
514 pull_request_id, controls['review_members'])
515 elif str2bool(request.POST.get('update_commits', 'false')):
515 elif str2bool(request.POST.get('update_commits', 'false')):
516 self._update_commits(pull_request)
516 self._update_commits(pull_request)
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
517 elif str2bool(request.POST.get('close_pull_request', 'false')):
518 self._reject_close(pull_request)
518 self._reject_close(pull_request)
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
519 elif str2bool(request.POST.get('edit_pull_request', 'false')):
520 self._edit_pull_request(pull_request)
520 self._edit_pull_request(pull_request)
521 else:
521 else:
522 raise HTTPBadRequest()
522 raise HTTPBadRequest()
523 return True
523 return True
524 raise HTTPForbidden()
524 raise HTTPForbidden()
525
525
526 def _edit_pull_request(self, pull_request):
526 def _edit_pull_request(self, pull_request):
527 try:
527 try:
528 PullRequestModel().edit(
528 PullRequestModel().edit(
529 pull_request, request.POST.get('title'),
529 pull_request, request.POST.get('title'),
530 request.POST.get('description'))
530 request.POST.get('description'))
531 except ValueError:
531 except ValueError:
532 msg = _(u'Cannot update closed pull requests.')
532 msg = _(u'Cannot update closed pull requests.')
533 h.flash(msg, category='error')
533 h.flash(msg, category='error')
534 return
534 return
535 else:
535 else:
536 Session().commit()
536 Session().commit()
537
537
538 msg = _(u'Pull request title & description updated.')
538 msg = _(u'Pull request title & description updated.')
539 h.flash(msg, category='success')
539 h.flash(msg, category='success')
540 return
540 return
541
541
542 def _update_commits(self, pull_request):
542 def _update_commits(self, pull_request):
543 resp = PullRequestModel().update_commits(pull_request)
543 resp = PullRequestModel().update_commits(pull_request)
544
544
545 if resp.executed:
545 if resp.executed:
546 msg = _(
546 msg = _(
547 u'Pull request updated to "{source_commit_id}" with '
547 u'Pull request updated to "{source_commit_id}" with '
548 u'{count_added} added, {count_removed} removed commits.')
548 u'{count_added} added, {count_removed} removed commits.')
549 msg = msg.format(
549 msg = msg.format(
550 source_commit_id=pull_request.source_ref_parts.commit_id,
550 source_commit_id=pull_request.source_ref_parts.commit_id,
551 count_added=len(resp.changes.added),
551 count_added=len(resp.changes.added),
552 count_removed=len(resp.changes.removed))
552 count_removed=len(resp.changes.removed))
553 h.flash(msg, category='success')
553 h.flash(msg, category='success')
554
554
555 registry = get_current_registry()
555 registry = get_current_registry()
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
556 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
557 channelstream_config = rhodecode_plugins.get('channelstream', {})
558 if channelstream_config.get('enabled'):
558 if channelstream_config.get('enabled'):
559 message = msg + (
559 message = msg + (
560 ' - <a onclick="window.location.reload()">'
560 ' - <a onclick="window.location.reload()">'
561 '<strong>{}</strong></a>'.format(_('Reload page')))
561 '<strong>{}</strong></a>'.format(_('Reload page')))
562 channel = '/repo${}$/pr/{}'.format(
562 channel = '/repo${}$/pr/{}'.format(
563 pull_request.target_repo.repo_name,
563 pull_request.target_repo.repo_name,
564 pull_request.pull_request_id
564 pull_request.pull_request_id
565 )
565 )
566 payload = {
566 payload = {
567 'type': 'message',
567 'type': 'message',
568 'user': 'system',
568 'user': 'system',
569 'exclude_users': [request.user.username],
569 'exclude_users': [request.user.username],
570 'channel': channel,
570 'channel': channel,
571 'message': {
571 'message': {
572 'message': message,
572 'message': message,
573 'level': 'success',
573 'level': 'success',
574 'topic': '/notifications'
574 'topic': '/notifications'
575 }
575 }
576 }
576 }
577 channelstream_request(
577 channelstream_request(
578 channelstream_config, [payload], '/message',
578 channelstream_config, [payload], '/message',
579 raise_exc=False)
579 raise_exc=False)
580 else:
580 else:
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
581 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
582 warning_reasons = [
582 warning_reasons = [
583 UpdateFailureReason.NO_CHANGE,
583 UpdateFailureReason.NO_CHANGE,
584 UpdateFailureReason.WRONG_REF_TPYE,
584 UpdateFailureReason.WRONG_REF_TPYE,
585 ]
585 ]
586 category = 'warning' if resp.reason in warning_reasons else 'error'
586 category = 'warning' if resp.reason in warning_reasons else 'error'
587 h.flash(msg, category=category)
587 h.flash(msg, category=category)
588
588
589 @auth.CSRFRequired()
589 @auth.CSRFRequired()
590 @LoginRequired()
590 @LoginRequired()
591 @NotAnonymous()
591 @NotAnonymous()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
593 'repository.admin')
593 'repository.admin')
594 def merge(self, repo_name, pull_request_id):
594 def merge(self, repo_name, pull_request_id):
595 """
595 """
596 POST /{repo_name}/pull-request/{pull_request_id}
596 POST /{repo_name}/pull-request/{pull_request_id}
597
597
598 Merge will perform a server-side merge of the specified
598 Merge will perform a server-side merge of the specified
599 pull request, if the pull request is approved and mergeable.
599 pull request, if the pull request is approved and mergeable.
600 After succesfull merging, the pull request is automatically
600 After succesfull merging, the pull request is automatically
601 closed, with a relevant comment.
601 closed, with a relevant comment.
602 """
602 """
603 pull_request_id = safe_int(pull_request_id)
603 pull_request_id = safe_int(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
604 pull_request = PullRequest.get_or_404(pull_request_id)
605 user = c.rhodecode_user
605 user = c.rhodecode_user
606
606
607 if self._meets_merge_pre_conditions(pull_request, user):
607 if self._meets_merge_pre_conditions(pull_request, user):
608 log.debug("Pre-conditions checked, trying to merge.")
608 log.debug("Pre-conditions checked, trying to merge.")
609 extras = vcs_operation_context(
609 extras = vcs_operation_context(
610 request.environ, repo_name=pull_request.target_repo.repo_name,
610 request.environ, repo_name=pull_request.target_repo.repo_name,
611 username=user.username, action='push',
611 username=user.username, action='push',
612 scm=pull_request.target_repo.repo_type)
612 scm=pull_request.target_repo.repo_type)
613 self._merge_pull_request(pull_request, user, extras)
613 self._merge_pull_request(pull_request, user, extras)
614
614
615 return redirect(url(
615 return redirect(url(
616 'pullrequest_show',
616 'pullrequest_show',
617 repo_name=pull_request.target_repo.repo_name,
617 repo_name=pull_request.target_repo.repo_name,
618 pull_request_id=pull_request.pull_request_id))
618 pull_request_id=pull_request.pull_request_id))
619
619
620 def _meets_merge_pre_conditions(self, pull_request, user):
620 def _meets_merge_pre_conditions(self, pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
621 if not PullRequestModel().check_user_merge(pull_request, user):
622 raise HTTPForbidden()
622 raise HTTPForbidden()
623
623
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
624 merge_status, msg = PullRequestModel().merge_status(pull_request)
625 if not merge_status:
625 if not merge_status:
626 log.debug("Cannot merge, not mergeable.")
626 log.debug("Cannot merge, not mergeable.")
627 h.flash(msg, category='error')
627 h.flash(msg, category='error')
628 return False
628 return False
629
629
630 if (pull_request.calculated_review_status()
630 if (pull_request.calculated_review_status()
631 is not ChangesetStatus.STATUS_APPROVED):
631 is not ChangesetStatus.STATUS_APPROVED):
632 log.debug("Cannot merge, approval is pending.")
632 log.debug("Cannot merge, approval is pending.")
633 msg = _('Pull request reviewer approval is pending.')
633 msg = _('Pull request reviewer approval is pending.')
634 h.flash(msg, category='error')
634 h.flash(msg, category='error')
635 return False
635 return False
636
637 todos = CommentsModel().get_unresolved_todos(pull_request)
638 if todos:
639 log.debug("Cannot merge, unresolved todos left.")
640 if len(todos) == 1:
641 msg = _('Cannot merge, {} todo still not resolved.').format(
642 len(todos))
643 else:
644 msg = _('Cannot merge, {} todos still not resolved.').format(
645 len(todos))
646 h.flash(msg, category='error')
647 return False
636 return True
648 return True
637
649
638 def _merge_pull_request(self, pull_request, user, extras):
650 def _merge_pull_request(self, pull_request, user, extras):
639 merge_resp = PullRequestModel().merge(
651 merge_resp = PullRequestModel().merge(
640 pull_request, user, extras=extras)
652 pull_request, user, extras=extras)
641
653
642 if merge_resp.executed:
654 if merge_resp.executed:
643 log.debug("The merge was successful, closing the pull request.")
655 log.debug("The merge was successful, closing the pull request.")
644 PullRequestModel().close_pull_request(
656 PullRequestModel().close_pull_request(
645 pull_request.pull_request_id, user)
657 pull_request.pull_request_id, user)
646 Session().commit()
658 Session().commit()
647 msg = _('Pull request was successfully merged and closed.')
659 msg = _('Pull request was successfully merged and closed.')
648 h.flash(msg, category='success')
660 h.flash(msg, category='success')
649 else:
661 else:
650 log.debug(
662 log.debug(
651 "The merge was not successful. Merge response: %s",
663 "The merge was not successful. Merge response: %s",
652 merge_resp)
664 merge_resp)
653 msg = PullRequestModel().merge_status_message(
665 msg = PullRequestModel().merge_status_message(
654 merge_resp.failure_reason)
666 merge_resp.failure_reason)
655 h.flash(msg, category='error')
667 h.flash(msg, category='error')
656
668
657 def _update_reviewers(self, pull_request_id, review_members):
669 def _update_reviewers(self, pull_request_id, review_members):
658 reviewers = [
670 reviewers = [
659 (int(r['user_id']), r['reasons']) for r in review_members]
671 (int(r['user_id']), r['reasons']) for r in review_members]
660 PullRequestModel().update_reviewers(pull_request_id, reviewers)
672 PullRequestModel().update_reviewers(pull_request_id, reviewers)
661 Session().commit()
673 Session().commit()
662
674
663 def _reject_close(self, pull_request):
675 def _reject_close(self, pull_request):
664 if pull_request.is_closed():
676 if pull_request.is_closed():
665 raise HTTPForbidden()
677 raise HTTPForbidden()
666
678
667 PullRequestModel().close_pull_request_with_comment(
679 PullRequestModel().close_pull_request_with_comment(
668 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
680 pull_request, c.rhodecode_user, c.rhodecode_db_repo)
669 Session().commit()
681 Session().commit()
670
682
671 @LoginRequired()
683 @LoginRequired()
672 @NotAnonymous()
684 @NotAnonymous()
673 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
685 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
674 'repository.admin')
686 'repository.admin')
675 @auth.CSRFRequired()
687 @auth.CSRFRequired()
676 @jsonify
688 @jsonify
677 def delete(self, repo_name, pull_request_id):
689 def delete(self, repo_name, pull_request_id):
678 pull_request_id = safe_int(pull_request_id)
690 pull_request_id = safe_int(pull_request_id)
679 pull_request = PullRequest.get_or_404(pull_request_id)
691 pull_request = PullRequest.get_or_404(pull_request_id)
680 # only owner can delete it !
692 # only owner can delete it !
681 if pull_request.author.user_id == c.rhodecode_user.user_id:
693 if pull_request.author.user_id == c.rhodecode_user.user_id:
682 PullRequestModel().delete(pull_request)
694 PullRequestModel().delete(pull_request)
683 Session().commit()
695 Session().commit()
684 h.flash(_('Successfully deleted pull request'),
696 h.flash(_('Successfully deleted pull request'),
685 category='success')
697 category='success')
686 return redirect(url('my_account_pullrequests'))
698 return redirect(url('my_account_pullrequests'))
687 raise HTTPForbidden()
699 raise HTTPForbidden()
688
700
689 def _get_pr_version(self, pull_request_id, version=None):
701 def _get_pr_version(self, pull_request_id, version=None):
690 pull_request_id = safe_int(pull_request_id)
702 pull_request_id = safe_int(pull_request_id)
691 at_version = None
703 at_version = None
692
704
693 if version and version == 'latest':
705 if version and version == 'latest':
694 pull_request_ver = PullRequest.get(pull_request_id)
706 pull_request_ver = PullRequest.get(pull_request_id)
695 pull_request_obj = pull_request_ver
707 pull_request_obj = pull_request_ver
696 _org_pull_request_obj = pull_request_obj
708 _org_pull_request_obj = pull_request_obj
697 at_version = 'latest'
709 at_version = 'latest'
698 elif version:
710 elif version:
699 pull_request_ver = PullRequestVersion.get_or_404(version)
711 pull_request_ver = PullRequestVersion.get_or_404(version)
700 pull_request_obj = pull_request_ver
712 pull_request_obj = pull_request_ver
701 _org_pull_request_obj = pull_request_ver.pull_request
713 _org_pull_request_obj = pull_request_ver.pull_request
702 at_version = pull_request_ver.pull_request_version_id
714 at_version = pull_request_ver.pull_request_version_id
703 else:
715 else:
704 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
716 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(pull_request_id)
705
717
706 pull_request_display_obj = PullRequest.get_pr_display_object(
718 pull_request_display_obj = PullRequest.get_pr_display_object(
707 pull_request_obj, _org_pull_request_obj)
719 pull_request_obj, _org_pull_request_obj)
708 return _org_pull_request_obj, pull_request_obj, \
720 return _org_pull_request_obj, pull_request_obj, \
709 pull_request_display_obj, at_version
721 pull_request_display_obj, at_version
710
722
711 def _get_pr_version_changes(self, version, pull_request_latest):
723 def _get_pr_version_changes(self, version, pull_request_latest):
712 """
724 """
713 Generate changes commits, and diff data based on the current pr version
725 Generate changes commits, and diff data based on the current pr version
714 """
726 """
715
727
716 #TODO(marcink): save those changes as JSON metadata for chaching later.
728 #TODO(marcink): save those changes as JSON metadata for chaching later.
717
729
718 # fake the version to add the "initial" state object
730 # fake the version to add the "initial" state object
719 pull_request_initial = PullRequest.get_pr_display_object(
731 pull_request_initial = PullRequest.get_pr_display_object(
720 pull_request_latest, pull_request_latest,
732 pull_request_latest, pull_request_latest,
721 internal_methods=['get_commit', 'versions'])
733 internal_methods=['get_commit', 'versions'])
722 pull_request_initial.revisions = []
734 pull_request_initial.revisions = []
723 pull_request_initial.source_repo.get_commit = types.MethodType(
735 pull_request_initial.source_repo.get_commit = types.MethodType(
724 lambda *a, **k: EmptyCommit(), pull_request_initial)
736 lambda *a, **k: EmptyCommit(), pull_request_initial)
725 pull_request_initial.source_repo.scm_instance = types.MethodType(
737 pull_request_initial.source_repo.scm_instance = types.MethodType(
726 lambda *a, **k: EmptyRepository(), pull_request_initial)
738 lambda *a, **k: EmptyRepository(), pull_request_initial)
727
739
728 _changes_versions = [pull_request_latest] + \
740 _changes_versions = [pull_request_latest] + \
729 list(reversed(c.versions)) + \
741 list(reversed(c.versions)) + \
730 [pull_request_initial]
742 [pull_request_initial]
731
743
732 if version == 'latest':
744 if version == 'latest':
733 index = 0
745 index = 0
734 else:
746 else:
735 for pos, prver in enumerate(_changes_versions):
747 for pos, prver in enumerate(_changes_versions):
736 ver = getattr(prver, 'pull_request_version_id', -1)
748 ver = getattr(prver, 'pull_request_version_id', -1)
737 if ver == safe_int(version):
749 if ver == safe_int(version):
738 index = pos
750 index = pos
739 break
751 break
740 else:
752 else:
741 index = 0
753 index = 0
742
754
743 cur_obj = _changes_versions[index]
755 cur_obj = _changes_versions[index]
744 prev_obj = _changes_versions[index + 1]
756 prev_obj = _changes_versions[index + 1]
745
757
746 old_commit_ids = set(prev_obj.revisions)
758 old_commit_ids = set(prev_obj.revisions)
747 new_commit_ids = set(cur_obj.revisions)
759 new_commit_ids = set(cur_obj.revisions)
748
760
749 changes = PullRequestModel()._calculate_commit_id_changes(
761 changes = PullRequestModel()._calculate_commit_id_changes(
750 old_commit_ids, new_commit_ids)
762 old_commit_ids, new_commit_ids)
751
763
752 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
764 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
753 cur_obj, prev_obj)
765 cur_obj, prev_obj)
754 file_changes = PullRequestModel()._calculate_file_changes(
766 file_changes = PullRequestModel()._calculate_file_changes(
755 old_diff_data, new_diff_data)
767 old_diff_data, new_diff_data)
756 return changes, file_changes
768 return changes, file_changes
757
769
758 @LoginRequired()
770 @LoginRequired()
759 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
771 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
760 'repository.admin')
772 'repository.admin')
761 def show(self, repo_name, pull_request_id):
773 def show(self, repo_name, pull_request_id):
762 pull_request_id = safe_int(pull_request_id)
774 pull_request_id = safe_int(pull_request_id)
763 version = request.GET.get('version')
775 version = request.GET.get('version')
776 merge_checks = request.GET.get('merge_checks')
764
777
765 (pull_request_latest,
778 (pull_request_latest,
766 pull_request_at_ver,
779 pull_request_at_ver,
767 pull_request_display_obj,
780 pull_request_display_obj,
768 at_version) = self._get_pr_version(pull_request_id, version=version)
781 at_version) = self._get_pr_version(pull_request_id, version=version)
769
782
770 c.template_context['pull_request_data']['pull_request_id'] = \
783 c.template_context['pull_request_data']['pull_request_id'] = \
771 pull_request_id
784 pull_request_id
772
785
773 # pull_requests repo_name we opened it against
786 # pull_requests repo_name we opened it against
774 # ie. target_repo must match
787 # ie. target_repo must match
775 if repo_name != pull_request_at_ver.target_repo.repo_name:
788 if repo_name != pull_request_at_ver.target_repo.repo_name:
776 raise HTTPNotFound
789 raise HTTPNotFound
777
790
778 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
791 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
779 pull_request_at_ver)
792 pull_request_at_ver)
780
793
794 c.ancestor = None # TODO: add ancestor here
795 c.pull_request = pull_request_display_obj
796 c.pull_request_latest = pull_request_latest
797
781 pr_closed = pull_request_latest.is_closed()
798 pr_closed = pull_request_latest.is_closed()
782 if at_version and not at_version == 'latest':
799 if at_version and not at_version == 'latest':
783 c.allowed_to_change_status = False
800 c.allowed_to_change_status = False
784 c.allowed_to_update = False
801 c.allowed_to_update = False
785 c.allowed_to_merge = False
802 c.allowed_to_merge = False
786 c.allowed_to_delete = False
803 c.allowed_to_delete = False
787 c.allowed_to_comment = False
804 c.allowed_to_comment = False
788 else:
805 else:
789 c.allowed_to_change_status = PullRequestModel(). \
806 c.allowed_to_change_status = PullRequestModel(). \
790 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
807 check_user_change_status(pull_request_at_ver, c.rhodecode_user)
791 c.allowed_to_update = PullRequestModel().check_user_update(
808 c.allowed_to_update = PullRequestModel().check_user_update(
792 pull_request_latest, c.rhodecode_user) and not pr_closed
809 pull_request_latest, c.rhodecode_user) and not pr_closed
793 c.allowed_to_merge = PullRequestModel().check_user_merge(
810 c.allowed_to_merge = PullRequestModel().check_user_merge(
794 pull_request_latest, c.rhodecode_user) and not pr_closed
811 pull_request_latest, c.rhodecode_user) and not pr_closed
795 c.allowed_to_delete = PullRequestModel().check_user_delete(
812 c.allowed_to_delete = PullRequestModel().check_user_delete(
796 pull_request_latest, c.rhodecode_user) and not pr_closed
813 pull_request_latest, c.rhodecode_user) and not pr_closed
797 c.allowed_to_comment = not pr_closed
814 c.allowed_to_comment = not pr_closed
798
815
799 cc_model = CommentsModel()
816 cc_model = CommentsModel()
800
817
801 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
818 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
802 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
819 c.pull_request_review_status = pull_request_at_ver.calculated_review_status()
803 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
804 pull_request_at_ver)
805 c.approval_msg = None
806 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
807 c.approval_msg = _('Reviewer approval is pending.')
808 c.pr_merge_status = False
809
820
810 c.versions = pull_request_display_obj.versions()
821 c.versions = pull_request_display_obj.versions()
811 c.at_version = at_version
822 c.at_version = at_version
812 c.at_version_num = at_version if at_version and at_version != 'latest' else None
823 c.at_version_num = at_version if at_version and at_version != 'latest' else None
813 c.at_version_pos = ChangesetComment.get_index_from_version(
824 c.at_version_pos = ChangesetComment.get_index_from_version(
814 c.at_version_num, c.versions)
825 c.at_version_num, c.versions)
815
826
816 # GENERAL COMMENTS with versions #
827 # GENERAL COMMENTS with versions #
817 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
828 q = cc_model._all_general_comments_of_pull_request(pull_request_latest)
818 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
829 general_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
819
830
820 # pick comments we want to render at current version
831 # pick comments we want to render at current version
821 c.comment_versions = cc_model.aggregate_comments(
832 c.comment_versions = cc_model.aggregate_comments(
822 general_comments, c.versions, c.at_version_num)
833 general_comments, c.versions, c.at_version_num)
823 c.comments = c.comment_versions[c.at_version_num]['until']
834 c.comments = c.comment_versions[c.at_version_num]['until']
824
835
825 # INLINE COMMENTS with versions #
836 # INLINE COMMENTS with versions #
826 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
837 q = cc_model._all_inline_comments_of_pull_request(pull_request_latest)
827 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
838 inline_comments = q.order_by(ChangesetComment.pull_request_version_id.asc())
828 c.inline_versions = cc_model.aggregate_comments(
839 c.inline_versions = cc_model.aggregate_comments(
829 inline_comments, c.versions, c.at_version_num, inline=True)
840 inline_comments, c.versions, c.at_version_num, inline=True)
830
841
831 # if we use version, then do not show later comments
842 # if we use version, then do not show later comments
832 # than current version
843 # than current version
833 paths = collections.defaultdict(lambda: collections.defaultdict(list))
844 display_inline_comments = collections.defaultdict(lambda: collections.defaultdict(list))
834 for co in inline_comments:
845 for co in inline_comments:
835 if c.at_version_num:
846 if c.at_version_num:
836 # pick comments that are at least UPTO given version, so we
847 # pick comments that are at least UPTO given version, so we
837 # don't render comments for higher version
848 # don't render comments for higher version
838 should_render = co.pull_request_version_id and \
849 should_render = co.pull_request_version_id and \
839 co.pull_request_version_id <= c.at_version_num
850 co.pull_request_version_id <= c.at_version_num
840 else:
851 else:
841 # showing all, for 'latest'
852 # showing all, for 'latest'
842 should_render = True
853 should_render = True
843
854
844 if should_render:
855 if should_render:
845 paths[co.f_path][co.line_no].append(co)
856 display_inline_comments[co.f_path][co.line_no].append(co)
846 inline_comments = paths
857
858 c.pr_merge_checks = []
859 c.pr_merge_status, c.pr_merge_msg = PullRequestModel().merge_status(
860 pull_request_at_ver)
861 c.pr_merge_checks.append(['warning' if not c.pr_merge_status else 'success', c.pr_merge_msg])
862
863 if c.pull_request_review_status != ChangesetStatus.STATUS_APPROVED:
864 approval_msg = _('Reviewer approval is pending.')
865 c.pr_merge_status = False
866 c.pr_merge_checks.append(['warning', approval_msg])
867
868 todos = cc_model.get_unresolved_todos(pull_request_latest)
869 if todos:
870 c.pr_merge_status = False
871 if len(todos) == 1:
872 msg = _('{} todo still not resolved.').format(len(todos))
873 else:
874 msg = _('{} todos still not resolved.').format(len(todos))
875 c.pr_merge_checks.append(['warning', msg])
876
877 if merge_checks:
878 return render('/pullrequests/pullrequest_merge_checks.mako')
847
879
848 # load compare data into template context
880 # load compare data into template context
849 self._load_compare_data(pull_request_at_ver, inline_comments)
881 self._load_compare_data(pull_request_at_ver, display_inline_comments)
850
882
851 # this is a hack to properly display links, when creating PR, the
883 # this is a hack to properly display links, when creating PR, the
852 # compare view and others uses different notation, and
884 # compare view and others uses different notation, and
853 # compare_commits.mako renders links based on the target_repo.
885 # compare_commits.mako renders links based on the target_repo.
854 # We need to swap that here to generate it properly on the html side
886 # We need to swap that here to generate it properly on the html side
855 c.target_repo = c.source_repo
887 c.target_repo = c.source_repo
856
888
857 if c.allowed_to_update:
889 if c.allowed_to_update:
858 force_close = ('forced_closed', _('Close Pull Request'))
890 force_close = ('forced_closed', _('Close Pull Request'))
859 statuses = ChangesetStatus.STATUSES + [force_close]
891 statuses = ChangesetStatus.STATUSES + [force_close]
860 else:
892 else:
861 statuses = ChangesetStatus.STATUSES
893 statuses = ChangesetStatus.STATUSES
862 c.commit_statuses = statuses
894 c.commit_statuses = statuses
863
895
864 c.ancestor = None # TODO: add ancestor here
865 c.pull_request = pull_request_display_obj
866 c.pull_request_latest = pull_request_latest
867
868 c.changes = None
896 c.changes = None
869 c.file_changes = None
897 c.file_changes = None
870
898
871 c.show_version_changes = 1 # control flag, not used yet
899 c.show_version_changes = 1 # control flag, not used yet
872
900
873 if at_version and c.show_version_changes:
901 if at_version and c.show_version_changes:
874 c.changes, c.file_changes = self._get_pr_version_changes(
902 c.changes, c.file_changes = self._get_pr_version_changes(
875 version, pull_request_latest)
903 version, pull_request_latest)
876
904
877 return render('/pullrequests/pullrequest_show.mako')
905 return render('/pullrequests/pullrequest_show.mako')
878
906
879 @LoginRequired()
907 @LoginRequired()
880 @NotAnonymous()
908 @NotAnonymous()
881 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
909 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
882 'repository.admin')
910 'repository.admin')
883 @auth.CSRFRequired()
911 @auth.CSRFRequired()
884 @jsonify
912 @jsonify
885 def comment(self, repo_name, pull_request_id):
913 def comment(self, repo_name, pull_request_id):
886 pull_request_id = safe_int(pull_request_id)
914 pull_request_id = safe_int(pull_request_id)
887 pull_request = PullRequest.get_or_404(pull_request_id)
915 pull_request = PullRequest.get_or_404(pull_request_id)
888 if pull_request.is_closed():
916 if pull_request.is_closed():
889 raise HTTPForbidden()
917 raise HTTPForbidden()
890
918
891 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
919 # TODO: johbo: Re-think this bit, "approved_closed" does not exist
892 # as a changeset status, still we want to send it in one value.
920 # as a changeset status, still we want to send it in one value.
893 status = request.POST.get('changeset_status', None)
921 status = request.POST.get('changeset_status', None)
894 text = request.POST.get('text')
922 text = request.POST.get('text')
895 comment_type = request.POST.get('comment_type')
923 comment_type = request.POST.get('comment_type')
896 resolves_comment_id = request.POST.get('resolves_comment_id', None)
924 resolves_comment_id = request.POST.get('resolves_comment_id', None)
897
925
898 if status and '_closed' in status:
926 if status and '_closed' in status:
899 close_pr = True
927 close_pr = True
900 status = status.replace('_closed', '')
928 status = status.replace('_closed', '')
901 else:
929 else:
902 close_pr = False
930 close_pr = False
903
931
904 forced = (status == 'forced')
932 forced = (status == 'forced')
905 if forced:
933 if forced:
906 status = 'rejected'
934 status = 'rejected'
907
935
908 allowed_to_change_status = PullRequestModel().check_user_change_status(
936 allowed_to_change_status = PullRequestModel().check_user_change_status(
909 pull_request, c.rhodecode_user)
937 pull_request, c.rhodecode_user)
910
938
911 if status and allowed_to_change_status:
939 if status and allowed_to_change_status:
912 message = (_('Status change %(transition_icon)s %(status)s')
940 message = (_('Status change %(transition_icon)s %(status)s')
913 % {'transition_icon': '>',
941 % {'transition_icon': '>',
914 'status': ChangesetStatus.get_status_lbl(status)})
942 'status': ChangesetStatus.get_status_lbl(status)})
915 if close_pr:
943 if close_pr:
916 message = _('Closing with') + ' ' + message
944 message = _('Closing with') + ' ' + message
917 text = text or message
945 text = text or message
918 comm = CommentsModel().create(
946 comm = CommentsModel().create(
919 text=text,
947 text=text,
920 repo=c.rhodecode_db_repo.repo_id,
948 repo=c.rhodecode_db_repo.repo_id,
921 user=c.rhodecode_user.user_id,
949 user=c.rhodecode_user.user_id,
922 pull_request=pull_request_id,
950 pull_request=pull_request_id,
923 f_path=request.POST.get('f_path'),
951 f_path=request.POST.get('f_path'),
924 line_no=request.POST.get('line'),
952 line_no=request.POST.get('line'),
925 status_change=(ChangesetStatus.get_status_lbl(status)
953 status_change=(ChangesetStatus.get_status_lbl(status)
926 if status and allowed_to_change_status else None),
954 if status and allowed_to_change_status else None),
927 status_change_type=(status
955 status_change_type=(status
928 if status and allowed_to_change_status else None),
956 if status and allowed_to_change_status else None),
929 closing_pr=close_pr,
957 closing_pr=close_pr,
930 comment_type=comment_type,
958 comment_type=comment_type,
931 resolves_comment_id=resolves_comment_id
959 resolves_comment_id=resolves_comment_id
932 )
960 )
933
961
934 if allowed_to_change_status:
962 if allowed_to_change_status:
935 old_calculated_status = pull_request.calculated_review_status()
963 old_calculated_status = pull_request.calculated_review_status()
936 # get status if set !
964 # get status if set !
937 if status:
965 if status:
938 ChangesetStatusModel().set_status(
966 ChangesetStatusModel().set_status(
939 c.rhodecode_db_repo.repo_id,
967 c.rhodecode_db_repo.repo_id,
940 status,
968 status,
941 c.rhodecode_user.user_id,
969 c.rhodecode_user.user_id,
942 comm,
970 comm,
943 pull_request=pull_request_id
971 pull_request=pull_request_id
944 )
972 )
945
973
946 Session().flush()
974 Session().flush()
947 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
975 events.trigger(events.PullRequestCommentEvent(pull_request, comm))
948 # we now calculate the status of pull request, and based on that
976 # we now calculate the status of pull request, and based on that
949 # calculation we set the commits status
977 # calculation we set the commits status
950 calculated_status = pull_request.calculated_review_status()
978 calculated_status = pull_request.calculated_review_status()
951 if old_calculated_status != calculated_status:
979 if old_calculated_status != calculated_status:
952 PullRequestModel()._trigger_pull_request_hook(
980 PullRequestModel()._trigger_pull_request_hook(
953 pull_request, c.rhodecode_user, 'review_status_change')
981 pull_request, c.rhodecode_user, 'review_status_change')
954
982
955 calculated_status_lbl = ChangesetStatus.get_status_lbl(
983 calculated_status_lbl = ChangesetStatus.get_status_lbl(
956 calculated_status)
984 calculated_status)
957
985
958 if close_pr:
986 if close_pr:
959 status_completed = (
987 status_completed = (
960 calculated_status in [ChangesetStatus.STATUS_APPROVED,
988 calculated_status in [ChangesetStatus.STATUS_APPROVED,
961 ChangesetStatus.STATUS_REJECTED])
989 ChangesetStatus.STATUS_REJECTED])
962 if forced or status_completed:
990 if forced or status_completed:
963 PullRequestModel().close_pull_request(
991 PullRequestModel().close_pull_request(
964 pull_request_id, c.rhodecode_user)
992 pull_request_id, c.rhodecode_user)
965 else:
993 else:
966 h.flash(_('Closing pull request on other statuses than '
994 h.flash(_('Closing pull request on other statuses than '
967 'rejected or approved is forbidden. '
995 'rejected or approved is forbidden. '
968 'Calculated status from all reviewers '
996 'Calculated status from all reviewers '
969 'is currently: %s') % calculated_status_lbl,
997 'is currently: %s') % calculated_status_lbl,
970 category='warning')
998 category='warning')
971
999
972 Session().commit()
1000 Session().commit()
973
1001
974 if not request.is_xhr:
1002 if not request.is_xhr:
975 return redirect(h.url('pullrequest_show', repo_name=repo_name,
1003 return redirect(h.url('pullrequest_show', repo_name=repo_name,
976 pull_request_id=pull_request_id))
1004 pull_request_id=pull_request_id))
977
1005
978 data = {
1006 data = {
979 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
1007 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
980 }
1008 }
981 if comm:
1009 if comm:
982 c.co = comm
1010 c.co = comm
983 c.inline_comment = True if comm.line_no else False
1011 c.inline_comment = True if comm.line_no else False
984 data.update(comm.get_dict())
1012 data.update(comm.get_dict())
985 data.update({'rendered_text':
1013 data.update({'rendered_text':
986 render('changeset/changeset_comment_block.mako')})
1014 render('changeset/changeset_comment_block.mako')})
987
1015
988 return data
1016 return data
989
1017
990 @LoginRequired()
1018 @LoginRequired()
991 @NotAnonymous()
1019 @NotAnonymous()
992 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
1020 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
993 'repository.admin')
1021 'repository.admin')
994 @auth.CSRFRequired()
1022 @auth.CSRFRequired()
995 @jsonify
1023 @jsonify
996 def delete_comment(self, repo_name, comment_id):
1024 def delete_comment(self, repo_name, comment_id):
997 return self._delete_comment(comment_id)
1025 return self._delete_comment(comment_id)
998
1026
999 def _delete_comment(self, comment_id):
1027 def _delete_comment(self, comment_id):
1000 comment_id = safe_int(comment_id)
1028 comment_id = safe_int(comment_id)
1001 co = ChangesetComment.get_or_404(comment_id)
1029 co = ChangesetComment.get_or_404(comment_id)
1002 if co.pull_request.is_closed():
1030 if co.pull_request.is_closed():
1003 # don't allow deleting comments on closed pull request
1031 # don't allow deleting comments on closed pull request
1004 raise HTTPForbidden()
1032 raise HTTPForbidden()
1005
1033
1006 is_owner = co.author.user_id == c.rhodecode_user.user_id
1034 is_owner = co.author.user_id == c.rhodecode_user.user_id
1007 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1035 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
1008 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1036 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
1009 old_calculated_status = co.pull_request.calculated_review_status()
1037 old_calculated_status = co.pull_request.calculated_review_status()
1010 CommentsModel().delete(comment=co)
1038 CommentsModel().delete(comment=co)
1011 Session().commit()
1039 Session().commit()
1012 calculated_status = co.pull_request.calculated_review_status()
1040 calculated_status = co.pull_request.calculated_review_status()
1013 if old_calculated_status != calculated_status:
1041 if old_calculated_status != calculated_status:
1014 PullRequestModel()._trigger_pull_request_hook(
1042 PullRequestModel()._trigger_pull_request_hook(
1015 co.pull_request, c.rhodecode_user, 'review_status_change')
1043 co.pull_request, c.rhodecode_user, 'review_status_change')
1016 return True
1044 return True
1017 else:
1045 else:
1018 raise HTTPForbidden()
1046 raise HTTPForbidden()
@@ -1,600 +1,612 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2011-2017 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 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24
24
25 import logging
25 import logging
26 import traceback
26 import traceback
27 import collections
27 import collections
28
28
29 from datetime import datetime
29 from datetime import datetime
30
30
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32 from pyramid.threadlocal import get_current_registry
32 from pyramid.threadlocal import get_current_registry
33 from sqlalchemy.sql.expression import null
33 from sqlalchemy.sql.expression import null
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib.channelstream import channelstream_request
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users
39 from rhodecode.lib.utils2 import extract_mentioned_users
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
42 ChangesetComment, User, Notification, PullRequest, AttributeDict)
43 from rhodecode.model.notification import NotificationModel
43 from rhodecode.model.notification import NotificationModel
44 from rhodecode.model.meta import Session
44 from rhodecode.model.meta import Session
45 from rhodecode.model.settings import VcsSettingsModel
45 from rhodecode.model.settings import VcsSettingsModel
46 from rhodecode.model.notification import EmailNotificationModel
46 from rhodecode.model.notification import EmailNotificationModel
47 from rhodecode.model.validation_schema.schemas import comment_schema
47 from rhodecode.model.validation_schema.schemas import comment_schema
48
48
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CommentsModel(BaseModel):
53 class CommentsModel(BaseModel):
54
54
55 cls = ChangesetComment
55 cls = ChangesetComment
56
56
57 DIFF_CONTEXT_BEFORE = 3
57 DIFF_CONTEXT_BEFORE = 3
58 DIFF_CONTEXT_AFTER = 3
58 DIFF_CONTEXT_AFTER = 3
59
59
60 def __get_commit_comment(self, changeset_comment):
60 def __get_commit_comment(self, changeset_comment):
61 return self._get_instance(ChangesetComment, changeset_comment)
61 return self._get_instance(ChangesetComment, changeset_comment)
62
62
63 def __get_pull_request(self, pull_request):
63 def __get_pull_request(self, pull_request):
64 return self._get_instance(PullRequest, pull_request)
64 return self._get_instance(PullRequest, pull_request)
65
65
66 def _extract_mentions(self, s):
66 def _extract_mentions(self, s):
67 user_objects = []
67 user_objects = []
68 for username in extract_mentioned_users(s):
68 for username in extract_mentioned_users(s):
69 user_obj = User.get_by_username(username, case_insensitive=True)
69 user_obj = User.get_by_username(username, case_insensitive=True)
70 if user_obj:
70 if user_obj:
71 user_objects.append(user_obj)
71 user_objects.append(user_obj)
72 return user_objects
72 return user_objects
73
73
74 def _get_renderer(self, global_renderer='rst'):
74 def _get_renderer(self, global_renderer='rst'):
75 try:
75 try:
76 # try reading from visual context
76 # try reading from visual context
77 from pylons import tmpl_context
77 from pylons import tmpl_context
78 global_renderer = tmpl_context.visual.default_renderer
78 global_renderer = tmpl_context.visual.default_renderer
79 except AttributeError:
79 except AttributeError:
80 log.debug("Renderer not set, falling back "
80 log.debug("Renderer not set, falling back "
81 "to default renderer '%s'", global_renderer)
81 "to default renderer '%s'", global_renderer)
82 except Exception:
82 except Exception:
83 log.error(traceback.format_exc())
83 log.error(traceback.format_exc())
84 return global_renderer
84 return global_renderer
85
85
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
86 def aggregate_comments(self, comments, versions, show_version, inline=False):
87 # group by versions, and count until, and display objects
87 # group by versions, and count until, and display objects
88
88
89 comment_groups = collections.defaultdict(list)
89 comment_groups = collections.defaultdict(list)
90 [comment_groups[
90 [comment_groups[
91 _co.pull_request_version_id].append(_co) for _co in comments]
91 _co.pull_request_version_id].append(_co) for _co in comments]
92
92
93 def yield_comments(pos):
93 def yield_comments(pos):
94 for co in comment_groups[pos]:
94 for co in comment_groups[pos]:
95 yield co
95 yield co
96
96
97 comment_versions = collections.defaultdict(
97 comment_versions = collections.defaultdict(
98 lambda: collections.defaultdict(list))
98 lambda: collections.defaultdict(list))
99 prev_prvid = -1
99 prev_prvid = -1
100 # fake last entry with None, to aggregate on "latest" version which
100 # fake last entry with None, to aggregate on "latest" version which
101 # doesn't have an pull_request_version_id
101 # doesn't have an pull_request_version_id
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
102 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
103 prvid = ver.pull_request_version_id
103 prvid = ver.pull_request_version_id
104 if prev_prvid == -1:
104 if prev_prvid == -1:
105 prev_prvid = prvid
105 prev_prvid = prvid
106
106
107 for co in yield_comments(prvid):
107 for co in yield_comments(prvid):
108 comment_versions[prvid]['at'].append(co)
108 comment_versions[prvid]['at'].append(co)
109
109
110 # save until
110 # save until
111 current = comment_versions[prvid]['at']
111 current = comment_versions[prvid]['at']
112 prev_until = comment_versions[prev_prvid]['until']
112 prev_until = comment_versions[prev_prvid]['until']
113 cur_until = prev_until + current
113 cur_until = prev_until + current
114 comment_versions[prvid]['until'].extend(cur_until)
114 comment_versions[prvid]['until'].extend(cur_until)
115
115
116 # save outdated
116 # save outdated
117 if inline:
117 if inline:
118 outdated = [x for x in cur_until
118 outdated = [x for x in cur_until
119 if x.outdated_at_version(show_version)]
119 if x.outdated_at_version(show_version)]
120 else:
120 else:
121 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
122 if x.older_than_version(show_version)]
122 if x.older_than_version(show_version)]
123 display = [x for x in cur_until if x not in outdated]
123 display = [x for x in cur_until if x not in outdated]
124
124
125 comment_versions[prvid]['outdated'] = outdated
125 comment_versions[prvid]['outdated'] = outdated
126 comment_versions[prvid]['display'] = display
126 comment_versions[prvid]['display'] = display
127
127
128 prev_prvid = prvid
128 prev_prvid = prvid
129
129
130 return comment_versions
130 return comment_versions
131
131
132 def get_unresolved_todos(self, pull_request):
133
134 todos = Session().query(ChangesetComment) \
135 .filter(ChangesetComment.pull_request == pull_request) \
136 .filter(ChangesetComment.resolved_by == None) \
137 .filter(ChangesetComment.comment_type
138 == ChangesetComment.COMMENT_TYPE_TODO) \
139 .filter(coalesce(ChangesetComment.display_state, '') !=
140 ChangesetComment.COMMENT_OUTDATED).all()
141
142 return todos
143
132 def create(self, text, repo, user, commit_id=None, pull_request=None,
144 def create(self, text, repo, user, commit_id=None, pull_request=None,
133 f_path=None, line_no=None, status_change=None,
145 f_path=None, line_no=None, status_change=None,
134 status_change_type=None, comment_type=None,
146 status_change_type=None, comment_type=None,
135 resolves_comment_id=None, closing_pr=False, send_email=True,
147 resolves_comment_id=None, closing_pr=False, send_email=True,
136 renderer=None):
148 renderer=None):
137 """
149 """
138 Creates new comment for commit or pull request.
150 Creates new comment for commit or pull request.
139 IF status_change is not none this comment is associated with a
151 IF status_change is not none this comment is associated with a
140 status change of commit or commit associated with pull request
152 status change of commit or commit associated with pull request
141
153
142 :param text:
154 :param text:
143 :param repo:
155 :param repo:
144 :param user:
156 :param user:
145 :param commit_id:
157 :param commit_id:
146 :param pull_request:
158 :param pull_request:
147 :param f_path:
159 :param f_path:
148 :param line_no:
160 :param line_no:
149 :param status_change: Label for status change
161 :param status_change: Label for status change
150 :param comment_type: Type of comment
162 :param comment_type: Type of comment
151 :param status_change_type: type of status change
163 :param status_change_type: type of status change
152 :param closing_pr:
164 :param closing_pr:
153 :param send_email:
165 :param send_email:
154 :param renderer: pick renderer for this comment
166 :param renderer: pick renderer for this comment
155 """
167 """
156 if not text:
168 if not text:
157 log.warning('Missing text for comment, skipping...')
169 log.warning('Missing text for comment, skipping...')
158 return
170 return
159
171
160 if not renderer:
172 if not renderer:
161 renderer = self._get_renderer()
173 renderer = self._get_renderer()
162
174
163 repo = self._get_repo(repo)
175 repo = self._get_repo(repo)
164 user = self._get_user(user)
176 user = self._get_user(user)
165
177
166 schema = comment_schema.CommentSchema()
178 schema = comment_schema.CommentSchema()
167 validated_kwargs = schema.deserialize(dict(
179 validated_kwargs = schema.deserialize(dict(
168 comment_body=text,
180 comment_body=text,
169 comment_type=comment_type,
181 comment_type=comment_type,
170 comment_file=f_path,
182 comment_file=f_path,
171 comment_line=line_no,
183 comment_line=line_no,
172 renderer_type=renderer,
184 renderer_type=renderer,
173 status_change=status_change_type,
185 status_change=status_change_type,
174 resolves_comment_id=resolves_comment_id,
186 resolves_comment_id=resolves_comment_id,
175 repo=repo.repo_id,
187 repo=repo.repo_id,
176 user=user.user_id,
188 user=user.user_id,
177 ))
189 ))
178
190
179 comment = ChangesetComment()
191 comment = ChangesetComment()
180 comment.renderer = validated_kwargs['renderer_type']
192 comment.renderer = validated_kwargs['renderer_type']
181 comment.text = validated_kwargs['comment_body']
193 comment.text = validated_kwargs['comment_body']
182 comment.f_path = validated_kwargs['comment_file']
194 comment.f_path = validated_kwargs['comment_file']
183 comment.line_no = validated_kwargs['comment_line']
195 comment.line_no = validated_kwargs['comment_line']
184 comment.comment_type = validated_kwargs['comment_type']
196 comment.comment_type = validated_kwargs['comment_type']
185
197
186 comment.repo = repo
198 comment.repo = repo
187 comment.author = user
199 comment.author = user
188 comment.resolved_comment = self.__get_commit_comment(
200 comment.resolved_comment = self.__get_commit_comment(
189 validated_kwargs['resolves_comment_id'])
201 validated_kwargs['resolves_comment_id'])
190
202
191 pull_request_id = pull_request
203 pull_request_id = pull_request
192
204
193 commit_obj = None
205 commit_obj = None
194 pull_request_obj = None
206 pull_request_obj = None
195
207
196 if commit_id:
208 if commit_id:
197 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
209 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
198 # do a lookup, so we don't pass something bad here
210 # do a lookup, so we don't pass something bad here
199 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
211 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
200 comment.revision = commit_obj.raw_id
212 comment.revision = commit_obj.raw_id
201
213
202 elif pull_request_id:
214 elif pull_request_id:
203 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
215 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
204 pull_request_obj = self.__get_pull_request(pull_request_id)
216 pull_request_obj = self.__get_pull_request(pull_request_id)
205 comment.pull_request = pull_request_obj
217 comment.pull_request = pull_request_obj
206 else:
218 else:
207 raise Exception('Please specify commit or pull_request_id')
219 raise Exception('Please specify commit or pull_request_id')
208
220
209 Session().add(comment)
221 Session().add(comment)
210 Session().flush()
222 Session().flush()
211 kwargs = {
223 kwargs = {
212 'user': user,
224 'user': user,
213 'renderer_type': renderer,
225 'renderer_type': renderer,
214 'repo_name': repo.repo_name,
226 'repo_name': repo.repo_name,
215 'status_change': status_change,
227 'status_change': status_change,
216 'status_change_type': status_change_type,
228 'status_change_type': status_change_type,
217 'comment_body': text,
229 'comment_body': text,
218 'comment_file': f_path,
230 'comment_file': f_path,
219 'comment_line': line_no,
231 'comment_line': line_no,
220 }
232 }
221
233
222 if commit_obj:
234 if commit_obj:
223 recipients = ChangesetComment.get_users(
235 recipients = ChangesetComment.get_users(
224 revision=commit_obj.raw_id)
236 revision=commit_obj.raw_id)
225 # add commit author if it's in RhodeCode system
237 # add commit author if it's in RhodeCode system
226 cs_author = User.get_from_cs_author(commit_obj.author)
238 cs_author = User.get_from_cs_author(commit_obj.author)
227 if not cs_author:
239 if not cs_author:
228 # use repo owner if we cannot extract the author correctly
240 # use repo owner if we cannot extract the author correctly
229 cs_author = repo.user
241 cs_author = repo.user
230 recipients += [cs_author]
242 recipients += [cs_author]
231
243
232 commit_comment_url = self.get_url(comment)
244 commit_comment_url = self.get_url(comment)
233
245
234 target_repo_url = h.link_to(
246 target_repo_url = h.link_to(
235 repo.repo_name,
247 repo.repo_name,
236 h.url('summary_home',
248 h.url('summary_home',
237 repo_name=repo.repo_name, qualified=True))
249 repo_name=repo.repo_name, qualified=True))
238
250
239 # commit specifics
251 # commit specifics
240 kwargs.update({
252 kwargs.update({
241 'commit': commit_obj,
253 'commit': commit_obj,
242 'commit_message': commit_obj.message,
254 'commit_message': commit_obj.message,
243 'commit_target_repo': target_repo_url,
255 'commit_target_repo': target_repo_url,
244 'commit_comment_url': commit_comment_url,
256 'commit_comment_url': commit_comment_url,
245 })
257 })
246
258
247 elif pull_request_obj:
259 elif pull_request_obj:
248 # get the current participants of this pull request
260 # get the current participants of this pull request
249 recipients = ChangesetComment.get_users(
261 recipients = ChangesetComment.get_users(
250 pull_request_id=pull_request_obj.pull_request_id)
262 pull_request_id=pull_request_obj.pull_request_id)
251 # add pull request author
263 # add pull request author
252 recipients += [pull_request_obj.author]
264 recipients += [pull_request_obj.author]
253
265
254 # add the reviewers to notification
266 # add the reviewers to notification
255 recipients += [x.user for x in pull_request_obj.reviewers]
267 recipients += [x.user for x in pull_request_obj.reviewers]
256
268
257 pr_target_repo = pull_request_obj.target_repo
269 pr_target_repo = pull_request_obj.target_repo
258 pr_source_repo = pull_request_obj.source_repo
270 pr_source_repo = pull_request_obj.source_repo
259
271
260 pr_comment_url = h.url(
272 pr_comment_url = h.url(
261 'pullrequest_show',
273 'pullrequest_show',
262 repo_name=pr_target_repo.repo_name,
274 repo_name=pr_target_repo.repo_name,
263 pull_request_id=pull_request_obj.pull_request_id,
275 pull_request_id=pull_request_obj.pull_request_id,
264 anchor='comment-%s' % comment.comment_id,
276 anchor='comment-%s' % comment.comment_id,
265 qualified=True,)
277 qualified=True,)
266
278
267 # set some variables for email notification
279 # set some variables for email notification
268 pr_target_repo_url = h.url(
280 pr_target_repo_url = h.url(
269 'summary_home', repo_name=pr_target_repo.repo_name,
281 'summary_home', repo_name=pr_target_repo.repo_name,
270 qualified=True)
282 qualified=True)
271
283
272 pr_source_repo_url = h.url(
284 pr_source_repo_url = h.url(
273 'summary_home', repo_name=pr_source_repo.repo_name,
285 'summary_home', repo_name=pr_source_repo.repo_name,
274 qualified=True)
286 qualified=True)
275
287
276 # pull request specifics
288 # pull request specifics
277 kwargs.update({
289 kwargs.update({
278 'pull_request': pull_request_obj,
290 'pull_request': pull_request_obj,
279 'pr_id': pull_request_obj.pull_request_id,
291 'pr_id': pull_request_obj.pull_request_id,
280 'pr_target_repo': pr_target_repo,
292 'pr_target_repo': pr_target_repo,
281 'pr_target_repo_url': pr_target_repo_url,
293 'pr_target_repo_url': pr_target_repo_url,
282 'pr_source_repo': pr_source_repo,
294 'pr_source_repo': pr_source_repo,
283 'pr_source_repo_url': pr_source_repo_url,
295 'pr_source_repo_url': pr_source_repo_url,
284 'pr_comment_url': pr_comment_url,
296 'pr_comment_url': pr_comment_url,
285 'pr_closing': closing_pr,
297 'pr_closing': closing_pr,
286 })
298 })
287 if send_email:
299 if send_email:
288 # pre-generate the subject for notification itself
300 # pre-generate the subject for notification itself
289 (subject,
301 (subject,
290 _h, _e, # we don't care about those
302 _h, _e, # we don't care about those
291 body_plaintext) = EmailNotificationModel().render_email(
303 body_plaintext) = EmailNotificationModel().render_email(
292 notification_type, **kwargs)
304 notification_type, **kwargs)
293
305
294 mention_recipients = set(
306 mention_recipients = set(
295 self._extract_mentions(text)).difference(recipients)
307 self._extract_mentions(text)).difference(recipients)
296
308
297 # create notification objects, and emails
309 # create notification objects, and emails
298 NotificationModel().create(
310 NotificationModel().create(
299 created_by=user,
311 created_by=user,
300 notification_subject=subject,
312 notification_subject=subject,
301 notification_body=body_plaintext,
313 notification_body=body_plaintext,
302 notification_type=notification_type,
314 notification_type=notification_type,
303 recipients=recipients,
315 recipients=recipients,
304 mention_recipients=mention_recipients,
316 mention_recipients=mention_recipients,
305 email_kwargs=kwargs,
317 email_kwargs=kwargs,
306 )
318 )
307
319
308 action = (
320 action = (
309 'user_commented_pull_request:{}'.format(
321 'user_commented_pull_request:{}'.format(
310 comment.pull_request.pull_request_id)
322 comment.pull_request.pull_request_id)
311 if comment.pull_request
323 if comment.pull_request
312 else 'user_commented_revision:{}'.format(comment.revision)
324 else 'user_commented_revision:{}'.format(comment.revision)
313 )
325 )
314 action_logger(user, action, comment.repo)
326 action_logger(user, action, comment.repo)
315
327
316 registry = get_current_registry()
328 registry = get_current_registry()
317 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
329 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
318 channelstream_config = rhodecode_plugins.get('channelstream', {})
330 channelstream_config = rhodecode_plugins.get('channelstream', {})
319 msg_url = ''
331 msg_url = ''
320 if commit_obj:
332 if commit_obj:
321 msg_url = commit_comment_url
333 msg_url = commit_comment_url
322 repo_name = repo.repo_name
334 repo_name = repo.repo_name
323 elif pull_request_obj:
335 elif pull_request_obj:
324 msg_url = pr_comment_url
336 msg_url = pr_comment_url
325 repo_name = pr_target_repo.repo_name
337 repo_name = pr_target_repo.repo_name
326
338
327 if channelstream_config.get('enabled'):
339 if channelstream_config.get('enabled'):
328 message = '<strong>{}</strong> {} - ' \
340 message = '<strong>{}</strong> {} - ' \
329 '<a onclick="window.location=\'{}\';' \
341 '<a onclick="window.location=\'{}\';' \
330 'window.location.reload()">' \
342 'window.location.reload()">' \
331 '<strong>{}</strong></a>'
343 '<strong>{}</strong></a>'
332 message = message.format(
344 message = message.format(
333 user.username, _('made a comment'), msg_url,
345 user.username, _('made a comment'), msg_url,
334 _('Show it now'))
346 _('Show it now'))
335 channel = '/repo${}$/pr/{}'.format(
347 channel = '/repo${}$/pr/{}'.format(
336 repo_name,
348 repo_name,
337 pull_request_id
349 pull_request_id
338 )
350 )
339 payload = {
351 payload = {
340 'type': 'message',
352 'type': 'message',
341 'timestamp': datetime.utcnow(),
353 'timestamp': datetime.utcnow(),
342 'user': 'system',
354 'user': 'system',
343 'exclude_users': [user.username],
355 'exclude_users': [user.username],
344 'channel': channel,
356 'channel': channel,
345 'message': {
357 'message': {
346 'message': message,
358 'message': message,
347 'level': 'info',
359 'level': 'info',
348 'topic': '/notifications'
360 'topic': '/notifications'
349 }
361 }
350 }
362 }
351 channelstream_request(channelstream_config, [payload],
363 channelstream_request(channelstream_config, [payload],
352 '/message', raise_exc=False)
364 '/message', raise_exc=False)
353
365
354 return comment
366 return comment
355
367
356 def delete(self, comment):
368 def delete(self, comment):
357 """
369 """
358 Deletes given comment
370 Deletes given comment
359
371
360 :param comment_id:
372 :param comment_id:
361 """
373 """
362 comment = self.__get_commit_comment(comment)
374 comment = self.__get_commit_comment(comment)
363 Session().delete(comment)
375 Session().delete(comment)
364
376
365 return comment
377 return comment
366
378
367 def get_all_comments(self, repo_id, revision=None, pull_request=None):
379 def get_all_comments(self, repo_id, revision=None, pull_request=None):
368 q = ChangesetComment.query()\
380 q = ChangesetComment.query()\
369 .filter(ChangesetComment.repo_id == repo_id)
381 .filter(ChangesetComment.repo_id == repo_id)
370 if revision:
382 if revision:
371 q = q.filter(ChangesetComment.revision == revision)
383 q = q.filter(ChangesetComment.revision == revision)
372 elif pull_request:
384 elif pull_request:
373 pull_request = self.__get_pull_request(pull_request)
385 pull_request = self.__get_pull_request(pull_request)
374 q = q.filter(ChangesetComment.pull_request == pull_request)
386 q = q.filter(ChangesetComment.pull_request == pull_request)
375 else:
387 else:
376 raise Exception('Please specify commit or pull_request')
388 raise Exception('Please specify commit or pull_request')
377 q = q.order_by(ChangesetComment.created_on)
389 q = q.order_by(ChangesetComment.created_on)
378 return q.all()
390 return q.all()
379
391
380 def get_url(self, comment):
392 def get_url(self, comment):
381 comment = self.__get_commit_comment(comment)
393 comment = self.__get_commit_comment(comment)
382 if comment.pull_request:
394 if comment.pull_request:
383 return h.url(
395 return h.url(
384 'pullrequest_show',
396 'pullrequest_show',
385 repo_name=comment.pull_request.target_repo.repo_name,
397 repo_name=comment.pull_request.target_repo.repo_name,
386 pull_request_id=comment.pull_request.pull_request_id,
398 pull_request_id=comment.pull_request.pull_request_id,
387 anchor='comment-%s' % comment.comment_id,
399 anchor='comment-%s' % comment.comment_id,
388 qualified=True,)
400 qualified=True,)
389 else:
401 else:
390 return h.url(
402 return h.url(
391 'changeset_home',
403 'changeset_home',
392 repo_name=comment.repo.repo_name,
404 repo_name=comment.repo.repo_name,
393 revision=comment.revision,
405 revision=comment.revision,
394 anchor='comment-%s' % comment.comment_id,
406 anchor='comment-%s' % comment.comment_id,
395 qualified=True,)
407 qualified=True,)
396
408
397 def get_comments(self, repo_id, revision=None, pull_request=None):
409 def get_comments(self, repo_id, revision=None, pull_request=None):
398 """
410 """
399 Gets main comments based on revision or pull_request_id
411 Gets main comments based on revision or pull_request_id
400
412
401 :param repo_id:
413 :param repo_id:
402 :param revision:
414 :param revision:
403 :param pull_request:
415 :param pull_request:
404 """
416 """
405
417
406 q = ChangesetComment.query()\
418 q = ChangesetComment.query()\
407 .filter(ChangesetComment.repo_id == repo_id)\
419 .filter(ChangesetComment.repo_id == repo_id)\
408 .filter(ChangesetComment.line_no == None)\
420 .filter(ChangesetComment.line_no == None)\
409 .filter(ChangesetComment.f_path == None)
421 .filter(ChangesetComment.f_path == None)
410 if revision:
422 if revision:
411 q = q.filter(ChangesetComment.revision == revision)
423 q = q.filter(ChangesetComment.revision == revision)
412 elif pull_request:
424 elif pull_request:
413 pull_request = self.__get_pull_request(pull_request)
425 pull_request = self.__get_pull_request(pull_request)
414 q = q.filter(ChangesetComment.pull_request == pull_request)
426 q = q.filter(ChangesetComment.pull_request == pull_request)
415 else:
427 else:
416 raise Exception('Please specify commit or pull_request')
428 raise Exception('Please specify commit or pull_request')
417 q = q.order_by(ChangesetComment.created_on)
429 q = q.order_by(ChangesetComment.created_on)
418 return q.all()
430 return q.all()
419
431
420 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
432 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
421 q = self._get_inline_comments_query(repo_id, revision, pull_request)
433 q = self._get_inline_comments_query(repo_id, revision, pull_request)
422 return self._group_comments_by_path_and_line_number(q)
434 return self._group_comments_by_path_and_line_number(q)
423
435
424 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
436 def get_inline_comments_count(self, inline_comments, skip_outdated=True,
425 version=None):
437 version=None):
426 inline_cnt = 0
438 inline_cnt = 0
427 for fname, per_line_comments in inline_comments.iteritems():
439 for fname, per_line_comments in inline_comments.iteritems():
428 for lno, comments in per_line_comments.iteritems():
440 for lno, comments in per_line_comments.iteritems():
429 for comm in comments:
441 for comm in comments:
430 if not comm.outdated_at_version(version) and skip_outdated:
442 if not comm.outdated_at_version(version) and skip_outdated:
431 inline_cnt += 1
443 inline_cnt += 1
432
444
433 return inline_cnt
445 return inline_cnt
434
446
435 def get_outdated_comments(self, repo_id, pull_request):
447 def get_outdated_comments(self, repo_id, pull_request):
436 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
448 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
437 # of a pull request.
449 # of a pull request.
438 q = self._all_inline_comments_of_pull_request(pull_request)
450 q = self._all_inline_comments_of_pull_request(pull_request)
439 q = q.filter(
451 q = q.filter(
440 ChangesetComment.display_state ==
452 ChangesetComment.display_state ==
441 ChangesetComment.COMMENT_OUTDATED
453 ChangesetComment.COMMENT_OUTDATED
442 ).order_by(ChangesetComment.comment_id.asc())
454 ).order_by(ChangesetComment.comment_id.asc())
443
455
444 return self._group_comments_by_path_and_line_number(q)
456 return self._group_comments_by_path_and_line_number(q)
445
457
446 def _get_inline_comments_query(self, repo_id, revision, pull_request):
458 def _get_inline_comments_query(self, repo_id, revision, pull_request):
447 # TODO: johbo: Split this into two methods: One for PR and one for
459 # TODO: johbo: Split this into two methods: One for PR and one for
448 # commit.
460 # commit.
449 if revision:
461 if revision:
450 q = Session().query(ChangesetComment).filter(
462 q = Session().query(ChangesetComment).filter(
451 ChangesetComment.repo_id == repo_id,
463 ChangesetComment.repo_id == repo_id,
452 ChangesetComment.line_no != null(),
464 ChangesetComment.line_no != null(),
453 ChangesetComment.f_path != null(),
465 ChangesetComment.f_path != null(),
454 ChangesetComment.revision == revision)
466 ChangesetComment.revision == revision)
455
467
456 elif pull_request:
468 elif pull_request:
457 pull_request = self.__get_pull_request(pull_request)
469 pull_request = self.__get_pull_request(pull_request)
458 if not CommentsModel.use_outdated_comments(pull_request):
470 if not CommentsModel.use_outdated_comments(pull_request):
459 q = self._visible_inline_comments_of_pull_request(pull_request)
471 q = self._visible_inline_comments_of_pull_request(pull_request)
460 else:
472 else:
461 q = self._all_inline_comments_of_pull_request(pull_request)
473 q = self._all_inline_comments_of_pull_request(pull_request)
462
474
463 else:
475 else:
464 raise Exception('Please specify commit or pull_request_id')
476 raise Exception('Please specify commit or pull_request_id')
465 q = q.order_by(ChangesetComment.comment_id.asc())
477 q = q.order_by(ChangesetComment.comment_id.asc())
466 return q
478 return q
467
479
468 def _group_comments_by_path_and_line_number(self, q):
480 def _group_comments_by_path_and_line_number(self, q):
469 comments = q.all()
481 comments = q.all()
470 paths = collections.defaultdict(lambda: collections.defaultdict(list))
482 paths = collections.defaultdict(lambda: collections.defaultdict(list))
471 for co in comments:
483 for co in comments:
472 paths[co.f_path][co.line_no].append(co)
484 paths[co.f_path][co.line_no].append(co)
473 return paths
485 return paths
474
486
475 @classmethod
487 @classmethod
476 def needed_extra_diff_context(cls):
488 def needed_extra_diff_context(cls):
477 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
489 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
478
490
479 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
491 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
480 if not CommentsModel.use_outdated_comments(pull_request):
492 if not CommentsModel.use_outdated_comments(pull_request):
481 return
493 return
482
494
483 comments = self._visible_inline_comments_of_pull_request(pull_request)
495 comments = self._visible_inline_comments_of_pull_request(pull_request)
484 comments_to_outdate = comments.all()
496 comments_to_outdate = comments.all()
485
497
486 for comment in comments_to_outdate:
498 for comment in comments_to_outdate:
487 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
499 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
488
500
489 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
501 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
490 diff_line = _parse_comment_line_number(comment.line_no)
502 diff_line = _parse_comment_line_number(comment.line_no)
491
503
492 try:
504 try:
493 old_context = old_diff_proc.get_context_of_line(
505 old_context = old_diff_proc.get_context_of_line(
494 path=comment.f_path, diff_line=diff_line)
506 path=comment.f_path, diff_line=diff_line)
495 new_context = new_diff_proc.get_context_of_line(
507 new_context = new_diff_proc.get_context_of_line(
496 path=comment.f_path, diff_line=diff_line)
508 path=comment.f_path, diff_line=diff_line)
497 except (diffs.LineNotInDiffException,
509 except (diffs.LineNotInDiffException,
498 diffs.FileNotInDiffException):
510 diffs.FileNotInDiffException):
499 comment.display_state = ChangesetComment.COMMENT_OUTDATED
511 comment.display_state = ChangesetComment.COMMENT_OUTDATED
500 return
512 return
501
513
502 if old_context == new_context:
514 if old_context == new_context:
503 return
515 return
504
516
505 if self._should_relocate_diff_line(diff_line):
517 if self._should_relocate_diff_line(diff_line):
506 new_diff_lines = new_diff_proc.find_context(
518 new_diff_lines = new_diff_proc.find_context(
507 path=comment.f_path, context=old_context,
519 path=comment.f_path, context=old_context,
508 offset=self.DIFF_CONTEXT_BEFORE)
520 offset=self.DIFF_CONTEXT_BEFORE)
509 if not new_diff_lines:
521 if not new_diff_lines:
510 comment.display_state = ChangesetComment.COMMENT_OUTDATED
522 comment.display_state = ChangesetComment.COMMENT_OUTDATED
511 else:
523 else:
512 new_diff_line = self._choose_closest_diff_line(
524 new_diff_line = self._choose_closest_diff_line(
513 diff_line, new_diff_lines)
525 diff_line, new_diff_lines)
514 comment.line_no = _diff_to_comment_line_number(new_diff_line)
526 comment.line_no = _diff_to_comment_line_number(new_diff_line)
515 else:
527 else:
516 comment.display_state = ChangesetComment.COMMENT_OUTDATED
528 comment.display_state = ChangesetComment.COMMENT_OUTDATED
517
529
518 def _should_relocate_diff_line(self, diff_line):
530 def _should_relocate_diff_line(self, diff_line):
519 """
531 """
520 Checks if relocation shall be tried for the given `diff_line`.
532 Checks if relocation shall be tried for the given `diff_line`.
521
533
522 If a comment points into the first lines, then we can have a situation
534 If a comment points into the first lines, then we can have a situation
523 that after an update another line has been added on top. In this case
535 that after an update another line has been added on top. In this case
524 we would find the context still and move the comment around. This
536 we would find the context still and move the comment around. This
525 would be wrong.
537 would be wrong.
526 """
538 """
527 should_relocate = (
539 should_relocate = (
528 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
540 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
529 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
541 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
530 return should_relocate
542 return should_relocate
531
543
532 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
544 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
533 candidate = new_diff_lines[0]
545 candidate = new_diff_lines[0]
534 best_delta = _diff_line_delta(diff_line, candidate)
546 best_delta = _diff_line_delta(diff_line, candidate)
535 for new_diff_line in new_diff_lines[1:]:
547 for new_diff_line in new_diff_lines[1:]:
536 delta = _diff_line_delta(diff_line, new_diff_line)
548 delta = _diff_line_delta(diff_line, new_diff_line)
537 if delta < best_delta:
549 if delta < best_delta:
538 candidate = new_diff_line
550 candidate = new_diff_line
539 best_delta = delta
551 best_delta = delta
540 return candidate
552 return candidate
541
553
542 def _visible_inline_comments_of_pull_request(self, pull_request):
554 def _visible_inline_comments_of_pull_request(self, pull_request):
543 comments = self._all_inline_comments_of_pull_request(pull_request)
555 comments = self._all_inline_comments_of_pull_request(pull_request)
544 comments = comments.filter(
556 comments = comments.filter(
545 coalesce(ChangesetComment.display_state, '') !=
557 coalesce(ChangesetComment.display_state, '') !=
546 ChangesetComment.COMMENT_OUTDATED)
558 ChangesetComment.COMMENT_OUTDATED)
547 return comments
559 return comments
548
560
549 def _all_inline_comments_of_pull_request(self, pull_request):
561 def _all_inline_comments_of_pull_request(self, pull_request):
550 comments = Session().query(ChangesetComment)\
562 comments = Session().query(ChangesetComment)\
551 .filter(ChangesetComment.line_no != None)\
563 .filter(ChangesetComment.line_no != None)\
552 .filter(ChangesetComment.f_path != None)\
564 .filter(ChangesetComment.f_path != None)\
553 .filter(ChangesetComment.pull_request == pull_request)
565 .filter(ChangesetComment.pull_request == pull_request)
554 return comments
566 return comments
555
567
556 def _all_general_comments_of_pull_request(self, pull_request):
568 def _all_general_comments_of_pull_request(self, pull_request):
557 comments = Session().query(ChangesetComment)\
569 comments = Session().query(ChangesetComment)\
558 .filter(ChangesetComment.line_no == None)\
570 .filter(ChangesetComment.line_no == None)\
559 .filter(ChangesetComment.f_path == None)\
571 .filter(ChangesetComment.f_path == None)\
560 .filter(ChangesetComment.pull_request == pull_request)
572 .filter(ChangesetComment.pull_request == pull_request)
561 return comments
573 return comments
562
574
563 @staticmethod
575 @staticmethod
564 def use_outdated_comments(pull_request):
576 def use_outdated_comments(pull_request):
565 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
577 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
566 settings = settings_model.get_general_settings()
578 settings = settings_model.get_general_settings()
567 return settings.get('rhodecode_use_outdated_comments', False)
579 return settings.get('rhodecode_use_outdated_comments', False)
568
580
569
581
570 def _parse_comment_line_number(line_no):
582 def _parse_comment_line_number(line_no):
571 """
583 """
572 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
584 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
573 """
585 """
574 old_line = None
586 old_line = None
575 new_line = None
587 new_line = None
576 if line_no.startswith('o'):
588 if line_no.startswith('o'):
577 old_line = int(line_no[1:])
589 old_line = int(line_no[1:])
578 elif line_no.startswith('n'):
590 elif line_no.startswith('n'):
579 new_line = int(line_no[1:])
591 new_line = int(line_no[1:])
580 else:
592 else:
581 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
593 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
582 return diffs.DiffLineNumber(old_line, new_line)
594 return diffs.DiffLineNumber(old_line, new_line)
583
595
584
596
585 def _diff_to_comment_line_number(diff_line):
597 def _diff_to_comment_line_number(diff_line):
586 if diff_line.new is not None:
598 if diff_line.new is not None:
587 return u'n{}'.format(diff_line.new)
599 return u'n{}'.format(diff_line.new)
588 elif diff_line.old is not None:
600 elif diff_line.old is not None:
589 return u'o{}'.format(diff_line.old)
601 return u'o{}'.format(diff_line.old)
590 return u''
602 return u''
591
603
592
604
593 def _diff_line_delta(a, b):
605 def _diff_line_delta(a, b):
594 if None not in (a.new, b.new):
606 if None not in (a.new, b.new):
595 return abs(a.new - b.new)
607 return abs(a.new - b.new)
596 elif None not in (a.old, b.old):
608 elif None not in (a.old, b.old):
597 return abs(a.old - b.old)
609 return abs(a.old - b.old)
598 else:
610 else:
599 raise ValueError(
611 raise ValueError(
600 "Cannot compute delta between {} and {}".format(a, b))
612 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,3842 +1,3846 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 hashlib
28 import hashlib
29 import logging
29 import logging
30 import datetime
30 import datetime
31 import warnings
31 import warnings
32 import ipaddress
32 import ipaddress
33 import functools
33 import functools
34 import traceback
34 import traceback
35 import collections
35 import collections
36
36
37
37
38 from sqlalchemy import *
38 from sqlalchemy import *
39 from sqlalchemy.ext.declarative import declared_attr
39 from sqlalchemy.ext.declarative import declared_attr
40 from sqlalchemy.ext.hybrid import hybrid_property
40 from sqlalchemy.ext.hybrid import hybrid_property
41 from sqlalchemy.orm import (
41 from sqlalchemy.orm import (
42 relationship, joinedload, class_mapper, validates, aliased)
42 relationship, joinedload, class_mapper, validates, aliased)
43 from sqlalchemy.sql.expression import true
43 from sqlalchemy.sql.expression import true
44 from beaker.cache import cache_region
44 from beaker.cache import cache_region
45 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
46 from zope.cachedescriptors.property import Lazy as LazyProperty
46 from zope.cachedescriptors.property import Lazy as LazyProperty
47
47
48 from pylons import url
48 from pylons import url
49 from pylons.i18n.translation import lazy_ugettext as _
49 from pylons.i18n.translation import lazy_ugettext as _
50
50
51 from rhodecode.lib.vcs import get_vcs_instance
51 from rhodecode.lib.vcs import get_vcs_instance
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
53 from rhodecode.lib.utils2 import (
53 from rhodecode.lib.utils2 import (
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
56 glob2re, StrictAttributeDict)
56 glob2re, StrictAttributeDict)
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
58 from rhodecode.lib.ext_json import json
58 from rhodecode.lib.ext_json import json
59 from rhodecode.lib.caching_query import FromCache
59 from rhodecode.lib.caching_query import FromCache
60 from rhodecode.lib.encrypt import AESCipher
60 from rhodecode.lib.encrypt import AESCipher
61
61
62 from rhodecode.model.meta import Base, Session
62 from rhodecode.model.meta import Base, Session
63
63
64 URL_SEP = '/'
64 URL_SEP = '/'
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67 # =============================================================================
67 # =============================================================================
68 # BASE CLASSES
68 # BASE CLASSES
69 # =============================================================================
69 # =============================================================================
70
70
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 # this is propagated from .ini file rhodecode.encrypted_values.secret or
72 # beaker.session.secret if first is not set.
72 # beaker.session.secret if first is not set.
73 # and initialized at environment.py
73 # and initialized at environment.py
74 ENCRYPTION_KEY = None
74 ENCRYPTION_KEY = None
75
75
76 # used to sort permissions by types, '#' used here is not allowed to be in
76 # used to sort permissions by types, '#' used here is not allowed to be in
77 # usernames, and it's very early in sorted string.printable table.
77 # usernames, and it's very early in sorted string.printable table.
78 PERMISSION_TYPE_SORT = {
78 PERMISSION_TYPE_SORT = {
79 'admin': '####',
79 'admin': '####',
80 'write': '###',
80 'write': '###',
81 'read': '##',
81 'read': '##',
82 'none': '#',
82 'none': '#',
83 }
83 }
84
84
85
85
86 def display_sort(obj):
86 def display_sort(obj):
87 """
87 """
88 Sort function used to sort permissions in .permissions() function of
88 Sort function used to sort permissions in .permissions() function of
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 Repository, RepoGroup, UserGroup. Also it put the default user in front
90 of all other resources
90 of all other resources
91 """
91 """
92
92
93 if obj.username == User.DEFAULT_USER:
93 if obj.username == User.DEFAULT_USER:
94 return '#####'
94 return '#####'
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
96 return prefix + obj.username
96 return prefix + obj.username
97
97
98
98
99 def _hash_key(k):
99 def _hash_key(k):
100 return md5_safe(k)
100 return md5_safe(k)
101
101
102
102
103 class EncryptedTextValue(TypeDecorator):
103 class EncryptedTextValue(TypeDecorator):
104 """
104 """
105 Special column for encrypted long text data, use like::
105 Special column for encrypted long text data, use like::
106
106
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 value = Column("encrypted_value", EncryptedValue(), nullable=False)
108
108
109 This column is intelligent so if value is in unencrypted form it return
109 This column is intelligent so if value is in unencrypted form it return
110 unencrypted form, but on save it always encrypts
110 unencrypted form, but on save it always encrypts
111 """
111 """
112 impl = Text
112 impl = Text
113
113
114 def process_bind_param(self, value, dialect):
114 def process_bind_param(self, value, dialect):
115 if not value:
115 if not value:
116 return value
116 return value
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
118 # protect against double encrypting if someone manually starts
118 # protect against double encrypting if someone manually starts
119 # doing
119 # doing
120 raise ValueError('value needs to be in unencrypted format, ie. '
120 raise ValueError('value needs to be in unencrypted format, ie. '
121 'not starting with enc$aes')
121 'not starting with enc$aes')
122 return 'enc$aes_hmac$%s' % AESCipher(
122 return 'enc$aes_hmac$%s' % AESCipher(
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 ENCRYPTION_KEY, hmac=True).encrypt(value)
124
124
125 def process_result_value(self, value, dialect):
125 def process_result_value(self, value, dialect):
126 import rhodecode
126 import rhodecode
127
127
128 if not value:
128 if not value:
129 return value
129 return value
130
130
131 parts = value.split('$', 3)
131 parts = value.split('$', 3)
132 if not len(parts) == 3:
132 if not len(parts) == 3:
133 # probably not encrypted values
133 # probably not encrypted values
134 return value
134 return value
135 else:
135 else:
136 if parts[0] != 'enc':
136 if parts[0] != 'enc':
137 # parts ok but without our header ?
137 # parts ok but without our header ?
138 return value
138 return value
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
140 'rhodecode.encrypted_values.strict') or True)
140 'rhodecode.encrypted_values.strict') or True)
141 # at that stage we know it's our encryption
141 # at that stage we know it's our encryption
142 if parts[1] == 'aes':
142 if parts[1] == 'aes':
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
144 elif parts[1] == 'aes_hmac':
144 elif parts[1] == 'aes_hmac':
145 decrypted_data = AESCipher(
145 decrypted_data = AESCipher(
146 ENCRYPTION_KEY, hmac=True,
146 ENCRYPTION_KEY, hmac=True,
147 strict_verification=enc_strict_mode).decrypt(parts[2])
147 strict_verification=enc_strict_mode).decrypt(parts[2])
148 else:
148 else:
149 raise ValueError(
149 raise ValueError(
150 'Encryption type part is wrong, must be `aes` '
150 'Encryption type part is wrong, must be `aes` '
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 'or `aes_hmac`, got `%s` instead' % (parts[1]))
152 return decrypted_data
152 return decrypted_data
153
153
154
154
155 class BaseModel(object):
155 class BaseModel(object):
156 """
156 """
157 Base Model for all classes
157 Base Model for all classes
158 """
158 """
159
159
160 @classmethod
160 @classmethod
161 def _get_keys(cls):
161 def _get_keys(cls):
162 """return column names for this model """
162 """return column names for this model """
163 return class_mapper(cls).c.keys()
163 return class_mapper(cls).c.keys()
164
164
165 def get_dict(self):
165 def get_dict(self):
166 """
166 """
167 return dict with keys and values corresponding
167 return dict with keys and values corresponding
168 to this model data """
168 to this model data """
169
169
170 d = {}
170 d = {}
171 for k in self._get_keys():
171 for k in self._get_keys():
172 d[k] = getattr(self, k)
172 d[k] = getattr(self, k)
173
173
174 # also use __json__() if present to get additional fields
174 # also use __json__() if present to get additional fields
175 _json_attr = getattr(self, '__json__', None)
175 _json_attr = getattr(self, '__json__', None)
176 if _json_attr:
176 if _json_attr:
177 # update with attributes from __json__
177 # update with attributes from __json__
178 if callable(_json_attr):
178 if callable(_json_attr):
179 _json_attr = _json_attr()
179 _json_attr = _json_attr()
180 for k, val in _json_attr.iteritems():
180 for k, val in _json_attr.iteritems():
181 d[k] = val
181 d[k] = val
182 return d
182 return d
183
183
184 def get_appstruct(self):
184 def get_appstruct(self):
185 """return list with keys and values tuples corresponding
185 """return list with keys and values tuples corresponding
186 to this model data """
186 to this model data """
187
187
188 l = []
188 l = []
189 for k in self._get_keys():
189 for k in self._get_keys():
190 l.append((k, getattr(self, k),))
190 l.append((k, getattr(self, k),))
191 return l
191 return l
192
192
193 def populate_obj(self, populate_dict):
193 def populate_obj(self, populate_dict):
194 """populate model with data from given populate_dict"""
194 """populate model with data from given populate_dict"""
195
195
196 for k in self._get_keys():
196 for k in self._get_keys():
197 if k in populate_dict:
197 if k in populate_dict:
198 setattr(self, k, populate_dict[k])
198 setattr(self, k, populate_dict[k])
199
199
200 @classmethod
200 @classmethod
201 def query(cls):
201 def query(cls):
202 return Session().query(cls)
202 return Session().query(cls)
203
203
204 @classmethod
204 @classmethod
205 def get(cls, id_):
205 def get(cls, id_):
206 if id_:
206 if id_:
207 return cls.query().get(id_)
207 return cls.query().get(id_)
208
208
209 @classmethod
209 @classmethod
210 def get_or_404(cls, id_):
210 def get_or_404(cls, id_):
211 try:
211 try:
212 id_ = int(id_)
212 id_ = int(id_)
213 except (TypeError, ValueError):
213 except (TypeError, ValueError):
214 raise HTTPNotFound
214 raise HTTPNotFound
215
215
216 res = cls.query().get(id_)
216 res = cls.query().get(id_)
217 if not res:
217 if not res:
218 raise HTTPNotFound
218 raise HTTPNotFound
219 return res
219 return res
220
220
221 @classmethod
221 @classmethod
222 def getAll(cls):
222 def getAll(cls):
223 # deprecated and left for backward compatibility
223 # deprecated and left for backward compatibility
224 return cls.get_all()
224 return cls.get_all()
225
225
226 @classmethod
226 @classmethod
227 def get_all(cls):
227 def get_all(cls):
228 return cls.query().all()
228 return cls.query().all()
229
229
230 @classmethod
230 @classmethod
231 def delete(cls, id_):
231 def delete(cls, id_):
232 obj = cls.query().get(id_)
232 obj = cls.query().get(id_)
233 Session().delete(obj)
233 Session().delete(obj)
234
234
235 @classmethod
235 @classmethod
236 def identity_cache(cls, session, attr_name, value):
236 def identity_cache(cls, session, attr_name, value):
237 exist_in_session = []
237 exist_in_session = []
238 for (item_cls, pkey), instance in session.identity_map.items():
238 for (item_cls, pkey), instance in session.identity_map.items():
239 if cls == item_cls and getattr(instance, attr_name) == value:
239 if cls == item_cls and getattr(instance, attr_name) == value:
240 exist_in_session.append(instance)
240 exist_in_session.append(instance)
241 if exist_in_session:
241 if exist_in_session:
242 if len(exist_in_session) == 1:
242 if len(exist_in_session) == 1:
243 return exist_in_session[0]
243 return exist_in_session[0]
244 log.exception(
244 log.exception(
245 'multiple objects with attr %s and '
245 'multiple objects with attr %s and '
246 'value %s found with same name: %r',
246 'value %s found with same name: %r',
247 attr_name, value, exist_in_session)
247 attr_name, value, exist_in_session)
248
248
249 def __repr__(self):
249 def __repr__(self):
250 if hasattr(self, '__unicode__'):
250 if hasattr(self, '__unicode__'):
251 # python repr needs to return str
251 # python repr needs to return str
252 try:
252 try:
253 return safe_str(self.__unicode__())
253 return safe_str(self.__unicode__())
254 except UnicodeDecodeError:
254 except UnicodeDecodeError:
255 pass
255 pass
256 return '<DB:%s>' % (self.__class__.__name__)
256 return '<DB:%s>' % (self.__class__.__name__)
257
257
258
258
259 class RhodeCodeSetting(Base, BaseModel):
259 class RhodeCodeSetting(Base, BaseModel):
260 __tablename__ = 'rhodecode_settings'
260 __tablename__ = 'rhodecode_settings'
261 __table_args__ = (
261 __table_args__ = (
262 UniqueConstraint('app_settings_name'),
262 UniqueConstraint('app_settings_name'),
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
263 {'extend_existing': True, 'mysql_engine': 'InnoDB',
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
264 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
265 )
265 )
266
266
267 SETTINGS_TYPES = {
267 SETTINGS_TYPES = {
268 'str': safe_str,
268 'str': safe_str,
269 'int': safe_int,
269 'int': safe_int,
270 'unicode': safe_unicode,
270 'unicode': safe_unicode,
271 'bool': str2bool,
271 'bool': str2bool,
272 'list': functools.partial(aslist, sep=',')
272 'list': functools.partial(aslist, sep=',')
273 }
273 }
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
274 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
275 GLOBAL_CONF_KEY = 'app_settings'
275 GLOBAL_CONF_KEY = 'app_settings'
276
276
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
277 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
278 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
279 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
280 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
281
281
282 def __init__(self, key='', val='', type='unicode'):
282 def __init__(self, key='', val='', type='unicode'):
283 self.app_settings_name = key
283 self.app_settings_name = key
284 self.app_settings_type = type
284 self.app_settings_type = type
285 self.app_settings_value = val
285 self.app_settings_value = val
286
286
287 @validates('_app_settings_value')
287 @validates('_app_settings_value')
288 def validate_settings_value(self, key, val):
288 def validate_settings_value(self, key, val):
289 assert type(val) == unicode
289 assert type(val) == unicode
290 return val
290 return val
291
291
292 @hybrid_property
292 @hybrid_property
293 def app_settings_value(self):
293 def app_settings_value(self):
294 v = self._app_settings_value
294 v = self._app_settings_value
295 _type = self.app_settings_type
295 _type = self.app_settings_type
296 if _type:
296 if _type:
297 _type = self.app_settings_type.split('.')[0]
297 _type = self.app_settings_type.split('.')[0]
298 # decode the encrypted value
298 # decode the encrypted value
299 if 'encrypted' in self.app_settings_type:
299 if 'encrypted' in self.app_settings_type:
300 cipher = EncryptedTextValue()
300 cipher = EncryptedTextValue()
301 v = safe_unicode(cipher.process_result_value(v, None))
301 v = safe_unicode(cipher.process_result_value(v, None))
302
302
303 converter = self.SETTINGS_TYPES.get(_type) or \
303 converter = self.SETTINGS_TYPES.get(_type) or \
304 self.SETTINGS_TYPES['unicode']
304 self.SETTINGS_TYPES['unicode']
305 return converter(v)
305 return converter(v)
306
306
307 @app_settings_value.setter
307 @app_settings_value.setter
308 def app_settings_value(self, val):
308 def app_settings_value(self, val):
309 """
309 """
310 Setter that will always make sure we use unicode in app_settings_value
310 Setter that will always make sure we use unicode in app_settings_value
311
311
312 :param val:
312 :param val:
313 """
313 """
314 val = safe_unicode(val)
314 val = safe_unicode(val)
315 # encode the encrypted value
315 # encode the encrypted value
316 if 'encrypted' in self.app_settings_type:
316 if 'encrypted' in self.app_settings_type:
317 cipher = EncryptedTextValue()
317 cipher = EncryptedTextValue()
318 val = safe_unicode(cipher.process_bind_param(val, None))
318 val = safe_unicode(cipher.process_bind_param(val, None))
319 self._app_settings_value = val
319 self._app_settings_value = val
320
320
321 @hybrid_property
321 @hybrid_property
322 def app_settings_type(self):
322 def app_settings_type(self):
323 return self._app_settings_type
323 return self._app_settings_type
324
324
325 @app_settings_type.setter
325 @app_settings_type.setter
326 def app_settings_type(self, val):
326 def app_settings_type(self, val):
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
327 if val.split('.')[0] not in self.SETTINGS_TYPES:
328 raise Exception('type must be one of %s got %s'
328 raise Exception('type must be one of %s got %s'
329 % (self.SETTINGS_TYPES.keys(), val))
329 % (self.SETTINGS_TYPES.keys(), val))
330 self._app_settings_type = val
330 self._app_settings_type = val
331
331
332 def __unicode__(self):
332 def __unicode__(self):
333 return u"<%s('%s:%s[%s]')>" % (
333 return u"<%s('%s:%s[%s]')>" % (
334 self.__class__.__name__,
334 self.__class__.__name__,
335 self.app_settings_name, self.app_settings_value,
335 self.app_settings_name, self.app_settings_value,
336 self.app_settings_type
336 self.app_settings_type
337 )
337 )
338
338
339
339
340 class RhodeCodeUi(Base, BaseModel):
340 class RhodeCodeUi(Base, BaseModel):
341 __tablename__ = 'rhodecode_ui'
341 __tablename__ = 'rhodecode_ui'
342 __table_args__ = (
342 __table_args__ = (
343 UniqueConstraint('ui_key'),
343 UniqueConstraint('ui_key'),
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
344 {'extend_existing': True, 'mysql_engine': 'InnoDB',
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
345 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
346 )
346 )
347
347
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
348 HOOK_REPO_SIZE = 'changegroup.repo_size'
349 # HG
349 # HG
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
350 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
351 HOOK_PULL = 'outgoing.pull_logger'
351 HOOK_PULL = 'outgoing.pull_logger'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
352 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
353 HOOK_PUSH = 'changegroup.push_logger'
353 HOOK_PUSH = 'changegroup.push_logger'
354
354
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
355 # TODO: johbo: Unify way how hooks are configured for git and hg,
356 # git part is currently hardcoded.
356 # git part is currently hardcoded.
357
357
358 # SVN PATTERNS
358 # SVN PATTERNS
359 SVN_BRANCH_ID = 'vcs_svn_branch'
359 SVN_BRANCH_ID = 'vcs_svn_branch'
360 SVN_TAG_ID = 'vcs_svn_tag'
360 SVN_TAG_ID = 'vcs_svn_tag'
361
361
362 ui_id = Column(
362 ui_id = Column(
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
363 "ui_id", Integer(), nullable=False, unique=True, default=None,
364 primary_key=True)
364 primary_key=True)
365 ui_section = Column(
365 ui_section = Column(
366 "ui_section", String(255), nullable=True, unique=None, default=None)
366 "ui_section", String(255), nullable=True, unique=None, default=None)
367 ui_key = Column(
367 ui_key = Column(
368 "ui_key", String(255), nullable=True, unique=None, default=None)
368 "ui_key", String(255), nullable=True, unique=None, default=None)
369 ui_value = Column(
369 ui_value = Column(
370 "ui_value", String(255), nullable=True, unique=None, default=None)
370 "ui_value", String(255), nullable=True, unique=None, default=None)
371 ui_active = Column(
371 ui_active = Column(
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
372 "ui_active", Boolean(), nullable=True, unique=None, default=True)
373
373
374 def __repr__(self):
374 def __repr__(self):
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
375 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
376 self.ui_key, self.ui_value)
376 self.ui_key, self.ui_value)
377
377
378
378
379 class RepoRhodeCodeSetting(Base, BaseModel):
379 class RepoRhodeCodeSetting(Base, BaseModel):
380 __tablename__ = 'repo_rhodecode_settings'
380 __tablename__ = 'repo_rhodecode_settings'
381 __table_args__ = (
381 __table_args__ = (
382 UniqueConstraint(
382 UniqueConstraint(
383 'app_settings_name', 'repository_id',
383 'app_settings_name', 'repository_id',
384 name='uq_repo_rhodecode_setting_name_repo_id'),
384 name='uq_repo_rhodecode_setting_name_repo_id'),
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
385 {'extend_existing': True, 'mysql_engine': 'InnoDB',
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
386 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
387 )
387 )
388
388
389 repository_id = Column(
389 repository_id = Column(
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
390 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
391 nullable=False)
391 nullable=False)
392 app_settings_id = Column(
392 app_settings_id = Column(
393 "app_settings_id", Integer(), nullable=False, unique=True,
393 "app_settings_id", Integer(), nullable=False, unique=True,
394 default=None, primary_key=True)
394 default=None, primary_key=True)
395 app_settings_name = Column(
395 app_settings_name = Column(
396 "app_settings_name", String(255), nullable=True, unique=None,
396 "app_settings_name", String(255), nullable=True, unique=None,
397 default=None)
397 default=None)
398 _app_settings_value = Column(
398 _app_settings_value = Column(
399 "app_settings_value", String(4096), nullable=True, unique=None,
399 "app_settings_value", String(4096), nullable=True, unique=None,
400 default=None)
400 default=None)
401 _app_settings_type = Column(
401 _app_settings_type = Column(
402 "app_settings_type", String(255), nullable=True, unique=None,
402 "app_settings_type", String(255), nullable=True, unique=None,
403 default=None)
403 default=None)
404
404
405 repository = relationship('Repository')
405 repository = relationship('Repository')
406
406
407 def __init__(self, repository_id, key='', val='', type='unicode'):
407 def __init__(self, repository_id, key='', val='', type='unicode'):
408 self.repository_id = repository_id
408 self.repository_id = repository_id
409 self.app_settings_name = key
409 self.app_settings_name = key
410 self.app_settings_type = type
410 self.app_settings_type = type
411 self.app_settings_value = val
411 self.app_settings_value = val
412
412
413 @validates('_app_settings_value')
413 @validates('_app_settings_value')
414 def validate_settings_value(self, key, val):
414 def validate_settings_value(self, key, val):
415 assert type(val) == unicode
415 assert type(val) == unicode
416 return val
416 return val
417
417
418 @hybrid_property
418 @hybrid_property
419 def app_settings_value(self):
419 def app_settings_value(self):
420 v = self._app_settings_value
420 v = self._app_settings_value
421 type_ = self.app_settings_type
421 type_ = self.app_settings_type
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
422 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
423 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
424 return converter(v)
424 return converter(v)
425
425
426 @app_settings_value.setter
426 @app_settings_value.setter
427 def app_settings_value(self, val):
427 def app_settings_value(self, val):
428 """
428 """
429 Setter that will always make sure we use unicode in app_settings_value
429 Setter that will always make sure we use unicode in app_settings_value
430
430
431 :param val:
431 :param val:
432 """
432 """
433 self._app_settings_value = safe_unicode(val)
433 self._app_settings_value = safe_unicode(val)
434
434
435 @hybrid_property
435 @hybrid_property
436 def app_settings_type(self):
436 def app_settings_type(self):
437 return self._app_settings_type
437 return self._app_settings_type
438
438
439 @app_settings_type.setter
439 @app_settings_type.setter
440 def app_settings_type(self, val):
440 def app_settings_type(self, val):
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
441 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
442 if val not in SETTINGS_TYPES:
442 if val not in SETTINGS_TYPES:
443 raise Exception('type must be one of %s got %s'
443 raise Exception('type must be one of %s got %s'
444 % (SETTINGS_TYPES.keys(), val))
444 % (SETTINGS_TYPES.keys(), val))
445 self._app_settings_type = val
445 self._app_settings_type = val
446
446
447 def __unicode__(self):
447 def __unicode__(self):
448 return u"<%s('%s:%s:%s[%s]')>" % (
448 return u"<%s('%s:%s:%s[%s]')>" % (
449 self.__class__.__name__, self.repository.repo_name,
449 self.__class__.__name__, self.repository.repo_name,
450 self.app_settings_name, self.app_settings_value,
450 self.app_settings_name, self.app_settings_value,
451 self.app_settings_type
451 self.app_settings_type
452 )
452 )
453
453
454
454
455 class RepoRhodeCodeUi(Base, BaseModel):
455 class RepoRhodeCodeUi(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_ui'
456 __tablename__ = 'repo_rhodecode_ui'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'repository_id', 'ui_section', 'ui_key',
459 'repository_id', 'ui_section', 'ui_key',
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
460 name='uq_repo_rhodecode_ui_repository_id_section_key'),
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
461 {'extend_existing': True, 'mysql_engine': 'InnoDB',
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
462 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
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 ui_id = Column(
468 ui_id = Column(
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
469 "ui_id", Integer(), nullable=False, unique=True, default=None,
470 primary_key=True)
470 primary_key=True)
471 ui_section = Column(
471 ui_section = Column(
472 "ui_section", String(255), nullable=True, unique=None, default=None)
472 "ui_section", String(255), nullable=True, unique=None, default=None)
473 ui_key = Column(
473 ui_key = Column(
474 "ui_key", String(255), nullable=True, unique=None, default=None)
474 "ui_key", String(255), nullable=True, unique=None, default=None)
475 ui_value = Column(
475 ui_value = Column(
476 "ui_value", String(255), nullable=True, unique=None, default=None)
476 "ui_value", String(255), nullable=True, unique=None, default=None)
477 ui_active = Column(
477 ui_active = Column(
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
478 "ui_active", Boolean(), nullable=True, unique=None, default=True)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __repr__(self):
482 def __repr__(self):
483 return '<%s[%s:%s]%s=>%s]>' % (
483 return '<%s[%s:%s]%s=>%s]>' % (
484 self.__class__.__name__, self.repository.repo_name,
484 self.__class__.__name__, self.repository.repo_name,
485 self.ui_section, self.ui_key, self.ui_value)
485 self.ui_section, self.ui_key, self.ui_value)
486
486
487
487
488 class User(Base, BaseModel):
488 class User(Base, BaseModel):
489 __tablename__ = 'users'
489 __tablename__ = 'users'
490 __table_args__ = (
490 __table_args__ = (
491 UniqueConstraint('username'), UniqueConstraint('email'),
491 UniqueConstraint('username'), UniqueConstraint('email'),
492 Index('u_username_idx', 'username'),
492 Index('u_username_idx', 'username'),
493 Index('u_email_idx', 'email'),
493 Index('u_email_idx', 'email'),
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 {'extend_existing': True, 'mysql_engine': 'InnoDB',
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
495 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
496 )
496 )
497 DEFAULT_USER = 'default'
497 DEFAULT_USER = 'default'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
498 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
499 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
500
500
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
501 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
502 username = Column("username", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
503 password = Column("password", String(255), nullable=True, unique=None, default=None)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
504 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
505 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
506 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
507 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
508 _email = Column("email", String(255), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
509 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
510 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
511 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
512 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
513 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
515 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
516
516
517 user_log = relationship('UserLog')
517 user_log = relationship('UserLog')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
518 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
519
519
520 repositories = relationship('Repository')
520 repositories = relationship('Repository')
521 repository_groups = relationship('RepoGroup')
521 repository_groups = relationship('RepoGroup')
522 user_groups = relationship('UserGroup')
522 user_groups = relationship('UserGroup')
523
523
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
524 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
525 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
526
526
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
527 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
528 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
529 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
530
530
531 group_member = relationship('UserGroupMember', cascade='all')
531 group_member = relationship('UserGroupMember', cascade='all')
532
532
533 notifications = relationship('UserNotification', cascade='all')
533 notifications = relationship('UserNotification', cascade='all')
534 # notifications assigned to this user
534 # notifications assigned to this user
535 user_created_notifications = relationship('Notification', cascade='all')
535 user_created_notifications = relationship('Notification', cascade='all')
536 # comments created by this user
536 # comments created by this user
537 user_comments = relationship('ChangesetComment', cascade='all')
537 user_comments = relationship('ChangesetComment', cascade='all')
538 # user profile extra info
538 # user profile extra info
539 user_emails = relationship('UserEmailMap', cascade='all')
539 user_emails = relationship('UserEmailMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
540 user_ip_map = relationship('UserIpMap', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
541 user_auth_tokens = relationship('UserApiKeys', cascade='all')
542 # gists
542 # gists
543 user_gists = relationship('Gist', cascade='all')
543 user_gists = relationship('Gist', cascade='all')
544 # user pull requests
544 # user pull requests
545 user_pull_requests = relationship('PullRequest', cascade='all')
545 user_pull_requests = relationship('PullRequest', cascade='all')
546 # external identities
546 # external identities
547 extenal_identities = relationship(
547 extenal_identities = relationship(
548 'ExternalIdentity',
548 'ExternalIdentity',
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
549 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
550 cascade='all')
550 cascade='all')
551
551
552 def __unicode__(self):
552 def __unicode__(self):
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
553 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
554 self.user_id, self.username)
554 self.user_id, self.username)
555
555
556 @hybrid_property
556 @hybrid_property
557 def email(self):
557 def email(self):
558 return self._email
558 return self._email
559
559
560 @email.setter
560 @email.setter
561 def email(self, val):
561 def email(self, val):
562 self._email = val.lower() if val else None
562 self._email = val.lower() if val else None
563
563
564 @property
564 @property
565 def firstname(self):
565 def firstname(self):
566 # alias for future
566 # alias for future
567 return self.name
567 return self.name
568
568
569 @property
569 @property
570 def emails(self):
570 def emails(self):
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
571 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
572 return [self.email] + [x.email for x in other]
572 return [self.email] + [x.email for x in other]
573
573
574 @property
574 @property
575 def auth_tokens(self):
575 def auth_tokens(self):
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
576 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
577
577
578 @property
578 @property
579 def extra_auth_tokens(self):
579 def extra_auth_tokens(self):
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
580 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
581
581
582 @property
582 @property
583 def feed_token(self):
583 def feed_token(self):
584 feed_tokens = UserApiKeys.query()\
584 feed_tokens = UserApiKeys.query()\
585 .filter(UserApiKeys.user == self)\
585 .filter(UserApiKeys.user == self)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
586 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
587 .all()
587 .all()
588 if feed_tokens:
588 if feed_tokens:
589 return feed_tokens[0].api_key
589 return feed_tokens[0].api_key
590 else:
590 else:
591 # use the main token so we don't end up with nothing...
591 # use the main token so we don't end up with nothing...
592 return self.api_key
592 return self.api_key
593
593
594 @classmethod
594 @classmethod
595 def extra_valid_auth_tokens(cls, user, role=None):
595 def extra_valid_auth_tokens(cls, user, role=None):
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
596 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
597 .filter(or_(UserApiKeys.expires == -1,
597 .filter(or_(UserApiKeys.expires == -1,
598 UserApiKeys.expires >= time.time()))
598 UserApiKeys.expires >= time.time()))
599 if role:
599 if role:
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
600 tokens = tokens.filter(or_(UserApiKeys.role == role,
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
601 UserApiKeys.role == UserApiKeys.ROLE_ALL))
602 return tokens.all()
602 return tokens.all()
603
603
604 @property
604 @property
605 def builtin_token_roles(self):
605 def builtin_token_roles(self):
606 return map(UserApiKeys._get_role_name, [
606 return map(UserApiKeys._get_role_name, [
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
607 UserApiKeys.ROLE_API, UserApiKeys.ROLE_FEED, UserApiKeys.ROLE_HTTP
608 ])
608 ])
609
609
610 @property
610 @property
611 def ip_addresses(self):
611 def ip_addresses(self):
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
613 return [x.ip_addr for x in ret]
613 return [x.ip_addr for x in ret]
614
614
615 @property
615 @property
616 def username_and_name(self):
616 def username_and_name(self):
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
618
618
619 @property
619 @property
620 def username_or_name_or_email(self):
620 def username_or_name_or_email(self):
621 full_name = self.full_name if self.full_name is not ' ' else None
621 full_name = self.full_name if self.full_name is not ' ' else None
622 return self.username or full_name or self.email
622 return self.username or full_name or self.email
623
623
624 @property
624 @property
625 def full_name(self):
625 def full_name(self):
626 return '%s %s' % (self.firstname, self.lastname)
626 return '%s %s' % (self.firstname, self.lastname)
627
627
628 @property
628 @property
629 def full_name_or_username(self):
629 def full_name_or_username(self):
630 return ('%s %s' % (self.firstname, self.lastname)
630 return ('%s %s' % (self.firstname, self.lastname)
631 if (self.firstname and self.lastname) else self.username)
631 if (self.firstname and self.lastname) else self.username)
632
632
633 @property
633 @property
634 def full_contact(self):
634 def full_contact(self):
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
636
636
637 @property
637 @property
638 def short_contact(self):
638 def short_contact(self):
639 return '%s %s' % (self.firstname, self.lastname)
639 return '%s %s' % (self.firstname, self.lastname)
640
640
641 @property
641 @property
642 def is_admin(self):
642 def is_admin(self):
643 return self.admin
643 return self.admin
644
644
645 @property
645 @property
646 def AuthUser(self):
646 def AuthUser(self):
647 """
647 """
648 Returns instance of AuthUser for this user
648 Returns instance of AuthUser for this user
649 """
649 """
650 from rhodecode.lib.auth import AuthUser
650 from rhodecode.lib.auth import AuthUser
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 return AuthUser(user_id=self.user_id, api_key=self.api_key,
652 username=self.username)
652 username=self.username)
653
653
654 @hybrid_property
654 @hybrid_property
655 def user_data(self):
655 def user_data(self):
656 if not self._user_data:
656 if not self._user_data:
657 return {}
657 return {}
658
658
659 try:
659 try:
660 return json.loads(self._user_data)
660 return json.loads(self._user_data)
661 except TypeError:
661 except TypeError:
662 return {}
662 return {}
663
663
664 @user_data.setter
664 @user_data.setter
665 def user_data(self, val):
665 def user_data(self, val):
666 if not isinstance(val, dict):
666 if not isinstance(val, dict):
667 raise Exception('user_data must be dict, got %s' % type(val))
667 raise Exception('user_data must be dict, got %s' % type(val))
668 try:
668 try:
669 self._user_data = json.dumps(val)
669 self._user_data = json.dumps(val)
670 except Exception:
670 except Exception:
671 log.error(traceback.format_exc())
671 log.error(traceback.format_exc())
672
672
673 @classmethod
673 @classmethod
674 def get_by_username(cls, username, case_insensitive=False,
674 def get_by_username(cls, username, case_insensitive=False,
675 cache=False, identity_cache=False):
675 cache=False, identity_cache=False):
676 session = Session()
676 session = Session()
677
677
678 if case_insensitive:
678 if case_insensitive:
679 q = cls.query().filter(
679 q = cls.query().filter(
680 func.lower(cls.username) == func.lower(username))
680 func.lower(cls.username) == func.lower(username))
681 else:
681 else:
682 q = cls.query().filter(cls.username == username)
682 q = cls.query().filter(cls.username == username)
683
683
684 if cache:
684 if cache:
685 if identity_cache:
685 if identity_cache:
686 val = cls.identity_cache(session, 'username', username)
686 val = cls.identity_cache(session, 'username', username)
687 if val:
687 if val:
688 return val
688 return val
689 else:
689 else:
690 q = q.options(
690 q = q.options(
691 FromCache("sql_cache_short",
691 FromCache("sql_cache_short",
692 "get_user_by_name_%s" % _hash_key(username)))
692 "get_user_by_name_%s" % _hash_key(username)))
693
693
694 return q.scalar()
694 return q.scalar()
695
695
696 @classmethod
696 @classmethod
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
698 q = cls.query().filter(cls.api_key == auth_token)
698 q = cls.query().filter(cls.api_key == auth_token)
699
699
700 if cache:
700 if cache:
701 q = q.options(FromCache("sql_cache_short",
701 q = q.options(FromCache("sql_cache_short",
702 "get_auth_token_%s" % auth_token))
702 "get_auth_token_%s" % auth_token))
703 res = q.scalar()
703 res = q.scalar()
704
704
705 if fallback and not res:
705 if fallback and not res:
706 #fallback to additional keys
706 #fallback to additional keys
707 _res = UserApiKeys.query()\
707 _res = UserApiKeys.query()\
708 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(UserApiKeys.api_key == auth_token)\
709 .filter(or_(UserApiKeys.expires == -1,
709 .filter(or_(UserApiKeys.expires == -1,
710 UserApiKeys.expires >= time.time()))\
710 UserApiKeys.expires >= time.time()))\
711 .first()
711 .first()
712 if _res:
712 if _res:
713 res = _res.user
713 res = _res.user
714 return res
714 return res
715
715
716 @classmethod
716 @classmethod
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
717 def get_by_email(cls, email, case_insensitive=False, cache=False):
718
718
719 if case_insensitive:
719 if case_insensitive:
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
721
721
722 else:
722 else:
723 q = cls.query().filter(cls.email == email)
723 q = cls.query().filter(cls.email == email)
724
724
725 if cache:
725 if cache:
726 q = q.options(FromCache("sql_cache_short",
726 q = q.options(FromCache("sql_cache_short",
727 "get_email_key_%s" % _hash_key(email)))
727 "get_email_key_%s" % _hash_key(email)))
728
728
729 ret = q.scalar()
729 ret = q.scalar()
730 if ret is None:
730 if ret is None:
731 q = UserEmailMap.query()
731 q = UserEmailMap.query()
732 # try fetching in alternate email map
732 # try fetching in alternate email map
733 if case_insensitive:
733 if case_insensitive:
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
735 else:
735 else:
736 q = q.filter(UserEmailMap.email == email)
736 q = q.filter(UserEmailMap.email == email)
737 q = q.options(joinedload(UserEmailMap.user))
737 q = q.options(joinedload(UserEmailMap.user))
738 if cache:
738 if cache:
739 q = q.options(FromCache("sql_cache_short",
739 q = q.options(FromCache("sql_cache_short",
740 "get_email_map_key_%s" % email))
740 "get_email_map_key_%s" % email))
741 ret = getattr(q.scalar(), 'user', None)
741 ret = getattr(q.scalar(), 'user', None)
742
742
743 return ret
743 return ret
744
744
745 @classmethod
745 @classmethod
746 def get_from_cs_author(cls, author):
746 def get_from_cs_author(cls, author):
747 """
747 """
748 Tries to get User objects out of commit author string
748 Tries to get User objects out of commit author string
749
749
750 :param author:
750 :param author:
751 """
751 """
752 from rhodecode.lib.helpers import email, author_name
752 from rhodecode.lib.helpers import email, author_name
753 # Valid email in the attribute passed, see if they're in the system
753 # Valid email in the attribute passed, see if they're in the system
754 _email = email(author)
754 _email = email(author)
755 if _email:
755 if _email:
756 user = cls.get_by_email(_email, case_insensitive=True)
756 user = cls.get_by_email(_email, case_insensitive=True)
757 if user:
757 if user:
758 return user
758 return user
759 # Maybe we can match by username?
759 # Maybe we can match by username?
760 _author = author_name(author)
760 _author = author_name(author)
761 user = cls.get_by_username(_author, case_insensitive=True)
761 user = cls.get_by_username(_author, case_insensitive=True)
762 if user:
762 if user:
763 return user
763 return user
764
764
765 def update_userdata(self, **kwargs):
765 def update_userdata(self, **kwargs):
766 usr = self
766 usr = self
767 old = usr.user_data
767 old = usr.user_data
768 old.update(**kwargs)
768 old.update(**kwargs)
769 usr.user_data = old
769 usr.user_data = old
770 Session().add(usr)
770 Session().add(usr)
771 log.debug('updated userdata with ', kwargs)
771 log.debug('updated userdata with ', kwargs)
772
772
773 def update_lastlogin(self):
773 def update_lastlogin(self):
774 """Update user lastlogin"""
774 """Update user lastlogin"""
775 self.last_login = datetime.datetime.now()
775 self.last_login = datetime.datetime.now()
776 Session().add(self)
776 Session().add(self)
777 log.debug('updated user %s lastlogin', self.username)
777 log.debug('updated user %s lastlogin', self.username)
778
778
779 def update_lastactivity(self):
779 def update_lastactivity(self):
780 """Update user lastactivity"""
780 """Update user lastactivity"""
781 usr = self
781 usr = self
782 old = usr.user_data
782 old = usr.user_data
783 old.update({'last_activity': time.time()})
783 old.update({'last_activity': time.time()})
784 usr.user_data = old
784 usr.user_data = old
785 Session().add(usr)
785 Session().add(usr)
786 log.debug('updated user %s lastactivity', usr.username)
786 log.debug('updated user %s lastactivity', usr.username)
787
787
788 def update_password(self, new_password, change_api_key=False):
788 def update_password(self, new_password, change_api_key=False):
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
790
790
791 self.password = get_crypt_password(new_password)
791 self.password = get_crypt_password(new_password)
792 if change_api_key:
792 if change_api_key:
793 self.api_key = generate_auth_token(self.username)
793 self.api_key = generate_auth_token(self.username)
794 Session().add(self)
794 Session().add(self)
795
795
796 @classmethod
796 @classmethod
797 def get_first_super_admin(cls):
797 def get_first_super_admin(cls):
798 user = User.query().filter(User.admin == true()).first()
798 user = User.query().filter(User.admin == true()).first()
799 if user is None:
799 if user is None:
800 raise Exception('FATAL: Missing administrative account!')
800 raise Exception('FATAL: Missing administrative account!')
801 return user
801 return user
802
802
803 @classmethod
803 @classmethod
804 def get_all_super_admins(cls):
804 def get_all_super_admins(cls):
805 """
805 """
806 Returns all admin accounts sorted by username
806 Returns all admin accounts sorted by username
807 """
807 """
808 return User.query().filter(User.admin == true())\
808 return User.query().filter(User.admin == true())\
809 .order_by(User.username.asc()).all()
809 .order_by(User.username.asc()).all()
810
810
811 @classmethod
811 @classmethod
812 def get_default_user(cls, cache=False):
812 def get_default_user(cls, cache=False):
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
814 if user is None:
814 if user is None:
815 raise Exception('FATAL: Missing default account!')
815 raise Exception('FATAL: Missing default account!')
816 return user
816 return user
817
817
818 def _get_default_perms(self, user, suffix=''):
818 def _get_default_perms(self, user, suffix=''):
819 from rhodecode.model.permission import PermissionModel
819 from rhodecode.model.permission import PermissionModel
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
820 return PermissionModel().get_default_perms(user.user_perms, suffix)
821
821
822 def get_default_perms(self, suffix=''):
822 def get_default_perms(self, suffix=''):
823 return self._get_default_perms(self, suffix)
823 return self._get_default_perms(self, suffix)
824
824
825 def get_api_data(self, include_secrets=False, details='full'):
825 def get_api_data(self, include_secrets=False, details='full'):
826 """
826 """
827 Common function for generating user related data for API
827 Common function for generating user related data for API
828
828
829 :param include_secrets: By default secrets in the API data will be replaced
829 :param include_secrets: By default secrets in the API data will be replaced
830 by a placeholder value to prevent exposing this data by accident. In case
830 by a placeholder value to prevent exposing this data by accident. In case
831 this data shall be exposed, set this flag to ``True``.
831 this data shall be exposed, set this flag to ``True``.
832
832
833 :param details: details can be 'basic|full' basic gives only a subset of
833 :param details: details can be 'basic|full' basic gives only a subset of
834 the available user information that includes user_id, name and emails.
834 the available user information that includes user_id, name and emails.
835 """
835 """
836 user = self
836 user = self
837 user_data = self.user_data
837 user_data = self.user_data
838 data = {
838 data = {
839 'user_id': user.user_id,
839 'user_id': user.user_id,
840 'username': user.username,
840 'username': user.username,
841 'firstname': user.name,
841 'firstname': user.name,
842 'lastname': user.lastname,
842 'lastname': user.lastname,
843 'email': user.email,
843 'email': user.email,
844 'emails': user.emails,
844 'emails': user.emails,
845 }
845 }
846 if details == 'basic':
846 if details == 'basic':
847 return data
847 return data
848
848
849 api_key_length = 40
849 api_key_length = 40
850 api_key_replacement = '*' * api_key_length
850 api_key_replacement = '*' * api_key_length
851
851
852 extras = {
852 extras = {
853 'api_key': api_key_replacement,
853 'api_key': api_key_replacement,
854 'api_keys': [api_key_replacement],
854 'api_keys': [api_key_replacement],
855 'active': user.active,
855 'active': user.active,
856 'admin': user.admin,
856 'admin': user.admin,
857 'extern_type': user.extern_type,
857 'extern_type': user.extern_type,
858 'extern_name': user.extern_name,
858 'extern_name': user.extern_name,
859 'last_login': user.last_login,
859 'last_login': user.last_login,
860 'ip_addresses': user.ip_addresses,
860 'ip_addresses': user.ip_addresses,
861 'language': user_data.get('language')
861 'language': user_data.get('language')
862 }
862 }
863 data.update(extras)
863 data.update(extras)
864
864
865 if include_secrets:
865 if include_secrets:
866 data['api_key'] = user.api_key
866 data['api_key'] = user.api_key
867 data['api_keys'] = user.auth_tokens
867 data['api_keys'] = user.auth_tokens
868 return data
868 return data
869
869
870 def __json__(self):
870 def __json__(self):
871 data = {
871 data = {
872 'full_name': self.full_name,
872 'full_name': self.full_name,
873 'full_name_or_username': self.full_name_or_username,
873 'full_name_or_username': self.full_name_or_username,
874 'short_contact': self.short_contact,
874 'short_contact': self.short_contact,
875 'full_contact': self.full_contact,
875 'full_contact': self.full_contact,
876 }
876 }
877 data.update(self.get_api_data())
877 data.update(self.get_api_data())
878 return data
878 return data
879
879
880
880
881 class UserApiKeys(Base, BaseModel):
881 class UserApiKeys(Base, BaseModel):
882 __tablename__ = 'user_api_keys'
882 __tablename__ = 'user_api_keys'
883 __table_args__ = (
883 __table_args__ = (
884 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_idx', 'api_key'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
886 UniqueConstraint('api_key'),
886 UniqueConstraint('api_key'),
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 {'extend_existing': True, 'mysql_engine': 'InnoDB',
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
889 )
889 )
890 __mapper_args__ = {}
890 __mapper_args__ = {}
891
891
892 # ApiKey role
892 # ApiKey role
893 ROLE_ALL = 'token_role_all'
893 ROLE_ALL = 'token_role_all'
894 ROLE_HTTP = 'token_role_http'
894 ROLE_HTTP = 'token_role_http'
895 ROLE_VCS = 'token_role_vcs'
895 ROLE_VCS = 'token_role_vcs'
896 ROLE_API = 'token_role_api'
896 ROLE_API = 'token_role_api'
897 ROLE_FEED = 'token_role_feed'
897 ROLE_FEED = 'token_role_feed'
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
899
899
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 api_key = Column("api_key", String(255), nullable=False, unique=True)
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
904 expires = Column('expires', Float(53), nullable=False)
904 expires = Column('expires', Float(53), nullable=False)
905 role = Column('role', String(255), nullable=True)
905 role = Column('role', String(255), nullable=True)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
907
907
908 user = relationship('User', lazy='joined')
908 user = relationship('User', lazy='joined')
909
909
910 @classmethod
910 @classmethod
911 def _get_role_name(cls, role):
911 def _get_role_name(cls, role):
912 return {
912 return {
913 cls.ROLE_ALL: _('all'),
913 cls.ROLE_ALL: _('all'),
914 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_HTTP: _('http/web interface'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
916 cls.ROLE_API: _('api calls'),
916 cls.ROLE_API: _('api calls'),
917 cls.ROLE_FEED: _('feed access'),
917 cls.ROLE_FEED: _('feed access'),
918 }.get(role, role)
918 }.get(role, role)
919
919
920 @property
920 @property
921 def expired(self):
921 def expired(self):
922 if self.expires == -1:
922 if self.expires == -1:
923 return False
923 return False
924 return time.time() > self.expires
924 return time.time() > self.expires
925
925
926 @property
926 @property
927 def role_humanized(self):
927 def role_humanized(self):
928 return self._get_role_name(self.role)
928 return self._get_role_name(self.role)
929
929
930
930
931 class UserEmailMap(Base, BaseModel):
931 class UserEmailMap(Base, BaseModel):
932 __tablename__ = 'user_email_map'
932 __tablename__ = 'user_email_map'
933 __table_args__ = (
933 __table_args__ = (
934 Index('uem_email_idx', 'email'),
934 Index('uem_email_idx', 'email'),
935 UniqueConstraint('email'),
935 UniqueConstraint('email'),
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 {'extend_existing': True, 'mysql_engine': 'InnoDB',
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
938 )
938 )
939 __mapper_args__ = {}
939 __mapper_args__ = {}
940
940
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 _email = Column("email", String(255), nullable=True, unique=False, default=None)
944 user = relationship('User', lazy='joined')
944 user = relationship('User', lazy='joined')
945
945
946 @validates('_email')
946 @validates('_email')
947 def validate_email(self, key, email):
947 def validate_email(self, key, email):
948 # check if this email is not main one
948 # check if this email is not main one
949 main_email = Session().query(User).filter(User.email == email).scalar()
949 main_email = Session().query(User).filter(User.email == email).scalar()
950 if main_email is not None:
950 if main_email is not None:
951 raise AttributeError('email %s is present is user table' % email)
951 raise AttributeError('email %s is present is user table' % email)
952 return email
952 return email
953
953
954 @hybrid_property
954 @hybrid_property
955 def email(self):
955 def email(self):
956 return self._email
956 return self._email
957
957
958 @email.setter
958 @email.setter
959 def email(self, val):
959 def email(self, val):
960 self._email = val.lower() if val else None
960 self._email = val.lower() if val else None
961
961
962
962
963 class UserIpMap(Base, BaseModel):
963 class UserIpMap(Base, BaseModel):
964 __tablename__ = 'user_ip_map'
964 __tablename__ = 'user_ip_map'
965 __table_args__ = (
965 __table_args__ = (
966 UniqueConstraint('user_id', 'ip_addr'),
966 UniqueConstraint('user_id', 'ip_addr'),
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 {'extend_existing': True, 'mysql_engine': 'InnoDB',
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
969 )
969 )
970 __mapper_args__ = {}
970 __mapper_args__ = {}
971
971
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 description = Column("description", String(10000), nullable=True, unique=None, default=None)
977 user = relationship('User', lazy='joined')
977 user = relationship('User', lazy='joined')
978
978
979 @classmethod
979 @classmethod
980 def _get_ip_range(cls, ip_addr):
980 def _get_ip_range(cls, ip_addr):
981 net = ipaddress.ip_network(ip_addr, strict=False)
981 net = ipaddress.ip_network(ip_addr, strict=False)
982 return [str(net.network_address), str(net.broadcast_address)]
982 return [str(net.network_address), str(net.broadcast_address)]
983
983
984 def __json__(self):
984 def __json__(self):
985 return {
985 return {
986 'ip_addr': self.ip_addr,
986 'ip_addr': self.ip_addr,
987 'ip_range': self._get_ip_range(self.ip_addr),
987 'ip_range': self._get_ip_range(self.ip_addr),
988 }
988 }
989
989
990 def __unicode__(self):
990 def __unicode__(self):
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
992 self.user_id, self.ip_addr)
992 self.user_id, self.ip_addr)
993
993
994 class UserLog(Base, BaseModel):
994 class UserLog(Base, BaseModel):
995 __tablename__ = 'user_logs'
995 __tablename__ = 'user_logs'
996 __table_args__ = (
996 __table_args__ = (
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 {'extend_existing': True, 'mysql_engine': 'InnoDB',
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
999 )
999 )
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 username = Column("username", String(255), nullable=True, unique=None, default=None)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1008
1008
1009 def __unicode__(self):
1009 def __unicode__(self):
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1011 self.repository_name,
1011 self.repository_name,
1012 self.action)
1012 self.action)
1013
1013
1014 @property
1014 @property
1015 def action_as_day(self):
1015 def action_as_day(self):
1016 return datetime.date(*self.action_date.timetuple()[:3])
1016 return datetime.date(*self.action_date.timetuple()[:3])
1017
1017
1018 user = relationship('User')
1018 user = relationship('User')
1019 repository = relationship('Repository', cascade='')
1019 repository = relationship('Repository', cascade='')
1020
1020
1021
1021
1022 class UserGroup(Base, BaseModel):
1022 class UserGroup(Base, BaseModel):
1023 __tablename__ = 'users_groups'
1023 __tablename__ = 'users_groups'
1024 __table_args__ = (
1024 __table_args__ = (
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1027 )
1027 )
1028
1028
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1037
1037
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1044
1044
1045 user = relationship('User')
1045 user = relationship('User')
1046
1046
1047 @hybrid_property
1047 @hybrid_property
1048 def group_data(self):
1048 def group_data(self):
1049 if not self._group_data:
1049 if not self._group_data:
1050 return {}
1050 return {}
1051
1051
1052 try:
1052 try:
1053 return json.loads(self._group_data)
1053 return json.loads(self._group_data)
1054 except TypeError:
1054 except TypeError:
1055 return {}
1055 return {}
1056
1056
1057 @group_data.setter
1057 @group_data.setter
1058 def group_data(self, val):
1058 def group_data(self, val):
1059 try:
1059 try:
1060 self._group_data = json.dumps(val)
1060 self._group_data = json.dumps(val)
1061 except Exception:
1061 except Exception:
1062 log.error(traceback.format_exc())
1062 log.error(traceback.format_exc())
1063
1063
1064 def __unicode__(self):
1064 def __unicode__(self):
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1066 self.users_group_id,
1066 self.users_group_id,
1067 self.users_group_name)
1067 self.users_group_name)
1068
1068
1069 @classmethod
1069 @classmethod
1070 def get_by_group_name(cls, group_name, cache=False,
1070 def get_by_group_name(cls, group_name, cache=False,
1071 case_insensitive=False):
1071 case_insensitive=False):
1072 if case_insensitive:
1072 if case_insensitive:
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 q = cls.query().filter(func.lower(cls.users_group_name) ==
1074 func.lower(group_name))
1074 func.lower(group_name))
1075
1075
1076 else:
1076 else:
1077 q = cls.query().filter(cls.users_group_name == group_name)
1077 q = cls.query().filter(cls.users_group_name == group_name)
1078 if cache:
1078 if cache:
1079 q = q.options(FromCache(
1079 q = q.options(FromCache(
1080 "sql_cache_short",
1080 "sql_cache_short",
1081 "get_group_%s" % _hash_key(group_name)))
1081 "get_group_%s" % _hash_key(group_name)))
1082 return q.scalar()
1082 return q.scalar()
1083
1083
1084 @classmethod
1084 @classmethod
1085 def get(cls, user_group_id, cache=False):
1085 def get(cls, user_group_id, cache=False):
1086 user_group = cls.query()
1086 user_group = cls.query()
1087 if cache:
1087 if cache:
1088 user_group = user_group.options(FromCache("sql_cache_short",
1088 user_group = user_group.options(FromCache("sql_cache_short",
1089 "get_users_group_%s" % user_group_id))
1089 "get_users_group_%s" % user_group_id))
1090 return user_group.get(user_group_id)
1090 return user_group.get(user_group_id)
1091
1091
1092 def permissions(self, with_admins=True, with_owner=True):
1092 def permissions(self, with_admins=True, with_owner=True):
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1095 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.user),
1096 joinedload(UserUserGroupToPerm.permission),)
1096 joinedload(UserUserGroupToPerm.permission),)
1097
1097
1098 # get owners and admins and permissions. We do a trick of re-writing
1098 # get owners and admins and permissions. We do a trick of re-writing
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1100 # has a global reference and changing one object propagates to all
1100 # has a global reference and changing one object propagates to all
1101 # others. This means if admin is also an owner admin_row that change
1101 # others. This means if admin is also an owner admin_row that change
1102 # would propagate to both objects
1102 # would propagate to both objects
1103 perm_rows = []
1103 perm_rows = []
1104 for _usr in q.all():
1104 for _usr in q.all():
1105 usr = AttributeDict(_usr.user.get_dict())
1105 usr = AttributeDict(_usr.user.get_dict())
1106 usr.permission = _usr.permission.permission_name
1106 usr.permission = _usr.permission.permission_name
1107 perm_rows.append(usr)
1107 perm_rows.append(usr)
1108
1108
1109 # filter the perm rows by 'default' first and then sort them by
1109 # filter the perm rows by 'default' first and then sort them by
1110 # admin,write,read,none permissions sorted again alphabetically in
1110 # admin,write,read,none permissions sorted again alphabetically in
1111 # each group
1111 # each group
1112 perm_rows = sorted(perm_rows, key=display_sort)
1112 perm_rows = sorted(perm_rows, key=display_sort)
1113
1113
1114 _admin_perm = 'usergroup.admin'
1114 _admin_perm = 'usergroup.admin'
1115 owner_row = []
1115 owner_row = []
1116 if with_owner:
1116 if with_owner:
1117 usr = AttributeDict(self.user.get_dict())
1117 usr = AttributeDict(self.user.get_dict())
1118 usr.owner_row = True
1118 usr.owner_row = True
1119 usr.permission = _admin_perm
1119 usr.permission = _admin_perm
1120 owner_row.append(usr)
1120 owner_row.append(usr)
1121
1121
1122 super_admin_rows = []
1122 super_admin_rows = []
1123 if with_admins:
1123 if with_admins:
1124 for usr in User.get_all_super_admins():
1124 for usr in User.get_all_super_admins():
1125 # if this admin is also owner, don't double the record
1125 # if this admin is also owner, don't double the record
1126 if usr.user_id == owner_row[0].user_id:
1126 if usr.user_id == owner_row[0].user_id:
1127 owner_row[0].admin_row = True
1127 owner_row[0].admin_row = True
1128 else:
1128 else:
1129 usr = AttributeDict(usr.get_dict())
1129 usr = AttributeDict(usr.get_dict())
1130 usr.admin_row = True
1130 usr.admin_row = True
1131 usr.permission = _admin_perm
1131 usr.permission = _admin_perm
1132 super_admin_rows.append(usr)
1132 super_admin_rows.append(usr)
1133
1133
1134 return super_admin_rows + owner_row + perm_rows
1134 return super_admin_rows + owner_row + perm_rows
1135
1135
1136 def permission_user_groups(self):
1136 def permission_user_groups(self):
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.target_user_group),
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1140 joinedload(UserGroupUserGroupToPerm.permission),)
1141
1141
1142 perm_rows = []
1142 perm_rows = []
1143 for _user_group in q.all():
1143 for _user_group in q.all():
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr = AttributeDict(_user_group.user_group.get_dict())
1145 usr.permission = _user_group.permission.permission_name
1145 usr.permission = _user_group.permission.permission_name
1146 perm_rows.append(usr)
1146 perm_rows.append(usr)
1147
1147
1148 return perm_rows
1148 return perm_rows
1149
1149
1150 def _get_default_perms(self, user_group, suffix=''):
1150 def _get_default_perms(self, user_group, suffix=''):
1151 from rhodecode.model.permission import PermissionModel
1151 from rhodecode.model.permission import PermissionModel
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1153
1153
1154 def get_default_perms(self, suffix=''):
1154 def get_default_perms(self, suffix=''):
1155 return self._get_default_perms(self, suffix)
1155 return self._get_default_perms(self, suffix)
1156
1156
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 def get_api_data(self, with_group_members=True, include_secrets=False):
1158 """
1158 """
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1160 basically forwarded.
1160 basically forwarded.
1161
1161
1162 """
1162 """
1163 user_group = self
1163 user_group = self
1164
1164
1165 data = {
1165 data = {
1166 'users_group_id': user_group.users_group_id,
1166 'users_group_id': user_group.users_group_id,
1167 'group_name': user_group.users_group_name,
1167 'group_name': user_group.users_group_name,
1168 'group_description': user_group.user_group_description,
1168 'group_description': user_group.user_group_description,
1169 'active': user_group.users_group_active,
1169 'active': user_group.users_group_active,
1170 'owner': user_group.user.username,
1170 'owner': user_group.user.username,
1171 }
1171 }
1172 if with_group_members:
1172 if with_group_members:
1173 users = []
1173 users = []
1174 for user in user_group.members:
1174 for user in user_group.members:
1175 user = user.user
1175 user = user.user
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1176 users.append(user.get_api_data(include_secrets=include_secrets))
1177 data['users'] = users
1177 data['users'] = users
1178
1178
1179 return data
1179 return data
1180
1180
1181
1181
1182 class UserGroupMember(Base, BaseModel):
1182 class UserGroupMember(Base, BaseModel):
1183 __tablename__ = 'users_groups_members'
1183 __tablename__ = 'users_groups_members'
1184 __table_args__ = (
1184 __table_args__ = (
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1187 )
1187 )
1188
1188
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1192
1192
1193 user = relationship('User', lazy='joined')
1193 user = relationship('User', lazy='joined')
1194 users_group = relationship('UserGroup')
1194 users_group = relationship('UserGroup')
1195
1195
1196 def __init__(self, gr_id='', u_id=''):
1196 def __init__(self, gr_id='', u_id=''):
1197 self.users_group_id = gr_id
1197 self.users_group_id = gr_id
1198 self.user_id = u_id
1198 self.user_id = u_id
1199
1199
1200
1200
1201 class RepositoryField(Base, BaseModel):
1201 class RepositoryField(Base, BaseModel):
1202 __tablename__ = 'repositories_fields'
1202 __tablename__ = 'repositories_fields'
1203 __table_args__ = (
1203 __table_args__ = (
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1207 )
1207 )
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1209
1209
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1212 field_key = Column("field_key", String(250))
1212 field_key = Column("field_key", String(250))
1213 field_label = Column("field_label", String(1024), nullable=False)
1213 field_label = Column("field_label", String(1024), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1214 field_value = Column("field_value", String(10000), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_desc = Column("field_desc", String(1024), nullable=False)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 field_type = Column("field_type", String(255), nullable=False, unique=None)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1218
1218
1219 repository = relationship('Repository')
1219 repository = relationship('Repository')
1220
1220
1221 @property
1221 @property
1222 def field_key_prefixed(self):
1222 def field_key_prefixed(self):
1223 return 'ex_%s' % self.field_key
1223 return 'ex_%s' % self.field_key
1224
1224
1225 @classmethod
1225 @classmethod
1226 def un_prefix_key(cls, key):
1226 def un_prefix_key(cls, key):
1227 if key.startswith(cls.PREFIX):
1227 if key.startswith(cls.PREFIX):
1228 return key[len(cls.PREFIX):]
1228 return key[len(cls.PREFIX):]
1229 return key
1229 return key
1230
1230
1231 @classmethod
1231 @classmethod
1232 def get_by_key_name(cls, key, repo):
1232 def get_by_key_name(cls, key, repo):
1233 row = cls.query()\
1233 row = cls.query()\
1234 .filter(cls.repository == repo)\
1234 .filter(cls.repository == repo)\
1235 .filter(cls.field_key == key).scalar()
1235 .filter(cls.field_key == key).scalar()
1236 return row
1236 return row
1237
1237
1238
1238
1239 class Repository(Base, BaseModel):
1239 class Repository(Base, BaseModel):
1240 __tablename__ = 'repositories'
1240 __tablename__ = 'repositories'
1241 __table_args__ = (
1241 __table_args__ = (
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1245 )
1245 )
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1248
1248
1249 STATE_CREATED = 'repo_state_created'
1249 STATE_CREATED = 'repo_state_created'
1250 STATE_PENDING = 'repo_state_pending'
1250 STATE_PENDING = 'repo_state_pending'
1251 STATE_ERROR = 'repo_state_error'
1251 STATE_ERROR = 'repo_state_error'
1252
1252
1253 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_AUTOMATIC = 'lock_auto'
1254 LOCK_API = 'lock_api'
1254 LOCK_API = 'lock_api'
1255 LOCK_WEB = 'lock_web'
1255 LOCK_WEB = 'lock_web'
1256 LOCK_PULL = 'lock_pull'
1256 LOCK_PULL = 'lock_pull'
1257
1257
1258 NAME_SEP = URL_SEP
1258 NAME_SEP = URL_SEP
1259
1259
1260 repo_id = Column(
1260 repo_id = Column(
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 "repo_id", Integer(), nullable=False, unique=True, default=None,
1262 primary_key=True)
1262 primary_key=True)
1263 _repo_name = Column(
1263 _repo_name = Column(
1264 "repo_name", Text(), nullable=False, default=None)
1264 "repo_name", Text(), nullable=False, default=None)
1265 _repo_name_hash = Column(
1265 _repo_name_hash = Column(
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1266 "repo_name_hash", String(255), nullable=False, unique=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1267 repo_state = Column("repo_state", String(255), nullable=True)
1268
1268
1269 clone_uri = Column(
1269 clone_uri = Column(
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1271 default=None)
1271 default=None)
1272 repo_type = Column(
1272 repo_type = Column(
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 "repo_type", String(255), nullable=False, unique=False, default=None)
1274 user_id = Column(
1274 user_id = Column(
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1276 unique=False, default=None)
1276 unique=False, default=None)
1277 private = Column(
1277 private = Column(
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1278 "private", Boolean(), nullable=True, unique=None, default=None)
1279 enable_statistics = Column(
1279 enable_statistics = Column(
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 "statistics", Boolean(), nullable=True, unique=None, default=True)
1281 enable_downloads = Column(
1281 enable_downloads = Column(
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 "downloads", Boolean(), nullable=True, unique=None, default=True)
1283 description = Column(
1283 description = Column(
1284 "description", String(10000), nullable=True, unique=None, default=None)
1284 "description", String(10000), nullable=True, unique=None, default=None)
1285 created_on = Column(
1285 created_on = Column(
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1287 default=datetime.datetime.now)
1287 default=datetime.datetime.now)
1288 updated_on = Column(
1288 updated_on = Column(
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1290 default=datetime.datetime.now)
1290 default=datetime.datetime.now)
1291 _landing_revision = Column(
1291 _landing_revision = Column(
1292 "landing_revision", String(255), nullable=False, unique=False,
1292 "landing_revision", String(255), nullable=False, unique=False,
1293 default=None)
1293 default=None)
1294 enable_locking = Column(
1294 enable_locking = Column(
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1295 "enable_locking", Boolean(), nullable=False, unique=None,
1296 default=False)
1296 default=False)
1297 _locked = Column(
1297 _locked = Column(
1298 "locked", String(255), nullable=True, unique=False, default=None)
1298 "locked", String(255), nullable=True, unique=False, default=None)
1299 _changeset_cache = Column(
1299 _changeset_cache = Column(
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1301
1301
1302 fork_id = Column(
1302 fork_id = Column(
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1304 nullable=True, unique=False, default=None)
1304 nullable=True, unique=False, default=None)
1305 group_id = Column(
1305 group_id = Column(
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1307 unique=False, default=None)
1307 unique=False, default=None)
1308
1308
1309 user = relationship('User', lazy='joined')
1309 user = relationship('User', lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1311 group = relationship('RepoGroup', lazy='joined')
1312 repo_to_perm = relationship(
1312 repo_to_perm = relationship(
1313 'UserRepoToPerm', cascade='all',
1313 'UserRepoToPerm', cascade='all',
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1314 order_by='UserRepoToPerm.repo_to_perm_id')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1316 stats = relationship('Statistics', cascade='all', uselist=False)
1317
1317
1318 followers = relationship(
1318 followers = relationship(
1319 'UserFollowing',
1319 'UserFollowing',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1321 cascade='all')
1321 cascade='all')
1322 extra_fields = relationship(
1322 extra_fields = relationship(
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1323 'RepositoryField', cascade="all, delete, delete-orphan")
1324 logs = relationship('UserLog')
1324 logs = relationship('UserLog')
1325 comments = relationship(
1325 comments = relationship(
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 'ChangesetComment', cascade="all, delete, delete-orphan")
1327 pull_requests_source = relationship(
1327 pull_requests_source = relationship(
1328 'PullRequest',
1328 'PullRequest',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1330 cascade="all, delete, delete-orphan")
1330 cascade="all, delete, delete-orphan")
1331 pull_requests_target = relationship(
1331 pull_requests_target = relationship(
1332 'PullRequest',
1332 'PullRequest',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1334 cascade="all, delete, delete-orphan")
1334 cascade="all, delete, delete-orphan")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 ui = relationship('RepoRhodeCodeUi', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1337 integrations = relationship('Integration',
1337 integrations = relationship('Integration',
1338 cascade="all, delete, delete-orphan")
1338 cascade="all, delete, delete-orphan")
1339
1339
1340 def __unicode__(self):
1340 def __unicode__(self):
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1342 safe_unicode(self.repo_name))
1342 safe_unicode(self.repo_name))
1343
1343
1344 @hybrid_property
1344 @hybrid_property
1345 def landing_rev(self):
1345 def landing_rev(self):
1346 # always should return [rev_type, rev]
1346 # always should return [rev_type, rev]
1347 if self._landing_revision:
1347 if self._landing_revision:
1348 _rev_info = self._landing_revision.split(':')
1348 _rev_info = self._landing_revision.split(':')
1349 if len(_rev_info) < 2:
1349 if len(_rev_info) < 2:
1350 _rev_info.insert(0, 'rev')
1350 _rev_info.insert(0, 'rev')
1351 return [_rev_info[0], _rev_info[1]]
1351 return [_rev_info[0], _rev_info[1]]
1352 return [None, None]
1352 return [None, None]
1353
1353
1354 @landing_rev.setter
1354 @landing_rev.setter
1355 def landing_rev(self, val):
1355 def landing_rev(self, val):
1356 if ':' not in val:
1356 if ':' not in val:
1357 raise ValueError('value must be delimited with `:` and consist '
1357 raise ValueError('value must be delimited with `:` and consist '
1358 'of <rev_type>:<rev>, got %s instead' % val)
1358 'of <rev_type>:<rev>, got %s instead' % val)
1359 self._landing_revision = val
1359 self._landing_revision = val
1360
1360
1361 @hybrid_property
1361 @hybrid_property
1362 def locked(self):
1362 def locked(self):
1363 if self._locked:
1363 if self._locked:
1364 user_id, timelocked, reason = self._locked.split(':')
1364 user_id, timelocked, reason = self._locked.split(':')
1365 lock_values = int(user_id), timelocked, reason
1365 lock_values = int(user_id), timelocked, reason
1366 else:
1366 else:
1367 lock_values = [None, None, None]
1367 lock_values = [None, None, None]
1368 return lock_values
1368 return lock_values
1369
1369
1370 @locked.setter
1370 @locked.setter
1371 def locked(self, val):
1371 def locked(self, val):
1372 if val and isinstance(val, (list, tuple)):
1372 if val and isinstance(val, (list, tuple)):
1373 self._locked = ':'.join(map(str, val))
1373 self._locked = ':'.join(map(str, val))
1374 else:
1374 else:
1375 self._locked = None
1375 self._locked = None
1376
1376
1377 @hybrid_property
1377 @hybrid_property
1378 def changeset_cache(self):
1378 def changeset_cache(self):
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 from rhodecode.lib.vcs.backends.base import EmptyCommit
1380 dummy = EmptyCommit().__json__()
1380 dummy = EmptyCommit().__json__()
1381 if not self._changeset_cache:
1381 if not self._changeset_cache:
1382 return dummy
1382 return dummy
1383 try:
1383 try:
1384 return json.loads(self._changeset_cache)
1384 return json.loads(self._changeset_cache)
1385 except TypeError:
1385 except TypeError:
1386 return dummy
1386 return dummy
1387 except Exception:
1387 except Exception:
1388 log.error(traceback.format_exc())
1388 log.error(traceback.format_exc())
1389 return dummy
1389 return dummy
1390
1390
1391 @changeset_cache.setter
1391 @changeset_cache.setter
1392 def changeset_cache(self, val):
1392 def changeset_cache(self, val):
1393 try:
1393 try:
1394 self._changeset_cache = json.dumps(val)
1394 self._changeset_cache = json.dumps(val)
1395 except Exception:
1395 except Exception:
1396 log.error(traceback.format_exc())
1396 log.error(traceback.format_exc())
1397
1397
1398 @hybrid_property
1398 @hybrid_property
1399 def repo_name(self):
1399 def repo_name(self):
1400 return self._repo_name
1400 return self._repo_name
1401
1401
1402 @repo_name.setter
1402 @repo_name.setter
1403 def repo_name(self, value):
1403 def repo_name(self, value):
1404 self._repo_name = value
1404 self._repo_name = value
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1406
1406
1407 @classmethod
1407 @classmethod
1408 def normalize_repo_name(cls, repo_name):
1408 def normalize_repo_name(cls, repo_name):
1409 """
1409 """
1410 Normalizes os specific repo_name to the format internally stored inside
1410 Normalizes os specific repo_name to the format internally stored inside
1411 database using URL_SEP
1411 database using URL_SEP
1412
1412
1413 :param cls:
1413 :param cls:
1414 :param repo_name:
1414 :param repo_name:
1415 """
1415 """
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416 return cls.NAME_SEP.join(repo_name.split(os.sep))
1417
1417
1418 @classmethod
1418 @classmethod
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1420 session = Session()
1420 session = Session()
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1421 q = session.query(cls).filter(cls.repo_name == repo_name)
1422
1422
1423 if cache:
1423 if cache:
1424 if identity_cache:
1424 if identity_cache:
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 val = cls.identity_cache(session, 'repo_name', repo_name)
1426 if val:
1426 if val:
1427 return val
1427 return val
1428 else:
1428 else:
1429 q = q.options(
1429 q = q.options(
1430 FromCache("sql_cache_short",
1430 FromCache("sql_cache_short",
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431 "get_repo_by_name_%s" % _hash_key(repo_name)))
1432
1432
1433 return q.scalar()
1433 return q.scalar()
1434
1434
1435 @classmethod
1435 @classmethod
1436 def get_by_full_path(cls, repo_full_path):
1436 def get_by_full_path(cls, repo_full_path):
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1438 repo_name = cls.normalize_repo_name(repo_name)
1438 repo_name = cls.normalize_repo_name(repo_name)
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1440
1440
1441 @classmethod
1441 @classmethod
1442 def get_repo_forks(cls, repo_id):
1442 def get_repo_forks(cls, repo_id):
1443 return cls.query().filter(Repository.fork_id == repo_id)
1443 return cls.query().filter(Repository.fork_id == repo_id)
1444
1444
1445 @classmethod
1445 @classmethod
1446 def base_path(cls):
1446 def base_path(cls):
1447 """
1447 """
1448 Returns base path when all repos are stored
1448 Returns base path when all repos are stored
1449
1449
1450 :param cls:
1450 :param cls:
1451 """
1451 """
1452 q = Session().query(RhodeCodeUi)\
1452 q = Session().query(RhodeCodeUi)\
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1455 return q.one().ui_value
1455 return q.one().ui_value
1456
1456
1457 @classmethod
1457 @classmethod
1458 def is_valid(cls, repo_name):
1458 def is_valid(cls, repo_name):
1459 """
1459 """
1460 returns True if given repo name is a valid filesystem repository
1460 returns True if given repo name is a valid filesystem repository
1461
1461
1462 :param cls:
1462 :param cls:
1463 :param repo_name:
1463 :param repo_name:
1464 """
1464 """
1465 from rhodecode.lib.utils import is_valid_repo
1465 from rhodecode.lib.utils import is_valid_repo
1466
1466
1467 return is_valid_repo(repo_name, cls.base_path())
1467 return is_valid_repo(repo_name, cls.base_path())
1468
1468
1469 @classmethod
1469 @classmethod
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1471 case_insensitive=True):
1471 case_insensitive=True):
1472 q = Repository.query()
1472 q = Repository.query()
1473
1473
1474 if not isinstance(user_id, Optional):
1474 if not isinstance(user_id, Optional):
1475 q = q.filter(Repository.user_id == user_id)
1475 q = q.filter(Repository.user_id == user_id)
1476
1476
1477 if not isinstance(group_id, Optional):
1477 if not isinstance(group_id, Optional):
1478 q = q.filter(Repository.group_id == group_id)
1478 q = q.filter(Repository.group_id == group_id)
1479
1479
1480 if case_insensitive:
1480 if case_insensitive:
1481 q = q.order_by(func.lower(Repository.repo_name))
1481 q = q.order_by(func.lower(Repository.repo_name))
1482 else:
1482 else:
1483 q = q.order_by(Repository.repo_name)
1483 q = q.order_by(Repository.repo_name)
1484 return q.all()
1484 return q.all()
1485
1485
1486 @property
1486 @property
1487 def forks(self):
1487 def forks(self):
1488 """
1488 """
1489 Return forks of this repo
1489 Return forks of this repo
1490 """
1490 """
1491 return Repository.get_repo_forks(self.repo_id)
1491 return Repository.get_repo_forks(self.repo_id)
1492
1492
1493 @property
1493 @property
1494 def parent(self):
1494 def parent(self):
1495 """
1495 """
1496 Returns fork parent
1496 Returns fork parent
1497 """
1497 """
1498 return self.fork
1498 return self.fork
1499
1499
1500 @property
1500 @property
1501 def just_name(self):
1501 def just_name(self):
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1502 return self.repo_name.split(self.NAME_SEP)[-1]
1503
1503
1504 @property
1504 @property
1505 def groups_with_parents(self):
1505 def groups_with_parents(self):
1506 groups = []
1506 groups = []
1507 if self.group is None:
1507 if self.group is None:
1508 return groups
1508 return groups
1509
1509
1510 cur_gr = self.group
1510 cur_gr = self.group
1511 groups.insert(0, cur_gr)
1511 groups.insert(0, cur_gr)
1512 while 1:
1512 while 1:
1513 gr = getattr(cur_gr, 'parent_group', None)
1513 gr = getattr(cur_gr, 'parent_group', None)
1514 cur_gr = cur_gr.parent_group
1514 cur_gr = cur_gr.parent_group
1515 if gr is None:
1515 if gr is None:
1516 break
1516 break
1517 groups.insert(0, gr)
1517 groups.insert(0, gr)
1518
1518
1519 return groups
1519 return groups
1520
1520
1521 @property
1521 @property
1522 def groups_and_repo(self):
1522 def groups_and_repo(self):
1523 return self.groups_with_parents, self
1523 return self.groups_with_parents, self
1524
1524
1525 @LazyProperty
1525 @LazyProperty
1526 def repo_path(self):
1526 def repo_path(self):
1527 """
1527 """
1528 Returns base full path for that repository means where it actually
1528 Returns base full path for that repository means where it actually
1529 exists on a filesystem
1529 exists on a filesystem
1530 """
1530 """
1531 q = Session().query(RhodeCodeUi).filter(
1531 q = Session().query(RhodeCodeUi).filter(
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 RhodeCodeUi.ui_key == self.NAME_SEP)
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1534 return q.one().ui_value
1534 return q.one().ui_value
1535
1535
1536 @property
1536 @property
1537 def repo_full_path(self):
1537 def repo_full_path(self):
1538 p = [self.repo_path]
1538 p = [self.repo_path]
1539 # we need to split the name by / since this is how we store the
1539 # we need to split the name by / since this is how we store the
1540 # names in the database, but that eventually needs to be converted
1540 # names in the database, but that eventually needs to be converted
1541 # into a valid system path
1541 # into a valid system path
1542 p += self.repo_name.split(self.NAME_SEP)
1542 p += self.repo_name.split(self.NAME_SEP)
1543 return os.path.join(*map(safe_unicode, p))
1543 return os.path.join(*map(safe_unicode, p))
1544
1544
1545 @property
1545 @property
1546 def cache_keys(self):
1546 def cache_keys(self):
1547 """
1547 """
1548 Returns associated cache keys for that repo
1548 Returns associated cache keys for that repo
1549 """
1549 """
1550 return CacheKey.query()\
1550 return CacheKey.query()\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1551 .filter(CacheKey.cache_args == self.repo_name)\
1552 .order_by(CacheKey.cache_key)\
1552 .order_by(CacheKey.cache_key)\
1553 .all()
1553 .all()
1554
1554
1555 def get_new_name(self, repo_name):
1555 def get_new_name(self, repo_name):
1556 """
1556 """
1557 returns new full repository name based on assigned group and new new
1557 returns new full repository name based on assigned group and new new
1558
1558
1559 :param group_name:
1559 :param group_name:
1560 """
1560 """
1561 path_prefix = self.group.full_path_splitted if self.group else []
1561 path_prefix = self.group.full_path_splitted if self.group else []
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1562 return self.NAME_SEP.join(path_prefix + [repo_name])
1563
1563
1564 @property
1564 @property
1565 def _config(self):
1565 def _config(self):
1566 """
1566 """
1567 Returns db based config object.
1567 Returns db based config object.
1568 """
1568 """
1569 from rhodecode.lib.utils import make_db_config
1569 from rhodecode.lib.utils import make_db_config
1570 return make_db_config(clear_session=False, repo=self)
1570 return make_db_config(clear_session=False, repo=self)
1571
1571
1572 def permissions(self, with_admins=True, with_owner=True):
1572 def permissions(self, with_admins=True, with_owner=True):
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1574 q = q.options(joinedload(UserRepoToPerm.repository),
1575 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.user),
1576 joinedload(UserRepoToPerm.permission),)
1576 joinedload(UserRepoToPerm.permission),)
1577
1577
1578 # get owners and admins and permissions. We do a trick of re-writing
1578 # get owners and admins and permissions. We do a trick of re-writing
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1580 # has a global reference and changing one object propagates to all
1580 # has a global reference and changing one object propagates to all
1581 # others. This means if admin is also an owner admin_row that change
1581 # others. This means if admin is also an owner admin_row that change
1582 # would propagate to both objects
1582 # would propagate to both objects
1583 perm_rows = []
1583 perm_rows = []
1584 for _usr in q.all():
1584 for _usr in q.all():
1585 usr = AttributeDict(_usr.user.get_dict())
1585 usr = AttributeDict(_usr.user.get_dict())
1586 usr.permission = _usr.permission.permission_name
1586 usr.permission = _usr.permission.permission_name
1587 perm_rows.append(usr)
1587 perm_rows.append(usr)
1588
1588
1589 # filter the perm rows by 'default' first and then sort them by
1589 # filter the perm rows by 'default' first and then sort them by
1590 # admin,write,read,none permissions sorted again alphabetically in
1590 # admin,write,read,none permissions sorted again alphabetically in
1591 # each group
1591 # each group
1592 perm_rows = sorted(perm_rows, key=display_sort)
1592 perm_rows = sorted(perm_rows, key=display_sort)
1593
1593
1594 _admin_perm = 'repository.admin'
1594 _admin_perm = 'repository.admin'
1595 owner_row = []
1595 owner_row = []
1596 if with_owner:
1596 if with_owner:
1597 usr = AttributeDict(self.user.get_dict())
1597 usr = AttributeDict(self.user.get_dict())
1598 usr.owner_row = True
1598 usr.owner_row = True
1599 usr.permission = _admin_perm
1599 usr.permission = _admin_perm
1600 owner_row.append(usr)
1600 owner_row.append(usr)
1601
1601
1602 super_admin_rows = []
1602 super_admin_rows = []
1603 if with_admins:
1603 if with_admins:
1604 for usr in User.get_all_super_admins():
1604 for usr in User.get_all_super_admins():
1605 # if this admin is also owner, don't double the record
1605 # if this admin is also owner, don't double the record
1606 if usr.user_id == owner_row[0].user_id:
1606 if usr.user_id == owner_row[0].user_id:
1607 owner_row[0].admin_row = True
1607 owner_row[0].admin_row = True
1608 else:
1608 else:
1609 usr = AttributeDict(usr.get_dict())
1609 usr = AttributeDict(usr.get_dict())
1610 usr.admin_row = True
1610 usr.admin_row = True
1611 usr.permission = _admin_perm
1611 usr.permission = _admin_perm
1612 super_admin_rows.append(usr)
1612 super_admin_rows.append(usr)
1613
1613
1614 return super_admin_rows + owner_row + perm_rows
1614 return super_admin_rows + owner_row + perm_rows
1615
1615
1616 def permission_user_groups(self):
1616 def permission_user_groups(self):
1617 q = UserGroupRepoToPerm.query().filter(
1617 q = UserGroupRepoToPerm.query().filter(
1618 UserGroupRepoToPerm.repository == self)
1618 UserGroupRepoToPerm.repository == self)
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1620 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.users_group),
1621 joinedload(UserGroupRepoToPerm.permission),)
1621 joinedload(UserGroupRepoToPerm.permission),)
1622
1622
1623 perm_rows = []
1623 perm_rows = []
1624 for _user_group in q.all():
1624 for _user_group in q.all():
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr = AttributeDict(_user_group.users_group.get_dict())
1626 usr.permission = _user_group.permission.permission_name
1626 usr.permission = _user_group.permission.permission_name
1627 perm_rows.append(usr)
1627 perm_rows.append(usr)
1628
1628
1629 return perm_rows
1629 return perm_rows
1630
1630
1631 def get_api_data(self, include_secrets=False):
1631 def get_api_data(self, include_secrets=False):
1632 """
1632 """
1633 Common function for generating repo api data
1633 Common function for generating repo api data
1634
1634
1635 :param include_secrets: See :meth:`User.get_api_data`.
1635 :param include_secrets: See :meth:`User.get_api_data`.
1636
1636
1637 """
1637 """
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1639 # move this methods on models level.
1639 # move this methods on models level.
1640 from rhodecode.model.settings import SettingsModel
1640 from rhodecode.model.settings import SettingsModel
1641
1641
1642 repo = self
1642 repo = self
1643 _user_id, _time, _reason = self.locked
1643 _user_id, _time, _reason = self.locked
1644
1644
1645 data = {
1645 data = {
1646 'repo_id': repo.repo_id,
1646 'repo_id': repo.repo_id,
1647 'repo_name': repo.repo_name,
1647 'repo_name': repo.repo_name,
1648 'repo_type': repo.repo_type,
1648 'repo_type': repo.repo_type,
1649 'clone_uri': repo.clone_uri or '',
1649 'clone_uri': repo.clone_uri or '',
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1651 'private': repo.private,
1651 'private': repo.private,
1652 'created_on': repo.created_on,
1652 'created_on': repo.created_on,
1653 'description': repo.description,
1653 'description': repo.description,
1654 'landing_rev': repo.landing_rev,
1654 'landing_rev': repo.landing_rev,
1655 'owner': repo.user.username,
1655 'owner': repo.user.username,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'fork_of': repo.fork.repo_name if repo.fork else None,
1657 'enable_statistics': repo.enable_statistics,
1657 'enable_statistics': repo.enable_statistics,
1658 'enable_locking': repo.enable_locking,
1658 'enable_locking': repo.enable_locking,
1659 'enable_downloads': repo.enable_downloads,
1659 'enable_downloads': repo.enable_downloads,
1660 'last_changeset': repo.changeset_cache,
1660 'last_changeset': repo.changeset_cache,
1661 'locked_by': User.get(_user_id).get_api_data(
1661 'locked_by': User.get(_user_id).get_api_data(
1662 include_secrets=include_secrets) if _user_id else None,
1662 include_secrets=include_secrets) if _user_id else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1663 'locked_date': time_to_datetime(_time) if _time else None,
1664 'lock_reason': _reason if _reason else None,
1664 'lock_reason': _reason if _reason else None,
1665 }
1665 }
1666
1666
1667 # TODO: mikhail: should be per-repo settings here
1667 # TODO: mikhail: should be per-repo settings here
1668 rc_config = SettingsModel().get_all_settings()
1668 rc_config = SettingsModel().get_all_settings()
1669 repository_fields = str2bool(
1669 repository_fields = str2bool(
1670 rc_config.get('rhodecode_repository_fields'))
1670 rc_config.get('rhodecode_repository_fields'))
1671 if repository_fields:
1671 if repository_fields:
1672 for f in self.extra_fields:
1672 for f in self.extra_fields:
1673 data[f.field_key_prefixed] = f.field_value
1673 data[f.field_key_prefixed] = f.field_value
1674
1674
1675 return data
1675 return data
1676
1676
1677 @classmethod
1677 @classmethod
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1679 if not lock_time:
1679 if not lock_time:
1680 lock_time = time.time()
1680 lock_time = time.time()
1681 if not lock_reason:
1681 if not lock_reason:
1682 lock_reason = cls.LOCK_AUTOMATIC
1682 lock_reason = cls.LOCK_AUTOMATIC
1683 repo.locked = [user_id, lock_time, lock_reason]
1683 repo.locked = [user_id, lock_time, lock_reason]
1684 Session().add(repo)
1684 Session().add(repo)
1685 Session().commit()
1685 Session().commit()
1686
1686
1687 @classmethod
1687 @classmethod
1688 def unlock(cls, repo):
1688 def unlock(cls, repo):
1689 repo.locked = None
1689 repo.locked = None
1690 Session().add(repo)
1690 Session().add(repo)
1691 Session().commit()
1691 Session().commit()
1692
1692
1693 @classmethod
1693 @classmethod
1694 def getlock(cls, repo):
1694 def getlock(cls, repo):
1695 return repo.locked
1695 return repo.locked
1696
1696
1697 def is_user_lock(self, user_id):
1697 def is_user_lock(self, user_id):
1698 if self.lock[0]:
1698 if self.lock[0]:
1699 lock_user_id = safe_int(self.lock[0])
1699 lock_user_id = safe_int(self.lock[0])
1700 user_id = safe_int(user_id)
1700 user_id = safe_int(user_id)
1701 # both are ints, and they are equal
1701 # both are ints, and they are equal
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702 return all([lock_user_id, user_id]) and lock_user_id == user_id
1703
1703
1704 return False
1704 return False
1705
1705
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 def get_locking_state(self, action, user_id, only_when_enabled=True):
1707 """
1707 """
1708 Checks locking on this repository, if locking is enabled and lock is
1708 Checks locking on this repository, if locking is enabled and lock is
1709 present returns a tuple of make_lock, locked, locked_by.
1709 present returns a tuple of make_lock, locked, locked_by.
1710 make_lock can have 3 states None (do nothing) True, make lock
1710 make_lock can have 3 states None (do nothing) True, make lock
1711 False release lock, This value is later propagated to hooks, which
1711 False release lock, This value is later propagated to hooks, which
1712 do the locking. Think about this as signals passed to hooks what to do.
1712 do the locking. Think about this as signals passed to hooks what to do.
1713
1713
1714 """
1714 """
1715 # TODO: johbo: This is part of the business logic and should be moved
1715 # TODO: johbo: This is part of the business logic and should be moved
1716 # into the RepositoryModel.
1716 # into the RepositoryModel.
1717
1717
1718 if action not in ('push', 'pull'):
1718 if action not in ('push', 'pull'):
1719 raise ValueError("Invalid action value: %s" % repr(action))
1719 raise ValueError("Invalid action value: %s" % repr(action))
1720
1720
1721 # defines if locked error should be thrown to user
1721 # defines if locked error should be thrown to user
1722 currently_locked = False
1722 currently_locked = False
1723 # defines if new lock should be made, tri-state
1723 # defines if new lock should be made, tri-state
1724 make_lock = None
1724 make_lock = None
1725 repo = self
1725 repo = self
1726 user = User.get(user_id)
1726 user = User.get(user_id)
1727
1727
1728 lock_info = repo.locked
1728 lock_info = repo.locked
1729
1729
1730 if repo and (repo.enable_locking or not only_when_enabled):
1730 if repo and (repo.enable_locking or not only_when_enabled):
1731 if action == 'push':
1731 if action == 'push':
1732 # check if it's already locked !, if it is compare users
1732 # check if it's already locked !, if it is compare users
1733 locked_by_user_id = lock_info[0]
1733 locked_by_user_id = lock_info[0]
1734 if user.user_id == locked_by_user_id:
1734 if user.user_id == locked_by_user_id:
1735 log.debug(
1735 log.debug(
1736 'Got `push` action from user %s, now unlocking', user)
1736 'Got `push` action from user %s, now unlocking', user)
1737 # unlock if we have push from user who locked
1737 # unlock if we have push from user who locked
1738 make_lock = False
1738 make_lock = False
1739 else:
1739 else:
1740 # we're not the same user who locked, ban with
1740 # we're not the same user who locked, ban with
1741 # code defined in settings (default is 423 HTTP Locked) !
1741 # code defined in settings (default is 423 HTTP Locked) !
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1742 log.debug('Repo %s is currently locked by %s', repo, user)
1743 currently_locked = True
1743 currently_locked = True
1744 elif action == 'pull':
1744 elif action == 'pull':
1745 # [0] user [1] date
1745 # [0] user [1] date
1746 if lock_info[0] and lock_info[1]:
1746 if lock_info[0] and lock_info[1]:
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1747 log.debug('Repo %s is currently locked by %s', repo, user)
1748 currently_locked = True
1748 currently_locked = True
1749 else:
1749 else:
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1750 log.debug('Setting lock on repo %s by %s', repo, user)
1751 make_lock = True
1751 make_lock = True
1752
1752
1753 else:
1753 else:
1754 log.debug('Repository %s do not have locking enabled', repo)
1754 log.debug('Repository %s do not have locking enabled', repo)
1755
1755
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1757 make_lock, currently_locked, lock_info)
1757 make_lock, currently_locked, lock_info)
1758
1758
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1759 from rhodecode.lib.auth import HasRepoPermissionAny
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1762 # if we don't have at least write permission we cannot make a lock
1762 # if we don't have at least write permission we cannot make a lock
1763 log.debug('lock state reset back to FALSE due to lack '
1763 log.debug('lock state reset back to FALSE due to lack '
1764 'of at least read permission')
1764 'of at least read permission')
1765 make_lock = False
1765 make_lock = False
1766
1766
1767 return make_lock, currently_locked, lock_info
1767 return make_lock, currently_locked, lock_info
1768
1768
1769 @property
1769 @property
1770 def last_db_change(self):
1770 def last_db_change(self):
1771 return self.updated_on
1771 return self.updated_on
1772
1772
1773 @property
1773 @property
1774 def clone_uri_hidden(self):
1774 def clone_uri_hidden(self):
1775 clone_uri = self.clone_uri
1775 clone_uri = self.clone_uri
1776 if clone_uri:
1776 if clone_uri:
1777 import urlobject
1777 import urlobject
1778 url_obj = urlobject.URLObject(clone_uri)
1778 url_obj = urlobject.URLObject(clone_uri)
1779 if url_obj.password:
1779 if url_obj.password:
1780 clone_uri = url_obj.with_password('*****')
1780 clone_uri = url_obj.with_password('*****')
1781 return clone_uri
1781 return clone_uri
1782
1782
1783 def clone_url(self, **override):
1783 def clone_url(self, **override):
1784 qualified_home_url = url('home', qualified=True)
1784 qualified_home_url = url('home', qualified=True)
1785
1785
1786 uri_tmpl = None
1786 uri_tmpl = None
1787 if 'with_id' in override:
1787 if 'with_id' in override:
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1789 del override['with_id']
1789 del override['with_id']
1790
1790
1791 if 'uri_tmpl' in override:
1791 if 'uri_tmpl' in override:
1792 uri_tmpl = override['uri_tmpl']
1792 uri_tmpl = override['uri_tmpl']
1793 del override['uri_tmpl']
1793 del override['uri_tmpl']
1794
1794
1795 # we didn't override our tmpl from **overrides
1795 # we didn't override our tmpl from **overrides
1796 if not uri_tmpl:
1796 if not uri_tmpl:
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1797 uri_tmpl = self.DEFAULT_CLONE_URI
1798 try:
1798 try:
1799 from pylons import tmpl_context as c
1799 from pylons import tmpl_context as c
1800 uri_tmpl = c.clone_uri_tmpl
1800 uri_tmpl = c.clone_uri_tmpl
1801 except Exception:
1801 except Exception:
1802 # in any case if we call this outside of request context,
1802 # in any case if we call this outside of request context,
1803 # ie, not having tmpl_context set up
1803 # ie, not having tmpl_context set up
1804 pass
1804 pass
1805
1805
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1806 return get_clone_url(uri_tmpl=uri_tmpl,
1807 qualifed_home_url=qualified_home_url,
1807 qualifed_home_url=qualified_home_url,
1808 repo_name=self.repo_name,
1808 repo_name=self.repo_name,
1809 repo_id=self.repo_id, **override)
1809 repo_id=self.repo_id, **override)
1810
1810
1811 def set_state(self, state):
1811 def set_state(self, state):
1812 self.repo_state = state
1812 self.repo_state = state
1813 Session().add(self)
1813 Session().add(self)
1814 #==========================================================================
1814 #==========================================================================
1815 # SCM PROPERTIES
1815 # SCM PROPERTIES
1816 #==========================================================================
1816 #==========================================================================
1817
1817
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1819 return get_commit_safe(
1819 return get_commit_safe(
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1821
1821
1822 def get_changeset(self, rev=None, pre_load=None):
1822 def get_changeset(self, rev=None, pre_load=None):
1823 warnings.warn("Use get_commit", DeprecationWarning)
1823 warnings.warn("Use get_commit", DeprecationWarning)
1824 commit_id = None
1824 commit_id = None
1825 commit_idx = None
1825 commit_idx = None
1826 if isinstance(rev, basestring):
1826 if isinstance(rev, basestring):
1827 commit_id = rev
1827 commit_id = rev
1828 else:
1828 else:
1829 commit_idx = rev
1829 commit_idx = rev
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1831 pre_load=pre_load)
1831 pre_load=pre_load)
1832
1832
1833 def get_landing_commit(self):
1833 def get_landing_commit(self):
1834 """
1834 """
1835 Returns landing commit, or if that doesn't exist returns the tip
1835 Returns landing commit, or if that doesn't exist returns the tip
1836 """
1836 """
1837 _rev_type, _rev = self.landing_rev
1837 _rev_type, _rev = self.landing_rev
1838 commit = self.get_commit(_rev)
1838 commit = self.get_commit(_rev)
1839 if isinstance(commit, EmptyCommit):
1839 if isinstance(commit, EmptyCommit):
1840 return self.get_commit()
1840 return self.get_commit()
1841 return commit
1841 return commit
1842
1842
1843 def update_commit_cache(self, cs_cache=None, config=None):
1843 def update_commit_cache(self, cs_cache=None, config=None):
1844 """
1844 """
1845 Update cache of last changeset for repository, keys should be::
1845 Update cache of last changeset for repository, keys should be::
1846
1846
1847 short_id
1847 short_id
1848 raw_id
1848 raw_id
1849 revision
1849 revision
1850 parents
1850 parents
1851 message
1851 message
1852 date
1852 date
1853 author
1853 author
1854
1854
1855 :param cs_cache:
1855 :param cs_cache:
1856 """
1856 """
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 from rhodecode.lib.vcs.backends.base import BaseChangeset
1858 if cs_cache is None:
1858 if cs_cache is None:
1859 # use no-cache version here
1859 # use no-cache version here
1860 scm_repo = self.scm_instance(cache=False, config=config)
1860 scm_repo = self.scm_instance(cache=False, config=config)
1861 if scm_repo:
1861 if scm_repo:
1862 cs_cache = scm_repo.get_commit(
1862 cs_cache = scm_repo.get_commit(
1863 pre_load=["author", "date", "message", "parents"])
1863 pre_load=["author", "date", "message", "parents"])
1864 else:
1864 else:
1865 cs_cache = EmptyCommit()
1865 cs_cache = EmptyCommit()
1866
1866
1867 if isinstance(cs_cache, BaseChangeset):
1867 if isinstance(cs_cache, BaseChangeset):
1868 cs_cache = cs_cache.__json__()
1868 cs_cache = cs_cache.__json__()
1869
1869
1870 def is_outdated(new_cs_cache):
1870 def is_outdated(new_cs_cache):
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 new_cs_cache['revision'] != self.changeset_cache['revision']):
1873 return True
1873 return True
1874 return False
1874 return False
1875
1875
1876 # check if we have maybe already latest cached revision
1876 # check if we have maybe already latest cached revision
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1877 if is_outdated(cs_cache) or not self.changeset_cache:
1878 _default = datetime.datetime.fromtimestamp(0)
1878 _default = datetime.datetime.fromtimestamp(0)
1879 last_change = cs_cache.get('date') or _default
1879 last_change = cs_cache.get('date') or _default
1880 log.debug('updated repo %s with new cs cache %s',
1880 log.debug('updated repo %s with new cs cache %s',
1881 self.repo_name, cs_cache)
1881 self.repo_name, cs_cache)
1882 self.updated_on = last_change
1882 self.updated_on = last_change
1883 self.changeset_cache = cs_cache
1883 self.changeset_cache = cs_cache
1884 Session().add(self)
1884 Session().add(self)
1885 Session().commit()
1885 Session().commit()
1886 else:
1886 else:
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 log.debug('Skipping update_commit_cache for repo:`%s` '
1888 'commit already with latest changes', self.repo_name)
1888 'commit already with latest changes', self.repo_name)
1889
1889
1890 @property
1890 @property
1891 def tip(self):
1891 def tip(self):
1892 return self.get_commit('tip')
1892 return self.get_commit('tip')
1893
1893
1894 @property
1894 @property
1895 def author(self):
1895 def author(self):
1896 return self.tip.author
1896 return self.tip.author
1897
1897
1898 @property
1898 @property
1899 def last_change(self):
1899 def last_change(self):
1900 return self.scm_instance().last_change
1900 return self.scm_instance().last_change
1901
1901
1902 def get_comments(self, revisions=None):
1902 def get_comments(self, revisions=None):
1903 """
1903 """
1904 Returns comments for this repository grouped by revisions
1904 Returns comments for this repository grouped by revisions
1905
1905
1906 :param revisions: filter query by revisions only
1906 :param revisions: filter query by revisions only
1907 """
1907 """
1908 cmts = ChangesetComment.query()\
1908 cmts = ChangesetComment.query()\
1909 .filter(ChangesetComment.repo == self)
1909 .filter(ChangesetComment.repo == self)
1910 if revisions:
1910 if revisions:
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1912 grouped = collections.defaultdict(list)
1912 grouped = collections.defaultdict(list)
1913 for cmt in cmts.all():
1913 for cmt in cmts.all():
1914 grouped[cmt.revision].append(cmt)
1914 grouped[cmt.revision].append(cmt)
1915 return grouped
1915 return grouped
1916
1916
1917 def statuses(self, revisions=None):
1917 def statuses(self, revisions=None):
1918 """
1918 """
1919 Returns statuses for this repository
1919 Returns statuses for this repository
1920
1920
1921 :param revisions: list of revisions to get statuses for
1921 :param revisions: list of revisions to get statuses for
1922 """
1922 """
1923 statuses = ChangesetStatus.query()\
1923 statuses = ChangesetStatus.query()\
1924 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.repo == self)\
1925 .filter(ChangesetStatus.version == 0)
1925 .filter(ChangesetStatus.version == 0)
1926
1926
1927 if revisions:
1927 if revisions:
1928 # Try doing the filtering in chunks to avoid hitting limits
1928 # Try doing the filtering in chunks to avoid hitting limits
1929 size = 500
1929 size = 500
1930 status_results = []
1930 status_results = []
1931 for chunk in xrange(0, len(revisions), size):
1931 for chunk in xrange(0, len(revisions), size):
1932 status_results += statuses.filter(
1932 status_results += statuses.filter(
1933 ChangesetStatus.revision.in_(
1933 ChangesetStatus.revision.in_(
1934 revisions[chunk: chunk+size])
1934 revisions[chunk: chunk+size])
1935 ).all()
1935 ).all()
1936 else:
1936 else:
1937 status_results = statuses.all()
1937 status_results = statuses.all()
1938
1938
1939 grouped = {}
1939 grouped = {}
1940
1940
1941 # maybe we have open new pullrequest without a status?
1941 # maybe we have open new pullrequest without a status?
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 status_lbl = ChangesetStatus.get_status_lbl(stat)
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1945 for rev in pr.revisions:
1945 for rev in pr.revisions:
1946 pr_id = pr.pull_request_id
1946 pr_id = pr.pull_request_id
1947 pr_repo = pr.target_repo.repo_name
1947 pr_repo = pr.target_repo.repo_name
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1949
1949
1950 for stat in status_results:
1950 for stat in status_results:
1951 pr_id = pr_repo = None
1951 pr_id = pr_repo = None
1952 if stat.pull_request:
1952 if stat.pull_request:
1953 pr_id = stat.pull_request.pull_request_id
1953 pr_id = stat.pull_request.pull_request_id
1954 pr_repo = stat.pull_request.target_repo.repo_name
1954 pr_repo = stat.pull_request.target_repo.repo_name
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1956 pr_id, pr_repo]
1956 pr_id, pr_repo]
1957 return grouped
1957 return grouped
1958
1958
1959 # ==========================================================================
1959 # ==========================================================================
1960 # SCM CACHE INSTANCE
1960 # SCM CACHE INSTANCE
1961 # ==========================================================================
1961 # ==========================================================================
1962
1962
1963 def scm_instance(self, **kwargs):
1963 def scm_instance(self, **kwargs):
1964 import rhodecode
1964 import rhodecode
1965
1965
1966 # Passing a config will not hit the cache currently only used
1966 # Passing a config will not hit the cache currently only used
1967 # for repo2dbmapper
1967 # for repo2dbmapper
1968 config = kwargs.pop('config', None)
1968 config = kwargs.pop('config', None)
1969 cache = kwargs.pop('cache', None)
1969 cache = kwargs.pop('cache', None)
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1971 # if cache is NOT defined use default global, else we have a full
1971 # if cache is NOT defined use default global, else we have a full
1972 # control over cache behaviour
1972 # control over cache behaviour
1973 if cache is None and full_cache and not config:
1973 if cache is None and full_cache and not config:
1974 return self._get_instance_cached()
1974 return self._get_instance_cached()
1975 return self._get_instance(cache=bool(cache), config=config)
1975 return self._get_instance(cache=bool(cache), config=config)
1976
1976
1977 def _get_instance_cached(self):
1977 def _get_instance_cached(self):
1978 @cache_region('long_term')
1978 @cache_region('long_term')
1979 def _get_repo(cache_key):
1979 def _get_repo(cache_key):
1980 return self._get_instance()
1980 return self._get_instance()
1981
1981
1982 invalidator_context = CacheKey.repo_context_cache(
1982 invalidator_context = CacheKey.repo_context_cache(
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1983 _get_repo, self.repo_name, None, thread_scoped=True)
1984
1984
1985 with invalidator_context as context:
1985 with invalidator_context as context:
1986 context.invalidate()
1986 context.invalidate()
1987 repo = context.compute()
1987 repo = context.compute()
1988
1988
1989 return repo
1989 return repo
1990
1990
1991 def _get_instance(self, cache=True, config=None):
1991 def _get_instance(self, cache=True, config=None):
1992 config = config or self._config
1992 config = config or self._config
1993 custom_wire = {
1993 custom_wire = {
1994 'cache': cache # controls the vcs.remote cache
1994 'cache': cache # controls the vcs.remote cache
1995 }
1995 }
1996 repo = get_vcs_instance(
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1998 config=config,
1999 with_wire=custom_wire,
1999 with_wire=custom_wire,
2000 create=False,
2000 create=False,
2001 _vcs_alias=self.repo_type)
2001 _vcs_alias=self.repo_type)
2002
2002
2003 return repo
2003 return repo
2004
2004
2005 def __json__(self):
2005 def __json__(self):
2006 return {'landing_rev': self.landing_rev}
2006 return {'landing_rev': self.landing_rev}
2007
2007
2008 def get_dict(self):
2008 def get_dict(self):
2009
2009
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # Since we transformed `repo_name` to a hybrid property, we need to
2011 # keep compatibility with the code which uses `repo_name` field.
2011 # keep compatibility with the code which uses `repo_name` field.
2012
2012
2013 result = super(Repository, self).get_dict()
2013 result = super(Repository, self).get_dict()
2014 result['repo_name'] = result.pop('_repo_name', None)
2014 result['repo_name'] = result.pop('_repo_name', None)
2015 return result
2015 return result
2016
2016
2017
2017
2018 class RepoGroup(Base, BaseModel):
2018 class RepoGroup(Base, BaseModel):
2019 __tablename__ = 'groups'
2019 __tablename__ = 'groups'
2020 __table_args__ = (
2020 __table_args__ = (
2021 UniqueConstraint('group_name', 'group_parent_id'),
2021 UniqueConstraint('group_name', 'group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2022 CheckConstraint('group_id != group_parent_id'),
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 )
2025 )
2026 __mapper_args__ = {'order_by': 'group_name'}
2026 __mapper_args__ = {'order_by': 'group_name'}
2027
2027
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2029
2029
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2037 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2038
2038
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2039 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2040 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2041 parent_group = relationship('RepoGroup', remote_side=group_id)
2042 user = relationship('User')
2042 user = relationship('User')
2043 integrations = relationship('Integration',
2043 integrations = relationship('Integration',
2044 cascade="all, delete, delete-orphan")
2044 cascade="all, delete, delete-orphan")
2045
2045
2046 def __init__(self, group_name='', parent_group=None):
2046 def __init__(self, group_name='', parent_group=None):
2047 self.group_name = group_name
2047 self.group_name = group_name
2048 self.parent_group = parent_group
2048 self.parent_group = parent_group
2049
2049
2050 def __unicode__(self):
2050 def __unicode__(self):
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2051 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2052 self.group_name)
2052 self.group_name)
2053
2053
2054 @classmethod
2054 @classmethod
2055 def _generate_choice(cls, repo_group):
2055 def _generate_choice(cls, repo_group):
2056 from webhelpers.html import literal as _literal
2056 from webhelpers.html import literal as _literal
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2057 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2058 return repo_group.group_id, _name(repo_group.full_path_splitted)
2059
2059
2060 @classmethod
2060 @classmethod
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2061 def groups_choices(cls, groups=None, show_empty_group=True):
2062 if not groups:
2062 if not groups:
2063 groups = cls.query().all()
2063 groups = cls.query().all()
2064
2064
2065 repo_groups = []
2065 repo_groups = []
2066 if show_empty_group:
2066 if show_empty_group:
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2067 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2068
2068
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2069 repo_groups.extend([cls._generate_choice(x) for x in groups])
2070
2070
2071 repo_groups = sorted(
2071 repo_groups = sorted(
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2072 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2073 return repo_groups
2073 return repo_groups
2074
2074
2075 @classmethod
2075 @classmethod
2076 def url_sep(cls):
2076 def url_sep(cls):
2077 return URL_SEP
2077 return URL_SEP
2078
2078
2079 @classmethod
2079 @classmethod
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2080 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2081 if case_insensitive:
2081 if case_insensitive:
2082 gr = cls.query().filter(func.lower(cls.group_name)
2082 gr = cls.query().filter(func.lower(cls.group_name)
2083 == func.lower(group_name))
2083 == func.lower(group_name))
2084 else:
2084 else:
2085 gr = cls.query().filter(cls.group_name == group_name)
2085 gr = cls.query().filter(cls.group_name == group_name)
2086 if cache:
2086 if cache:
2087 gr = gr.options(FromCache(
2087 gr = gr.options(FromCache(
2088 "sql_cache_short",
2088 "sql_cache_short",
2089 "get_group_%s" % _hash_key(group_name)))
2089 "get_group_%s" % _hash_key(group_name)))
2090 return gr.scalar()
2090 return gr.scalar()
2091
2091
2092 @classmethod
2092 @classmethod
2093 def get_user_personal_repo_group(cls, user_id):
2093 def get_user_personal_repo_group(cls, user_id):
2094 user = User.get(user_id)
2094 user = User.get(user_id)
2095 return cls.query()\
2095 return cls.query()\
2096 .filter(cls.personal == true())\
2096 .filter(cls.personal == true())\
2097 .filter(cls.user == user).scalar()
2097 .filter(cls.user == user).scalar()
2098
2098
2099 @classmethod
2099 @classmethod
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2100 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2101 case_insensitive=True):
2101 case_insensitive=True):
2102 q = RepoGroup.query()
2102 q = RepoGroup.query()
2103
2103
2104 if not isinstance(user_id, Optional):
2104 if not isinstance(user_id, Optional):
2105 q = q.filter(RepoGroup.user_id == user_id)
2105 q = q.filter(RepoGroup.user_id == user_id)
2106
2106
2107 if not isinstance(group_id, Optional):
2107 if not isinstance(group_id, Optional):
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2108 q = q.filter(RepoGroup.group_parent_id == group_id)
2109
2109
2110 if case_insensitive:
2110 if case_insensitive:
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2111 q = q.order_by(func.lower(RepoGroup.group_name))
2112 else:
2112 else:
2113 q = q.order_by(RepoGroup.group_name)
2113 q = q.order_by(RepoGroup.group_name)
2114 return q.all()
2114 return q.all()
2115
2115
2116 @property
2116 @property
2117 def parents(self):
2117 def parents(self):
2118 parents_recursion_limit = 10
2118 parents_recursion_limit = 10
2119 groups = []
2119 groups = []
2120 if self.parent_group is None:
2120 if self.parent_group is None:
2121 return groups
2121 return groups
2122 cur_gr = self.parent_group
2122 cur_gr = self.parent_group
2123 groups.insert(0, cur_gr)
2123 groups.insert(0, cur_gr)
2124 cnt = 0
2124 cnt = 0
2125 while 1:
2125 while 1:
2126 cnt += 1
2126 cnt += 1
2127 gr = getattr(cur_gr, 'parent_group', None)
2127 gr = getattr(cur_gr, 'parent_group', None)
2128 cur_gr = cur_gr.parent_group
2128 cur_gr = cur_gr.parent_group
2129 if gr is None:
2129 if gr is None:
2130 break
2130 break
2131 if cnt == parents_recursion_limit:
2131 if cnt == parents_recursion_limit:
2132 # this will prevent accidental infinit loops
2132 # this will prevent accidental infinit loops
2133 log.error(('more than %s parents found for group %s, stopping '
2133 log.error(('more than %s parents found for group %s, stopping '
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2134 'recursive parent fetching' % (parents_recursion_limit, self)))
2135 break
2135 break
2136
2136
2137 groups.insert(0, gr)
2137 groups.insert(0, gr)
2138 return groups
2138 return groups
2139
2139
2140 @property
2140 @property
2141 def children(self):
2141 def children(self):
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2142 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2143
2143
2144 @property
2144 @property
2145 def name(self):
2145 def name(self):
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2146 return self.group_name.split(RepoGroup.url_sep())[-1]
2147
2147
2148 @property
2148 @property
2149 def full_path(self):
2149 def full_path(self):
2150 return self.group_name
2150 return self.group_name
2151
2151
2152 @property
2152 @property
2153 def full_path_splitted(self):
2153 def full_path_splitted(self):
2154 return self.group_name.split(RepoGroup.url_sep())
2154 return self.group_name.split(RepoGroup.url_sep())
2155
2155
2156 @property
2156 @property
2157 def repositories(self):
2157 def repositories(self):
2158 return Repository.query()\
2158 return Repository.query()\
2159 .filter(Repository.group == self)\
2159 .filter(Repository.group == self)\
2160 .order_by(Repository.repo_name)
2160 .order_by(Repository.repo_name)
2161
2161
2162 @property
2162 @property
2163 def repositories_recursive_count(self):
2163 def repositories_recursive_count(self):
2164 cnt = self.repositories.count()
2164 cnt = self.repositories.count()
2165
2165
2166 def children_count(group):
2166 def children_count(group):
2167 cnt = 0
2167 cnt = 0
2168 for child in group.children:
2168 for child in group.children:
2169 cnt += child.repositories.count()
2169 cnt += child.repositories.count()
2170 cnt += children_count(child)
2170 cnt += children_count(child)
2171 return cnt
2171 return cnt
2172
2172
2173 return cnt + children_count(self)
2173 return cnt + children_count(self)
2174
2174
2175 def _recursive_objects(self, include_repos=True):
2175 def _recursive_objects(self, include_repos=True):
2176 all_ = []
2176 all_ = []
2177
2177
2178 def _get_members(root_gr):
2178 def _get_members(root_gr):
2179 if include_repos:
2179 if include_repos:
2180 for r in root_gr.repositories:
2180 for r in root_gr.repositories:
2181 all_.append(r)
2181 all_.append(r)
2182 childs = root_gr.children.all()
2182 childs = root_gr.children.all()
2183 if childs:
2183 if childs:
2184 for gr in childs:
2184 for gr in childs:
2185 all_.append(gr)
2185 all_.append(gr)
2186 _get_members(gr)
2186 _get_members(gr)
2187
2187
2188 _get_members(self)
2188 _get_members(self)
2189 return [self] + all_
2189 return [self] + all_
2190
2190
2191 def recursive_groups_and_repos(self):
2191 def recursive_groups_and_repos(self):
2192 """
2192 """
2193 Recursive return all groups, with repositories in those groups
2193 Recursive return all groups, with repositories in those groups
2194 """
2194 """
2195 return self._recursive_objects()
2195 return self._recursive_objects()
2196
2196
2197 def recursive_groups(self):
2197 def recursive_groups(self):
2198 """
2198 """
2199 Returns all children groups for this group including children of children
2199 Returns all children groups for this group including children of children
2200 """
2200 """
2201 return self._recursive_objects(include_repos=False)
2201 return self._recursive_objects(include_repos=False)
2202
2202
2203 def get_new_name(self, group_name):
2203 def get_new_name(self, group_name):
2204 """
2204 """
2205 returns new full group name based on parent and new name
2205 returns new full group name based on parent and new name
2206
2206
2207 :param group_name:
2207 :param group_name:
2208 """
2208 """
2209 path_prefix = (self.parent_group.full_path_splitted if
2209 path_prefix = (self.parent_group.full_path_splitted if
2210 self.parent_group else [])
2210 self.parent_group else [])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2211 return RepoGroup.url_sep().join(path_prefix + [group_name])
2212
2212
2213 def permissions(self, with_admins=True, with_owner=True):
2213 def permissions(self, with_admins=True, with_owner=True):
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2214 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2215 q = q.options(joinedload(UserRepoGroupToPerm.group),
2216 joinedload(UserRepoGroupToPerm.user),
2216 joinedload(UserRepoGroupToPerm.user),
2217 joinedload(UserRepoGroupToPerm.permission),)
2217 joinedload(UserRepoGroupToPerm.permission),)
2218
2218
2219 # get owners and admins and permissions. We do a trick of re-writing
2219 # get owners and admins and permissions. We do a trick of re-writing
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2220 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2221 # has a global reference and changing one object propagates to all
2221 # has a global reference and changing one object propagates to all
2222 # others. This means if admin is also an owner admin_row that change
2222 # others. This means if admin is also an owner admin_row that change
2223 # would propagate to both objects
2223 # would propagate to both objects
2224 perm_rows = []
2224 perm_rows = []
2225 for _usr in q.all():
2225 for _usr in q.all():
2226 usr = AttributeDict(_usr.user.get_dict())
2226 usr = AttributeDict(_usr.user.get_dict())
2227 usr.permission = _usr.permission.permission_name
2227 usr.permission = _usr.permission.permission_name
2228 perm_rows.append(usr)
2228 perm_rows.append(usr)
2229
2229
2230 # filter the perm rows by 'default' first and then sort them by
2230 # filter the perm rows by 'default' first and then sort them by
2231 # admin,write,read,none permissions sorted again alphabetically in
2231 # admin,write,read,none permissions sorted again alphabetically in
2232 # each group
2232 # each group
2233 perm_rows = sorted(perm_rows, key=display_sort)
2233 perm_rows = sorted(perm_rows, key=display_sort)
2234
2234
2235 _admin_perm = 'group.admin'
2235 _admin_perm = 'group.admin'
2236 owner_row = []
2236 owner_row = []
2237 if with_owner:
2237 if with_owner:
2238 usr = AttributeDict(self.user.get_dict())
2238 usr = AttributeDict(self.user.get_dict())
2239 usr.owner_row = True
2239 usr.owner_row = True
2240 usr.permission = _admin_perm
2240 usr.permission = _admin_perm
2241 owner_row.append(usr)
2241 owner_row.append(usr)
2242
2242
2243 super_admin_rows = []
2243 super_admin_rows = []
2244 if with_admins:
2244 if with_admins:
2245 for usr in User.get_all_super_admins():
2245 for usr in User.get_all_super_admins():
2246 # if this admin is also owner, don't double the record
2246 # if this admin is also owner, don't double the record
2247 if usr.user_id == owner_row[0].user_id:
2247 if usr.user_id == owner_row[0].user_id:
2248 owner_row[0].admin_row = True
2248 owner_row[0].admin_row = True
2249 else:
2249 else:
2250 usr = AttributeDict(usr.get_dict())
2250 usr = AttributeDict(usr.get_dict())
2251 usr.admin_row = True
2251 usr.admin_row = True
2252 usr.permission = _admin_perm
2252 usr.permission = _admin_perm
2253 super_admin_rows.append(usr)
2253 super_admin_rows.append(usr)
2254
2254
2255 return super_admin_rows + owner_row + perm_rows
2255 return super_admin_rows + owner_row + perm_rows
2256
2256
2257 def permission_user_groups(self):
2257 def permission_user_groups(self):
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2258 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2259 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2260 joinedload(UserGroupRepoGroupToPerm.users_group),
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2261 joinedload(UserGroupRepoGroupToPerm.permission),)
2262
2262
2263 perm_rows = []
2263 perm_rows = []
2264 for _user_group in q.all():
2264 for _user_group in q.all():
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2265 usr = AttributeDict(_user_group.users_group.get_dict())
2266 usr.permission = _user_group.permission.permission_name
2266 usr.permission = _user_group.permission.permission_name
2267 perm_rows.append(usr)
2267 perm_rows.append(usr)
2268
2268
2269 return perm_rows
2269 return perm_rows
2270
2270
2271 def get_api_data(self):
2271 def get_api_data(self):
2272 """
2272 """
2273 Common function for generating api data
2273 Common function for generating api data
2274
2274
2275 """
2275 """
2276 group = self
2276 group = self
2277 data = {
2277 data = {
2278 'group_id': group.group_id,
2278 'group_id': group.group_id,
2279 'group_name': group.group_name,
2279 'group_name': group.group_name,
2280 'group_description': group.group_description,
2280 'group_description': group.group_description,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2281 'parent_group': group.parent_group.group_name if group.parent_group else None,
2282 'repositories': [x.repo_name for x in group.repositories],
2282 'repositories': [x.repo_name for x in group.repositories],
2283 'owner': group.user.username,
2283 'owner': group.user.username,
2284 }
2284 }
2285 return data
2285 return data
2286
2286
2287
2287
2288 class Permission(Base, BaseModel):
2288 class Permission(Base, BaseModel):
2289 __tablename__ = 'permissions'
2289 __tablename__ = 'permissions'
2290 __table_args__ = (
2290 __table_args__ = (
2291 Index('p_perm_name_idx', 'permission_name'),
2291 Index('p_perm_name_idx', 'permission_name'),
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2292 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2293 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2294 )
2294 )
2295 PERMS = [
2295 PERMS = [
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2296 ('hg.admin', _('RhodeCode Super Administrator')),
2297
2297
2298 ('repository.none', _('Repository no access')),
2298 ('repository.none', _('Repository no access')),
2299 ('repository.read', _('Repository read access')),
2299 ('repository.read', _('Repository read access')),
2300 ('repository.write', _('Repository write access')),
2300 ('repository.write', _('Repository write access')),
2301 ('repository.admin', _('Repository admin access')),
2301 ('repository.admin', _('Repository admin access')),
2302
2302
2303 ('group.none', _('Repository group no access')),
2303 ('group.none', _('Repository group no access')),
2304 ('group.read', _('Repository group read access')),
2304 ('group.read', _('Repository group read access')),
2305 ('group.write', _('Repository group write access')),
2305 ('group.write', _('Repository group write access')),
2306 ('group.admin', _('Repository group admin access')),
2306 ('group.admin', _('Repository group admin access')),
2307
2307
2308 ('usergroup.none', _('User group no access')),
2308 ('usergroup.none', _('User group no access')),
2309 ('usergroup.read', _('User group read access')),
2309 ('usergroup.read', _('User group read access')),
2310 ('usergroup.write', _('User group write access')),
2310 ('usergroup.write', _('User group write access')),
2311 ('usergroup.admin', _('User group admin access')),
2311 ('usergroup.admin', _('User group admin access')),
2312
2312
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2313 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2314 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2315
2315
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2316 ('hg.usergroup.create.false', _('User Group creation disabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2317 ('hg.usergroup.create.true', _('User Group creation enabled')),
2318
2318
2319 ('hg.create.none', _('Repository creation disabled')),
2319 ('hg.create.none', _('Repository creation disabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2320 ('hg.create.repository', _('Repository creation enabled')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2321 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2322 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2323
2323
2324 ('hg.fork.none', _('Repository forking disabled')),
2324 ('hg.fork.none', _('Repository forking disabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2325 ('hg.fork.repository', _('Repository forking enabled')),
2326
2326
2327 ('hg.register.none', _('Registration disabled')),
2327 ('hg.register.none', _('Registration disabled')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2328 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2329 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2330
2330
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2331 ('hg.password_reset.enabled', _('Password reset enabled')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2332 ('hg.password_reset.hidden', _('Password reset hidden')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2333 ('hg.password_reset.disabled', _('Password reset disabled')),
2334
2334
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2335 ('hg.extern_activate.manual', _('Manual activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2336 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2337
2337
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2338 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2339 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2340 ]
2340 ]
2341
2341
2342 # definition of system default permissions for DEFAULT user
2342 # definition of system default permissions for DEFAULT user
2343 DEFAULT_USER_PERMISSIONS = [
2343 DEFAULT_USER_PERMISSIONS = [
2344 'repository.read',
2344 'repository.read',
2345 'group.read',
2345 'group.read',
2346 'usergroup.read',
2346 'usergroup.read',
2347 'hg.create.repository',
2347 'hg.create.repository',
2348 'hg.repogroup.create.false',
2348 'hg.repogroup.create.false',
2349 'hg.usergroup.create.false',
2349 'hg.usergroup.create.false',
2350 'hg.create.write_on_repogroup.true',
2350 'hg.create.write_on_repogroup.true',
2351 'hg.fork.repository',
2351 'hg.fork.repository',
2352 'hg.register.manual_activate',
2352 'hg.register.manual_activate',
2353 'hg.password_reset.enabled',
2353 'hg.password_reset.enabled',
2354 'hg.extern_activate.auto',
2354 'hg.extern_activate.auto',
2355 'hg.inherit_default_perms.true',
2355 'hg.inherit_default_perms.true',
2356 ]
2356 ]
2357
2357
2358 # defines which permissions are more important higher the more important
2358 # defines which permissions are more important higher the more important
2359 # Weight defines which permissions are more important.
2359 # Weight defines which permissions are more important.
2360 # The higher number the more important.
2360 # The higher number the more important.
2361 PERM_WEIGHTS = {
2361 PERM_WEIGHTS = {
2362 'repository.none': 0,
2362 'repository.none': 0,
2363 'repository.read': 1,
2363 'repository.read': 1,
2364 'repository.write': 3,
2364 'repository.write': 3,
2365 'repository.admin': 4,
2365 'repository.admin': 4,
2366
2366
2367 'group.none': 0,
2367 'group.none': 0,
2368 'group.read': 1,
2368 'group.read': 1,
2369 'group.write': 3,
2369 'group.write': 3,
2370 'group.admin': 4,
2370 'group.admin': 4,
2371
2371
2372 'usergroup.none': 0,
2372 'usergroup.none': 0,
2373 'usergroup.read': 1,
2373 'usergroup.read': 1,
2374 'usergroup.write': 3,
2374 'usergroup.write': 3,
2375 'usergroup.admin': 4,
2375 'usergroup.admin': 4,
2376
2376
2377 'hg.repogroup.create.false': 0,
2377 'hg.repogroup.create.false': 0,
2378 'hg.repogroup.create.true': 1,
2378 'hg.repogroup.create.true': 1,
2379
2379
2380 'hg.usergroup.create.false': 0,
2380 'hg.usergroup.create.false': 0,
2381 'hg.usergroup.create.true': 1,
2381 'hg.usergroup.create.true': 1,
2382
2382
2383 'hg.fork.none': 0,
2383 'hg.fork.none': 0,
2384 'hg.fork.repository': 1,
2384 'hg.fork.repository': 1,
2385 'hg.create.none': 0,
2385 'hg.create.none': 0,
2386 'hg.create.repository': 1
2386 'hg.create.repository': 1
2387 }
2387 }
2388
2388
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2389 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2390 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2391 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2392
2392
2393 def __unicode__(self):
2393 def __unicode__(self):
2394 return u"<%s('%s:%s')>" % (
2394 return u"<%s('%s:%s')>" % (
2395 self.__class__.__name__, self.permission_id, self.permission_name
2395 self.__class__.__name__, self.permission_id, self.permission_name
2396 )
2396 )
2397
2397
2398 @classmethod
2398 @classmethod
2399 def get_by_key(cls, key):
2399 def get_by_key(cls, key):
2400 return cls.query().filter(cls.permission_name == key).scalar()
2400 return cls.query().filter(cls.permission_name == key).scalar()
2401
2401
2402 @classmethod
2402 @classmethod
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2403 def get_default_repo_perms(cls, user_id, repo_id=None):
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2404 q = Session().query(UserRepoToPerm, Repository, Permission)\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2405 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2406 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2407 .filter(UserRepoToPerm.user_id == user_id)
2407 .filter(UserRepoToPerm.user_id == user_id)
2408 if repo_id:
2408 if repo_id:
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2409 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2410 return q.all()
2410 return q.all()
2411
2411
2412 @classmethod
2412 @classmethod
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2413 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2414 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2415 .join(
2415 .join(
2416 Permission,
2416 Permission,
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2417 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2418 .join(
2418 .join(
2419 Repository,
2419 Repository,
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2420 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2421 .join(
2421 .join(
2422 UserGroup,
2422 UserGroup,
2423 UserGroupRepoToPerm.users_group_id ==
2423 UserGroupRepoToPerm.users_group_id ==
2424 UserGroup.users_group_id)\
2424 UserGroup.users_group_id)\
2425 .join(
2425 .join(
2426 UserGroupMember,
2426 UserGroupMember,
2427 UserGroupRepoToPerm.users_group_id ==
2427 UserGroupRepoToPerm.users_group_id ==
2428 UserGroupMember.users_group_id)\
2428 UserGroupMember.users_group_id)\
2429 .filter(
2429 .filter(
2430 UserGroupMember.user_id == user_id,
2430 UserGroupMember.user_id == user_id,
2431 UserGroup.users_group_active == true())
2431 UserGroup.users_group_active == true())
2432 if repo_id:
2432 if repo_id:
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2433 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2434 return q.all()
2434 return q.all()
2435
2435
2436 @classmethod
2436 @classmethod
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2437 def get_default_group_perms(cls, user_id, repo_group_id=None):
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2438 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2439 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2440 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2441 .filter(UserRepoGroupToPerm.user_id == user_id)
2442 if repo_group_id:
2442 if repo_group_id:
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2443 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2444 return q.all()
2444 return q.all()
2445
2445
2446 @classmethod
2446 @classmethod
2447 def get_default_group_perms_from_user_group(
2447 def get_default_group_perms_from_user_group(
2448 cls, user_id, repo_group_id=None):
2448 cls, user_id, repo_group_id=None):
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2449 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2450 .join(
2450 .join(
2451 Permission,
2451 Permission,
2452 UserGroupRepoGroupToPerm.permission_id ==
2452 UserGroupRepoGroupToPerm.permission_id ==
2453 Permission.permission_id)\
2453 Permission.permission_id)\
2454 .join(
2454 .join(
2455 RepoGroup,
2455 RepoGroup,
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2456 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2457 .join(
2457 .join(
2458 UserGroup,
2458 UserGroup,
2459 UserGroupRepoGroupToPerm.users_group_id ==
2459 UserGroupRepoGroupToPerm.users_group_id ==
2460 UserGroup.users_group_id)\
2460 UserGroup.users_group_id)\
2461 .join(
2461 .join(
2462 UserGroupMember,
2462 UserGroupMember,
2463 UserGroupRepoGroupToPerm.users_group_id ==
2463 UserGroupRepoGroupToPerm.users_group_id ==
2464 UserGroupMember.users_group_id)\
2464 UserGroupMember.users_group_id)\
2465 .filter(
2465 .filter(
2466 UserGroupMember.user_id == user_id,
2466 UserGroupMember.user_id == user_id,
2467 UserGroup.users_group_active == true())
2467 UserGroup.users_group_active == true())
2468 if repo_group_id:
2468 if repo_group_id:
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2469 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2470 return q.all()
2470 return q.all()
2471
2471
2472 @classmethod
2472 @classmethod
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2473 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2474 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2475 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2476 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2477 .filter(UserUserGroupToPerm.user_id == user_id)
2478 if user_group_id:
2478 if user_group_id:
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2479 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2480 return q.all()
2480 return q.all()
2481
2481
2482 @classmethod
2482 @classmethod
2483 def get_default_user_group_perms_from_user_group(
2483 def get_default_user_group_perms_from_user_group(
2484 cls, user_id, user_group_id=None):
2484 cls, user_id, user_group_id=None):
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2485 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2486 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2487 .join(
2487 .join(
2488 Permission,
2488 Permission,
2489 UserGroupUserGroupToPerm.permission_id ==
2489 UserGroupUserGroupToPerm.permission_id ==
2490 Permission.permission_id)\
2490 Permission.permission_id)\
2491 .join(
2491 .join(
2492 TargetUserGroup,
2492 TargetUserGroup,
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2493 UserGroupUserGroupToPerm.target_user_group_id ==
2494 TargetUserGroup.users_group_id)\
2494 TargetUserGroup.users_group_id)\
2495 .join(
2495 .join(
2496 UserGroup,
2496 UserGroup,
2497 UserGroupUserGroupToPerm.user_group_id ==
2497 UserGroupUserGroupToPerm.user_group_id ==
2498 UserGroup.users_group_id)\
2498 UserGroup.users_group_id)\
2499 .join(
2499 .join(
2500 UserGroupMember,
2500 UserGroupMember,
2501 UserGroupUserGroupToPerm.user_group_id ==
2501 UserGroupUserGroupToPerm.user_group_id ==
2502 UserGroupMember.users_group_id)\
2502 UserGroupMember.users_group_id)\
2503 .filter(
2503 .filter(
2504 UserGroupMember.user_id == user_id,
2504 UserGroupMember.user_id == user_id,
2505 UserGroup.users_group_active == true())
2505 UserGroup.users_group_active == true())
2506 if user_group_id:
2506 if user_group_id:
2507 q = q.filter(
2507 q = q.filter(
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2508 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2509
2509
2510 return q.all()
2510 return q.all()
2511
2511
2512
2512
2513 class UserRepoToPerm(Base, BaseModel):
2513 class UserRepoToPerm(Base, BaseModel):
2514 __tablename__ = 'repo_to_perm'
2514 __tablename__ = 'repo_to_perm'
2515 __table_args__ = (
2515 __table_args__ = (
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2516 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2517 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2518 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2519 )
2519 )
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2520 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2521 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2522 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2523 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2524
2524
2525 user = relationship('User')
2525 user = relationship('User')
2526 repository = relationship('Repository')
2526 repository = relationship('Repository')
2527 permission = relationship('Permission')
2527 permission = relationship('Permission')
2528
2528
2529 @classmethod
2529 @classmethod
2530 def create(cls, user, repository, permission):
2530 def create(cls, user, repository, permission):
2531 n = cls()
2531 n = cls()
2532 n.user = user
2532 n.user = user
2533 n.repository = repository
2533 n.repository = repository
2534 n.permission = permission
2534 n.permission = permission
2535 Session().add(n)
2535 Session().add(n)
2536 return n
2536 return n
2537
2537
2538 def __unicode__(self):
2538 def __unicode__(self):
2539 return u'<%s => %s >' % (self.user, self.repository)
2539 return u'<%s => %s >' % (self.user, self.repository)
2540
2540
2541
2541
2542 class UserUserGroupToPerm(Base, BaseModel):
2542 class UserUserGroupToPerm(Base, BaseModel):
2543 __tablename__ = 'user_user_group_to_perm'
2543 __tablename__ = 'user_user_group_to_perm'
2544 __table_args__ = (
2544 __table_args__ = (
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2545 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2547 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2548 )
2548 )
2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2549 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2550 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2551 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2552 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2553
2553
2554 user = relationship('User')
2554 user = relationship('User')
2555 user_group = relationship('UserGroup')
2555 user_group = relationship('UserGroup')
2556 permission = relationship('Permission')
2556 permission = relationship('Permission')
2557
2557
2558 @classmethod
2558 @classmethod
2559 def create(cls, user, user_group, permission):
2559 def create(cls, user, user_group, permission):
2560 n = cls()
2560 n = cls()
2561 n.user = user
2561 n.user = user
2562 n.user_group = user_group
2562 n.user_group = user_group
2563 n.permission = permission
2563 n.permission = permission
2564 Session().add(n)
2564 Session().add(n)
2565 return n
2565 return n
2566
2566
2567 def __unicode__(self):
2567 def __unicode__(self):
2568 return u'<%s => %s >' % (self.user, self.user_group)
2568 return u'<%s => %s >' % (self.user, self.user_group)
2569
2569
2570
2570
2571 class UserToPerm(Base, BaseModel):
2571 class UserToPerm(Base, BaseModel):
2572 __tablename__ = 'user_to_perm'
2572 __tablename__ = 'user_to_perm'
2573 __table_args__ = (
2573 __table_args__ = (
2574 UniqueConstraint('user_id', 'permission_id'),
2574 UniqueConstraint('user_id', 'permission_id'),
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 )
2577 )
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2578 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2579 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581
2581
2582 user = relationship('User')
2582 user = relationship('User')
2583 permission = relationship('Permission', lazy='joined')
2583 permission = relationship('Permission', lazy='joined')
2584
2584
2585 def __unicode__(self):
2585 def __unicode__(self):
2586 return u'<%s => %s >' % (self.user, self.permission)
2586 return u'<%s => %s >' % (self.user, self.permission)
2587
2587
2588
2588
2589 class UserGroupRepoToPerm(Base, BaseModel):
2589 class UserGroupRepoToPerm(Base, BaseModel):
2590 __tablename__ = 'users_group_repo_to_perm'
2590 __tablename__ = 'users_group_repo_to_perm'
2591 __table_args__ = (
2591 __table_args__ = (
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2592 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2593 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2594 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2595 )
2595 )
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2596 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2597 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2598 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2599 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2600
2600
2601 users_group = relationship('UserGroup')
2601 users_group = relationship('UserGroup')
2602 permission = relationship('Permission')
2602 permission = relationship('Permission')
2603 repository = relationship('Repository')
2603 repository = relationship('Repository')
2604
2604
2605 @classmethod
2605 @classmethod
2606 def create(cls, users_group, repository, permission):
2606 def create(cls, users_group, repository, permission):
2607 n = cls()
2607 n = cls()
2608 n.users_group = users_group
2608 n.users_group = users_group
2609 n.repository = repository
2609 n.repository = repository
2610 n.permission = permission
2610 n.permission = permission
2611 Session().add(n)
2611 Session().add(n)
2612 return n
2612 return n
2613
2613
2614 def __unicode__(self):
2614 def __unicode__(self):
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2615 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2616
2616
2617
2617
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2618 class UserGroupUserGroupToPerm(Base, BaseModel):
2619 __tablename__ = 'user_group_user_group_to_perm'
2619 __tablename__ = 'user_group_user_group_to_perm'
2620 __table_args__ = (
2620 __table_args__ = (
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2621 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2622 CheckConstraint('target_user_group_id != user_group_id'),
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2623 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2624 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2625 )
2625 )
2626 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)
2626 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)
2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2627 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2628 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2629 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2630
2630
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2631 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2632 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2633 permission = relationship('Permission')
2633 permission = relationship('Permission')
2634
2634
2635 @classmethod
2635 @classmethod
2636 def create(cls, target_user_group, user_group, permission):
2636 def create(cls, target_user_group, user_group, permission):
2637 n = cls()
2637 n = cls()
2638 n.target_user_group = target_user_group
2638 n.target_user_group = target_user_group
2639 n.user_group = user_group
2639 n.user_group = user_group
2640 n.permission = permission
2640 n.permission = permission
2641 Session().add(n)
2641 Session().add(n)
2642 return n
2642 return n
2643
2643
2644 def __unicode__(self):
2644 def __unicode__(self):
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2645 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2646
2646
2647
2647
2648 class UserGroupToPerm(Base, BaseModel):
2648 class UserGroupToPerm(Base, BaseModel):
2649 __tablename__ = 'users_group_to_perm'
2649 __tablename__ = 'users_group_to_perm'
2650 __table_args__ = (
2650 __table_args__ = (
2651 UniqueConstraint('users_group_id', 'permission_id',),
2651 UniqueConstraint('users_group_id', 'permission_id',),
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2652 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2653 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2654 )
2654 )
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2655 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2656 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2658
2658
2659 users_group = relationship('UserGroup')
2659 users_group = relationship('UserGroup')
2660 permission = relationship('Permission')
2660 permission = relationship('Permission')
2661
2661
2662
2662
2663 class UserRepoGroupToPerm(Base, BaseModel):
2663 class UserRepoGroupToPerm(Base, BaseModel):
2664 __tablename__ = 'user_repo_group_to_perm'
2664 __tablename__ = 'user_repo_group_to_perm'
2665 __table_args__ = (
2665 __table_args__ = (
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2666 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2667 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2668 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2669 )
2669 )
2670
2670
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2671 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2672 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2673 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2674 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2675
2675
2676 user = relationship('User')
2676 user = relationship('User')
2677 group = relationship('RepoGroup')
2677 group = relationship('RepoGroup')
2678 permission = relationship('Permission')
2678 permission = relationship('Permission')
2679
2679
2680 @classmethod
2680 @classmethod
2681 def create(cls, user, repository_group, permission):
2681 def create(cls, user, repository_group, permission):
2682 n = cls()
2682 n = cls()
2683 n.user = user
2683 n.user = user
2684 n.group = repository_group
2684 n.group = repository_group
2685 n.permission = permission
2685 n.permission = permission
2686 Session().add(n)
2686 Session().add(n)
2687 return n
2687 return n
2688
2688
2689
2689
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2690 class UserGroupRepoGroupToPerm(Base, BaseModel):
2691 __tablename__ = 'users_group_repo_group_to_perm'
2691 __tablename__ = 'users_group_repo_group_to_perm'
2692 __table_args__ = (
2692 __table_args__ = (
2693 UniqueConstraint('users_group_id', 'group_id'),
2693 UniqueConstraint('users_group_id', 'group_id'),
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2695 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2696 )
2696 )
2697
2697
2698 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)
2698 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)
2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2700 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2701 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2702
2702
2703 users_group = relationship('UserGroup')
2703 users_group = relationship('UserGroup')
2704 permission = relationship('Permission')
2704 permission = relationship('Permission')
2705 group = relationship('RepoGroup')
2705 group = relationship('RepoGroup')
2706
2706
2707 @classmethod
2707 @classmethod
2708 def create(cls, user_group, repository_group, permission):
2708 def create(cls, user_group, repository_group, permission):
2709 n = cls()
2709 n = cls()
2710 n.users_group = user_group
2710 n.users_group = user_group
2711 n.group = repository_group
2711 n.group = repository_group
2712 n.permission = permission
2712 n.permission = permission
2713 Session().add(n)
2713 Session().add(n)
2714 return n
2714 return n
2715
2715
2716 def __unicode__(self):
2716 def __unicode__(self):
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2717 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2718
2718
2719
2719
2720 class Statistics(Base, BaseModel):
2720 class Statistics(Base, BaseModel):
2721 __tablename__ = 'statistics'
2721 __tablename__ = 'statistics'
2722 __table_args__ = (
2722 __table_args__ = (
2723 UniqueConstraint('repository_id'),
2723 UniqueConstraint('repository_id'),
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 )
2726 )
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2727 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2728 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2729 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2730 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2731 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2732 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2733
2733
2734 repository = relationship('Repository', single_parent=True)
2734 repository = relationship('Repository', single_parent=True)
2735
2735
2736
2736
2737 class UserFollowing(Base, BaseModel):
2737 class UserFollowing(Base, BaseModel):
2738 __tablename__ = 'user_followings'
2738 __tablename__ = 'user_followings'
2739 __table_args__ = (
2739 __table_args__ = (
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2740 UniqueConstraint('user_id', 'follows_repository_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2741 UniqueConstraint('user_id', 'follows_user_id'),
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2742 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2743 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2744 )
2744 )
2745
2745
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2746 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2747 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2748 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2749 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2750 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2751
2751
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2752 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2753
2753
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2754 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2755 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2756
2756
2757 @classmethod
2757 @classmethod
2758 def get_repo_followers(cls, repo_id):
2758 def get_repo_followers(cls, repo_id):
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2759 return cls.query().filter(cls.follows_repo_id == repo_id)
2760
2760
2761
2761
2762 class CacheKey(Base, BaseModel):
2762 class CacheKey(Base, BaseModel):
2763 __tablename__ = 'cache_invalidation'
2763 __tablename__ = 'cache_invalidation'
2764 __table_args__ = (
2764 __table_args__ = (
2765 UniqueConstraint('cache_key'),
2765 UniqueConstraint('cache_key'),
2766 Index('key_idx', 'cache_key'),
2766 Index('key_idx', 'cache_key'),
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2767 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2768 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2769 )
2769 )
2770 CACHE_TYPE_ATOM = 'ATOM'
2770 CACHE_TYPE_ATOM = 'ATOM'
2771 CACHE_TYPE_RSS = 'RSS'
2771 CACHE_TYPE_RSS = 'RSS'
2772 CACHE_TYPE_README = 'README'
2772 CACHE_TYPE_README = 'README'
2773
2773
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2774 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2775 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2776 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2777 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2778
2778
2779 def __init__(self, cache_key, cache_args=''):
2779 def __init__(self, cache_key, cache_args=''):
2780 self.cache_key = cache_key
2780 self.cache_key = cache_key
2781 self.cache_args = cache_args
2781 self.cache_args = cache_args
2782 self.cache_active = False
2782 self.cache_active = False
2783
2783
2784 def __unicode__(self):
2784 def __unicode__(self):
2785 return u"<%s('%s:%s[%s]')>" % (
2785 return u"<%s('%s:%s[%s]')>" % (
2786 self.__class__.__name__,
2786 self.__class__.__name__,
2787 self.cache_id, self.cache_key, self.cache_active)
2787 self.cache_id, self.cache_key, self.cache_active)
2788
2788
2789 def _cache_key_partition(self):
2789 def _cache_key_partition(self):
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2790 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2791 return prefix, repo_name, suffix
2791 return prefix, repo_name, suffix
2792
2792
2793 def get_prefix(self):
2793 def get_prefix(self):
2794 """
2794 """
2795 Try to extract prefix from existing cache key. The key could consist
2795 Try to extract prefix from existing cache key. The key could consist
2796 of prefix, repo_name, suffix
2796 of prefix, repo_name, suffix
2797 """
2797 """
2798 # this returns prefix, repo_name, suffix
2798 # this returns prefix, repo_name, suffix
2799 return self._cache_key_partition()[0]
2799 return self._cache_key_partition()[0]
2800
2800
2801 def get_suffix(self):
2801 def get_suffix(self):
2802 """
2802 """
2803 get suffix that might have been used in _get_cache_key to
2803 get suffix that might have been used in _get_cache_key to
2804 generate self.cache_key. Only used for informational purposes
2804 generate self.cache_key. Only used for informational purposes
2805 in repo_edit.mako.
2805 in repo_edit.mako.
2806 """
2806 """
2807 # prefix, repo_name, suffix
2807 # prefix, repo_name, suffix
2808 return self._cache_key_partition()[2]
2808 return self._cache_key_partition()[2]
2809
2809
2810 @classmethod
2810 @classmethod
2811 def delete_all_cache(cls):
2811 def delete_all_cache(cls):
2812 """
2812 """
2813 Delete all cache keys from database.
2813 Delete all cache keys from database.
2814 Should only be run when all instances are down and all entries
2814 Should only be run when all instances are down and all entries
2815 thus stale.
2815 thus stale.
2816 """
2816 """
2817 cls.query().delete()
2817 cls.query().delete()
2818 Session().commit()
2818 Session().commit()
2819
2819
2820 @classmethod
2820 @classmethod
2821 def get_cache_key(cls, repo_name, cache_type):
2821 def get_cache_key(cls, repo_name, cache_type):
2822 """
2822 """
2823
2823
2824 Generate a cache key for this process of RhodeCode instance.
2824 Generate a cache key for this process of RhodeCode instance.
2825 Prefix most likely will be process id or maybe explicitly set
2825 Prefix most likely will be process id or maybe explicitly set
2826 instance_id from .ini file.
2826 instance_id from .ini file.
2827 """
2827 """
2828 import rhodecode
2828 import rhodecode
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2829 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2830
2830
2831 repo_as_unicode = safe_unicode(repo_name)
2831 repo_as_unicode = safe_unicode(repo_name)
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2832 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2833 if cache_type else repo_as_unicode
2833 if cache_type else repo_as_unicode
2834
2834
2835 return u'{}{}'.format(prefix, key)
2835 return u'{}{}'.format(prefix, key)
2836
2836
2837 @classmethod
2837 @classmethod
2838 def set_invalidate(cls, repo_name, delete=False):
2838 def set_invalidate(cls, repo_name, delete=False):
2839 """
2839 """
2840 Mark all caches of a repo as invalid in the database.
2840 Mark all caches of a repo as invalid in the database.
2841 """
2841 """
2842
2842
2843 try:
2843 try:
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2844 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2845 if delete:
2845 if delete:
2846 log.debug('cache objects deleted for repo %s',
2846 log.debug('cache objects deleted for repo %s',
2847 safe_str(repo_name))
2847 safe_str(repo_name))
2848 qry.delete()
2848 qry.delete()
2849 else:
2849 else:
2850 log.debug('cache objects marked as invalid for repo %s',
2850 log.debug('cache objects marked as invalid for repo %s',
2851 safe_str(repo_name))
2851 safe_str(repo_name))
2852 qry.update({"cache_active": False})
2852 qry.update({"cache_active": False})
2853
2853
2854 Session().commit()
2854 Session().commit()
2855 except Exception:
2855 except Exception:
2856 log.exception(
2856 log.exception(
2857 'Cache key invalidation failed for repository %s',
2857 'Cache key invalidation failed for repository %s',
2858 safe_str(repo_name))
2858 safe_str(repo_name))
2859 Session().rollback()
2859 Session().rollback()
2860
2860
2861 @classmethod
2861 @classmethod
2862 def get_active_cache(cls, cache_key):
2862 def get_active_cache(cls, cache_key):
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2863 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2864 if inv_obj:
2864 if inv_obj:
2865 return inv_obj
2865 return inv_obj
2866 return None
2866 return None
2867
2867
2868 @classmethod
2868 @classmethod
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2869 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2870 thread_scoped=False):
2870 thread_scoped=False):
2871 """
2871 """
2872 @cache_region('long_term')
2872 @cache_region('long_term')
2873 def _heavy_calculation(cache_key):
2873 def _heavy_calculation(cache_key):
2874 return 'result'
2874 return 'result'
2875
2875
2876 cache_context = CacheKey.repo_context_cache(
2876 cache_context = CacheKey.repo_context_cache(
2877 _heavy_calculation, repo_name, cache_type)
2877 _heavy_calculation, repo_name, cache_type)
2878
2878
2879 with cache_context as context:
2879 with cache_context as context:
2880 context.invalidate()
2880 context.invalidate()
2881 computed = context.compute()
2881 computed = context.compute()
2882
2882
2883 assert computed == 'result'
2883 assert computed == 'result'
2884 """
2884 """
2885 from rhodecode.lib import caches
2885 from rhodecode.lib import caches
2886 return caches.InvalidationContext(
2886 return caches.InvalidationContext(
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2887 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2888
2888
2889
2889
2890 class ChangesetComment(Base, BaseModel):
2890 class ChangesetComment(Base, BaseModel):
2891 __tablename__ = 'changeset_comments'
2891 __tablename__ = 'changeset_comments'
2892 __table_args__ = (
2892 __table_args__ = (
2893 Index('cc_revision_idx', 'revision'),
2893 Index('cc_revision_idx', 'revision'),
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2894 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2895 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2896 )
2896 )
2897
2897
2898 COMMENT_OUTDATED = u'comment_outdated'
2898 COMMENT_OUTDATED = u'comment_outdated'
2899 COMMENT_TYPE_NOTE = u'note'
2899 COMMENT_TYPE_NOTE = u'note'
2900 COMMENT_TYPE_TODO = u'todo'
2900 COMMENT_TYPE_TODO = u'todo'
2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2901 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
2902
2902
2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2903 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2904 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2905 revision = Column('revision', String(40), nullable=True)
2905 revision = Column('revision', String(40), nullable=True)
2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2906 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2907 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2908 line_no = Column('line_no', Unicode(10), nullable=True)
2908 line_no = Column('line_no', Unicode(10), nullable=True)
2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2909 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2910 f_path = Column('f_path', Unicode(1000), nullable=True)
2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2911 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2912 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2913 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2914 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2915 renderer = Column('renderer', Unicode(64), nullable=True)
2915 renderer = Column('renderer', Unicode(64), nullable=True)
2916 display_state = Column('display_state', Unicode(128), nullable=True)
2916 display_state = Column('display_state', Unicode(128), nullable=True)
2917
2917
2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2918 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2919 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2920 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
2921 author = relationship('User', lazy='joined')
2921 author = relationship('User', lazy='joined')
2922 repo = relationship('Repository')
2922 repo = relationship('Repository')
2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2923 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2924 pull_request = relationship('PullRequest', lazy='joined')
2924 pull_request = relationship('PullRequest', lazy='joined')
2925 pull_request_version = relationship('PullRequestVersion')
2925 pull_request_version = relationship('PullRequestVersion')
2926
2926
2927 @classmethod
2927 @classmethod
2928 def get_users(cls, revision=None, pull_request_id=None):
2928 def get_users(cls, revision=None, pull_request_id=None):
2929 """
2929 """
2930 Returns user associated with this ChangesetComment. ie those
2930 Returns user associated with this ChangesetComment. ie those
2931 who actually commented
2931 who actually commented
2932
2932
2933 :param cls:
2933 :param cls:
2934 :param revision:
2934 :param revision:
2935 """
2935 """
2936 q = Session().query(User)\
2936 q = Session().query(User)\
2937 .join(ChangesetComment.author)
2937 .join(ChangesetComment.author)
2938 if revision:
2938 if revision:
2939 q = q.filter(cls.revision == revision)
2939 q = q.filter(cls.revision == revision)
2940 elif pull_request_id:
2940 elif pull_request_id:
2941 q = q.filter(cls.pull_request_id == pull_request_id)
2941 q = q.filter(cls.pull_request_id == pull_request_id)
2942 return q.all()
2942 return q.all()
2943
2943
2944 @classmethod
2944 @classmethod
2945 def get_index_from_version(cls, pr_version, versions):
2945 def get_index_from_version(cls, pr_version, versions):
2946 num_versions = [x.pull_request_version_id for x in versions]
2946 num_versions = [x.pull_request_version_id for x in versions]
2947 try:
2947 try:
2948 return num_versions.index(pr_version) +1
2948 return num_versions.index(pr_version) +1
2949 except (IndexError, ValueError):
2949 except (IndexError, ValueError):
2950 return
2950 return
2951
2951
2952 @property
2952 @property
2953 def outdated(self):
2953 def outdated(self):
2954 return self.display_state == self.COMMENT_OUTDATED
2954 return self.display_state == self.COMMENT_OUTDATED
2955
2955
2956 def outdated_at_version(self, version):
2956 def outdated_at_version(self, version):
2957 """
2957 """
2958 Checks if comment is outdated for given pull request version
2958 Checks if comment is outdated for given pull request version
2959 """
2959 """
2960 return self.outdated and self.pull_request_version_id != version
2960 return self.outdated and self.pull_request_version_id != version
2961
2961
2962 def older_than_version(self, version):
2962 def older_than_version(self, version):
2963 """
2963 """
2964 Checks if comment is made from previous version than given
2964 Checks if comment is made from previous version than given
2965 """
2965 """
2966 if version is None:
2966 if version is None:
2967 return self.pull_request_version_id is not None
2967 return self.pull_request_version_id is not None
2968
2968
2969 return self.pull_request_version_id < version
2969 return self.pull_request_version_id < version
2970
2970
2971 @property
2971 @property
2972 def resolved(self):
2972 def resolved(self):
2973 return self.resolved_by[0] if self.resolved_by else None
2973 return self.resolved_by[0] if self.resolved_by else None
2974
2974
2975 @property
2976 def is_todo(self):
2977 return self.comment_type == self.COMMENT_TYPE_TODO
2978
2975 def get_index_version(self, versions):
2979 def get_index_version(self, versions):
2976 return self.get_index_from_version(
2980 return self.get_index_from_version(
2977 self.pull_request_version_id, versions)
2981 self.pull_request_version_id, versions)
2978
2982
2979 def render(self, mentions=False):
2983 def render(self, mentions=False):
2980 from rhodecode.lib import helpers as h
2984 from rhodecode.lib import helpers as h
2981 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2985 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2982
2986
2983 def __repr__(self):
2987 def __repr__(self):
2984 if self.comment_id:
2988 if self.comment_id:
2985 return '<DB:Comment #%s>' % self.comment_id
2989 return '<DB:Comment #%s>' % self.comment_id
2986 else:
2990 else:
2987 return '<DB:Comment at %#x>' % id(self)
2991 return '<DB:Comment at %#x>' % id(self)
2988
2992
2989
2993
2990 class ChangesetStatus(Base, BaseModel):
2994 class ChangesetStatus(Base, BaseModel):
2991 __tablename__ = 'changeset_statuses'
2995 __tablename__ = 'changeset_statuses'
2992 __table_args__ = (
2996 __table_args__ = (
2993 Index('cs_revision_idx', 'revision'),
2997 Index('cs_revision_idx', 'revision'),
2994 Index('cs_version_idx', 'version'),
2998 Index('cs_version_idx', 'version'),
2995 UniqueConstraint('repo_id', 'revision', 'version'),
2999 UniqueConstraint('repo_id', 'revision', 'version'),
2996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3000 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3001 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2998 )
3002 )
2999 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3003 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3000 STATUS_APPROVED = 'approved'
3004 STATUS_APPROVED = 'approved'
3001 STATUS_REJECTED = 'rejected'
3005 STATUS_REJECTED = 'rejected'
3002 STATUS_UNDER_REVIEW = 'under_review'
3006 STATUS_UNDER_REVIEW = 'under_review'
3003
3007
3004 STATUSES = [
3008 STATUSES = [
3005 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3009 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3006 (STATUS_APPROVED, _("Approved")),
3010 (STATUS_APPROVED, _("Approved")),
3007 (STATUS_REJECTED, _("Rejected")),
3011 (STATUS_REJECTED, _("Rejected")),
3008 (STATUS_UNDER_REVIEW, _("Under Review")),
3012 (STATUS_UNDER_REVIEW, _("Under Review")),
3009 ]
3013 ]
3010
3014
3011 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3015 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3012 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3016 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3013 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3017 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3014 revision = Column('revision', String(40), nullable=False)
3018 revision = Column('revision', String(40), nullable=False)
3015 status = Column('status', String(128), nullable=False, default=DEFAULT)
3019 status = Column('status', String(128), nullable=False, default=DEFAULT)
3016 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3020 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3017 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3021 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3018 version = Column('version', Integer(), nullable=False, default=0)
3022 version = Column('version', Integer(), nullable=False, default=0)
3019 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3023 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3020
3024
3021 author = relationship('User', lazy='joined')
3025 author = relationship('User', lazy='joined')
3022 repo = relationship('Repository')
3026 repo = relationship('Repository')
3023 comment = relationship('ChangesetComment', lazy='joined')
3027 comment = relationship('ChangesetComment', lazy='joined')
3024 pull_request = relationship('PullRequest', lazy='joined')
3028 pull_request = relationship('PullRequest', lazy='joined')
3025
3029
3026 def __unicode__(self):
3030 def __unicode__(self):
3027 return u"<%s('%s[%s]:%s')>" % (
3031 return u"<%s('%s[%s]:%s')>" % (
3028 self.__class__.__name__,
3032 self.__class__.__name__,
3029 self.status, self.version, self.author
3033 self.status, self.version, self.author
3030 )
3034 )
3031
3035
3032 @classmethod
3036 @classmethod
3033 def get_status_lbl(cls, value):
3037 def get_status_lbl(cls, value):
3034 return dict(cls.STATUSES).get(value)
3038 return dict(cls.STATUSES).get(value)
3035
3039
3036 @property
3040 @property
3037 def status_lbl(self):
3041 def status_lbl(self):
3038 return ChangesetStatus.get_status_lbl(self.status)
3042 return ChangesetStatus.get_status_lbl(self.status)
3039
3043
3040
3044
3041 class _PullRequestBase(BaseModel):
3045 class _PullRequestBase(BaseModel):
3042 """
3046 """
3043 Common attributes of pull request and version entries.
3047 Common attributes of pull request and version entries.
3044 """
3048 """
3045
3049
3046 # .status values
3050 # .status values
3047 STATUS_NEW = u'new'
3051 STATUS_NEW = u'new'
3048 STATUS_OPEN = u'open'
3052 STATUS_OPEN = u'open'
3049 STATUS_CLOSED = u'closed'
3053 STATUS_CLOSED = u'closed'
3050
3054
3051 title = Column('title', Unicode(255), nullable=True)
3055 title = Column('title', Unicode(255), nullable=True)
3052 description = Column(
3056 description = Column(
3053 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3057 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3054 nullable=True)
3058 nullable=True)
3055 # new/open/closed status of pull request (not approve/reject/etc)
3059 # new/open/closed status of pull request (not approve/reject/etc)
3056 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3060 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3057 created_on = Column(
3061 created_on = Column(
3058 'created_on', DateTime(timezone=False), nullable=False,
3062 'created_on', DateTime(timezone=False), nullable=False,
3059 default=datetime.datetime.now)
3063 default=datetime.datetime.now)
3060 updated_on = Column(
3064 updated_on = Column(
3061 'updated_on', DateTime(timezone=False), nullable=False,
3065 'updated_on', DateTime(timezone=False), nullable=False,
3062 default=datetime.datetime.now)
3066 default=datetime.datetime.now)
3063
3067
3064 @declared_attr
3068 @declared_attr
3065 def user_id(cls):
3069 def user_id(cls):
3066 return Column(
3070 return Column(
3067 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3071 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3068 unique=None)
3072 unique=None)
3069
3073
3070 # 500 revisions max
3074 # 500 revisions max
3071 _revisions = Column(
3075 _revisions = Column(
3072 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3076 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3073
3077
3074 @declared_attr
3078 @declared_attr
3075 def source_repo_id(cls):
3079 def source_repo_id(cls):
3076 # TODO: dan: rename column to source_repo_id
3080 # TODO: dan: rename column to source_repo_id
3077 return Column(
3081 return Column(
3078 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3082 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3079 nullable=False)
3083 nullable=False)
3080
3084
3081 source_ref = Column('org_ref', Unicode(255), nullable=False)
3085 source_ref = Column('org_ref', Unicode(255), nullable=False)
3082
3086
3083 @declared_attr
3087 @declared_attr
3084 def target_repo_id(cls):
3088 def target_repo_id(cls):
3085 # TODO: dan: rename column to target_repo_id
3089 # TODO: dan: rename column to target_repo_id
3086 return Column(
3090 return Column(
3087 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3091 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3088 nullable=False)
3092 nullable=False)
3089
3093
3090 target_ref = Column('other_ref', Unicode(255), nullable=False)
3094 target_ref = Column('other_ref', Unicode(255), nullable=False)
3091 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3095 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3092
3096
3093 # TODO: dan: rename column to last_merge_source_rev
3097 # TODO: dan: rename column to last_merge_source_rev
3094 _last_merge_source_rev = Column(
3098 _last_merge_source_rev = Column(
3095 'last_merge_org_rev', String(40), nullable=True)
3099 'last_merge_org_rev', String(40), nullable=True)
3096 # TODO: dan: rename column to last_merge_target_rev
3100 # TODO: dan: rename column to last_merge_target_rev
3097 _last_merge_target_rev = Column(
3101 _last_merge_target_rev = Column(
3098 'last_merge_other_rev', String(40), nullable=True)
3102 'last_merge_other_rev', String(40), nullable=True)
3099 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3103 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3100 merge_rev = Column('merge_rev', String(40), nullable=True)
3104 merge_rev = Column('merge_rev', String(40), nullable=True)
3101
3105
3102 @hybrid_property
3106 @hybrid_property
3103 def revisions(self):
3107 def revisions(self):
3104 return self._revisions.split(':') if self._revisions else []
3108 return self._revisions.split(':') if self._revisions else []
3105
3109
3106 @revisions.setter
3110 @revisions.setter
3107 def revisions(self, val):
3111 def revisions(self, val):
3108 self._revisions = ':'.join(val)
3112 self._revisions = ':'.join(val)
3109
3113
3110 @declared_attr
3114 @declared_attr
3111 def author(cls):
3115 def author(cls):
3112 return relationship('User', lazy='joined')
3116 return relationship('User', lazy='joined')
3113
3117
3114 @declared_attr
3118 @declared_attr
3115 def source_repo(cls):
3119 def source_repo(cls):
3116 return relationship(
3120 return relationship(
3117 'Repository',
3121 'Repository',
3118 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3122 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3119
3123
3120 @property
3124 @property
3121 def source_ref_parts(self):
3125 def source_ref_parts(self):
3122 return self.unicode_to_reference(self.source_ref)
3126 return self.unicode_to_reference(self.source_ref)
3123
3127
3124 @declared_attr
3128 @declared_attr
3125 def target_repo(cls):
3129 def target_repo(cls):
3126 return relationship(
3130 return relationship(
3127 'Repository',
3131 'Repository',
3128 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3132 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3129
3133
3130 @property
3134 @property
3131 def target_ref_parts(self):
3135 def target_ref_parts(self):
3132 return self.unicode_to_reference(self.target_ref)
3136 return self.unicode_to_reference(self.target_ref)
3133
3137
3134 @property
3138 @property
3135 def shadow_merge_ref(self):
3139 def shadow_merge_ref(self):
3136 return self.unicode_to_reference(self._shadow_merge_ref)
3140 return self.unicode_to_reference(self._shadow_merge_ref)
3137
3141
3138 @shadow_merge_ref.setter
3142 @shadow_merge_ref.setter
3139 def shadow_merge_ref(self, ref):
3143 def shadow_merge_ref(self, ref):
3140 self._shadow_merge_ref = self.reference_to_unicode(ref)
3144 self._shadow_merge_ref = self.reference_to_unicode(ref)
3141
3145
3142 def unicode_to_reference(self, raw):
3146 def unicode_to_reference(self, raw):
3143 """
3147 """
3144 Convert a unicode (or string) to a reference object.
3148 Convert a unicode (or string) to a reference object.
3145 If unicode evaluates to False it returns None.
3149 If unicode evaluates to False it returns None.
3146 """
3150 """
3147 if raw:
3151 if raw:
3148 refs = raw.split(':')
3152 refs = raw.split(':')
3149 return Reference(*refs)
3153 return Reference(*refs)
3150 else:
3154 else:
3151 return None
3155 return None
3152
3156
3153 def reference_to_unicode(self, ref):
3157 def reference_to_unicode(self, ref):
3154 """
3158 """
3155 Convert a reference object to unicode.
3159 Convert a reference object to unicode.
3156 If reference is None it returns None.
3160 If reference is None it returns None.
3157 """
3161 """
3158 if ref:
3162 if ref:
3159 return u':'.join(ref)
3163 return u':'.join(ref)
3160 else:
3164 else:
3161 return None
3165 return None
3162
3166
3163 def get_api_data(self):
3167 def get_api_data(self):
3164 from rhodecode.model.pull_request import PullRequestModel
3168 from rhodecode.model.pull_request import PullRequestModel
3165 pull_request = self
3169 pull_request = self
3166 merge_status = PullRequestModel().merge_status(pull_request)
3170 merge_status = PullRequestModel().merge_status(pull_request)
3167
3171
3168 pull_request_url = url(
3172 pull_request_url = url(
3169 'pullrequest_show', repo_name=self.target_repo.repo_name,
3173 'pullrequest_show', repo_name=self.target_repo.repo_name,
3170 pull_request_id=self.pull_request_id, qualified=True)
3174 pull_request_id=self.pull_request_id, qualified=True)
3171
3175
3172 merge_data = {
3176 merge_data = {
3173 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3177 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3174 'reference': (
3178 'reference': (
3175 pull_request.shadow_merge_ref._asdict()
3179 pull_request.shadow_merge_ref._asdict()
3176 if pull_request.shadow_merge_ref else None),
3180 if pull_request.shadow_merge_ref else None),
3177 }
3181 }
3178
3182
3179 data = {
3183 data = {
3180 'pull_request_id': pull_request.pull_request_id,
3184 'pull_request_id': pull_request.pull_request_id,
3181 'url': pull_request_url,
3185 'url': pull_request_url,
3182 'title': pull_request.title,
3186 'title': pull_request.title,
3183 'description': pull_request.description,
3187 'description': pull_request.description,
3184 'status': pull_request.status,
3188 'status': pull_request.status,
3185 'created_on': pull_request.created_on,
3189 'created_on': pull_request.created_on,
3186 'updated_on': pull_request.updated_on,
3190 'updated_on': pull_request.updated_on,
3187 'commit_ids': pull_request.revisions,
3191 'commit_ids': pull_request.revisions,
3188 'review_status': pull_request.calculated_review_status(),
3192 'review_status': pull_request.calculated_review_status(),
3189 'mergeable': {
3193 'mergeable': {
3190 'status': merge_status[0],
3194 'status': merge_status[0],
3191 'message': unicode(merge_status[1]),
3195 'message': unicode(merge_status[1]),
3192 },
3196 },
3193 'source': {
3197 'source': {
3194 'clone_url': pull_request.source_repo.clone_url(),
3198 'clone_url': pull_request.source_repo.clone_url(),
3195 'repository': pull_request.source_repo.repo_name,
3199 'repository': pull_request.source_repo.repo_name,
3196 'reference': {
3200 'reference': {
3197 'name': pull_request.source_ref_parts.name,
3201 'name': pull_request.source_ref_parts.name,
3198 'type': pull_request.source_ref_parts.type,
3202 'type': pull_request.source_ref_parts.type,
3199 'commit_id': pull_request.source_ref_parts.commit_id,
3203 'commit_id': pull_request.source_ref_parts.commit_id,
3200 },
3204 },
3201 },
3205 },
3202 'target': {
3206 'target': {
3203 'clone_url': pull_request.target_repo.clone_url(),
3207 'clone_url': pull_request.target_repo.clone_url(),
3204 'repository': pull_request.target_repo.repo_name,
3208 'repository': pull_request.target_repo.repo_name,
3205 'reference': {
3209 'reference': {
3206 'name': pull_request.target_ref_parts.name,
3210 'name': pull_request.target_ref_parts.name,
3207 'type': pull_request.target_ref_parts.type,
3211 'type': pull_request.target_ref_parts.type,
3208 'commit_id': pull_request.target_ref_parts.commit_id,
3212 'commit_id': pull_request.target_ref_parts.commit_id,
3209 },
3213 },
3210 },
3214 },
3211 'merge': merge_data,
3215 'merge': merge_data,
3212 'author': pull_request.author.get_api_data(include_secrets=False,
3216 'author': pull_request.author.get_api_data(include_secrets=False,
3213 details='basic'),
3217 details='basic'),
3214 'reviewers': [
3218 'reviewers': [
3215 {
3219 {
3216 'user': reviewer.get_api_data(include_secrets=False,
3220 'user': reviewer.get_api_data(include_secrets=False,
3217 details='basic'),
3221 details='basic'),
3218 'reasons': reasons,
3222 'reasons': reasons,
3219 'review_status': st[0][1].status if st else 'not_reviewed',
3223 'review_status': st[0][1].status if st else 'not_reviewed',
3220 }
3224 }
3221 for reviewer, reasons, st in pull_request.reviewers_statuses()
3225 for reviewer, reasons, st in pull_request.reviewers_statuses()
3222 ]
3226 ]
3223 }
3227 }
3224
3228
3225 return data
3229 return data
3226
3230
3227
3231
3228 class PullRequest(Base, _PullRequestBase):
3232 class PullRequest(Base, _PullRequestBase):
3229 __tablename__ = 'pull_requests'
3233 __tablename__ = 'pull_requests'
3230 __table_args__ = (
3234 __table_args__ = (
3231 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3235 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3232 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3236 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3233 )
3237 )
3234
3238
3235 pull_request_id = Column(
3239 pull_request_id = Column(
3236 'pull_request_id', Integer(), nullable=False, primary_key=True)
3240 'pull_request_id', Integer(), nullable=False, primary_key=True)
3237
3241
3238 def __repr__(self):
3242 def __repr__(self):
3239 if self.pull_request_id:
3243 if self.pull_request_id:
3240 return '<DB:PullRequest #%s>' % self.pull_request_id
3244 return '<DB:PullRequest #%s>' % self.pull_request_id
3241 else:
3245 else:
3242 return '<DB:PullRequest at %#x>' % id(self)
3246 return '<DB:PullRequest at %#x>' % id(self)
3243
3247
3244 reviewers = relationship('PullRequestReviewers',
3248 reviewers = relationship('PullRequestReviewers',
3245 cascade="all, delete, delete-orphan")
3249 cascade="all, delete, delete-orphan")
3246 statuses = relationship('ChangesetStatus')
3250 statuses = relationship('ChangesetStatus')
3247 comments = relationship('ChangesetComment',
3251 comments = relationship('ChangesetComment',
3248 cascade="all, delete, delete-orphan")
3252 cascade="all, delete, delete-orphan")
3249 versions = relationship('PullRequestVersion',
3253 versions = relationship('PullRequestVersion',
3250 cascade="all, delete, delete-orphan",
3254 cascade="all, delete, delete-orphan",
3251 lazy='dynamic')
3255 lazy='dynamic')
3252
3256
3253
3257
3254 @classmethod
3258 @classmethod
3255 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3259 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3256 internal_methods=None):
3260 internal_methods=None):
3257
3261
3258 class PullRequestDisplay(object):
3262 class PullRequestDisplay(object):
3259 """
3263 """
3260 Special object wrapper for showing PullRequest data via Versions
3264 Special object wrapper for showing PullRequest data via Versions
3261 It mimics PR object as close as possible. This is read only object
3265 It mimics PR object as close as possible. This is read only object
3262 just for display
3266 just for display
3263 """
3267 """
3264
3268
3265 def __init__(self, attrs, internal=None):
3269 def __init__(self, attrs, internal=None):
3266 self.attrs = attrs
3270 self.attrs = attrs
3267 # internal have priority over the given ones via attrs
3271 # internal have priority over the given ones via attrs
3268 self.internal = internal or ['versions']
3272 self.internal = internal or ['versions']
3269
3273
3270 def __getattr__(self, item):
3274 def __getattr__(self, item):
3271 if item in self.internal:
3275 if item in self.internal:
3272 return getattr(self, item)
3276 return getattr(self, item)
3273 try:
3277 try:
3274 return self.attrs[item]
3278 return self.attrs[item]
3275 except KeyError:
3279 except KeyError:
3276 raise AttributeError(
3280 raise AttributeError(
3277 '%s object has no attribute %s' % (self, item))
3281 '%s object has no attribute %s' % (self, item))
3278
3282
3279 def __repr__(self):
3283 def __repr__(self):
3280 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3284 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3281
3285
3282 def versions(self):
3286 def versions(self):
3283 return pull_request_obj.versions.order_by(
3287 return pull_request_obj.versions.order_by(
3284 PullRequestVersion.pull_request_version_id).all()
3288 PullRequestVersion.pull_request_version_id).all()
3285
3289
3286 def is_closed(self):
3290 def is_closed(self):
3287 return pull_request_obj.is_closed()
3291 return pull_request_obj.is_closed()
3288
3292
3289 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3293 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3290
3294
3291 attrs.author = StrictAttributeDict(
3295 attrs.author = StrictAttributeDict(
3292 pull_request_obj.author.get_api_data())
3296 pull_request_obj.author.get_api_data())
3293 if pull_request_obj.target_repo:
3297 if pull_request_obj.target_repo:
3294 attrs.target_repo = StrictAttributeDict(
3298 attrs.target_repo = StrictAttributeDict(
3295 pull_request_obj.target_repo.get_api_data())
3299 pull_request_obj.target_repo.get_api_data())
3296 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3300 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3297
3301
3298 if pull_request_obj.source_repo:
3302 if pull_request_obj.source_repo:
3299 attrs.source_repo = StrictAttributeDict(
3303 attrs.source_repo = StrictAttributeDict(
3300 pull_request_obj.source_repo.get_api_data())
3304 pull_request_obj.source_repo.get_api_data())
3301 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3305 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3302
3306
3303 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3307 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3304 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3308 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3305 attrs.revisions = pull_request_obj.revisions
3309 attrs.revisions = pull_request_obj.revisions
3306
3310
3307 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3311 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3308
3312
3309 return PullRequestDisplay(attrs, internal=internal_methods)
3313 return PullRequestDisplay(attrs, internal=internal_methods)
3310
3314
3311 def is_closed(self):
3315 def is_closed(self):
3312 return self.status == self.STATUS_CLOSED
3316 return self.status == self.STATUS_CLOSED
3313
3317
3314 def __json__(self):
3318 def __json__(self):
3315 return {
3319 return {
3316 'revisions': self.revisions,
3320 'revisions': self.revisions,
3317 }
3321 }
3318
3322
3319 def calculated_review_status(self):
3323 def calculated_review_status(self):
3320 from rhodecode.model.changeset_status import ChangesetStatusModel
3324 from rhodecode.model.changeset_status import ChangesetStatusModel
3321 return ChangesetStatusModel().calculated_review_status(self)
3325 return ChangesetStatusModel().calculated_review_status(self)
3322
3326
3323 def reviewers_statuses(self):
3327 def reviewers_statuses(self):
3324 from rhodecode.model.changeset_status import ChangesetStatusModel
3328 from rhodecode.model.changeset_status import ChangesetStatusModel
3325 return ChangesetStatusModel().reviewers_statuses(self)
3329 return ChangesetStatusModel().reviewers_statuses(self)
3326
3330
3327
3331
3328 class PullRequestVersion(Base, _PullRequestBase):
3332 class PullRequestVersion(Base, _PullRequestBase):
3329 __tablename__ = 'pull_request_versions'
3333 __tablename__ = 'pull_request_versions'
3330 __table_args__ = (
3334 __table_args__ = (
3331 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3335 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3332 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3336 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3333 )
3337 )
3334
3338
3335 pull_request_version_id = Column(
3339 pull_request_version_id = Column(
3336 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3340 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3337 pull_request_id = Column(
3341 pull_request_id = Column(
3338 'pull_request_id', Integer(),
3342 'pull_request_id', Integer(),
3339 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3343 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3340 pull_request = relationship('PullRequest')
3344 pull_request = relationship('PullRequest')
3341
3345
3342 def __repr__(self):
3346 def __repr__(self):
3343 if self.pull_request_version_id:
3347 if self.pull_request_version_id:
3344 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3348 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3345 else:
3349 else:
3346 return '<DB:PullRequestVersion at %#x>' % id(self)
3350 return '<DB:PullRequestVersion at %#x>' % id(self)
3347
3351
3348 @property
3352 @property
3349 def reviewers(self):
3353 def reviewers(self):
3350 return self.pull_request.reviewers
3354 return self.pull_request.reviewers
3351
3355
3352 @property
3356 @property
3353 def versions(self):
3357 def versions(self):
3354 return self.pull_request.versions
3358 return self.pull_request.versions
3355
3359
3356 def is_closed(self):
3360 def is_closed(self):
3357 # calculate from original
3361 # calculate from original
3358 return self.pull_request.status == self.STATUS_CLOSED
3362 return self.pull_request.status == self.STATUS_CLOSED
3359
3363
3360 def calculated_review_status(self):
3364 def calculated_review_status(self):
3361 return self.pull_request.calculated_review_status()
3365 return self.pull_request.calculated_review_status()
3362
3366
3363 def reviewers_statuses(self):
3367 def reviewers_statuses(self):
3364 return self.pull_request.reviewers_statuses()
3368 return self.pull_request.reviewers_statuses()
3365
3369
3366
3370
3367 class PullRequestReviewers(Base, BaseModel):
3371 class PullRequestReviewers(Base, BaseModel):
3368 __tablename__ = 'pull_request_reviewers'
3372 __tablename__ = 'pull_request_reviewers'
3369 __table_args__ = (
3373 __table_args__ = (
3370 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3374 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3371 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3375 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3372 )
3376 )
3373
3377
3374 def __init__(self, user=None, pull_request=None, reasons=None):
3378 def __init__(self, user=None, pull_request=None, reasons=None):
3375 self.user = user
3379 self.user = user
3376 self.pull_request = pull_request
3380 self.pull_request = pull_request
3377 self.reasons = reasons or []
3381 self.reasons = reasons or []
3378
3382
3379 @hybrid_property
3383 @hybrid_property
3380 def reasons(self):
3384 def reasons(self):
3381 if not self._reasons:
3385 if not self._reasons:
3382 return []
3386 return []
3383 return self._reasons
3387 return self._reasons
3384
3388
3385 @reasons.setter
3389 @reasons.setter
3386 def reasons(self, val):
3390 def reasons(self, val):
3387 val = val or []
3391 val = val or []
3388 if any(not isinstance(x, basestring) for x in val):
3392 if any(not isinstance(x, basestring) for x in val):
3389 raise Exception('invalid reasons type, must be list of strings')
3393 raise Exception('invalid reasons type, must be list of strings')
3390 self._reasons = val
3394 self._reasons = val
3391
3395
3392 pull_requests_reviewers_id = Column(
3396 pull_requests_reviewers_id = Column(
3393 'pull_requests_reviewers_id', Integer(), nullable=False,
3397 'pull_requests_reviewers_id', Integer(), nullable=False,
3394 primary_key=True)
3398 primary_key=True)
3395 pull_request_id = Column(
3399 pull_request_id = Column(
3396 "pull_request_id", Integer(),
3400 "pull_request_id", Integer(),
3397 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3401 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3398 user_id = Column(
3402 user_id = Column(
3399 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3403 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3400 _reasons = Column(
3404 _reasons = Column(
3401 'reason', MutationList.as_mutable(
3405 'reason', MutationList.as_mutable(
3402 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3406 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3403
3407
3404 user = relationship('User')
3408 user = relationship('User')
3405 pull_request = relationship('PullRequest')
3409 pull_request = relationship('PullRequest')
3406
3410
3407
3411
3408 class Notification(Base, BaseModel):
3412 class Notification(Base, BaseModel):
3409 __tablename__ = 'notifications'
3413 __tablename__ = 'notifications'
3410 __table_args__ = (
3414 __table_args__ = (
3411 Index('notification_type_idx', 'type'),
3415 Index('notification_type_idx', 'type'),
3412 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3416 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3413 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3417 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3414 )
3418 )
3415
3419
3416 TYPE_CHANGESET_COMMENT = u'cs_comment'
3420 TYPE_CHANGESET_COMMENT = u'cs_comment'
3417 TYPE_MESSAGE = u'message'
3421 TYPE_MESSAGE = u'message'
3418 TYPE_MENTION = u'mention'
3422 TYPE_MENTION = u'mention'
3419 TYPE_REGISTRATION = u'registration'
3423 TYPE_REGISTRATION = u'registration'
3420 TYPE_PULL_REQUEST = u'pull_request'
3424 TYPE_PULL_REQUEST = u'pull_request'
3421 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3425 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3422
3426
3423 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3427 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3424 subject = Column('subject', Unicode(512), nullable=True)
3428 subject = Column('subject', Unicode(512), nullable=True)
3425 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3429 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3426 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3430 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3427 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3431 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3428 type_ = Column('type', Unicode(255))
3432 type_ = Column('type', Unicode(255))
3429
3433
3430 created_by_user = relationship('User')
3434 created_by_user = relationship('User')
3431 notifications_to_users = relationship('UserNotification', lazy='joined',
3435 notifications_to_users = relationship('UserNotification', lazy='joined',
3432 cascade="all, delete, delete-orphan")
3436 cascade="all, delete, delete-orphan")
3433
3437
3434 @property
3438 @property
3435 def recipients(self):
3439 def recipients(self):
3436 return [x.user for x in UserNotification.query()\
3440 return [x.user for x in UserNotification.query()\
3437 .filter(UserNotification.notification == self)\
3441 .filter(UserNotification.notification == self)\
3438 .order_by(UserNotification.user_id.asc()).all()]
3442 .order_by(UserNotification.user_id.asc()).all()]
3439
3443
3440 @classmethod
3444 @classmethod
3441 def create(cls, created_by, subject, body, recipients, type_=None):
3445 def create(cls, created_by, subject, body, recipients, type_=None):
3442 if type_ is None:
3446 if type_ is None:
3443 type_ = Notification.TYPE_MESSAGE
3447 type_ = Notification.TYPE_MESSAGE
3444
3448
3445 notification = cls()
3449 notification = cls()
3446 notification.created_by_user = created_by
3450 notification.created_by_user = created_by
3447 notification.subject = subject
3451 notification.subject = subject
3448 notification.body = body
3452 notification.body = body
3449 notification.type_ = type_
3453 notification.type_ = type_
3450 notification.created_on = datetime.datetime.now()
3454 notification.created_on = datetime.datetime.now()
3451
3455
3452 for u in recipients:
3456 for u in recipients:
3453 assoc = UserNotification()
3457 assoc = UserNotification()
3454 assoc.notification = notification
3458 assoc.notification = notification
3455
3459
3456 # if created_by is inside recipients mark his notification
3460 # if created_by is inside recipients mark his notification
3457 # as read
3461 # as read
3458 if u.user_id == created_by.user_id:
3462 if u.user_id == created_by.user_id:
3459 assoc.read = True
3463 assoc.read = True
3460
3464
3461 u.notifications.append(assoc)
3465 u.notifications.append(assoc)
3462 Session().add(notification)
3466 Session().add(notification)
3463
3467
3464 return notification
3468 return notification
3465
3469
3466 @property
3470 @property
3467 def description(self):
3471 def description(self):
3468 from rhodecode.model.notification import NotificationModel
3472 from rhodecode.model.notification import NotificationModel
3469 return NotificationModel().make_description(self)
3473 return NotificationModel().make_description(self)
3470
3474
3471
3475
3472 class UserNotification(Base, BaseModel):
3476 class UserNotification(Base, BaseModel):
3473 __tablename__ = 'user_to_notification'
3477 __tablename__ = 'user_to_notification'
3474 __table_args__ = (
3478 __table_args__ = (
3475 UniqueConstraint('user_id', 'notification_id'),
3479 UniqueConstraint('user_id', 'notification_id'),
3476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3480 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3481 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3478 )
3482 )
3479 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3483 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3480 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3484 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3481 read = Column('read', Boolean, default=False)
3485 read = Column('read', Boolean, default=False)
3482 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3486 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3483
3487
3484 user = relationship('User', lazy="joined")
3488 user = relationship('User', lazy="joined")
3485 notification = relationship('Notification', lazy="joined",
3489 notification = relationship('Notification', lazy="joined",
3486 order_by=lambda: Notification.created_on.desc(),)
3490 order_by=lambda: Notification.created_on.desc(),)
3487
3491
3488 def mark_as_read(self):
3492 def mark_as_read(self):
3489 self.read = True
3493 self.read = True
3490 Session().add(self)
3494 Session().add(self)
3491
3495
3492
3496
3493 class Gist(Base, BaseModel):
3497 class Gist(Base, BaseModel):
3494 __tablename__ = 'gists'
3498 __tablename__ = 'gists'
3495 __table_args__ = (
3499 __table_args__ = (
3496 Index('g_gist_access_id_idx', 'gist_access_id'),
3500 Index('g_gist_access_id_idx', 'gist_access_id'),
3497 Index('g_created_on_idx', 'created_on'),
3501 Index('g_created_on_idx', 'created_on'),
3498 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3499 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3500 )
3504 )
3501 GIST_PUBLIC = u'public'
3505 GIST_PUBLIC = u'public'
3502 GIST_PRIVATE = u'private'
3506 GIST_PRIVATE = u'private'
3503 DEFAULT_FILENAME = u'gistfile1.txt'
3507 DEFAULT_FILENAME = u'gistfile1.txt'
3504
3508
3505 ACL_LEVEL_PUBLIC = u'acl_public'
3509 ACL_LEVEL_PUBLIC = u'acl_public'
3506 ACL_LEVEL_PRIVATE = u'acl_private'
3510 ACL_LEVEL_PRIVATE = u'acl_private'
3507
3511
3508 gist_id = Column('gist_id', Integer(), primary_key=True)
3512 gist_id = Column('gist_id', Integer(), primary_key=True)
3509 gist_access_id = Column('gist_access_id', Unicode(250))
3513 gist_access_id = Column('gist_access_id', Unicode(250))
3510 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3514 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3511 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3515 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3512 gist_expires = Column('gist_expires', Float(53), nullable=False)
3516 gist_expires = Column('gist_expires', Float(53), nullable=False)
3513 gist_type = Column('gist_type', Unicode(128), nullable=False)
3517 gist_type = Column('gist_type', Unicode(128), nullable=False)
3514 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3518 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3515 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3519 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3516 acl_level = Column('acl_level', Unicode(128), nullable=True)
3520 acl_level = Column('acl_level', Unicode(128), nullable=True)
3517
3521
3518 owner = relationship('User')
3522 owner = relationship('User')
3519
3523
3520 def __repr__(self):
3524 def __repr__(self):
3521 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3525 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3522
3526
3523 @classmethod
3527 @classmethod
3524 def get_or_404(cls, id_):
3528 def get_or_404(cls, id_):
3525 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3529 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3526 if not res:
3530 if not res:
3527 raise HTTPNotFound
3531 raise HTTPNotFound
3528 return res
3532 return res
3529
3533
3530 @classmethod
3534 @classmethod
3531 def get_by_access_id(cls, gist_access_id):
3535 def get_by_access_id(cls, gist_access_id):
3532 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3536 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3533
3537
3534 def gist_url(self):
3538 def gist_url(self):
3535 import rhodecode
3539 import rhodecode
3536 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3540 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3537 if alias_url:
3541 if alias_url:
3538 return alias_url.replace('{gistid}', self.gist_access_id)
3542 return alias_url.replace('{gistid}', self.gist_access_id)
3539
3543
3540 return url('gist', gist_id=self.gist_access_id, qualified=True)
3544 return url('gist', gist_id=self.gist_access_id, qualified=True)
3541
3545
3542 @classmethod
3546 @classmethod
3543 def base_path(cls):
3547 def base_path(cls):
3544 """
3548 """
3545 Returns base path when all gists are stored
3549 Returns base path when all gists are stored
3546
3550
3547 :param cls:
3551 :param cls:
3548 """
3552 """
3549 from rhodecode.model.gist import GIST_STORE_LOC
3553 from rhodecode.model.gist import GIST_STORE_LOC
3550 q = Session().query(RhodeCodeUi)\
3554 q = Session().query(RhodeCodeUi)\
3551 .filter(RhodeCodeUi.ui_key == URL_SEP)
3555 .filter(RhodeCodeUi.ui_key == URL_SEP)
3552 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3556 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3553 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3557 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3554
3558
3555 def get_api_data(self):
3559 def get_api_data(self):
3556 """
3560 """
3557 Common function for generating gist related data for API
3561 Common function for generating gist related data for API
3558 """
3562 """
3559 gist = self
3563 gist = self
3560 data = {
3564 data = {
3561 'gist_id': gist.gist_id,
3565 'gist_id': gist.gist_id,
3562 'type': gist.gist_type,
3566 'type': gist.gist_type,
3563 'access_id': gist.gist_access_id,
3567 'access_id': gist.gist_access_id,
3564 'description': gist.gist_description,
3568 'description': gist.gist_description,
3565 'url': gist.gist_url(),
3569 'url': gist.gist_url(),
3566 'expires': gist.gist_expires,
3570 'expires': gist.gist_expires,
3567 'created_on': gist.created_on,
3571 'created_on': gist.created_on,
3568 'modified_at': gist.modified_at,
3572 'modified_at': gist.modified_at,
3569 'content': None,
3573 'content': None,
3570 'acl_level': gist.acl_level,
3574 'acl_level': gist.acl_level,
3571 }
3575 }
3572 return data
3576 return data
3573
3577
3574 def __json__(self):
3578 def __json__(self):
3575 data = dict(
3579 data = dict(
3576 )
3580 )
3577 data.update(self.get_api_data())
3581 data.update(self.get_api_data())
3578 return data
3582 return data
3579 # SCM functions
3583 # SCM functions
3580
3584
3581 def scm_instance(self, **kwargs):
3585 def scm_instance(self, **kwargs):
3582 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3586 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3583 return get_vcs_instance(
3587 return get_vcs_instance(
3584 repo_path=safe_str(full_repo_path), create=False)
3588 repo_path=safe_str(full_repo_path), create=False)
3585
3589
3586
3590
3587 class ExternalIdentity(Base, BaseModel):
3591 class ExternalIdentity(Base, BaseModel):
3588 __tablename__ = 'external_identities'
3592 __tablename__ = 'external_identities'
3589 __table_args__ = (
3593 __table_args__ = (
3590 Index('local_user_id_idx', 'local_user_id'),
3594 Index('local_user_id_idx', 'local_user_id'),
3591 Index('external_id_idx', 'external_id'),
3595 Index('external_id_idx', 'external_id'),
3592 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3596 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3593 'mysql_charset': 'utf8'})
3597 'mysql_charset': 'utf8'})
3594
3598
3595 external_id = Column('external_id', Unicode(255), default=u'',
3599 external_id = Column('external_id', Unicode(255), default=u'',
3596 primary_key=True)
3600 primary_key=True)
3597 external_username = Column('external_username', Unicode(1024), default=u'')
3601 external_username = Column('external_username', Unicode(1024), default=u'')
3598 local_user_id = Column('local_user_id', Integer(),
3602 local_user_id = Column('local_user_id', Integer(),
3599 ForeignKey('users.user_id'), primary_key=True)
3603 ForeignKey('users.user_id'), primary_key=True)
3600 provider_name = Column('provider_name', Unicode(255), default=u'',
3604 provider_name = Column('provider_name', Unicode(255), default=u'',
3601 primary_key=True)
3605 primary_key=True)
3602 access_token = Column('access_token', String(1024), default=u'')
3606 access_token = Column('access_token', String(1024), default=u'')
3603 alt_token = Column('alt_token', String(1024), default=u'')
3607 alt_token = Column('alt_token', String(1024), default=u'')
3604 token_secret = Column('token_secret', String(1024), default=u'')
3608 token_secret = Column('token_secret', String(1024), default=u'')
3605
3609
3606 @classmethod
3610 @classmethod
3607 def by_external_id_and_provider(cls, external_id, provider_name,
3611 def by_external_id_and_provider(cls, external_id, provider_name,
3608 local_user_id=None):
3612 local_user_id=None):
3609 """
3613 """
3610 Returns ExternalIdentity instance based on search params
3614 Returns ExternalIdentity instance based on search params
3611
3615
3612 :param external_id:
3616 :param external_id:
3613 :param provider_name:
3617 :param provider_name:
3614 :return: ExternalIdentity
3618 :return: ExternalIdentity
3615 """
3619 """
3616 query = cls.query()
3620 query = cls.query()
3617 query = query.filter(cls.external_id == external_id)
3621 query = query.filter(cls.external_id == external_id)
3618 query = query.filter(cls.provider_name == provider_name)
3622 query = query.filter(cls.provider_name == provider_name)
3619 if local_user_id:
3623 if local_user_id:
3620 query = query.filter(cls.local_user_id == local_user_id)
3624 query = query.filter(cls.local_user_id == local_user_id)
3621 return query.first()
3625 return query.first()
3622
3626
3623 @classmethod
3627 @classmethod
3624 def user_by_external_id_and_provider(cls, external_id, provider_name):
3628 def user_by_external_id_and_provider(cls, external_id, provider_name):
3625 """
3629 """
3626 Returns User instance based on search params
3630 Returns User instance based on search params
3627
3631
3628 :param external_id:
3632 :param external_id:
3629 :param provider_name:
3633 :param provider_name:
3630 :return: User
3634 :return: User
3631 """
3635 """
3632 query = User.query()
3636 query = User.query()
3633 query = query.filter(cls.external_id == external_id)
3637 query = query.filter(cls.external_id == external_id)
3634 query = query.filter(cls.provider_name == provider_name)
3638 query = query.filter(cls.provider_name == provider_name)
3635 query = query.filter(User.user_id == cls.local_user_id)
3639 query = query.filter(User.user_id == cls.local_user_id)
3636 return query.first()
3640 return query.first()
3637
3641
3638 @classmethod
3642 @classmethod
3639 def by_local_user_id(cls, local_user_id):
3643 def by_local_user_id(cls, local_user_id):
3640 """
3644 """
3641 Returns all tokens for user
3645 Returns all tokens for user
3642
3646
3643 :param local_user_id:
3647 :param local_user_id:
3644 :return: ExternalIdentity
3648 :return: ExternalIdentity
3645 """
3649 """
3646 query = cls.query()
3650 query = cls.query()
3647 query = query.filter(cls.local_user_id == local_user_id)
3651 query = query.filter(cls.local_user_id == local_user_id)
3648 return query
3652 return query
3649
3653
3650
3654
3651 class Integration(Base, BaseModel):
3655 class Integration(Base, BaseModel):
3652 __tablename__ = 'integrations'
3656 __tablename__ = 'integrations'
3653 __table_args__ = (
3657 __table_args__ = (
3654 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3658 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3655 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3659 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3656 )
3660 )
3657
3661
3658 integration_id = Column('integration_id', Integer(), primary_key=True)
3662 integration_id = Column('integration_id', Integer(), primary_key=True)
3659 integration_type = Column('integration_type', String(255))
3663 integration_type = Column('integration_type', String(255))
3660 enabled = Column('enabled', Boolean(), nullable=False)
3664 enabled = Column('enabled', Boolean(), nullable=False)
3661 name = Column('name', String(255), nullable=False)
3665 name = Column('name', String(255), nullable=False)
3662 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3666 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3663 default=False)
3667 default=False)
3664
3668
3665 settings = Column(
3669 settings = Column(
3666 'settings_json', MutationObj.as_mutable(
3670 'settings_json', MutationObj.as_mutable(
3667 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3671 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3668 repo_id = Column(
3672 repo_id = Column(
3669 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3673 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3670 nullable=True, unique=None, default=None)
3674 nullable=True, unique=None, default=None)
3671 repo = relationship('Repository', lazy='joined')
3675 repo = relationship('Repository', lazy='joined')
3672
3676
3673 repo_group_id = Column(
3677 repo_group_id = Column(
3674 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3678 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3675 nullable=True, unique=None, default=None)
3679 nullable=True, unique=None, default=None)
3676 repo_group = relationship('RepoGroup', lazy='joined')
3680 repo_group = relationship('RepoGroup', lazy='joined')
3677
3681
3678 @property
3682 @property
3679 def scope(self):
3683 def scope(self):
3680 if self.repo:
3684 if self.repo:
3681 return repr(self.repo)
3685 return repr(self.repo)
3682 if self.repo_group:
3686 if self.repo_group:
3683 if self.child_repos_only:
3687 if self.child_repos_only:
3684 return repr(self.repo_group) + ' (child repos only)'
3688 return repr(self.repo_group) + ' (child repos only)'
3685 else:
3689 else:
3686 return repr(self.repo_group) + ' (recursive)'
3690 return repr(self.repo_group) + ' (recursive)'
3687 if self.child_repos_only:
3691 if self.child_repos_only:
3688 return 'root_repos'
3692 return 'root_repos'
3689 return 'global'
3693 return 'global'
3690
3694
3691 def __repr__(self):
3695 def __repr__(self):
3692 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3696 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3693
3697
3694
3698
3695 class RepoReviewRuleUser(Base, BaseModel):
3699 class RepoReviewRuleUser(Base, BaseModel):
3696 __tablename__ = 'repo_review_rules_users'
3700 __tablename__ = 'repo_review_rules_users'
3697 __table_args__ = (
3701 __table_args__ = (
3698 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3702 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3699 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3703 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3700 )
3704 )
3701 repo_review_rule_user_id = Column(
3705 repo_review_rule_user_id = Column(
3702 'repo_review_rule_user_id', Integer(), primary_key=True)
3706 'repo_review_rule_user_id', Integer(), primary_key=True)
3703 repo_review_rule_id = Column("repo_review_rule_id",
3707 repo_review_rule_id = Column("repo_review_rule_id",
3704 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3708 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3705 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3709 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3706 nullable=False)
3710 nullable=False)
3707 user = relationship('User')
3711 user = relationship('User')
3708
3712
3709
3713
3710 class RepoReviewRuleUserGroup(Base, BaseModel):
3714 class RepoReviewRuleUserGroup(Base, BaseModel):
3711 __tablename__ = 'repo_review_rules_users_groups'
3715 __tablename__ = 'repo_review_rules_users_groups'
3712 __table_args__ = (
3716 __table_args__ = (
3713 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3717 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3714 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3718 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3715 )
3719 )
3716 repo_review_rule_users_group_id = Column(
3720 repo_review_rule_users_group_id = Column(
3717 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3721 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3718 repo_review_rule_id = Column("repo_review_rule_id",
3722 repo_review_rule_id = Column("repo_review_rule_id",
3719 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3723 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3720 users_group_id = Column("users_group_id", Integer(),
3724 users_group_id = Column("users_group_id", Integer(),
3721 ForeignKey('users_groups.users_group_id'), nullable=False)
3725 ForeignKey('users_groups.users_group_id'), nullable=False)
3722 users_group = relationship('UserGroup')
3726 users_group = relationship('UserGroup')
3723
3727
3724
3728
3725 class RepoReviewRule(Base, BaseModel):
3729 class RepoReviewRule(Base, BaseModel):
3726 __tablename__ = 'repo_review_rules'
3730 __tablename__ = 'repo_review_rules'
3727 __table_args__ = (
3731 __table_args__ = (
3728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3732 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3733 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3730 )
3734 )
3731
3735
3732 repo_review_rule_id = Column(
3736 repo_review_rule_id = Column(
3733 'repo_review_rule_id', Integer(), primary_key=True)
3737 'repo_review_rule_id', Integer(), primary_key=True)
3734 repo_id = Column(
3738 repo_id = Column(
3735 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3739 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3736 repo = relationship('Repository', backref='review_rules')
3740 repo = relationship('Repository', backref='review_rules')
3737
3741
3738 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3742 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3739 default=u'*') # glob
3743 default=u'*') # glob
3740 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3744 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3741 default=u'*') # glob
3745 default=u'*') # glob
3742
3746
3743 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3747 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3744 nullable=False, default=False)
3748 nullable=False, default=False)
3745 rule_users = relationship('RepoReviewRuleUser')
3749 rule_users = relationship('RepoReviewRuleUser')
3746 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3750 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3747
3751
3748 @hybrid_property
3752 @hybrid_property
3749 def branch_pattern(self):
3753 def branch_pattern(self):
3750 return self._branch_pattern or '*'
3754 return self._branch_pattern or '*'
3751
3755
3752 def _validate_glob(self, value):
3756 def _validate_glob(self, value):
3753 re.compile('^' + glob2re(value) + '$')
3757 re.compile('^' + glob2re(value) + '$')
3754
3758
3755 @branch_pattern.setter
3759 @branch_pattern.setter
3756 def branch_pattern(self, value):
3760 def branch_pattern(self, value):
3757 self._validate_glob(value)
3761 self._validate_glob(value)
3758 self._branch_pattern = value or '*'
3762 self._branch_pattern = value or '*'
3759
3763
3760 @hybrid_property
3764 @hybrid_property
3761 def file_pattern(self):
3765 def file_pattern(self):
3762 return self._file_pattern or '*'
3766 return self._file_pattern or '*'
3763
3767
3764 @file_pattern.setter
3768 @file_pattern.setter
3765 def file_pattern(self, value):
3769 def file_pattern(self, value):
3766 self._validate_glob(value)
3770 self._validate_glob(value)
3767 self._file_pattern = value or '*'
3771 self._file_pattern = value or '*'
3768
3772
3769 def matches(self, branch, files_changed):
3773 def matches(self, branch, files_changed):
3770 """
3774 """
3771 Check if this review rule matches a branch/files in a pull request
3775 Check if this review rule matches a branch/files in a pull request
3772
3776
3773 :param branch: branch name for the commit
3777 :param branch: branch name for the commit
3774 :param files_changed: list of file paths changed in the pull request
3778 :param files_changed: list of file paths changed in the pull request
3775 """
3779 """
3776
3780
3777 branch = branch or ''
3781 branch = branch or ''
3778 files_changed = files_changed or []
3782 files_changed = files_changed or []
3779
3783
3780 branch_matches = True
3784 branch_matches = True
3781 if branch:
3785 if branch:
3782 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3786 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3783 branch_matches = bool(branch_regex.search(branch))
3787 branch_matches = bool(branch_regex.search(branch))
3784
3788
3785 files_matches = True
3789 files_matches = True
3786 if self.file_pattern != '*':
3790 if self.file_pattern != '*':
3787 files_matches = False
3791 files_matches = False
3788 file_regex = re.compile(glob2re(self.file_pattern))
3792 file_regex = re.compile(glob2re(self.file_pattern))
3789 for filename in files_changed:
3793 for filename in files_changed:
3790 if file_regex.search(filename):
3794 if file_regex.search(filename):
3791 files_matches = True
3795 files_matches = True
3792 break
3796 break
3793
3797
3794 return branch_matches and files_matches
3798 return branch_matches and files_matches
3795
3799
3796 @property
3800 @property
3797 def review_users(self):
3801 def review_users(self):
3798 """ Returns the users which this rule applies to """
3802 """ Returns the users which this rule applies to """
3799
3803
3800 users = set()
3804 users = set()
3801 users |= set([
3805 users |= set([
3802 rule_user.user for rule_user in self.rule_users
3806 rule_user.user for rule_user in self.rule_users
3803 if rule_user.user.active])
3807 if rule_user.user.active])
3804 users |= set(
3808 users |= set(
3805 member.user
3809 member.user
3806 for rule_user_group in self.rule_user_groups
3810 for rule_user_group in self.rule_user_groups
3807 for member in rule_user_group.users_group.members
3811 for member in rule_user_group.users_group.members
3808 if member.user.active
3812 if member.user.active
3809 )
3813 )
3810 return users
3814 return users
3811
3815
3812 def __repr__(self):
3816 def __repr__(self):
3813 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3817 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3814 self.repo_review_rule_id, self.repo)
3818 self.repo_review_rule_id, self.repo)
3815
3819
3816
3820
3817 class DbMigrateVersion(Base, BaseModel):
3821 class DbMigrateVersion(Base, BaseModel):
3818 __tablename__ = 'db_migrate_version'
3822 __tablename__ = 'db_migrate_version'
3819 __table_args__ = (
3823 __table_args__ = (
3820 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3824 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3821 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3825 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3822 )
3826 )
3823 repository_id = Column('repository_id', String(250), primary_key=True)
3827 repository_id = Column('repository_id', String(250), primary_key=True)
3824 repository_path = Column('repository_path', Text)
3828 repository_path = Column('repository_path', Text)
3825 version = Column('version', Integer)
3829 version = Column('version', Integer)
3826
3830
3827
3831
3828 class DbSession(Base, BaseModel):
3832 class DbSession(Base, BaseModel):
3829 __tablename__ = 'db_session'
3833 __tablename__ = 'db_session'
3830 __table_args__ = (
3834 __table_args__ = (
3831 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3835 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3832 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3836 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3833 )
3837 )
3834
3838
3835 def __repr__(self):
3839 def __repr__(self):
3836 return '<DB:DbSession({})>'.format(self.id)
3840 return '<DB:DbSession({})>'.format(self.id)
3837
3841
3838 id = Column('id', Integer())
3842 id = Column('id', Integer())
3839 namespace = Column('namespace', String(255), primary_key=True)
3843 namespace = Column('namespace', String(255), primary_key=True)
3840 accessed = Column('accessed', DateTime, nullable=False)
3844 accessed = Column('accessed', DateTime, nullable=False)
3841 created = Column('created', DateTime, nullable=False)
3845 created = Column('created', DateTime, nullable=False)
3842 data = Column('data', PickleType, nullable=False)
3846 data = Column('data', PickleType, nullable=False)
@@ -1,2223 +1,2257 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'fonts';
8 @import 'fonts';
9 @import 'variables';
9 @import 'variables';
10 @import 'bootstrap-variables';
10 @import 'bootstrap-variables';
11 @import 'form-bootstrap';
11 @import 'form-bootstrap';
12 @import 'codemirror';
12 @import 'codemirror';
13 @import 'legacy_code_styles';
13 @import 'legacy_code_styles';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29
29
30 //--- BASE ------------------//
30 //--- BASE ------------------//
31 .noscript-error {
31 .noscript-error {
32 top: 0;
32 top: 0;
33 left: 0;
33 left: 0;
34 width: 100%;
34 width: 100%;
35 z-index: 101;
35 z-index: 101;
36 text-align: center;
36 text-align: center;
37 font-family: @text-semibold;
37 font-family: @text-semibold;
38 font-size: 120%;
38 font-size: 120%;
39 color: white;
39 color: white;
40 background-color: @alert2;
40 background-color: @alert2;
41 padding: 5px 0 5px 0;
41 padding: 5px 0 5px 0;
42 }
42 }
43
43
44 html {
44 html {
45 display: table;
45 display: table;
46 height: 100%;
46 height: 100%;
47 width: 100%;
47 width: 100%;
48 }
48 }
49
49
50 body {
50 body {
51 display: table-cell;
51 display: table-cell;
52 width: 100%;
52 width: 100%;
53 }
53 }
54
54
55 //--- LAYOUT ------------------//
55 //--- LAYOUT ------------------//
56
56
57 .hidden{
57 .hidden{
58 display: none !important;
58 display: none !important;
59 }
59 }
60
60
61 .box{
61 .box{
62 float: left;
62 float: left;
63 width: 100%;
63 width: 100%;
64 }
64 }
65
65
66 .browser-header {
66 .browser-header {
67 clear: both;
67 clear: both;
68 }
68 }
69 .main {
69 .main {
70 clear: both;
70 clear: both;
71 padding:0 0 @pagepadding;
71 padding:0 0 @pagepadding;
72 height: auto;
72 height: auto;
73
73
74 &:after { //clearfix
74 &:after { //clearfix
75 content:"";
75 content:"";
76 clear:both;
76 clear:both;
77 width:100%;
77 width:100%;
78 display:block;
78 display:block;
79 }
79 }
80 }
80 }
81
81
82 .action-link{
82 .action-link{
83 margin-left: @padding;
83 margin-left: @padding;
84 padding-left: @padding;
84 padding-left: @padding;
85 border-left: @border-thickness solid @border-default-color;
85 border-left: @border-thickness solid @border-default-color;
86 }
86 }
87
87
88 input + .action-link, .action-link.first{
88 input + .action-link, .action-link.first{
89 border-left: none;
89 border-left: none;
90 }
90 }
91
91
92 .action-link.last{
92 .action-link.last{
93 margin-right: @padding;
93 margin-right: @padding;
94 padding-right: @padding;
94 padding-right: @padding;
95 }
95 }
96
96
97 .action-link.active,
97 .action-link.active,
98 .action-link.active a{
98 .action-link.active a{
99 color: @grey4;
99 color: @grey4;
100 }
100 }
101
101
102 ul.simple-list{
102 ul.simple-list{
103 list-style: none;
103 list-style: none;
104 margin: 0;
104 margin: 0;
105 padding: 0;
105 padding: 0;
106 }
106 }
107
107
108 .main-content {
108 .main-content {
109 padding-bottom: @pagepadding;
109 padding-bottom: @pagepadding;
110 }
110 }
111
111
112 .wide-mode-wrapper {
112 .wide-mode-wrapper {
113 max-width:4000px !important;
113 max-width:4000px !important;
114 }
114 }
115
115
116 .wrapper {
116 .wrapper {
117 position: relative;
117 position: relative;
118 max-width: @wrapper-maxwidth;
118 max-width: @wrapper-maxwidth;
119 margin: 0 auto;
119 margin: 0 auto;
120 }
120 }
121
121
122 #content {
122 #content {
123 clear: both;
123 clear: both;
124 padding: 0 @contentpadding;
124 padding: 0 @contentpadding;
125 }
125 }
126
126
127 .advanced-settings-fields{
127 .advanced-settings-fields{
128 input{
128 input{
129 margin-left: @textmargin;
129 margin-left: @textmargin;
130 margin-right: @padding/2;
130 margin-right: @padding/2;
131 }
131 }
132 }
132 }
133
133
134 .cs_files_title {
134 .cs_files_title {
135 margin: @pagepadding 0 0;
135 margin: @pagepadding 0 0;
136 }
136 }
137
137
138 input.inline[type="file"] {
138 input.inline[type="file"] {
139 display: inline;
139 display: inline;
140 }
140 }
141
141
142 .error_page {
142 .error_page {
143 margin: 10% auto;
143 margin: 10% auto;
144
144
145 h1 {
145 h1 {
146 color: @grey2;
146 color: @grey2;
147 }
147 }
148
148
149 .alert {
149 .alert {
150 margin: @padding 0;
150 margin: @padding 0;
151 }
151 }
152
152
153 .error-branding {
153 .error-branding {
154 font-family: @text-semibold;
154 font-family: @text-semibold;
155 color: @grey4;
155 color: @grey4;
156 }
156 }
157
157
158 .error_message {
158 .error_message {
159 font-family: @text-regular;
159 font-family: @text-regular;
160 }
160 }
161
161
162 .sidebar {
162 .sidebar {
163 min-height: 275px;
163 min-height: 275px;
164 margin: 0;
164 margin: 0;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
165 padding: 0 0 @sidebarpadding @sidebarpadding;
166 border: none;
166 border: none;
167 }
167 }
168
168
169 .main-content {
169 .main-content {
170 position: relative;
170 position: relative;
171 margin: 0 @sidebarpadding @sidebarpadding;
171 margin: 0 @sidebarpadding @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
172 padding: 0 0 0 @sidebarpadding;
173 border-left: @border-thickness solid @grey5;
173 border-left: @border-thickness solid @grey5;
174
174
175 @media (max-width:767px) {
175 @media (max-width:767px) {
176 clear: both;
176 clear: both;
177 width: 100%;
177 width: 100%;
178 margin: 0;
178 margin: 0;
179 border: none;
179 border: none;
180 }
180 }
181 }
181 }
182
182
183 .inner-column {
183 .inner-column {
184 float: left;
184 float: left;
185 width: 29.75%;
185 width: 29.75%;
186 min-height: 150px;
186 min-height: 150px;
187 margin: @sidebarpadding 2% 0 0;
187 margin: @sidebarpadding 2% 0 0;
188 padding: 0 2% 0 0;
188 padding: 0 2% 0 0;
189 border-right: @border-thickness solid @grey5;
189 border-right: @border-thickness solid @grey5;
190
190
191 @media (max-width:767px) {
191 @media (max-width:767px) {
192 clear: both;
192 clear: both;
193 width: 100%;
193 width: 100%;
194 border: none;
194 border: none;
195 }
195 }
196
196
197 ul {
197 ul {
198 padding-left: 1.25em;
198 padding-left: 1.25em;
199 }
199 }
200
200
201 &:last-child {
201 &:last-child {
202 margin: @sidebarpadding 0 0;
202 margin: @sidebarpadding 0 0;
203 border: none;
203 border: none;
204 }
204 }
205
205
206 h4 {
206 h4 {
207 margin: 0 0 @padding;
207 margin: 0 0 @padding;
208 font-family: @text-semibold;
208 font-family: @text-semibold;
209 }
209 }
210 }
210 }
211 }
211 }
212 .error-page-logo {
212 .error-page-logo {
213 width: 130px;
213 width: 130px;
214 height: 160px;
214 height: 160px;
215 }
215 }
216
216
217 // HEADER
217 // HEADER
218 .header {
218 .header {
219
219
220 // TODO: johbo: Fix login pages, so that they work without a min-height
220 // TODO: johbo: Fix login pages, so that they work without a min-height
221 // for the header and then remove the min-height. I chose a smaller value
221 // for the header and then remove the min-height. I chose a smaller value
222 // intentionally here to avoid rendering issues in the main navigation.
222 // intentionally here to avoid rendering issues in the main navigation.
223 min-height: 49px;
223 min-height: 49px;
224
224
225 position: relative;
225 position: relative;
226 vertical-align: bottom;
226 vertical-align: bottom;
227 padding: 0 @header-padding;
227 padding: 0 @header-padding;
228 background-color: @grey2;
228 background-color: @grey2;
229 color: @grey5;
229 color: @grey5;
230
230
231 .title {
231 .title {
232 overflow: visible;
232 overflow: visible;
233 }
233 }
234
234
235 &:before,
235 &:before,
236 &:after {
236 &:after {
237 content: "";
237 content: "";
238 clear: both;
238 clear: both;
239 width: 100%;
239 width: 100%;
240 }
240 }
241
241
242 // TODO: johbo: Avoids breaking "Repositories" chooser
242 // TODO: johbo: Avoids breaking "Repositories" chooser
243 .select2-container .select2-choice .select2-arrow {
243 .select2-container .select2-choice .select2-arrow {
244 display: none;
244 display: none;
245 }
245 }
246 }
246 }
247
247
248 #header-inner {
248 #header-inner {
249 &.title {
249 &.title {
250 margin: 0;
250 margin: 0;
251 }
251 }
252 &:before,
252 &:before,
253 &:after {
253 &:after {
254 content: "";
254 content: "";
255 clear: both;
255 clear: both;
256 }
256 }
257 }
257 }
258
258
259 // Gists
259 // Gists
260 #files_data {
260 #files_data {
261 clear: both; //for firefox
261 clear: both; //for firefox
262 }
262 }
263 #gistid {
263 #gistid {
264 margin-right: @padding;
264 margin-right: @padding;
265 }
265 }
266
266
267 // Global Settings Editor
267 // Global Settings Editor
268 .textarea.editor {
268 .textarea.editor {
269 float: left;
269 float: left;
270 position: relative;
270 position: relative;
271 max-width: @texteditor-width;
271 max-width: @texteditor-width;
272
272
273 select {
273 select {
274 position: absolute;
274 position: absolute;
275 top:10px;
275 top:10px;
276 right:0;
276 right:0;
277 }
277 }
278
278
279 .CodeMirror {
279 .CodeMirror {
280 margin: 0;
280 margin: 0;
281 }
281 }
282
282
283 .help-block {
283 .help-block {
284 margin: 0 0 @padding;
284 margin: 0 0 @padding;
285 padding:.5em;
285 padding:.5em;
286 background-color: @grey6;
286 background-color: @grey6;
287 }
287 }
288 }
288 }
289
289
290 ul.auth_plugins {
290 ul.auth_plugins {
291 margin: @padding 0 @padding @legend-width;
291 margin: @padding 0 @padding @legend-width;
292 padding: 0;
292 padding: 0;
293
293
294 li {
294 li {
295 margin-bottom: @padding;
295 margin-bottom: @padding;
296 line-height: 1em;
296 line-height: 1em;
297 list-style-type: none;
297 list-style-type: none;
298
298
299 .auth_buttons .btn {
299 .auth_buttons .btn {
300 margin-right: @padding;
300 margin-right: @padding;
301 }
301 }
302
302
303 &:before { content: none; }
303 &:before { content: none; }
304 }
304 }
305 }
305 }
306
306
307
307
308 // My Account PR list
308 // My Account PR list
309
309
310 #show_closed {
310 #show_closed {
311 margin: 0 1em 0 0;
311 margin: 0 1em 0 0;
312 }
312 }
313
313
314 .pullrequestlist {
314 .pullrequestlist {
315 .closed {
315 .closed {
316 background-color: @grey6;
316 background-color: @grey6;
317 }
317 }
318 .td-status {
318 .td-status {
319 padding-left: .5em;
319 padding-left: .5em;
320 }
320 }
321 .log-container .truncate {
321 .log-container .truncate {
322 height: 2.75em;
322 height: 2.75em;
323 white-space: pre-line;
323 white-space: pre-line;
324 }
324 }
325 table.rctable .user {
325 table.rctable .user {
326 padding-left: 0;
326 padding-left: 0;
327 }
327 }
328 table.rctable {
328 table.rctable {
329 td.td-description,
329 td.td-description,
330 .rc-user {
330 .rc-user {
331 min-width: auto;
331 min-width: auto;
332 }
332 }
333 }
333 }
334 }
334 }
335
335
336 // Pull Requests
336 // Pull Requests
337
337
338 .pullrequests_section_head {
338 .pullrequests_section_head {
339 display: block;
339 display: block;
340 clear: both;
340 clear: both;
341 margin: @padding 0;
341 margin: @padding 0;
342 font-family: @text-bold;
342 font-family: @text-bold;
343 }
343 }
344
344
345 .pr-origininfo, .pr-targetinfo {
345 .pr-origininfo, .pr-targetinfo {
346 position: relative;
346 position: relative;
347
347
348 .tag {
348 .tag {
349 display: inline-block;
349 display: inline-block;
350 margin: 0 1em .5em 0;
350 margin: 0 1em .5em 0;
351 }
351 }
352
352
353 .clone-url {
353 .clone-url {
354 display: inline-block;
354 display: inline-block;
355 margin: 0 0 .5em 0;
355 margin: 0 0 .5em 0;
356 padding: 0;
356 padding: 0;
357 line-height: 1.2em;
357 line-height: 1.2em;
358 }
358 }
359 }
359 }
360
360
361 .pr-pullinfo {
361 .pr-pullinfo {
362 clear: both;
362 clear: both;
363 margin: .5em 0;
363 margin: .5em 0;
364 }
364 }
365
365
366 #pr-title-input {
366 #pr-title-input {
367 width: 72%;
367 width: 72%;
368 font-size: 1em;
368 font-size: 1em;
369 font-family: @text-bold;
369 font-family: @text-bold;
370 margin: 0;
370 margin: 0;
371 padding: 0 0 0 @padding/4;
371 padding: 0 0 0 @padding/4;
372 line-height: 1.7em;
372 line-height: 1.7em;
373 color: @text-color;
373 color: @text-color;
374 letter-spacing: .02em;
374 letter-spacing: .02em;
375 }
375 }
376
376
377 #pullrequest_title {
377 #pullrequest_title {
378 width: 100%;
378 width: 100%;
379 box-sizing: border-box;
379 box-sizing: border-box;
380 }
380 }
381
381
382 #pr_open_message {
382 #pr_open_message {
383 border: @border-thickness solid #fff;
383 border: @border-thickness solid #fff;
384 border-radius: @border-radius;
384 border-radius: @border-radius;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
385 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
386 text-align: right;
386 text-align: right;
387 overflow: hidden;
387 overflow: hidden;
388 }
388 }
389
389
390 .pr-submit-button {
390 .pr-submit-button {
391 float: right;
391 float: right;
392 margin: 0 0 0 5px;
392 margin: 0 0 0 5px;
393 }
393 }
394
394
395 .pr-spacing-container {
395 .pr-spacing-container {
396 padding: 20px;
396 padding: 20px;
397 clear: both
397 clear: both
398 }
398 }
399
399
400 #pr-description-input {
400 #pr-description-input {
401 margin-bottom: 0;
401 margin-bottom: 0;
402 }
402 }
403
403
404 .pr-description-label {
404 .pr-description-label {
405 vertical-align: top;
405 vertical-align: top;
406 }
406 }
407
407
408 .perms_section_head {
408 .perms_section_head {
409 min-width: 625px;
409 min-width: 625px;
410
410
411 h2 {
411 h2 {
412 margin-bottom: 0;
412 margin-bottom: 0;
413 }
413 }
414
414
415 .label-checkbox {
415 .label-checkbox {
416 float: left;
416 float: left;
417 }
417 }
418
418
419 &.field {
419 &.field {
420 margin: @space 0 @padding;
420 margin: @space 0 @padding;
421 }
421 }
422
422
423 &:first-child.field {
423 &:first-child.field {
424 margin-top: 0;
424 margin-top: 0;
425
425
426 .label {
426 .label {
427 margin-top: 0;
427 margin-top: 0;
428 padding-top: 0;
428 padding-top: 0;
429 }
429 }
430
430
431 .radios {
431 .radios {
432 padding-top: 0;
432 padding-top: 0;
433 }
433 }
434 }
434 }
435
435
436 .radios {
436 .radios {
437 float: right;
437 float: right;
438 position: relative;
438 position: relative;
439 width: 405px;
439 width: 405px;
440 }
440 }
441 }
441 }
442
442
443 //--- MODULES ------------------//
443 //--- MODULES ------------------//
444
444
445
445
446 // Server Announcement
446 // Server Announcement
447 #server-announcement {
447 #server-announcement {
448 width: 95%;
448 width: 95%;
449 margin: @padding auto;
449 margin: @padding auto;
450 padding: @padding;
450 padding: @padding;
451 border-width: 2px;
451 border-width: 2px;
452 border-style: solid;
452 border-style: solid;
453 .border-radius(2px);
453 .border-radius(2px);
454 font-family: @text-bold;
454 font-family: @text-bold;
455
455
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
456 &.info { border-color: @alert4; background-color: @alert4-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
457 &.warning { border-color: @alert3; background-color: @alert3-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
458 &.error { border-color: @alert2; background-color: @alert2-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
459 &.success { border-color: @alert1; background-color: @alert1-inner; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
460 &.neutral { border-color: @grey3; background-color: @grey6; }
461 }
461 }
462
462
463 // Fixed Sidebar Column
463 // Fixed Sidebar Column
464 .sidebar-col-wrapper {
464 .sidebar-col-wrapper {
465 padding-left: @sidebar-all-width;
465 padding-left: @sidebar-all-width;
466
466
467 .sidebar {
467 .sidebar {
468 width: @sidebar-width;
468 width: @sidebar-width;
469 margin-left: -@sidebar-all-width;
469 margin-left: -@sidebar-all-width;
470 }
470 }
471 }
471 }
472
472
473 .sidebar-col-wrapper.scw-small {
473 .sidebar-col-wrapper.scw-small {
474 padding-left: @sidebar-small-all-width;
474 padding-left: @sidebar-small-all-width;
475
475
476 .sidebar {
476 .sidebar {
477 width: @sidebar-small-width;
477 width: @sidebar-small-width;
478 margin-left: -@sidebar-small-all-width;
478 margin-left: -@sidebar-small-all-width;
479 }
479 }
480 }
480 }
481
481
482
482
483 // FOOTER
483 // FOOTER
484 #footer {
484 #footer {
485 padding: 0;
485 padding: 0;
486 text-align: center;
486 text-align: center;
487 vertical-align: middle;
487 vertical-align: middle;
488 color: @grey2;
488 color: @grey2;
489 background-color: @grey6;
489 background-color: @grey6;
490
490
491 p {
491 p {
492 margin: 0;
492 margin: 0;
493 padding: 1em;
493 padding: 1em;
494 line-height: 1em;
494 line-height: 1em;
495 }
495 }
496
496
497 .server-instance { //server instance
497 .server-instance { //server instance
498 display: none;
498 display: none;
499 }
499 }
500
500
501 .title {
501 .title {
502 float: none;
502 float: none;
503 margin: 0 auto;
503 margin: 0 auto;
504 }
504 }
505 }
505 }
506
506
507 button.close {
507 button.close {
508 padding: 0;
508 padding: 0;
509 cursor: pointer;
509 cursor: pointer;
510 background: transparent;
510 background: transparent;
511 border: 0;
511 border: 0;
512 .box-shadow(none);
512 .box-shadow(none);
513 -webkit-appearance: none;
513 -webkit-appearance: none;
514 }
514 }
515
515
516 .close {
516 .close {
517 float: right;
517 float: right;
518 font-size: 21px;
518 font-size: 21px;
519 font-family: @text-bootstrap;
519 font-family: @text-bootstrap;
520 line-height: 1em;
520 line-height: 1em;
521 font-weight: bold;
521 font-weight: bold;
522 color: @grey2;
522 color: @grey2;
523
523
524 &:hover,
524 &:hover,
525 &:focus {
525 &:focus {
526 color: @grey1;
526 color: @grey1;
527 text-decoration: none;
527 text-decoration: none;
528 cursor: pointer;
528 cursor: pointer;
529 }
529 }
530 }
530 }
531
531
532 // GRID
532 // GRID
533 .sorting,
533 .sorting,
534 .sorting_desc,
534 .sorting_desc,
535 .sorting_asc {
535 .sorting_asc {
536 cursor: pointer;
536 cursor: pointer;
537 }
537 }
538 .sorting_desc:after {
538 .sorting_desc:after {
539 content: "\00A0\25B2";
539 content: "\00A0\25B2";
540 font-size: .75em;
540 font-size: .75em;
541 }
541 }
542 .sorting_asc:after {
542 .sorting_asc:after {
543 content: "\00A0\25BC";
543 content: "\00A0\25BC";
544 font-size: .68em;
544 font-size: .68em;
545 }
545 }
546
546
547
547
548 .user_auth_tokens {
548 .user_auth_tokens {
549
549
550 &.truncate {
550 &.truncate {
551 white-space: nowrap;
551 white-space: nowrap;
552 overflow: hidden;
552 overflow: hidden;
553 text-overflow: ellipsis;
553 text-overflow: ellipsis;
554 }
554 }
555
555
556 .fields .field .input {
556 .fields .field .input {
557 margin: 0;
557 margin: 0;
558 }
558 }
559
559
560 input#description {
560 input#description {
561 width: 100px;
561 width: 100px;
562 margin: 0;
562 margin: 0;
563 }
563 }
564
564
565 .drop-menu {
565 .drop-menu {
566 // TODO: johbo: Remove this, should work out of the box when
566 // TODO: johbo: Remove this, should work out of the box when
567 // having multiple inputs inline
567 // having multiple inputs inline
568 margin: 0 0 0 5px;
568 margin: 0 0 0 5px;
569 }
569 }
570 }
570 }
571 #user_list_table {
571 #user_list_table {
572 .closed {
572 .closed {
573 background-color: @grey6;
573 background-color: @grey6;
574 }
574 }
575 }
575 }
576
576
577
577
578 input {
578 input {
579 &.disabled {
579 &.disabled {
580 opacity: .5;
580 opacity: .5;
581 }
581 }
582 }
582 }
583
583
584 // remove extra padding in firefox
584 // remove extra padding in firefox
585 input::-moz-focus-inner { border:0; padding:0 }
585 input::-moz-focus-inner { border:0; padding:0 }
586
586
587 .adjacent input {
587 .adjacent input {
588 margin-bottom: @padding;
588 margin-bottom: @padding;
589 }
589 }
590
590
591 .permissions_boxes {
591 .permissions_boxes {
592 display: block;
592 display: block;
593 }
593 }
594
594
595 //TODO: lisa: this should be in tables
595 //TODO: lisa: this should be in tables
596 .show_more_col {
596 .show_more_col {
597 width: 20px;
597 width: 20px;
598 }
598 }
599
599
600 //FORMS
600 //FORMS
601
601
602 .medium-inline,
602 .medium-inline,
603 input#description.medium-inline {
603 input#description.medium-inline {
604 display: inline;
604 display: inline;
605 width: @medium-inline-input-width;
605 width: @medium-inline-input-width;
606 min-width: 100px;
606 min-width: 100px;
607 }
607 }
608
608
609 select {
609 select {
610 //reset
610 //reset
611 -webkit-appearance: none;
611 -webkit-appearance: none;
612 -moz-appearance: none;
612 -moz-appearance: none;
613
613
614 display: inline-block;
614 display: inline-block;
615 height: 28px;
615 height: 28px;
616 width: auto;
616 width: auto;
617 margin: 0 @padding @padding 0;
617 margin: 0 @padding @padding 0;
618 padding: 0 18px 0 8px;
618 padding: 0 18px 0 8px;
619 line-height:1em;
619 line-height:1em;
620 font-size: @basefontsize;
620 font-size: @basefontsize;
621 border: @border-thickness solid @rcblue;
621 border: @border-thickness solid @rcblue;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
622 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
623 color: @rcblue;
623 color: @rcblue;
624
624
625 &:after {
625 &:after {
626 content: "\00A0\25BE";
626 content: "\00A0\25BE";
627 }
627 }
628
628
629 &:focus {
629 &:focus {
630 outline: none;
630 outline: none;
631 }
631 }
632 }
632 }
633
633
634 option {
634 option {
635 &:focus {
635 &:focus {
636 outline: none;
636 outline: none;
637 }
637 }
638 }
638 }
639
639
640 input,
640 input,
641 textarea {
641 textarea {
642 padding: @input-padding;
642 padding: @input-padding;
643 border: @input-border-thickness solid @border-highlight-color;
643 border: @input-border-thickness solid @border-highlight-color;
644 .border-radius (@border-radius);
644 .border-radius (@border-radius);
645 font-family: @text-light;
645 font-family: @text-light;
646 font-size: @basefontsize;
646 font-size: @basefontsize;
647
647
648 &.input-sm {
648 &.input-sm {
649 padding: 5px;
649 padding: 5px;
650 }
650 }
651
651
652 &#description {
652 &#description {
653 min-width: @input-description-minwidth;
653 min-width: @input-description-minwidth;
654 min-height: 1em;
654 min-height: 1em;
655 padding: 10px;
655 padding: 10px;
656 }
656 }
657 }
657 }
658
658
659 .field-sm {
659 .field-sm {
660 input,
660 input,
661 textarea {
661 textarea {
662 padding: 5px;
662 padding: 5px;
663 }
663 }
664 }
664 }
665
665
666 textarea {
666 textarea {
667 display: block;
667 display: block;
668 clear: both;
668 clear: both;
669 width: 100%;
669 width: 100%;
670 min-height: 100px;
670 min-height: 100px;
671 margin-bottom: @padding;
671 margin-bottom: @padding;
672 .box-sizing(border-box);
672 .box-sizing(border-box);
673 overflow: auto;
673 overflow: auto;
674 }
674 }
675
675
676 label {
676 label {
677 font-family: @text-light;
677 font-family: @text-light;
678 }
678 }
679
679
680 // GRAVATARS
680 // GRAVATARS
681 // centers gravatar on username to the right
681 // centers gravatar on username to the right
682
682
683 .gravatar {
683 .gravatar {
684 display: inline;
684 display: inline;
685 min-width: 16px;
685 min-width: 16px;
686 min-height: 16px;
686 min-height: 16px;
687 margin: -5px 0;
687 margin: -5px 0;
688 padding: 0;
688 padding: 0;
689 line-height: 1em;
689 line-height: 1em;
690 border: 1px solid @grey4;
690 border: 1px solid @grey4;
691 box-sizing: content-box;
691 box-sizing: content-box;
692
692
693 &.gravatar-large {
693 &.gravatar-large {
694 margin: -0.5em .25em -0.5em 0;
694 margin: -0.5em .25em -0.5em 0;
695 }
695 }
696
696
697 & + .user {
697 & + .user {
698 display: inline;
698 display: inline;
699 margin: 0;
699 margin: 0;
700 padding: 0 0 0 .17em;
700 padding: 0 0 0 .17em;
701 line-height: 1em;
701 line-height: 1em;
702 }
702 }
703 }
703 }
704
704
705 .user-inline-data {
705 .user-inline-data {
706 display: inline-block;
706 display: inline-block;
707 float: left;
707 float: left;
708 padding-left: .5em;
708 padding-left: .5em;
709 line-height: 1.3em;
709 line-height: 1.3em;
710 }
710 }
711
711
712 .rc-user { // gravatar + user wrapper
712 .rc-user { // gravatar + user wrapper
713 float: left;
713 float: left;
714 position: relative;
714 position: relative;
715 min-width: 100px;
715 min-width: 100px;
716 max-width: 200px;
716 max-width: 200px;
717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
717 min-height: (@gravatar-size + @border-thickness * 2); // account for border
718 display: block;
718 display: block;
719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
719 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
720
720
721
721
722 .gravatar {
722 .gravatar {
723 display: block;
723 display: block;
724 position: absolute;
724 position: absolute;
725 top: 0;
725 top: 0;
726 left: 0;
726 left: 0;
727 min-width: @gravatar-size;
727 min-width: @gravatar-size;
728 min-height: @gravatar-size;
728 min-height: @gravatar-size;
729 margin: 0;
729 margin: 0;
730 }
730 }
731
731
732 .user {
732 .user {
733 display: block;
733 display: block;
734 max-width: 175px;
734 max-width: 175px;
735 padding-top: 2px;
735 padding-top: 2px;
736 overflow: hidden;
736 overflow: hidden;
737 text-overflow: ellipsis;
737 text-overflow: ellipsis;
738 }
738 }
739 }
739 }
740
740
741 .gist-gravatar,
741 .gist-gravatar,
742 .journal_container {
742 .journal_container {
743 .gravatar-large {
743 .gravatar-large {
744 margin: 0 .5em -10px 0;
744 margin: 0 .5em -10px 0;
745 }
745 }
746 }
746 }
747
747
748
748
749 // ADMIN SETTINGS
749 // ADMIN SETTINGS
750
750
751 // Tag Patterns
751 // Tag Patterns
752 .tag_patterns {
752 .tag_patterns {
753 .tag_input {
753 .tag_input {
754 margin-bottom: @padding;
754 margin-bottom: @padding;
755 }
755 }
756 }
756 }
757
757
758 .locked_input {
758 .locked_input {
759 position: relative;
759 position: relative;
760
760
761 input {
761 input {
762 display: inline;
762 display: inline;
763 margin-top: 3px;
763 margin-top: 3px;
764 }
764 }
765
765
766 br {
766 br {
767 display: none;
767 display: none;
768 }
768 }
769
769
770 .error-message {
770 .error-message {
771 float: left;
771 float: left;
772 width: 100%;
772 width: 100%;
773 }
773 }
774
774
775 .lock_input_button {
775 .lock_input_button {
776 display: inline;
776 display: inline;
777 }
777 }
778
778
779 .help-block {
779 .help-block {
780 clear: both;
780 clear: both;
781 }
781 }
782 }
782 }
783
783
784 // Notifications
784 // Notifications
785
785
786 .notifications_buttons {
786 .notifications_buttons {
787 margin: 0 0 @space 0;
787 margin: 0 0 @space 0;
788 padding: 0;
788 padding: 0;
789
789
790 .btn {
790 .btn {
791 display: inline-block;
791 display: inline-block;
792 }
792 }
793 }
793 }
794
794
795 .notification-list {
795 .notification-list {
796
796
797 div {
797 div {
798 display: inline-block;
798 display: inline-block;
799 vertical-align: middle;
799 vertical-align: middle;
800 }
800 }
801
801
802 .container {
802 .container {
803 display: block;
803 display: block;
804 margin: 0 0 @padding 0;
804 margin: 0 0 @padding 0;
805 }
805 }
806
806
807 .delete-notifications {
807 .delete-notifications {
808 margin-left: @padding;
808 margin-left: @padding;
809 text-align: right;
809 text-align: right;
810 cursor: pointer;
810 cursor: pointer;
811 }
811 }
812
812
813 .read-notifications {
813 .read-notifications {
814 margin-left: @padding/2;
814 margin-left: @padding/2;
815 text-align: right;
815 text-align: right;
816 width: 35px;
816 width: 35px;
817 cursor: pointer;
817 cursor: pointer;
818 }
818 }
819
819
820 .icon-minus-sign {
820 .icon-minus-sign {
821 color: @alert2;
821 color: @alert2;
822 }
822 }
823
823
824 .icon-ok-sign {
824 .icon-ok-sign {
825 color: @alert1;
825 color: @alert1;
826 }
826 }
827 }
827 }
828
828
829 .user_settings {
829 .user_settings {
830 float: left;
830 float: left;
831 clear: both;
831 clear: both;
832 display: block;
832 display: block;
833 width: 100%;
833 width: 100%;
834
834
835 .gravatar_box {
835 .gravatar_box {
836 margin-bottom: @padding;
836 margin-bottom: @padding;
837
837
838 &:after {
838 &:after {
839 content: " ";
839 content: " ";
840 clear: both;
840 clear: both;
841 width: 100%;
841 width: 100%;
842 }
842 }
843 }
843 }
844
844
845 .fields .field {
845 .fields .field {
846 clear: both;
846 clear: both;
847 }
847 }
848 }
848 }
849
849
850 .advanced_settings {
850 .advanced_settings {
851 margin-bottom: @space;
851 margin-bottom: @space;
852
852
853 .help-block {
853 .help-block {
854 margin-left: 0;
854 margin-left: 0;
855 }
855 }
856
856
857 button + .help-block {
857 button + .help-block {
858 margin-top: @padding;
858 margin-top: @padding;
859 }
859 }
860 }
860 }
861
861
862 // admin settings radio buttons and labels
862 // admin settings radio buttons and labels
863 .label-2 {
863 .label-2 {
864 float: left;
864 float: left;
865 width: @label2-width;
865 width: @label2-width;
866
866
867 label {
867 label {
868 color: @grey1;
868 color: @grey1;
869 }
869 }
870 }
870 }
871 .checkboxes {
871 .checkboxes {
872 float: left;
872 float: left;
873 width: @checkboxes-width;
873 width: @checkboxes-width;
874 margin-bottom: @padding;
874 margin-bottom: @padding;
875
875
876 .checkbox {
876 .checkbox {
877 width: 100%;
877 width: 100%;
878
878
879 label {
879 label {
880 margin: 0;
880 margin: 0;
881 padding: 0;
881 padding: 0;
882 }
882 }
883 }
883 }
884
884
885 .checkbox + .checkbox {
885 .checkbox + .checkbox {
886 display: inline-block;
886 display: inline-block;
887 }
887 }
888
888
889 label {
889 label {
890 margin-right: 1em;
890 margin-right: 1em;
891 }
891 }
892 }
892 }
893
893
894 // CHANGELOG
894 // CHANGELOG
895 .container_header {
895 .container_header {
896 float: left;
896 float: left;
897 display: block;
897 display: block;
898 width: 100%;
898 width: 100%;
899 margin: @padding 0 @padding;
899 margin: @padding 0 @padding;
900
900
901 #filter_changelog {
901 #filter_changelog {
902 float: left;
902 float: left;
903 margin-right: @padding;
903 margin-right: @padding;
904 }
904 }
905
905
906 .breadcrumbs_light {
906 .breadcrumbs_light {
907 display: inline-block;
907 display: inline-block;
908 }
908 }
909 }
909 }
910
910
911 .info_box {
911 .info_box {
912 float: right;
912 float: right;
913 }
913 }
914
914
915
915
916 #graph_nodes {
916 #graph_nodes {
917 padding-top: 43px;
917 padding-top: 43px;
918 }
918 }
919
919
920 #graph_content{
920 #graph_content{
921
921
922 // adjust for table headers so that graph renders properly
922 // adjust for table headers so that graph renders properly
923 // #graph_nodes padding - table cell padding
923 // #graph_nodes padding - table cell padding
924 padding-top: (@space - (@basefontsize * 2.4));
924 padding-top: (@space - (@basefontsize * 2.4));
925
925
926 &.graph_full_width {
926 &.graph_full_width {
927 width: 100%;
927 width: 100%;
928 max-width: 100%;
928 max-width: 100%;
929 }
929 }
930 }
930 }
931
931
932 #graph {
932 #graph {
933 .flag_status {
933 .flag_status {
934 margin: 0;
934 margin: 0;
935 }
935 }
936
936
937 .pagination-left {
937 .pagination-left {
938 float: left;
938 float: left;
939 clear: both;
939 clear: both;
940 }
940 }
941
941
942 .log-container {
942 .log-container {
943 max-width: 345px;
943 max-width: 345px;
944
944
945 .message{
945 .message{
946 max-width: 340px;
946 max-width: 340px;
947 }
947 }
948 }
948 }
949
949
950 .graph-col-wrapper {
950 .graph-col-wrapper {
951 padding-left: 110px;
951 padding-left: 110px;
952
952
953 #graph_nodes {
953 #graph_nodes {
954 width: 100px;
954 width: 100px;
955 margin-left: -110px;
955 margin-left: -110px;
956 float: left;
956 float: left;
957 clear: left;
957 clear: left;
958 }
958 }
959 }
959 }
960 }
960 }
961
961
962 #filter_changelog {
962 #filter_changelog {
963 float: left;
963 float: left;
964 }
964 }
965
965
966
966
967 //--- THEME ------------------//
967 //--- THEME ------------------//
968
968
969 #logo {
969 #logo {
970 float: left;
970 float: left;
971 margin: 9px 0 0 0;
971 margin: 9px 0 0 0;
972
972
973 .header {
973 .header {
974 background-color: transparent;
974 background-color: transparent;
975 }
975 }
976
976
977 a {
977 a {
978 display: inline-block;
978 display: inline-block;
979 }
979 }
980
980
981 img {
981 img {
982 height:30px;
982 height:30px;
983 }
983 }
984 }
984 }
985
985
986 .logo-wrapper {
986 .logo-wrapper {
987 float:left;
987 float:left;
988 }
988 }
989
989
990 .branding{
990 .branding{
991 float: left;
991 float: left;
992 padding: 9px 2px;
992 padding: 9px 2px;
993 line-height: 1em;
993 line-height: 1em;
994 font-size: @navigation-fontsize;
994 font-size: @navigation-fontsize;
995 }
995 }
996
996
997 img {
997 img {
998 border: none;
998 border: none;
999 outline: none;
999 outline: none;
1000 }
1000 }
1001 user-profile-header
1001 user-profile-header
1002 label {
1002 label {
1003
1003
1004 input[type="checkbox"] {
1004 input[type="checkbox"] {
1005 margin-right: 1em;
1005 margin-right: 1em;
1006 }
1006 }
1007 input[type="radio"] {
1007 input[type="radio"] {
1008 margin-right: 1em;
1008 margin-right: 1em;
1009 }
1009 }
1010 }
1010 }
1011
1011
1012 .flag_status {
1012 .flag_status {
1013 margin: 2px 8px 6px 2px;
1013 margin: 2px 8px 6px 2px;
1014 &.under_review {
1014 &.under_review {
1015 .circle(5px, @alert3);
1015 .circle(5px, @alert3);
1016 }
1016 }
1017 &.approved {
1017 &.approved {
1018 .circle(5px, @alert1);
1018 .circle(5px, @alert1);
1019 }
1019 }
1020 &.rejected,
1020 &.rejected,
1021 &.forced_closed{
1021 &.forced_closed{
1022 .circle(5px, @alert2);
1022 .circle(5px, @alert2);
1023 }
1023 }
1024 &.not_reviewed {
1024 &.not_reviewed {
1025 .circle(5px, @grey5);
1025 .circle(5px, @grey5);
1026 }
1026 }
1027 }
1027 }
1028
1028
1029 .flag_status_comment_box {
1029 .flag_status_comment_box {
1030 margin: 5px 6px 0px 2px;
1030 margin: 5px 6px 0px 2px;
1031 }
1031 }
1032 .test_pattern_preview {
1032 .test_pattern_preview {
1033 margin: @space 0;
1033 margin: @space 0;
1034
1034
1035 p {
1035 p {
1036 margin-bottom: 0;
1036 margin-bottom: 0;
1037 border-bottom: @border-thickness solid @border-default-color;
1037 border-bottom: @border-thickness solid @border-default-color;
1038 color: @grey3;
1038 color: @grey3;
1039 }
1039 }
1040
1040
1041 .btn {
1041 .btn {
1042 margin-bottom: @padding;
1042 margin-bottom: @padding;
1043 }
1043 }
1044 }
1044 }
1045 #test_pattern_result {
1045 #test_pattern_result {
1046 display: none;
1046 display: none;
1047 &:extend(pre);
1047 &:extend(pre);
1048 padding: .9em;
1048 padding: .9em;
1049 color: @grey3;
1049 color: @grey3;
1050 background-color: @grey7;
1050 background-color: @grey7;
1051 border-right: @border-thickness solid @border-default-color;
1051 border-right: @border-thickness solid @border-default-color;
1052 border-bottom: @border-thickness solid @border-default-color;
1052 border-bottom: @border-thickness solid @border-default-color;
1053 border-left: @border-thickness solid @border-default-color;
1053 border-left: @border-thickness solid @border-default-color;
1054 }
1054 }
1055
1055
1056 #repo_vcs_settings {
1056 #repo_vcs_settings {
1057 #inherit_overlay_vcs_default {
1057 #inherit_overlay_vcs_default {
1058 display: none;
1058 display: none;
1059 }
1059 }
1060 #inherit_overlay_vcs_custom {
1060 #inherit_overlay_vcs_custom {
1061 display: custom;
1061 display: custom;
1062 }
1062 }
1063 &.inherited {
1063 &.inherited {
1064 #inherit_overlay_vcs_default {
1064 #inherit_overlay_vcs_default {
1065 display: block;
1065 display: block;
1066 }
1066 }
1067 #inherit_overlay_vcs_custom {
1067 #inherit_overlay_vcs_custom {
1068 display: none;
1068 display: none;
1069 }
1069 }
1070 }
1070 }
1071 }
1071 }
1072
1072
1073 .issue-tracker-link {
1073 .issue-tracker-link {
1074 color: @rcblue;
1074 color: @rcblue;
1075 }
1075 }
1076
1076
1077 // Issue Tracker Table Show/Hide
1077 // Issue Tracker Table Show/Hide
1078 #repo_issue_tracker {
1078 #repo_issue_tracker {
1079 #inherit_overlay {
1079 #inherit_overlay {
1080 display: none;
1080 display: none;
1081 }
1081 }
1082 #custom_overlay {
1082 #custom_overlay {
1083 display: custom;
1083 display: custom;
1084 }
1084 }
1085 &.inherited {
1085 &.inherited {
1086 #inherit_overlay {
1086 #inherit_overlay {
1087 display: block;
1087 display: block;
1088 }
1088 }
1089 #custom_overlay {
1089 #custom_overlay {
1090 display: none;
1090 display: none;
1091 }
1091 }
1092 }
1092 }
1093 }
1093 }
1094 table.issuetracker {
1094 table.issuetracker {
1095 &.readonly {
1095 &.readonly {
1096 tr, td {
1096 tr, td {
1097 color: @grey3;
1097 color: @grey3;
1098 }
1098 }
1099 }
1099 }
1100 .edit {
1100 .edit {
1101 display: none;
1101 display: none;
1102 }
1102 }
1103 .editopen {
1103 .editopen {
1104 .edit {
1104 .edit {
1105 display: inline;
1105 display: inline;
1106 }
1106 }
1107 .entry {
1107 .entry {
1108 display: none;
1108 display: none;
1109 }
1109 }
1110 }
1110 }
1111 tr td.td-action {
1111 tr td.td-action {
1112 min-width: 117px;
1112 min-width: 117px;
1113 }
1113 }
1114 td input {
1114 td input {
1115 max-width: none;
1115 max-width: none;
1116 min-width: 30px;
1116 min-width: 30px;
1117 width: 80%;
1117 width: 80%;
1118 }
1118 }
1119 .issuetracker_pref input {
1119 .issuetracker_pref input {
1120 width: 40%;
1120 width: 40%;
1121 }
1121 }
1122 input.edit_issuetracker_update {
1122 input.edit_issuetracker_update {
1123 margin-right: 0;
1123 margin-right: 0;
1124 width: auto;
1124 width: auto;
1125 }
1125 }
1126 }
1126 }
1127
1127
1128 table.integrations {
1128 table.integrations {
1129 .td-icon {
1129 .td-icon {
1130 width: 20px;
1130 width: 20px;
1131 .integration-icon {
1131 .integration-icon {
1132 height: 20px;
1132 height: 20px;
1133 width: 20px;
1133 width: 20px;
1134 }
1134 }
1135 }
1135 }
1136 }
1136 }
1137
1137
1138 .integrations {
1138 .integrations {
1139 a.integration-box {
1139 a.integration-box {
1140 color: @text-color;
1140 color: @text-color;
1141 &:hover {
1141 &:hover {
1142 .panel {
1142 .panel {
1143 background: #fbfbfb;
1143 background: #fbfbfb;
1144 }
1144 }
1145 }
1145 }
1146 .integration-icon {
1146 .integration-icon {
1147 width: 30px;
1147 width: 30px;
1148 height: 30px;
1148 height: 30px;
1149 margin-right: 20px;
1149 margin-right: 20px;
1150 float: left;
1150 float: left;
1151 }
1151 }
1152
1152
1153 .panel-body {
1153 .panel-body {
1154 padding: 10px;
1154 padding: 10px;
1155 }
1155 }
1156 .panel {
1156 .panel {
1157 margin-bottom: 10px;
1157 margin-bottom: 10px;
1158 }
1158 }
1159 h2 {
1159 h2 {
1160 display: inline-block;
1160 display: inline-block;
1161 margin: 0;
1161 margin: 0;
1162 min-width: 140px;
1162 min-width: 140px;
1163 }
1163 }
1164 }
1164 }
1165 }
1165 }
1166
1166
1167 //Permissions Settings
1167 //Permissions Settings
1168 #add_perm {
1168 #add_perm {
1169 margin: 0 0 @padding;
1169 margin: 0 0 @padding;
1170 cursor: pointer;
1170 cursor: pointer;
1171 }
1171 }
1172
1172
1173 .perm_ac {
1173 .perm_ac {
1174 input {
1174 input {
1175 width: 95%;
1175 width: 95%;
1176 }
1176 }
1177 }
1177 }
1178
1178
1179 .autocomplete-suggestions {
1179 .autocomplete-suggestions {
1180 width: auto !important; // overrides autocomplete.js
1180 width: auto !important; // overrides autocomplete.js
1181 margin: 0;
1181 margin: 0;
1182 border: @border-thickness solid @rcblue;
1182 border: @border-thickness solid @rcblue;
1183 border-radius: @border-radius;
1183 border-radius: @border-radius;
1184 color: @rcblue;
1184 color: @rcblue;
1185 background-color: white;
1185 background-color: white;
1186 }
1186 }
1187 .autocomplete-selected {
1187 .autocomplete-selected {
1188 background: #F0F0F0;
1188 background: #F0F0F0;
1189 }
1189 }
1190 .ac-container-wrap {
1190 .ac-container-wrap {
1191 margin: 0;
1191 margin: 0;
1192 padding: 8px;
1192 padding: 8px;
1193 border-bottom: @border-thickness solid @rclightblue;
1193 border-bottom: @border-thickness solid @rclightblue;
1194 list-style-type: none;
1194 list-style-type: none;
1195 cursor: pointer;
1195 cursor: pointer;
1196
1196
1197 &:hover {
1197 &:hover {
1198 background-color: @rclightblue;
1198 background-color: @rclightblue;
1199 }
1199 }
1200
1200
1201 img {
1201 img {
1202 height: @gravatar-size;
1202 height: @gravatar-size;
1203 width: @gravatar-size;
1203 width: @gravatar-size;
1204 margin-right: 1em;
1204 margin-right: 1em;
1205 }
1205 }
1206
1206
1207 strong {
1207 strong {
1208 font-weight: normal;
1208 font-weight: normal;
1209 }
1209 }
1210 }
1210 }
1211
1211
1212 // Settings Dropdown
1212 // Settings Dropdown
1213 .user-menu .container {
1213 .user-menu .container {
1214 padding: 0 4px;
1214 padding: 0 4px;
1215 margin: 0;
1215 margin: 0;
1216 }
1216 }
1217
1217
1218 .user-menu .gravatar {
1218 .user-menu .gravatar {
1219 cursor: pointer;
1219 cursor: pointer;
1220 }
1220 }
1221
1221
1222 .codeblock {
1222 .codeblock {
1223 margin-bottom: @padding;
1223 margin-bottom: @padding;
1224 clear: both;
1224 clear: both;
1225
1225
1226 .stats{
1226 .stats{
1227 overflow: hidden;
1227 overflow: hidden;
1228 }
1228 }
1229
1229
1230 .message{
1230 .message{
1231 textarea{
1231 textarea{
1232 margin: 0;
1232 margin: 0;
1233 }
1233 }
1234 }
1234 }
1235
1235
1236 .code-header {
1236 .code-header {
1237 .stats {
1237 .stats {
1238 line-height: 2em;
1238 line-height: 2em;
1239
1239
1240 .revision_id {
1240 .revision_id {
1241 margin-left: 0;
1241 margin-left: 0;
1242 }
1242 }
1243 .buttons {
1243 .buttons {
1244 padding-right: 0;
1244 padding-right: 0;
1245 }
1245 }
1246 }
1246 }
1247
1247
1248 .item{
1248 .item{
1249 margin-right: 0.5em;
1249 margin-right: 0.5em;
1250 }
1250 }
1251 }
1251 }
1252
1252
1253 #editor_container{
1253 #editor_container{
1254 position: relative;
1254 position: relative;
1255 margin: @padding;
1255 margin: @padding;
1256 }
1256 }
1257 }
1257 }
1258
1258
1259 #file_history_container {
1259 #file_history_container {
1260 display: none;
1260 display: none;
1261 }
1261 }
1262
1262
1263 .file-history-inner {
1263 .file-history-inner {
1264 margin-bottom: 10px;
1264 margin-bottom: 10px;
1265 }
1265 }
1266
1266
1267 // Pull Requests
1267 // Pull Requests
1268 .summary-details {
1268 .summary-details {
1269 width: 72%;
1269 width: 72%;
1270 }
1270 }
1271 .pr-summary {
1271 .pr-summary {
1272 border-bottom: @border-thickness solid @grey5;
1272 border-bottom: @border-thickness solid @grey5;
1273 margin-bottom: @space;
1273 margin-bottom: @space;
1274 }
1274 }
1275 .reviewers-title {
1275 .reviewers-title {
1276 width: 25%;
1276 width: 25%;
1277 min-width: 200px;
1277 min-width: 200px;
1278 }
1278 }
1279 .reviewers {
1279 .reviewers {
1280 width: 25%;
1280 width: 25%;
1281 min-width: 200px;
1281 min-width: 200px;
1282 }
1282 }
1283 .reviewers ul li {
1283 .reviewers ul li {
1284 position: relative;
1284 position: relative;
1285 width: 100%;
1285 width: 100%;
1286 margin-bottom: 8px;
1286 margin-bottom: 8px;
1287 }
1287 }
1288 .reviewers_member {
1288 .reviewers_member {
1289 width: 100%;
1289 width: 100%;
1290 overflow: auto;
1290 overflow: auto;
1291 }
1291 }
1292 .reviewer_reason {
1292 .reviewer_reason {
1293 padding-left: 20px;
1293 padding-left: 20px;
1294 }
1294 }
1295 .reviewer_status {
1295 .reviewer_status {
1296 display: inline-block;
1296 display: inline-block;
1297 vertical-align: top;
1297 vertical-align: top;
1298 width: 7%;
1298 width: 7%;
1299 min-width: 20px;
1299 min-width: 20px;
1300 height: 1.2em;
1300 height: 1.2em;
1301 margin-top: 3px;
1301 margin-top: 3px;
1302 line-height: 1em;
1302 line-height: 1em;
1303 }
1303 }
1304
1304
1305 .reviewer_name {
1305 .reviewer_name {
1306 display: inline-block;
1306 display: inline-block;
1307 max-width: 83%;
1307 max-width: 83%;
1308 padding-right: 20px;
1308 padding-right: 20px;
1309 vertical-align: middle;
1309 vertical-align: middle;
1310 line-height: 1;
1310 line-height: 1;
1311
1311
1312 .rc-user {
1312 .rc-user {
1313 min-width: 0;
1313 min-width: 0;
1314 margin: -2px 1em 0 0;
1314 margin: -2px 1em 0 0;
1315 }
1315 }
1316
1316
1317 .reviewer {
1317 .reviewer {
1318 float: left;
1318 float: left;
1319 }
1319 }
1320
1320
1321 &.to-delete {
1321 &.to-delete {
1322 .user,
1322 .user,
1323 .reviewer {
1323 .reviewer {
1324 text-decoration: line-through;
1324 text-decoration: line-through;
1325 }
1325 }
1326 }
1326 }
1327 }
1327 }
1328
1328
1329 .reviewer_member_remove {
1329 .reviewer_member_remove {
1330 position: absolute;
1330 position: absolute;
1331 right: 0;
1331 right: 0;
1332 top: 0;
1332 top: 0;
1333 width: 16px;
1333 width: 16px;
1334 margin-bottom: 10px;
1334 margin-bottom: 10px;
1335 padding: 0;
1335 padding: 0;
1336 color: black;
1336 color: black;
1337 }
1337 }
1338 .reviewer_member_status {
1338 .reviewer_member_status {
1339 margin-top: 5px;
1339 margin-top: 5px;
1340 }
1340 }
1341 .pr-summary #summary{
1341 .pr-summary #summary{
1342 width: 100%;
1342 width: 100%;
1343 }
1343 }
1344 .pr-summary .action_button:hover {
1344 .pr-summary .action_button:hover {
1345 border: 0;
1345 border: 0;
1346 cursor: pointer;
1346 cursor: pointer;
1347 }
1347 }
1348 .pr-details-title {
1348 .pr-details-title {
1349 padding-bottom: 8px;
1349 padding-bottom: 8px;
1350 border-bottom: @border-thickness solid @grey5;
1350 border-bottom: @border-thickness solid @grey5;
1351
1351
1352 .action_button.disabled {
1352 .action_button.disabled {
1353 color: @grey4;
1353 color: @grey4;
1354 cursor: inherit;
1354 cursor: inherit;
1355 }
1355 }
1356 .action_button {
1356 .action_button {
1357 color: @rcblue;
1357 color: @rcblue;
1358 }
1358 }
1359 }
1359 }
1360 .pr-details-content {
1360 .pr-details-content {
1361 margin-top: @textmargin;
1361 margin-top: @textmargin;
1362 margin-bottom: @textmargin;
1362 margin-bottom: @textmargin;
1363 }
1363 }
1364 .pr-description {
1364 .pr-description {
1365 white-space:pre-wrap;
1365 white-space:pre-wrap;
1366 }
1366 }
1367 .group_members {
1367 .group_members {
1368 margin-top: 0;
1368 margin-top: 0;
1369 padding: 0;
1369 padding: 0;
1370 list-style: outside none none;
1370 list-style: outside none none;
1371
1371
1372 img {
1372 img {
1373 height: @gravatar-size;
1373 height: @gravatar-size;
1374 width: @gravatar-size;
1374 width: @gravatar-size;
1375 margin-right: .5em;
1375 margin-right: .5em;
1376 margin-left: 3px;
1376 margin-left: 3px;
1377 }
1377 }
1378
1378
1379 .to-delete {
1379 .to-delete {
1380 .user {
1380 .user {
1381 text-decoration: line-through;
1381 text-decoration: line-through;
1382 }
1382 }
1383 }
1383 }
1384 }
1384 }
1385
1385
1386 .compare_view_commits_title {
1386 .compare_view_commits_title {
1387 .disabled {
1387 .disabled {
1388 cursor: inherit;
1388 cursor: inherit;
1389 &:hover{
1389 &:hover{
1390 background-color: inherit;
1390 background-color: inherit;
1391 color: inherit;
1391 color: inherit;
1392 }
1392 }
1393 }
1393 }
1394 }
1394 }
1395
1395
1396 // new entry in group_members
1396 // new entry in group_members
1397 .td-author-new-entry {
1397 .td-author-new-entry {
1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1398 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1399 }
1399 }
1400
1400
1401 .usergroup_member_remove {
1401 .usergroup_member_remove {
1402 width: 16px;
1402 width: 16px;
1403 margin-bottom: 10px;
1403 margin-bottom: 10px;
1404 padding: 0;
1404 padding: 0;
1405 color: black !important;
1405 color: black !important;
1406 cursor: pointer;
1406 cursor: pointer;
1407 }
1407 }
1408
1408
1409 .reviewer_ac .ac-input {
1409 .reviewer_ac .ac-input {
1410 width: 92%;
1410 width: 92%;
1411 margin-bottom: 1em;
1411 margin-bottom: 1em;
1412 }
1412 }
1413
1413
1414 .compare_view_commits tr{
1414 .compare_view_commits tr{
1415 height: 20px;
1415 height: 20px;
1416 }
1416 }
1417 .compare_view_commits td {
1417 .compare_view_commits td {
1418 vertical-align: top;
1418 vertical-align: top;
1419 padding-top: 10px;
1419 padding-top: 10px;
1420 }
1420 }
1421 .compare_view_commits .author {
1421 .compare_view_commits .author {
1422 margin-left: 5px;
1422 margin-left: 5px;
1423 }
1423 }
1424
1424
1425 .compare_view_files {
1425 .compare_view_files {
1426 width: 100%;
1426 width: 100%;
1427
1427
1428 td {
1428 td {
1429 vertical-align: middle;
1429 vertical-align: middle;
1430 }
1430 }
1431 }
1431 }
1432
1432
1433 .compare_view_filepath {
1433 .compare_view_filepath {
1434 color: @grey1;
1434 color: @grey1;
1435 }
1435 }
1436
1436
1437 .show_more {
1437 .show_more {
1438 display: inline-block;
1438 display: inline-block;
1439 position: relative;
1439 position: relative;
1440 vertical-align: middle;
1440 vertical-align: middle;
1441 width: 4px;
1441 width: 4px;
1442 height: @basefontsize;
1442 height: @basefontsize;
1443
1443
1444 &:after {
1444 &:after {
1445 content: "\00A0\25BE";
1445 content: "\00A0\25BE";
1446 display: inline-block;
1446 display: inline-block;
1447 width:10px;
1447 width:10px;
1448 line-height: 5px;
1448 line-height: 5px;
1449 font-size: 12px;
1449 font-size: 12px;
1450 cursor: pointer;
1450 cursor: pointer;
1451 }
1451 }
1452 }
1452 }
1453
1453
1454 .journal_more .show_more {
1454 .journal_more .show_more {
1455 display: inline;
1455 display: inline;
1456
1456
1457 &:after {
1457 &:after {
1458 content: none;
1458 content: none;
1459 }
1459 }
1460 }
1460 }
1461
1461
1462 .open .show_more:after,
1462 .open .show_more:after,
1463 .select2-dropdown-open .show_more:after {
1463 .select2-dropdown-open .show_more:after {
1464 .rotate(180deg);
1464 .rotate(180deg);
1465 margin-left: 4px;
1465 margin-left: 4px;
1466 }
1466 }
1467
1467
1468
1468
1469 .compare_view_commits .collapse_commit:after {
1469 .compare_view_commits .collapse_commit:after {
1470 cursor: pointer;
1470 cursor: pointer;
1471 content: "\00A0\25B4";
1471 content: "\00A0\25B4";
1472 margin-left: -3px;
1472 margin-left: -3px;
1473 font-size: 17px;
1473 font-size: 17px;
1474 color: @grey4;
1474 color: @grey4;
1475 }
1475 }
1476
1476
1477 .diff_links {
1477 .diff_links {
1478 margin-left: 8px;
1478 margin-left: 8px;
1479 }
1479 }
1480
1480
1481 div.ancestor {
1481 div.ancestor {
1482 margin: -30px 0px;
1482 margin: -30px 0px;
1483 }
1483 }
1484
1484
1485 .cs_icon_td input[type="checkbox"] {
1485 .cs_icon_td input[type="checkbox"] {
1486 display: none;
1486 display: none;
1487 }
1487 }
1488
1488
1489 .cs_icon_td .expand_file_icon:after {
1489 .cs_icon_td .expand_file_icon:after {
1490 cursor: pointer;
1490 cursor: pointer;
1491 content: "\00A0\25B6";
1491 content: "\00A0\25B6";
1492 font-size: 12px;
1492 font-size: 12px;
1493 color: @grey4;
1493 color: @grey4;
1494 }
1494 }
1495
1495
1496 .cs_icon_td .collapse_file_icon:after {
1496 .cs_icon_td .collapse_file_icon:after {
1497 cursor: pointer;
1497 cursor: pointer;
1498 content: "\00A0\25BC";
1498 content: "\00A0\25BC";
1499 font-size: 12px;
1499 font-size: 12px;
1500 color: @grey4;
1500 color: @grey4;
1501 }
1501 }
1502
1502
1503 /*new binary
1503 /*new binary
1504 NEW_FILENODE = 1
1504 NEW_FILENODE = 1
1505 DEL_FILENODE = 2
1505 DEL_FILENODE = 2
1506 MOD_FILENODE = 3
1506 MOD_FILENODE = 3
1507 RENAMED_FILENODE = 4
1507 RENAMED_FILENODE = 4
1508 COPIED_FILENODE = 5
1508 COPIED_FILENODE = 5
1509 CHMOD_FILENODE = 6
1509 CHMOD_FILENODE = 6
1510 BIN_FILENODE = 7
1510 BIN_FILENODE = 7
1511 */
1511 */
1512 .cs_files_expand {
1512 .cs_files_expand {
1513 font-size: @basefontsize + 5px;
1513 font-size: @basefontsize + 5px;
1514 line-height: 1.8em;
1514 line-height: 1.8em;
1515 float: right;
1515 float: right;
1516 }
1516 }
1517
1517
1518 .cs_files_expand span{
1518 .cs_files_expand span{
1519 color: @rcblue;
1519 color: @rcblue;
1520 cursor: pointer;
1520 cursor: pointer;
1521 }
1521 }
1522 .cs_files {
1522 .cs_files {
1523 clear: both;
1523 clear: both;
1524 padding-bottom: @padding;
1524 padding-bottom: @padding;
1525
1525
1526 .cur_cs {
1526 .cur_cs {
1527 margin: 10px 2px;
1527 margin: 10px 2px;
1528 font-weight: bold;
1528 font-weight: bold;
1529 }
1529 }
1530
1530
1531 .node {
1531 .node {
1532 float: left;
1532 float: left;
1533 }
1533 }
1534
1534
1535 .changes {
1535 .changes {
1536 float: right;
1536 float: right;
1537 color: white;
1537 color: white;
1538 font-size: @basefontsize - 4px;
1538 font-size: @basefontsize - 4px;
1539 margin-top: 4px;
1539 margin-top: 4px;
1540 opacity: 0.6;
1540 opacity: 0.6;
1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1541 filter: Alpha(opacity=60); /* IE8 and earlier */
1542
1542
1543 .added {
1543 .added {
1544 background-color: @alert1;
1544 background-color: @alert1;
1545 float: left;
1545 float: left;
1546 text-align: center;
1546 text-align: center;
1547 }
1547 }
1548
1548
1549 .deleted {
1549 .deleted {
1550 background-color: @alert2;
1550 background-color: @alert2;
1551 float: left;
1551 float: left;
1552 text-align: center;
1552 text-align: center;
1553 }
1553 }
1554
1554
1555 .bin {
1555 .bin {
1556 background-color: @alert1;
1556 background-color: @alert1;
1557 text-align: center;
1557 text-align: center;
1558 }
1558 }
1559
1559
1560 /*new binary*/
1560 /*new binary*/
1561 .bin.bin1 {
1561 .bin.bin1 {
1562 background-color: @alert1;
1562 background-color: @alert1;
1563 text-align: center;
1563 text-align: center;
1564 }
1564 }
1565
1565
1566 /*deleted binary*/
1566 /*deleted binary*/
1567 .bin.bin2 {
1567 .bin.bin2 {
1568 background-color: @alert2;
1568 background-color: @alert2;
1569 text-align: center;
1569 text-align: center;
1570 }
1570 }
1571
1571
1572 /*mod binary*/
1572 /*mod binary*/
1573 .bin.bin3 {
1573 .bin.bin3 {
1574 background-color: @grey2;
1574 background-color: @grey2;
1575 text-align: center;
1575 text-align: center;
1576 }
1576 }
1577
1577
1578 /*rename file*/
1578 /*rename file*/
1579 .bin.bin4 {
1579 .bin.bin4 {
1580 background-color: @alert4;
1580 background-color: @alert4;
1581 text-align: center;
1581 text-align: center;
1582 }
1582 }
1583
1583
1584 /*copied file*/
1584 /*copied file*/
1585 .bin.bin5 {
1585 .bin.bin5 {
1586 background-color: @alert4;
1586 background-color: @alert4;
1587 text-align: center;
1587 text-align: center;
1588 }
1588 }
1589
1589
1590 /*chmod file*/
1590 /*chmod file*/
1591 .bin.bin6 {
1591 .bin.bin6 {
1592 background-color: @grey2;
1592 background-color: @grey2;
1593 text-align: center;
1593 text-align: center;
1594 }
1594 }
1595 }
1595 }
1596 }
1596 }
1597
1597
1598 .cs_files .cs_added, .cs_files .cs_A,
1598 .cs_files .cs_added, .cs_files .cs_A,
1599 .cs_files .cs_added, .cs_files .cs_M,
1599 .cs_files .cs_added, .cs_files .cs_M,
1600 .cs_files .cs_added, .cs_files .cs_D {
1600 .cs_files .cs_added, .cs_files .cs_D {
1601 height: 16px;
1601 height: 16px;
1602 padding-right: 10px;
1602 padding-right: 10px;
1603 margin-top: 7px;
1603 margin-top: 7px;
1604 text-align: left;
1604 text-align: left;
1605 }
1605 }
1606
1606
1607 .cs_icon_td {
1607 .cs_icon_td {
1608 min-width: 16px;
1608 min-width: 16px;
1609 width: 16px;
1609 width: 16px;
1610 }
1610 }
1611
1611
1612 .pull-request-merge {
1612 .pull-request-merge {
1613 padding: 10px 0;
1613 border: 1px solid @grey5;
1614 padding: 10px 0px 20px;
1614 margin-top: 10px;
1615 margin-top: 10px;
1615 margin-bottom: 20px;
1616 margin-bottom: 20px;
1616 }
1617 }
1617
1618
1619 .pull-request-merge ul {
1620 padding: 0px 0px;
1621 }
1622
1623 .pull-request-merge li:before{
1624 content:none;
1625 }
1626
1618 .pull-request-merge .pull-request-wrap {
1627 .pull-request-merge .pull-request-wrap {
1619 height: 25px;
1628 height: auto;
1620 padding: 5px 0;
1629 padding: 0px 0px;
1630 text-align: right;
1621 }
1631 }
1622
1632
1623 .pull-request-merge span {
1633 .pull-request-merge span {
1624 margin-right: 10px;
1634 margin-right: 5px;
1635 }
1636
1637 .pull-request-merge-actions {
1638 height: 30px;
1639 padding: 0px 0px;
1640 }
1641
1642 .merge-message {
1643 font-size: 1.2em
1625 }
1644 }
1645 .merge-message li{
1646 text-decoration: none;
1647 }
1648
1649 .merge-message.success i {
1650 color:@alert1;
1651 }
1652 .merge-message.warning i {
1653 color: @alert3;
1654 }
1655 .merge-message.error i {
1656 color:@alert2;
1657 }
1658
1659
1626
1660
1627 .pr-versions {
1661 .pr-versions {
1628 position: relative;
1662 position: relative;
1629 top: 6px;
1663 top: 6px;
1630 }
1664 }
1631
1665
1632 #close_pull_request {
1666 #close_pull_request {
1633 margin-right: 0px;
1667 margin-right: 0px;
1634 }
1668 }
1635
1669
1636 .empty_data {
1670 .empty_data {
1637 color: @grey4;
1671 color: @grey4;
1638 }
1672 }
1639
1673
1640 #changeset_compare_view_content {
1674 #changeset_compare_view_content {
1641 margin-bottom: @space;
1675 margin-bottom: @space;
1642 clear: both;
1676 clear: both;
1643 width: 100%;
1677 width: 100%;
1644 box-sizing: border-box;
1678 box-sizing: border-box;
1645 .border-radius(@border-radius);
1679 .border-radius(@border-radius);
1646
1680
1647 .help-block {
1681 .help-block {
1648 margin: @padding 0;
1682 margin: @padding 0;
1649 color: @text-color;
1683 color: @text-color;
1650 }
1684 }
1651
1685
1652 .empty_data {
1686 .empty_data {
1653 margin: @padding 0;
1687 margin: @padding 0;
1654 }
1688 }
1655
1689
1656 .alert {
1690 .alert {
1657 margin-bottom: @space;
1691 margin-bottom: @space;
1658 }
1692 }
1659 }
1693 }
1660
1694
1661 .table_disp {
1695 .table_disp {
1662 .status {
1696 .status {
1663 width: auto;
1697 width: auto;
1664
1698
1665 .flag_status {
1699 .flag_status {
1666 float: left;
1700 float: left;
1667 }
1701 }
1668 }
1702 }
1669 }
1703 }
1670
1704
1671 .status_box_menu {
1705 .status_box_menu {
1672 margin: 0;
1706 margin: 0;
1673 }
1707 }
1674
1708
1675 .notification-table{
1709 .notification-table{
1676 margin-bottom: @space;
1710 margin-bottom: @space;
1677 display: table;
1711 display: table;
1678 width: 100%;
1712 width: 100%;
1679
1713
1680 .container{
1714 .container{
1681 display: table-row;
1715 display: table-row;
1682
1716
1683 .notification-header{
1717 .notification-header{
1684 border-bottom: @border-thickness solid @border-default-color;
1718 border-bottom: @border-thickness solid @border-default-color;
1685 }
1719 }
1686
1720
1687 .notification-subject{
1721 .notification-subject{
1688 display: table-cell;
1722 display: table-cell;
1689 }
1723 }
1690 }
1724 }
1691 }
1725 }
1692
1726
1693 // Notifications
1727 // Notifications
1694 .notification-header{
1728 .notification-header{
1695 display: table;
1729 display: table;
1696 width: 100%;
1730 width: 100%;
1697 padding: floor(@basefontsize/2) 0;
1731 padding: floor(@basefontsize/2) 0;
1698 line-height: 1em;
1732 line-height: 1em;
1699
1733
1700 .desc, .delete-notifications, .read-notifications{
1734 .desc, .delete-notifications, .read-notifications{
1701 display: table-cell;
1735 display: table-cell;
1702 text-align: left;
1736 text-align: left;
1703 }
1737 }
1704
1738
1705 .desc{
1739 .desc{
1706 width: 1163px;
1740 width: 1163px;
1707 }
1741 }
1708
1742
1709 .delete-notifications, .read-notifications{
1743 .delete-notifications, .read-notifications{
1710 width: 35px;
1744 width: 35px;
1711 min-width: 35px; //fixes when only one button is displayed
1745 min-width: 35px; //fixes when only one button is displayed
1712 }
1746 }
1713 }
1747 }
1714
1748
1715 .notification-body {
1749 .notification-body {
1716 .markdown-block,
1750 .markdown-block,
1717 .rst-block {
1751 .rst-block {
1718 padding: @padding 0;
1752 padding: @padding 0;
1719 }
1753 }
1720
1754
1721 .notification-subject {
1755 .notification-subject {
1722 padding: @textmargin 0;
1756 padding: @textmargin 0;
1723 border-bottom: @border-thickness solid @border-default-color;
1757 border-bottom: @border-thickness solid @border-default-color;
1724 }
1758 }
1725 }
1759 }
1726
1760
1727
1761
1728 .notifications_buttons{
1762 .notifications_buttons{
1729 float: right;
1763 float: right;
1730 }
1764 }
1731
1765
1732 #notification-status{
1766 #notification-status{
1733 display: inline;
1767 display: inline;
1734 }
1768 }
1735
1769
1736 // Repositories
1770 // Repositories
1737
1771
1738 #summary.fields{
1772 #summary.fields{
1739 display: table;
1773 display: table;
1740
1774
1741 .field{
1775 .field{
1742 display: table-row;
1776 display: table-row;
1743
1777
1744 .label-summary{
1778 .label-summary{
1745 display: table-cell;
1779 display: table-cell;
1746 min-width: @label-summary-minwidth;
1780 min-width: @label-summary-minwidth;
1747 padding-top: @padding/2;
1781 padding-top: @padding/2;
1748 padding-bottom: @padding/2;
1782 padding-bottom: @padding/2;
1749 padding-right: @padding/2;
1783 padding-right: @padding/2;
1750 }
1784 }
1751
1785
1752 .input{
1786 .input{
1753 display: table-cell;
1787 display: table-cell;
1754 padding: @padding/2;
1788 padding: @padding/2;
1755
1789
1756 input{
1790 input{
1757 min-width: 29em;
1791 min-width: 29em;
1758 padding: @padding/4;
1792 padding: @padding/4;
1759 }
1793 }
1760 }
1794 }
1761 .statistics, .downloads{
1795 .statistics, .downloads{
1762 .disabled{
1796 .disabled{
1763 color: @grey4;
1797 color: @grey4;
1764 }
1798 }
1765 }
1799 }
1766 }
1800 }
1767 }
1801 }
1768
1802
1769 #summary{
1803 #summary{
1770 width: 70%;
1804 width: 70%;
1771 }
1805 }
1772
1806
1773
1807
1774 // Journal
1808 // Journal
1775 .journal.title {
1809 .journal.title {
1776 h5 {
1810 h5 {
1777 float: left;
1811 float: left;
1778 margin: 0;
1812 margin: 0;
1779 width: 70%;
1813 width: 70%;
1780 }
1814 }
1781
1815
1782 ul {
1816 ul {
1783 float: right;
1817 float: right;
1784 display: inline-block;
1818 display: inline-block;
1785 margin: 0;
1819 margin: 0;
1786 width: 30%;
1820 width: 30%;
1787 text-align: right;
1821 text-align: right;
1788
1822
1789 li {
1823 li {
1790 display: inline;
1824 display: inline;
1791 font-size: @journal-fontsize;
1825 font-size: @journal-fontsize;
1792 line-height: 1em;
1826 line-height: 1em;
1793
1827
1794 &:before { content: none; }
1828 &:before { content: none; }
1795 }
1829 }
1796 }
1830 }
1797 }
1831 }
1798
1832
1799 .filterexample {
1833 .filterexample {
1800 position: absolute;
1834 position: absolute;
1801 top: 95px;
1835 top: 95px;
1802 left: @contentpadding;
1836 left: @contentpadding;
1803 color: @rcblue;
1837 color: @rcblue;
1804 font-size: 11px;
1838 font-size: 11px;
1805 font-family: @text-regular;
1839 font-family: @text-regular;
1806 cursor: help;
1840 cursor: help;
1807
1841
1808 &:hover {
1842 &:hover {
1809 color: @rcdarkblue;
1843 color: @rcdarkblue;
1810 }
1844 }
1811
1845
1812 @media (max-width:768px) {
1846 @media (max-width:768px) {
1813 position: relative;
1847 position: relative;
1814 top: auto;
1848 top: auto;
1815 left: auto;
1849 left: auto;
1816 display: block;
1850 display: block;
1817 }
1851 }
1818 }
1852 }
1819
1853
1820
1854
1821 #journal{
1855 #journal{
1822 margin-bottom: @space;
1856 margin-bottom: @space;
1823
1857
1824 .journal_day{
1858 .journal_day{
1825 margin-bottom: @textmargin/2;
1859 margin-bottom: @textmargin/2;
1826 padding-bottom: @textmargin/2;
1860 padding-bottom: @textmargin/2;
1827 font-size: @journal-fontsize;
1861 font-size: @journal-fontsize;
1828 border-bottom: @border-thickness solid @border-default-color;
1862 border-bottom: @border-thickness solid @border-default-color;
1829 }
1863 }
1830
1864
1831 .journal_container{
1865 .journal_container{
1832 margin-bottom: @space;
1866 margin-bottom: @space;
1833
1867
1834 .journal_user{
1868 .journal_user{
1835 display: inline-block;
1869 display: inline-block;
1836 }
1870 }
1837 .journal_action_container{
1871 .journal_action_container{
1838 display: block;
1872 display: block;
1839 margin-top: @textmargin;
1873 margin-top: @textmargin;
1840
1874
1841 div{
1875 div{
1842 display: inline;
1876 display: inline;
1843 }
1877 }
1844
1878
1845 div.journal_action_params{
1879 div.journal_action_params{
1846 display: block;
1880 display: block;
1847 }
1881 }
1848
1882
1849 div.journal_repo:after{
1883 div.journal_repo:after{
1850 content: "\A";
1884 content: "\A";
1851 white-space: pre;
1885 white-space: pre;
1852 }
1886 }
1853
1887
1854 div.date{
1888 div.date{
1855 display: block;
1889 display: block;
1856 margin-bottom: @textmargin;
1890 margin-bottom: @textmargin;
1857 }
1891 }
1858 }
1892 }
1859 }
1893 }
1860 }
1894 }
1861
1895
1862 // Files
1896 // Files
1863 .edit-file-title {
1897 .edit-file-title {
1864 border-bottom: @border-thickness solid @border-default-color;
1898 border-bottom: @border-thickness solid @border-default-color;
1865
1899
1866 .breadcrumbs {
1900 .breadcrumbs {
1867 margin-bottom: 0;
1901 margin-bottom: 0;
1868 }
1902 }
1869 }
1903 }
1870
1904
1871 .edit-file-fieldset {
1905 .edit-file-fieldset {
1872 margin-top: @sidebarpadding;
1906 margin-top: @sidebarpadding;
1873
1907
1874 .fieldset {
1908 .fieldset {
1875 .left-label {
1909 .left-label {
1876 width: 13%;
1910 width: 13%;
1877 }
1911 }
1878 .right-content {
1912 .right-content {
1879 width: 87%;
1913 width: 87%;
1880 max-width: 100%;
1914 max-width: 100%;
1881 }
1915 }
1882 .filename-label {
1916 .filename-label {
1883 margin-top: 13px;
1917 margin-top: 13px;
1884 }
1918 }
1885 .commit-message-label {
1919 .commit-message-label {
1886 margin-top: 4px;
1920 margin-top: 4px;
1887 }
1921 }
1888 .file-upload-input {
1922 .file-upload-input {
1889 input {
1923 input {
1890 display: none;
1924 display: none;
1891 }
1925 }
1892 }
1926 }
1893 p {
1927 p {
1894 margin-top: 5px;
1928 margin-top: 5px;
1895 }
1929 }
1896
1930
1897 }
1931 }
1898 .custom-path-link {
1932 .custom-path-link {
1899 margin-left: 5px;
1933 margin-left: 5px;
1900 }
1934 }
1901 #commit {
1935 #commit {
1902 resize: vertical;
1936 resize: vertical;
1903 }
1937 }
1904 }
1938 }
1905
1939
1906 .delete-file-preview {
1940 .delete-file-preview {
1907 max-height: 250px;
1941 max-height: 250px;
1908 }
1942 }
1909
1943
1910 .new-file,
1944 .new-file,
1911 #filter_activate,
1945 #filter_activate,
1912 #filter_deactivate {
1946 #filter_deactivate {
1913 float: left;
1947 float: left;
1914 margin: 0 0 0 15px;
1948 margin: 0 0 0 15px;
1915 }
1949 }
1916
1950
1917 h3.files_location{
1951 h3.files_location{
1918 line-height: 2.4em;
1952 line-height: 2.4em;
1919 }
1953 }
1920
1954
1921 .browser-nav {
1955 .browser-nav {
1922 display: table;
1956 display: table;
1923 margin-bottom: @space;
1957 margin-bottom: @space;
1924
1958
1925
1959
1926 .info_box {
1960 .info_box {
1927 display: inline-table;
1961 display: inline-table;
1928 height: 2.5em;
1962 height: 2.5em;
1929
1963
1930 .browser-cur-rev, .info_box_elem {
1964 .browser-cur-rev, .info_box_elem {
1931 display: table-cell;
1965 display: table-cell;
1932 vertical-align: middle;
1966 vertical-align: middle;
1933 }
1967 }
1934
1968
1935 .info_box_elem {
1969 .info_box_elem {
1936 border-top: @border-thickness solid @rcblue;
1970 border-top: @border-thickness solid @rcblue;
1937 border-bottom: @border-thickness solid @rcblue;
1971 border-bottom: @border-thickness solid @rcblue;
1938
1972
1939 #at_rev, a {
1973 #at_rev, a {
1940 padding: 0.6em 0.9em;
1974 padding: 0.6em 0.9em;
1941 margin: 0;
1975 margin: 0;
1942 .box-shadow(none);
1976 .box-shadow(none);
1943 border: 0;
1977 border: 0;
1944 height: 12px;
1978 height: 12px;
1945 }
1979 }
1946
1980
1947 input#at_rev {
1981 input#at_rev {
1948 max-width: 50px;
1982 max-width: 50px;
1949 text-align: right;
1983 text-align: right;
1950 }
1984 }
1951
1985
1952 &.previous {
1986 &.previous {
1953 border: @border-thickness solid @rcblue;
1987 border: @border-thickness solid @rcblue;
1954 .disabled {
1988 .disabled {
1955 color: @grey4;
1989 color: @grey4;
1956 cursor: not-allowed;
1990 cursor: not-allowed;
1957 }
1991 }
1958 }
1992 }
1959
1993
1960 &.next {
1994 &.next {
1961 border: @border-thickness solid @rcblue;
1995 border: @border-thickness solid @rcblue;
1962 .disabled {
1996 .disabled {
1963 color: @grey4;
1997 color: @grey4;
1964 cursor: not-allowed;
1998 cursor: not-allowed;
1965 }
1999 }
1966 }
2000 }
1967 }
2001 }
1968
2002
1969 .browser-cur-rev {
2003 .browser-cur-rev {
1970
2004
1971 span{
2005 span{
1972 margin: 0;
2006 margin: 0;
1973 color: @rcblue;
2007 color: @rcblue;
1974 height: 12px;
2008 height: 12px;
1975 display: inline-block;
2009 display: inline-block;
1976 padding: 0.7em 1em ;
2010 padding: 0.7em 1em ;
1977 border: @border-thickness solid @rcblue;
2011 border: @border-thickness solid @rcblue;
1978 margin-right: @padding;
2012 margin-right: @padding;
1979 }
2013 }
1980 }
2014 }
1981 }
2015 }
1982
2016
1983 .search_activate {
2017 .search_activate {
1984 display: table-cell;
2018 display: table-cell;
1985 vertical-align: middle;
2019 vertical-align: middle;
1986
2020
1987 input, label{
2021 input, label{
1988 margin: 0;
2022 margin: 0;
1989 padding: 0;
2023 padding: 0;
1990 }
2024 }
1991
2025
1992 input{
2026 input{
1993 margin-left: @textmargin;
2027 margin-left: @textmargin;
1994 }
2028 }
1995
2029
1996 }
2030 }
1997 }
2031 }
1998
2032
1999 .browser-cur-rev{
2033 .browser-cur-rev{
2000 margin-bottom: @textmargin;
2034 margin-bottom: @textmargin;
2001 }
2035 }
2002
2036
2003 #node_filter_box_loading{
2037 #node_filter_box_loading{
2004 .info_text;
2038 .info_text;
2005 }
2039 }
2006
2040
2007 .browser-search {
2041 .browser-search {
2008 margin: -25px 0px 5px 0px;
2042 margin: -25px 0px 5px 0px;
2009 }
2043 }
2010
2044
2011 .node-filter {
2045 .node-filter {
2012 font-size: @repo-title-fontsize;
2046 font-size: @repo-title-fontsize;
2013 padding: 4px 0px 0px 0px;
2047 padding: 4px 0px 0px 0px;
2014
2048
2015 .node-filter-path {
2049 .node-filter-path {
2016 float: left;
2050 float: left;
2017 color: @grey4;
2051 color: @grey4;
2018 }
2052 }
2019 .node-filter-input {
2053 .node-filter-input {
2020 float: left;
2054 float: left;
2021 margin: -2px 0px 0px 2px;
2055 margin: -2px 0px 0px 2px;
2022 input {
2056 input {
2023 padding: 2px;
2057 padding: 2px;
2024 border: none;
2058 border: none;
2025 font-size: @repo-title-fontsize;
2059 font-size: @repo-title-fontsize;
2026 }
2060 }
2027 }
2061 }
2028 }
2062 }
2029
2063
2030
2064
2031 .browser-result{
2065 .browser-result{
2032 td a{
2066 td a{
2033 margin-left: 0.5em;
2067 margin-left: 0.5em;
2034 display: inline-block;
2068 display: inline-block;
2035
2069
2036 em{
2070 em{
2037 font-family: @text-bold;
2071 font-family: @text-bold;
2038 }
2072 }
2039 }
2073 }
2040 }
2074 }
2041
2075
2042 .browser-highlight{
2076 .browser-highlight{
2043 background-color: @grey5-alpha;
2077 background-color: @grey5-alpha;
2044 }
2078 }
2045
2079
2046
2080
2047 // Search
2081 // Search
2048
2082
2049 .search-form{
2083 .search-form{
2050 #q {
2084 #q {
2051 width: @search-form-width;
2085 width: @search-form-width;
2052 }
2086 }
2053 .fields{
2087 .fields{
2054 margin: 0 0 @space;
2088 margin: 0 0 @space;
2055 }
2089 }
2056
2090
2057 label{
2091 label{
2058 display: inline-block;
2092 display: inline-block;
2059 margin-right: @textmargin;
2093 margin-right: @textmargin;
2060 padding-top: 0.25em;
2094 padding-top: 0.25em;
2061 }
2095 }
2062
2096
2063
2097
2064 .results{
2098 .results{
2065 clear: both;
2099 clear: both;
2066 margin: 0 0 @padding;
2100 margin: 0 0 @padding;
2067 }
2101 }
2068 }
2102 }
2069
2103
2070 div.search-feedback-items {
2104 div.search-feedback-items {
2071 display: inline-block;
2105 display: inline-block;
2072 padding:0px 0px 0px 96px;
2106 padding:0px 0px 0px 96px;
2073 }
2107 }
2074
2108
2075 div.search-code-body {
2109 div.search-code-body {
2076 background-color: #ffffff; padding: 5px 0 5px 10px;
2110 background-color: #ffffff; padding: 5px 0 5px 10px;
2077 pre {
2111 pre {
2078 .match { background-color: #faffa6;}
2112 .match { background-color: #faffa6;}
2079 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2113 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2080 }
2114 }
2081 }
2115 }
2082
2116
2083 .expand_commit.search {
2117 .expand_commit.search {
2084 .show_more.open {
2118 .show_more.open {
2085 height: auto;
2119 height: auto;
2086 max-height: none;
2120 max-height: none;
2087 }
2121 }
2088 }
2122 }
2089
2123
2090 .search-results {
2124 .search-results {
2091
2125
2092 h2 {
2126 h2 {
2093 margin-bottom: 0;
2127 margin-bottom: 0;
2094 }
2128 }
2095 .codeblock {
2129 .codeblock {
2096 border: none;
2130 border: none;
2097 background: transparent;
2131 background: transparent;
2098 }
2132 }
2099
2133
2100 .codeblock-header {
2134 .codeblock-header {
2101 border: none;
2135 border: none;
2102 background: transparent;
2136 background: transparent;
2103 }
2137 }
2104
2138
2105 .code-body {
2139 .code-body {
2106 border: @border-thickness solid @border-default-color;
2140 border: @border-thickness solid @border-default-color;
2107 .border-radius(@border-radius);
2141 .border-radius(@border-radius);
2108 }
2142 }
2109
2143
2110 .td-commit {
2144 .td-commit {
2111 &:extend(pre);
2145 &:extend(pre);
2112 border-bottom: @border-thickness solid @border-default-color;
2146 border-bottom: @border-thickness solid @border-default-color;
2113 }
2147 }
2114
2148
2115 .message {
2149 .message {
2116 height: auto;
2150 height: auto;
2117 max-width: 350px;
2151 max-width: 350px;
2118 white-space: normal;
2152 white-space: normal;
2119 text-overflow: initial;
2153 text-overflow: initial;
2120 overflow: visible;
2154 overflow: visible;
2121
2155
2122 .match { background-color: #faffa6;}
2156 .match { background-color: #faffa6;}
2123 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2157 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2124 }
2158 }
2125
2159
2126 }
2160 }
2127
2161
2128 table.rctable td.td-search-results div {
2162 table.rctable td.td-search-results div {
2129 max-width: 100%;
2163 max-width: 100%;
2130 }
2164 }
2131
2165
2132 #tip-box, .tip-box{
2166 #tip-box, .tip-box{
2133 padding: @menupadding/2;
2167 padding: @menupadding/2;
2134 display: block;
2168 display: block;
2135 border: @border-thickness solid @border-highlight-color;
2169 border: @border-thickness solid @border-highlight-color;
2136 .border-radius(@border-radius);
2170 .border-radius(@border-radius);
2137 background-color: white;
2171 background-color: white;
2138 z-index: 99;
2172 z-index: 99;
2139 white-space: pre-wrap;
2173 white-space: pre-wrap;
2140 }
2174 }
2141
2175
2142 #linktt {
2176 #linktt {
2143 width: 79px;
2177 width: 79px;
2144 }
2178 }
2145
2179
2146 #help_kb .modal-content{
2180 #help_kb .modal-content{
2147 max-width: 750px;
2181 max-width: 750px;
2148 margin: 10% auto;
2182 margin: 10% auto;
2149
2183
2150 table{
2184 table{
2151 td,th{
2185 td,th{
2152 border-bottom: none;
2186 border-bottom: none;
2153 line-height: 2.5em;
2187 line-height: 2.5em;
2154 }
2188 }
2155 th{
2189 th{
2156 padding-bottom: @textmargin/2;
2190 padding-bottom: @textmargin/2;
2157 }
2191 }
2158 td.keys{
2192 td.keys{
2159 text-align: center;
2193 text-align: center;
2160 }
2194 }
2161 }
2195 }
2162
2196
2163 .block-left{
2197 .block-left{
2164 width: 45%;
2198 width: 45%;
2165 margin-right: 5%;
2199 margin-right: 5%;
2166 }
2200 }
2167 .modal-footer{
2201 .modal-footer{
2168 clear: both;
2202 clear: both;
2169 }
2203 }
2170 .key.tag{
2204 .key.tag{
2171 padding: 0.5em;
2205 padding: 0.5em;
2172 background-color: @rcblue;
2206 background-color: @rcblue;
2173 color: white;
2207 color: white;
2174 border-color: @rcblue;
2208 border-color: @rcblue;
2175 .box-shadow(none);
2209 .box-shadow(none);
2176 }
2210 }
2177 }
2211 }
2178
2212
2179
2213
2180
2214
2181 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2215 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2182
2216
2183 @import 'statistics-graph';
2217 @import 'statistics-graph';
2184 @import 'tables';
2218 @import 'tables';
2185 @import 'forms';
2219 @import 'forms';
2186 @import 'diff';
2220 @import 'diff';
2187 @import 'summary';
2221 @import 'summary';
2188 @import 'navigation';
2222 @import 'navigation';
2189
2223
2190 //--- SHOW/HIDE SECTIONS --//
2224 //--- SHOW/HIDE SECTIONS --//
2191
2225
2192 .btn-collapse {
2226 .btn-collapse {
2193 float: right;
2227 float: right;
2194 text-align: right;
2228 text-align: right;
2195 font-family: @text-light;
2229 font-family: @text-light;
2196 font-size: @basefontsize;
2230 font-size: @basefontsize;
2197 cursor: pointer;
2231 cursor: pointer;
2198 border: none;
2232 border: none;
2199 color: @rcblue;
2233 color: @rcblue;
2200 }
2234 }
2201
2235
2202 table.rctable,
2236 table.rctable,
2203 table.dataTable {
2237 table.dataTable {
2204 .btn-collapse {
2238 .btn-collapse {
2205 float: right;
2239 float: right;
2206 text-align: right;
2240 text-align: right;
2207 }
2241 }
2208 }
2242 }
2209
2243
2210
2244
2211 // TODO: johbo: Fix for IE10, this avoids that we see a border
2245 // TODO: johbo: Fix for IE10, this avoids that we see a border
2212 // and padding around checkboxes and radio boxes. Move to the right place,
2246 // and padding around checkboxes and radio boxes. Move to the right place,
2213 // or better: Remove this once we did the form refactoring.
2247 // or better: Remove this once we did the form refactoring.
2214 input[type=checkbox],
2248 input[type=checkbox],
2215 input[type=radio] {
2249 input[type=radio] {
2216 padding: 0;
2250 padding: 0;
2217 border: none;
2251 border: none;
2218 }
2252 }
2219
2253
2220 .toggle-ajax-spinner{
2254 .toggle-ajax-spinner{
2221 height: 16px;
2255 height: 16px;
2222 width: 16px;
2256 width: 16px;
2223 }
2257 }
@@ -1,796 +1,802 b''
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
1 // # Copyright (C) 2010-2017 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45 /* Comment form for main and inline comments */
45 /* Comment form for main and inline comments */
46 (function(mod) {
46 (function(mod) {
47
47
48 if (typeof exports == "object" && typeof module == "object") {
48 if (typeof exports == "object" && typeof module == "object") {
49 // CommonJS
49 // CommonJS
50 module.exports = mod();
50 module.exports = mod();
51 }
51 }
52 else {
52 else {
53 // Plain browser env
53 // Plain browser env
54 (this || window).CommentForm = mod();
54 (this || window).CommentForm = mod();
55 }
55 }
56
56
57 })(function() {
57 })(function() {
58 "use strict";
58 "use strict";
59
59
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
60 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
61 if (!(this instanceof CommentForm)) {
61 if (!(this instanceof CommentForm)) {
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
62 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
63 }
63 }
64
64
65 // bind the element instance to our Form
65 // bind the element instance to our Form
66 $(formElement).get(0).CommentForm = this;
66 $(formElement).get(0).CommentForm = this;
67
67
68 this.withLineNo = function(selector) {
68 this.withLineNo = function(selector) {
69 var lineNo = this.lineNo;
69 var lineNo = this.lineNo;
70 if (lineNo === undefined) {
70 if (lineNo === undefined) {
71 return selector
71 return selector
72 } else {
72 } else {
73 return selector + '_' + lineNo;
73 return selector + '_' + lineNo;
74 }
74 }
75 };
75 };
76
76
77 this.commitId = commitId;
77 this.commitId = commitId;
78 this.pullRequestId = pullRequestId;
78 this.pullRequestId = pullRequestId;
79 this.lineNo = lineNo;
79 this.lineNo = lineNo;
80 this.initAutocompleteActions = initAutocompleteActions;
80 this.initAutocompleteActions = initAutocompleteActions;
81
81
82 this.previewButton = this.withLineNo('#preview-btn');
82 this.previewButton = this.withLineNo('#preview-btn');
83 this.previewContainer = this.withLineNo('#preview-container');
83 this.previewContainer = this.withLineNo('#preview-container');
84
84
85 this.previewBoxSelector = this.withLineNo('#preview-box');
85 this.previewBoxSelector = this.withLineNo('#preview-box');
86
86
87 this.editButton = this.withLineNo('#edit-btn');
87 this.editButton = this.withLineNo('#edit-btn');
88 this.editContainer = this.withLineNo('#edit-container');
88 this.editContainer = this.withLineNo('#edit-container');
89 this.cancelButton = this.withLineNo('#cancel-btn');
89 this.cancelButton = this.withLineNo('#cancel-btn');
90 this.commentType = this.withLineNo('#comment_type');
90 this.commentType = this.withLineNo('#comment_type');
91
91
92 this.resolvesId = null;
92 this.resolvesId = null;
93 this.resolvesActionId = null;
93 this.resolvesActionId = null;
94
94
95 this.cmBox = this.withLineNo('#text');
95 this.cmBox = this.withLineNo('#text');
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
96 this.cm = initCommentBoxCodeMirror(this.cmBox, this.initAutocompleteActions);
97
97
98 this.statusChange = this.withLineNo('#change_status');
98 this.statusChange = this.withLineNo('#change_status');
99
99
100 this.submitForm = formElement;
100 this.submitForm = formElement;
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
101 this.submitButton = $(this.submitForm).find('input[type="submit"]');
102 this.submitButtonText = this.submitButton.val();
102 this.submitButtonText = this.submitButton.val();
103
103
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
104 this.previewUrl = pyroutes.url('changeset_comment_preview',
105 {'repo_name': templateContext.repo_name});
105 {'repo_name': templateContext.repo_name});
106
106
107 if (resolvesCommentId){
107 if (resolvesCommentId){
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
108 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
109 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
110 $(this.commentType).prop('disabled', true);
110 $(this.commentType).prop('disabled', true);
111 $(this.commentType).addClass('disabled');
111 $(this.commentType).addClass('disabled');
112
112
113 // disable select
113 // disable select
114 setTimeout(function() {
114 setTimeout(function() {
115 $(self.statusChange).select2('readonly', true);
115 $(self.statusChange).select2('readonly', true);
116 }, 10);
116 }, 10);
117
117
118 var resolvedInfo = (
118 var resolvedInfo = (
119 '<li class="">' +
119 '<li class="">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
120 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
121 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
122 '</li>'
122 '</li>'
123 ).format(resolvesCommentId, _gettext('resolve comment'));
123 ).format(resolvesCommentId, _gettext('resolve comment'));
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
124 $(resolvedInfo).insertAfter($(this.commentType).parent());
125 }
125 }
126
126
127 // based on commitId, or pullRequestId decide where do we submit
127 // based on commitId, or pullRequestId decide where do we submit
128 // out data
128 // out data
129 if (this.commitId){
129 if (this.commitId){
130 this.submitUrl = pyroutes.url('changeset_comment',
130 this.submitUrl = pyroutes.url('changeset_comment',
131 {'repo_name': templateContext.repo_name,
131 {'repo_name': templateContext.repo_name,
132 'revision': this.commitId});
132 'revision': this.commitId});
133 this.selfUrl = pyroutes.url('changeset_home',
133 this.selfUrl = pyroutes.url('changeset_home',
134 {'repo_name': templateContext.repo_name,
134 {'repo_name': templateContext.repo_name,
135 'revision': this.commitId});
135 'revision': this.commitId});
136
136
137 } else if (this.pullRequestId) {
137 } else if (this.pullRequestId) {
138 this.submitUrl = pyroutes.url('pullrequest_comment',
138 this.submitUrl = pyroutes.url('pullrequest_comment',
139 {'repo_name': templateContext.repo_name,
139 {'repo_name': templateContext.repo_name,
140 'pull_request_id': this.pullRequestId});
140 'pull_request_id': this.pullRequestId});
141 this.selfUrl = pyroutes.url('pullrequest_show',
141 this.selfUrl = pyroutes.url('pullrequest_show',
142 {'repo_name': templateContext.repo_name,
142 {'repo_name': templateContext.repo_name,
143 'pull_request_id': this.pullRequestId});
143 'pull_request_id': this.pullRequestId});
144
144
145 } else {
145 } else {
146 throw new Error(
146 throw new Error(
147 'CommentForm requires pullRequestId, or commitId to be specified.')
147 'CommentForm requires pullRequestId, or commitId to be specified.')
148 }
148 }
149
149
150 // FUNCTIONS and helpers
150 // FUNCTIONS and helpers
151 var self = this;
151 var self = this;
152
152
153 this.isInline = function(){
153 this.isInline = function(){
154 return this.lineNo && this.lineNo != 'general';
154 return this.lineNo && this.lineNo != 'general';
155 };
155 };
156
156
157 this.getCmInstance = function(){
157 this.getCmInstance = function(){
158 return this.cm
158 return this.cm
159 };
159 };
160
160
161 this.setPlaceholder = function(placeholder) {
161 this.setPlaceholder = function(placeholder) {
162 var cm = this.getCmInstance();
162 var cm = this.getCmInstance();
163 if (cm){
163 if (cm){
164 cm.setOption('placeholder', placeholder);
164 cm.setOption('placeholder', placeholder);
165 }
165 }
166 };
166 };
167
167
168 this.getCommentStatus = function() {
168 this.getCommentStatus = function() {
169 return $(this.submitForm).find(this.statusChange).val();
169 return $(this.submitForm).find(this.statusChange).val();
170 };
170 };
171 this.getCommentType = function() {
171 this.getCommentType = function() {
172 return $(this.submitForm).find(this.commentType).val();
172 return $(this.submitForm).find(this.commentType).val();
173 };
173 };
174
174
175 this.getResolvesId = function() {
175 this.getResolvesId = function() {
176 return $(this.submitForm).find(this.resolvesId).val() || null;
176 return $(this.submitForm).find(this.resolvesId).val() || null;
177 };
177 };
178 this.markCommentResolved = function(resolvedCommentId){
178 this.markCommentResolved = function(resolvedCommentId){
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
179 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
180 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
181 };
181 };
182
182
183 this.isAllowedToSubmit = function() {
183 this.isAllowedToSubmit = function() {
184 return !$(this.submitButton).prop('disabled');
184 return !$(this.submitButton).prop('disabled');
185 };
185 };
186
186
187 this.initStatusChangeSelector = function(){
187 this.initStatusChangeSelector = function(){
188 var formatChangeStatus = function(state, escapeMarkup) {
188 var formatChangeStatus = function(state, escapeMarkup) {
189 var originalOption = state.element;
189 var originalOption = state.element;
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
190 return '<div class="flag_status ' + $(originalOption).data('status') + ' pull-left"></div>' +
191 '<span>' + escapeMarkup(state.text) + '</span>';
191 '<span>' + escapeMarkup(state.text) + '</span>';
192 };
192 };
193 var formatResult = function(result, container, query, escapeMarkup) {
193 var formatResult = function(result, container, query, escapeMarkup) {
194 return formatChangeStatus(result, escapeMarkup);
194 return formatChangeStatus(result, escapeMarkup);
195 };
195 };
196
196
197 var formatSelection = function(data, container, escapeMarkup) {
197 var formatSelection = function(data, container, escapeMarkup) {
198 return formatChangeStatus(data, escapeMarkup);
198 return formatChangeStatus(data, escapeMarkup);
199 };
199 };
200
200
201 $(this.submitForm).find(this.statusChange).select2({
201 $(this.submitForm).find(this.statusChange).select2({
202 placeholder: _gettext('Status Review'),
202 placeholder: _gettext('Status Review'),
203 formatResult: formatResult,
203 formatResult: formatResult,
204 formatSelection: formatSelection,
204 formatSelection: formatSelection,
205 containerCssClass: "drop-menu status_box_menu",
205 containerCssClass: "drop-menu status_box_menu",
206 dropdownCssClass: "drop-menu-dropdown",
206 dropdownCssClass: "drop-menu-dropdown",
207 dropdownAutoWidth: true,
207 dropdownAutoWidth: true,
208 minimumResultsForSearch: -1
208 minimumResultsForSearch: -1
209 });
209 });
210 $(this.submitForm).find(this.statusChange).on('change', function() {
210 $(this.submitForm).find(this.statusChange).on('change', function() {
211 var status = self.getCommentStatus();
211 var status = self.getCommentStatus();
212 if (status && !self.isInline()) {
212 if (status && !self.isInline()) {
213 $(self.submitButton).prop('disabled', false);
213 $(self.submitButton).prop('disabled', false);
214 }
214 }
215
215
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
216 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
217 self.setPlaceholder(placeholderText)
217 self.setPlaceholder(placeholderText)
218 })
218 })
219 };
219 };
220
220
221 // reset the comment form into it's original state
221 // reset the comment form into it's original state
222 this.resetCommentFormState = function(content) {
222 this.resetCommentFormState = function(content) {
223 content = content || '';
223 content = content || '';
224
224
225 $(this.editContainer).show();
225 $(this.editContainer).show();
226 $(this.editButton).parent().addClass('active');
226 $(this.editButton).parent().addClass('active');
227
227
228 $(this.previewContainer).hide();
228 $(this.previewContainer).hide();
229 $(this.previewButton).parent().removeClass('active');
229 $(this.previewButton).parent().removeClass('active');
230
230
231 this.setActionButtonsDisabled(true);
231 this.setActionButtonsDisabled(true);
232 self.cm.setValue(content);
232 self.cm.setValue(content);
233 self.cm.setOption("readOnly", false);
233 self.cm.setOption("readOnly", false);
234
234
235 if (this.resolvesId) {
235 if (this.resolvesId) {
236 // destroy the resolve action
236 // destroy the resolve action
237 $(this.resolvesId).parent().remove();
237 $(this.resolvesId).parent().remove();
238 }
238 }
239
239
240 $(this.statusChange).select2('readonly', false);
240 $(this.statusChange).select2('readonly', false);
241 };
241 };
242
242
243 this.globalSubmitSuccessCallback = function(){};
243 this.globalSubmitSuccessCallback = function(){
244 // default behaviour is to call GLOBAL hook, if it's registered.
245 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
246 commentFormGlobalSubmitSuccessCallback()
247 }
248 };
244
249
245 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
250 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
246 failHandler = failHandler || function() {};
251 failHandler = failHandler || function() {};
247 var postData = toQueryString(postData);
252 var postData = toQueryString(postData);
248 var request = $.ajax({
253 var request = $.ajax({
249 url: url,
254 url: url,
250 type: 'POST',
255 type: 'POST',
251 data: postData,
256 data: postData,
252 headers: {'X-PARTIAL-XHR': true}
257 headers: {'X-PARTIAL-XHR': true}
253 })
258 })
254 .done(function(data) {
259 .done(function(data) {
255 successHandler(data);
260 successHandler(data);
256 })
261 })
257 .fail(function(data, textStatus, errorThrown){
262 .fail(function(data, textStatus, errorThrown){
258 alert(
263 alert(
259 "Error while submitting comment.\n" +
264 "Error while submitting comment.\n" +
260 "Error code {0} ({1}).".format(data.status, data.statusText));
265 "Error code {0} ({1}).".format(data.status, data.statusText));
261 failHandler()
266 failHandler()
262 });
267 });
263 return request;
268 return request;
264 };
269 };
265
270
266 // overwrite a submitHandler, we need to do it for inline comments
271 // overwrite a submitHandler, we need to do it for inline comments
267 this.setHandleFormSubmit = function(callback) {
272 this.setHandleFormSubmit = function(callback) {
268 this.handleFormSubmit = callback;
273 this.handleFormSubmit = callback;
269 };
274 };
270
275
271 // overwrite a submitSuccessHandler
276 // overwrite a submitSuccessHandler
272 this.setGlobalSubmitSuccessCallback = function(callback) {
277 this.setGlobalSubmitSuccessCallback = function(callback) {
273 this.globalSubmitSuccessCallback = callback;
278 this.globalSubmitSuccessCallback = callback;
274 };
279 };
275
280
276 // default handler for for submit for main comments
281 // default handler for for submit for main comments
277 this.handleFormSubmit = function() {
282 this.handleFormSubmit = function() {
278 var text = self.cm.getValue();
283 var text = self.cm.getValue();
279 var status = self.getCommentStatus();
284 var status = self.getCommentStatus();
280 var commentType = self.getCommentType();
285 var commentType = self.getCommentType();
281 var resolvesCommentId = self.getResolvesId();
286 var resolvesCommentId = self.getResolvesId();
282
287
283 if (text === "" && !status) {
288 if (text === "" && !status) {
284 return;
289 return;
285 }
290 }
286
291
287 var excludeCancelBtn = false;
292 var excludeCancelBtn = false;
288 var submitEvent = true;
293 var submitEvent = true;
289 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
294 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
290 self.cm.setOption("readOnly", true);
295 self.cm.setOption("readOnly", true);
291
296
292 var postData = {
297 var postData = {
293 'text': text,
298 'text': text,
294 'changeset_status': status,
299 'changeset_status': status,
295 'comment_type': commentType,
300 'comment_type': commentType,
296 'csrf_token': CSRF_TOKEN
301 'csrf_token': CSRF_TOKEN
297 };
302 };
298 if (resolvesCommentId){
303 if (resolvesCommentId){
299 postData['resolves_comment_id'] = resolvesCommentId;
304 postData['resolves_comment_id'] = resolvesCommentId;
300 }
305 }
301
306
302 var submitSuccessCallback = function(o) {
307 var submitSuccessCallback = function(o) {
303 if (status) {
308 // reload page if we change status for single commit.
309 if (status && self.commitId) {
304 location.reload(true);
310 location.reload(true);
305 } else {
311 } else {
306 $('#injected_page_comments').append(o.rendered_text);
312 $('#injected_page_comments').append(o.rendered_text);
307 self.resetCommentFormState();
313 self.resetCommentFormState();
308 timeagoActivate();
314 timeagoActivate();
309
315
310 // mark visually which comment was resolved
316 // mark visually which comment was resolved
311 if (resolvesCommentId) {
317 if (resolvesCommentId) {
312 self.markCommentResolved(resolvesCommentId);
318 self.markCommentResolved(resolvesCommentId);
313 }
319 }
314 }
320 }
315
321
316 // run global callback on submit
322 // run global callback on submit
317 self.globalSubmitSuccessCallback();
323 self.globalSubmitSuccessCallback();
318
324
319 };
325 };
320 var submitFailCallback = function(){
326 var submitFailCallback = function(){
321 self.resetCommentFormState(text);
327 self.resetCommentFormState(text);
322 };
328 };
323 self.submitAjaxPOST(
329 self.submitAjaxPOST(
324 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
330 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
325 };
331 };
326
332
327 this.previewSuccessCallback = function(o) {
333 this.previewSuccessCallback = function(o) {
328 $(self.previewBoxSelector).html(o);
334 $(self.previewBoxSelector).html(o);
329 $(self.previewBoxSelector).removeClass('unloaded');
335 $(self.previewBoxSelector).removeClass('unloaded');
330
336
331 // swap buttons, making preview active
337 // swap buttons, making preview active
332 $(self.previewButton).parent().addClass('active');
338 $(self.previewButton).parent().addClass('active');
333 $(self.editButton).parent().removeClass('active');
339 $(self.editButton).parent().removeClass('active');
334
340
335 // unlock buttons
341 // unlock buttons
336 self.setActionButtonsDisabled(false);
342 self.setActionButtonsDisabled(false);
337 };
343 };
338
344
339 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
345 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
340 excludeCancelBtn = excludeCancelBtn || false;
346 excludeCancelBtn = excludeCancelBtn || false;
341 submitEvent = submitEvent || false;
347 submitEvent = submitEvent || false;
342
348
343 $(this.editButton).prop('disabled', state);
349 $(this.editButton).prop('disabled', state);
344 $(this.previewButton).prop('disabled', state);
350 $(this.previewButton).prop('disabled', state);
345
351
346 if (!excludeCancelBtn) {
352 if (!excludeCancelBtn) {
347 $(this.cancelButton).prop('disabled', state);
353 $(this.cancelButton).prop('disabled', state);
348 }
354 }
349
355
350 var submitState = state;
356 var submitState = state;
351 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
357 if (!submitEvent && this.getCommentStatus() && !this.lineNo) {
352 // if the value of commit review status is set, we allow
358 // if the value of commit review status is set, we allow
353 // submit button, but only on Main form, lineNo means inline
359 // submit button, but only on Main form, lineNo means inline
354 submitState = false
360 submitState = false
355 }
361 }
356 $(this.submitButton).prop('disabled', submitState);
362 $(this.submitButton).prop('disabled', submitState);
357 if (submitEvent) {
363 if (submitEvent) {
358 $(this.submitButton).val(_gettext('Submitting...'));
364 $(this.submitButton).val(_gettext('Submitting...'));
359 } else {
365 } else {
360 $(this.submitButton).val(this.submitButtonText);
366 $(this.submitButton).val(this.submitButtonText);
361 }
367 }
362
368
363 };
369 };
364
370
365 // lock preview/edit/submit buttons on load, but exclude cancel button
371 // lock preview/edit/submit buttons on load, but exclude cancel button
366 var excludeCancelBtn = true;
372 var excludeCancelBtn = true;
367 this.setActionButtonsDisabled(true, excludeCancelBtn);
373 this.setActionButtonsDisabled(true, excludeCancelBtn);
368
374
369 // anonymous users don't have access to initialized CM instance
375 // anonymous users don't have access to initialized CM instance
370 if (this.cm !== undefined){
376 if (this.cm !== undefined){
371 this.cm.on('change', function(cMirror) {
377 this.cm.on('change', function(cMirror) {
372 if (cMirror.getValue() === "") {
378 if (cMirror.getValue() === "") {
373 self.setActionButtonsDisabled(true, excludeCancelBtn)
379 self.setActionButtonsDisabled(true, excludeCancelBtn)
374 } else {
380 } else {
375 self.setActionButtonsDisabled(false, excludeCancelBtn)
381 self.setActionButtonsDisabled(false, excludeCancelBtn)
376 }
382 }
377 });
383 });
378 }
384 }
379
385
380 $(this.editButton).on('click', function(e) {
386 $(this.editButton).on('click', function(e) {
381 e.preventDefault();
387 e.preventDefault();
382
388
383 $(self.previewButton).parent().removeClass('active');
389 $(self.previewButton).parent().removeClass('active');
384 $(self.previewContainer).hide();
390 $(self.previewContainer).hide();
385
391
386 $(self.editButton).parent().addClass('active');
392 $(self.editButton).parent().addClass('active');
387 $(self.editContainer).show();
393 $(self.editContainer).show();
388
394
389 });
395 });
390
396
391 $(this.previewButton).on('click', function(e) {
397 $(this.previewButton).on('click', function(e) {
392 e.preventDefault();
398 e.preventDefault();
393 var text = self.cm.getValue();
399 var text = self.cm.getValue();
394
400
395 if (text === "") {
401 if (text === "") {
396 return;
402 return;
397 }
403 }
398
404
399 var postData = {
405 var postData = {
400 'text': text,
406 'text': text,
401 'renderer': templateContext.visual.default_renderer,
407 'renderer': templateContext.visual.default_renderer,
402 'csrf_token': CSRF_TOKEN
408 'csrf_token': CSRF_TOKEN
403 };
409 };
404
410
405 // lock ALL buttons on preview
411 // lock ALL buttons on preview
406 self.setActionButtonsDisabled(true);
412 self.setActionButtonsDisabled(true);
407
413
408 $(self.previewBoxSelector).addClass('unloaded');
414 $(self.previewBoxSelector).addClass('unloaded');
409 $(self.previewBoxSelector).html(_gettext('Loading ...'));
415 $(self.previewBoxSelector).html(_gettext('Loading ...'));
410
416
411 $(self.editContainer).hide();
417 $(self.editContainer).hide();
412 $(self.previewContainer).show();
418 $(self.previewContainer).show();
413
419
414 // by default we reset state of comment preserving the text
420 // by default we reset state of comment preserving the text
415 var previewFailCallback = function(){
421 var previewFailCallback = function(){
416 self.resetCommentFormState(text)
422 self.resetCommentFormState(text)
417 };
423 };
418 self.submitAjaxPOST(
424 self.submitAjaxPOST(
419 self.previewUrl, postData, self.previewSuccessCallback,
425 self.previewUrl, postData, self.previewSuccessCallback,
420 previewFailCallback);
426 previewFailCallback);
421
427
422 $(self.previewButton).parent().addClass('active');
428 $(self.previewButton).parent().addClass('active');
423 $(self.editButton).parent().removeClass('active');
429 $(self.editButton).parent().removeClass('active');
424 });
430 });
425
431
426 $(this.submitForm).submit(function(e) {
432 $(this.submitForm).submit(function(e) {
427 e.preventDefault();
433 e.preventDefault();
428 var allowedToSubmit = self.isAllowedToSubmit();
434 var allowedToSubmit = self.isAllowedToSubmit();
429 if (!allowedToSubmit){
435 if (!allowedToSubmit){
430 return false;
436 return false;
431 }
437 }
432 self.handleFormSubmit();
438 self.handleFormSubmit();
433 });
439 });
434
440
435 }
441 }
436
442
437 return CommentForm;
443 return CommentForm;
438 });
444 });
439
445
440 /* comments controller */
446 /* comments controller */
441 var CommentsController = function() {
447 var CommentsController = function() {
442 var mainComment = '#text';
448 var mainComment = '#text';
443 var self = this;
449 var self = this;
444
450
445 this.cancelComment = function(node) {
451 this.cancelComment = function(node) {
446 var $node = $(node);
452 var $node = $(node);
447 var $td = $node.closest('td');
453 var $td = $node.closest('td');
448 $node.closest('.comment-inline-form').remove();
454 $node.closest('.comment-inline-form').remove();
449 return false;
455 return false;
450 };
456 };
451
457
452 this.getLineNumber = function(node) {
458 this.getLineNumber = function(node) {
453 var $node = $(node);
459 var $node = $(node);
454 return $node.closest('td').attr('data-line-number');
460 return $node.closest('td').attr('data-line-number');
455 };
461 };
456
462
457 this.scrollToComment = function(node, offset, outdated) {
463 this.scrollToComment = function(node, offset, outdated) {
458 var outdated = outdated || false;
464 var outdated = outdated || false;
459 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
465 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
460
466
461 if (!node) {
467 if (!node) {
462 node = $('.comment-selected');
468 node = $('.comment-selected');
463 if (!node.length) {
469 if (!node.length) {
464 node = $('comment-current')
470 node = $('comment-current')
465 }
471 }
466 }
472 }
467 $comment = $(node).closest(klass);
473 $comment = $(node).closest(klass);
468 $comments = $(klass);
474 $comments = $(klass);
469
475
470 $('.comment-selected').removeClass('comment-selected');
476 $('.comment-selected').removeClass('comment-selected');
471
477
472 var nextIdx = $(klass).index($comment) + offset;
478 var nextIdx = $(klass).index($comment) + offset;
473 if (nextIdx >= $comments.length) {
479 if (nextIdx >= $comments.length) {
474 nextIdx = 0;
480 nextIdx = 0;
475 }
481 }
476 var $next = $(klass).eq(nextIdx);
482 var $next = $(klass).eq(nextIdx);
477 var $cb = $next.closest('.cb');
483 var $cb = $next.closest('.cb');
478 $cb.removeClass('cb-collapsed');
484 $cb.removeClass('cb-collapsed');
479
485
480 var $filediffCollapseState = $cb.closest('.filediff').prev();
486 var $filediffCollapseState = $cb.closest('.filediff').prev();
481 $filediffCollapseState.prop('checked', false);
487 $filediffCollapseState.prop('checked', false);
482 $next.addClass('comment-selected');
488 $next.addClass('comment-selected');
483 scrollToElement($next);
489 scrollToElement($next);
484 return false;
490 return false;
485 };
491 };
486
492
487 this.nextComment = function(node) {
493 this.nextComment = function(node) {
488 return self.scrollToComment(node, 1);
494 return self.scrollToComment(node, 1);
489 };
495 };
490
496
491 this.prevComment = function(node) {
497 this.prevComment = function(node) {
492 return self.scrollToComment(node, -1);
498 return self.scrollToComment(node, -1);
493 };
499 };
494
500
495 this.nextOutdatedComment = function(node) {
501 this.nextOutdatedComment = function(node) {
496 return self.scrollToComment(node, 1, true);
502 return self.scrollToComment(node, 1, true);
497 };
503 };
498
504
499 this.prevOutdatedComment = function(node) {
505 this.prevOutdatedComment = function(node) {
500 return self.scrollToComment(node, -1, true);
506 return self.scrollToComment(node, -1, true);
501 };
507 };
502
508
503 this.deleteComment = function(node) {
509 this.deleteComment = function(node) {
504 if (!confirm(_gettext('Delete this comment?'))) {
510 if (!confirm(_gettext('Delete this comment?'))) {
505 return false;
511 return false;
506 }
512 }
507 var $node = $(node);
513 var $node = $(node);
508 var $td = $node.closest('td');
514 var $td = $node.closest('td');
509 var $comment = $node.closest('.comment');
515 var $comment = $node.closest('.comment');
510 var comment_id = $comment.attr('data-comment-id');
516 var comment_id = $comment.attr('data-comment-id');
511 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
517 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
512 var postData = {
518 var postData = {
513 '_method': 'delete',
519 '_method': 'delete',
514 'csrf_token': CSRF_TOKEN
520 'csrf_token': CSRF_TOKEN
515 };
521 };
516
522
517 $comment.addClass('comment-deleting');
523 $comment.addClass('comment-deleting');
518 $comment.hide('fast');
524 $comment.hide('fast');
519
525
520 var success = function(response) {
526 var success = function(response) {
521 $comment.remove();
527 $comment.remove();
522 return false;
528 return false;
523 };
529 };
524 var failure = function(data, textStatus, xhr) {
530 var failure = function(data, textStatus, xhr) {
525 alert("error processing request: " + textStatus);
531 alert("error processing request: " + textStatus);
526 $comment.show('fast');
532 $comment.show('fast');
527 $comment.removeClass('comment-deleting');
533 $comment.removeClass('comment-deleting');
528 return false;
534 return false;
529 };
535 };
530 ajaxPOST(url, postData, success, failure);
536 ajaxPOST(url, postData, success, failure);
531 };
537 };
532
538
533 this.toggleWideMode = function (node) {
539 this.toggleWideMode = function (node) {
534 if ($('#content').hasClass('wrapper')) {
540 if ($('#content').hasClass('wrapper')) {
535 $('#content').removeClass("wrapper");
541 $('#content').removeClass("wrapper");
536 $('#content').addClass("wide-mode-wrapper");
542 $('#content').addClass("wide-mode-wrapper");
537 $(node).addClass('btn-success');
543 $(node).addClass('btn-success');
538 } else {
544 } else {
539 $('#content').removeClass("wide-mode-wrapper");
545 $('#content').removeClass("wide-mode-wrapper");
540 $('#content').addClass("wrapper");
546 $('#content').addClass("wrapper");
541 $(node).removeClass('btn-success');
547 $(node).removeClass('btn-success');
542 }
548 }
543 return false;
549 return false;
544 };
550 };
545
551
546 this.toggleComments = function(node, show) {
552 this.toggleComments = function(node, show) {
547 var $filediff = $(node).closest('.filediff');
553 var $filediff = $(node).closest('.filediff');
548 if (show === true) {
554 if (show === true) {
549 $filediff.removeClass('hide-comments');
555 $filediff.removeClass('hide-comments');
550 } else if (show === false) {
556 } else if (show === false) {
551 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
557 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
552 $filediff.addClass('hide-comments');
558 $filediff.addClass('hide-comments');
553 } else {
559 } else {
554 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
560 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
555 $filediff.toggleClass('hide-comments');
561 $filediff.toggleClass('hide-comments');
556 }
562 }
557 return false;
563 return false;
558 };
564 };
559
565
560 this.toggleLineComments = function(node) {
566 this.toggleLineComments = function(node) {
561 self.toggleComments(node, true);
567 self.toggleComments(node, true);
562 var $node = $(node);
568 var $node = $(node);
563 $node.closest('tr').toggleClass('hide-line-comments');
569 $node.closest('tr').toggleClass('hide-line-comments');
564 };
570 };
565
571
566 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
572 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
567 var pullRequestId = templateContext.pull_request_data.pull_request_id;
573 var pullRequestId = templateContext.pull_request_data.pull_request_id;
568 var commitId = templateContext.commit_data.commit_id;
574 var commitId = templateContext.commit_data.commit_id;
569
575
570 var commentForm = new CommentForm(
576 var commentForm = new CommentForm(
571 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
577 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
572 var cm = commentForm.getCmInstance();
578 var cm = commentForm.getCmInstance();
573
579
574 if (resolvesCommentId){
580 if (resolvesCommentId){
575 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
581 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
576 }
582 }
577
583
578 setTimeout(function() {
584 setTimeout(function() {
579 // callbacks
585 // callbacks
580 if (cm !== undefined) {
586 if (cm !== undefined) {
581 commentForm.setPlaceholder(placeholderText);
587 commentForm.setPlaceholder(placeholderText);
582 if (commentForm.isInline()) {
588 if (commentForm.isInline()) {
583 cm.focus();
589 cm.focus();
584 cm.refresh();
590 cm.refresh();
585 }
591 }
586 }
592 }
587 }, 10);
593 }, 10);
588
594
589 // trigger scrolldown to the resolve comment, since it might be away
595 // trigger scrolldown to the resolve comment, since it might be away
590 // from the clicked
596 // from the clicked
591 if (resolvesCommentId){
597 if (resolvesCommentId){
592 var actionNode = $(commentForm.resolvesActionId).offset();
598 var actionNode = $(commentForm.resolvesActionId).offset();
593
599
594 setTimeout(function() {
600 setTimeout(function() {
595 if (actionNode) {
601 if (actionNode) {
596 $('body, html').animate({scrollTop: actionNode.top}, 10);
602 $('body, html').animate({scrollTop: actionNode.top}, 10);
597 }
603 }
598 }, 100);
604 }, 100);
599 }
605 }
600
606
601 return commentForm;
607 return commentForm;
602 };
608 };
603
609
604 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
610 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
605
611
606 var tmpl = $('#cb-comment-general-form-template').html();
612 var tmpl = $('#cb-comment-general-form-template').html();
607 tmpl = tmpl.format(null, 'general');
613 tmpl = tmpl.format(null, 'general');
608 var $form = $(tmpl);
614 var $form = $(tmpl);
609
615
610 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
616 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
611 var curForm = $formPlaceholder.find('form');
617 var curForm = $formPlaceholder.find('form');
612 if (curForm){
618 if (curForm){
613 curForm.remove();
619 curForm.remove();
614 }
620 }
615 $formPlaceholder.append($form);
621 $formPlaceholder.append($form);
616
622
617 var _form = $($form[0]);
623 var _form = $($form[0]);
618 var commentForm = this.createCommentForm(
624 var commentForm = this.createCommentForm(
619 _form, lineNo, placeholderText, true, resolvesCommentId);
625 _form, lineNo, placeholderText, true, resolvesCommentId);
620 commentForm.initStatusChangeSelector();
626 commentForm.initStatusChangeSelector();
621
627
622 return commentForm;
628 return commentForm;
623 };
629 };
624
630
625 this.createComment = function(node, resolutionComment) {
631 this.createComment = function(node, resolutionComment) {
626 var resolvesCommentId = resolutionComment || null;
632 var resolvesCommentId = resolutionComment || null;
627 var $node = $(node);
633 var $node = $(node);
628 var $td = $node.closest('td');
634 var $td = $node.closest('td');
629 var $form = $td.find('.comment-inline-form');
635 var $form = $td.find('.comment-inline-form');
630
636
631 if (!$form.length) {
637 if (!$form.length) {
632
638
633 var $filediff = $node.closest('.filediff');
639 var $filediff = $node.closest('.filediff');
634 $filediff.removeClass('hide-comments');
640 $filediff.removeClass('hide-comments');
635 var f_path = $filediff.attr('data-f-path');
641 var f_path = $filediff.attr('data-f-path');
636 var lineno = self.getLineNumber(node);
642 var lineno = self.getLineNumber(node);
637 // create a new HTML from template
643 // create a new HTML from template
638 var tmpl = $('#cb-comment-inline-form-template').html();
644 var tmpl = $('#cb-comment-inline-form-template').html();
639 tmpl = tmpl.format(f_path, lineno);
645 tmpl = tmpl.format(f_path, lineno);
640 $form = $(tmpl);
646 $form = $(tmpl);
641
647
642 var $comments = $td.find('.inline-comments');
648 var $comments = $td.find('.inline-comments');
643 if (!$comments.length) {
649 if (!$comments.length) {
644 $comments = $(
650 $comments = $(
645 $('#cb-comments-inline-container-template').html());
651 $('#cb-comments-inline-container-template').html());
646 $td.append($comments);
652 $td.append($comments);
647 }
653 }
648
654
649 $td.find('.cb-comment-add-button').before($form);
655 $td.find('.cb-comment-add-button').before($form);
650
656
651 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
657 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
652 var _form = $($form[0]).find('form');
658 var _form = $($form[0]).find('form');
653
659
654 var commentForm = this.createCommentForm(
660 var commentForm = this.createCommentForm(
655 _form, lineno, placeholderText, false, resolvesCommentId);
661 _form, lineno, placeholderText, false, resolvesCommentId);
656
662
657 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
663 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
658 form: _form,
664 form: _form,
659 parent: $td[0],
665 parent: $td[0],
660 lineno: lineno,
666 lineno: lineno,
661 f_path: f_path}
667 f_path: f_path}
662 );
668 );
663
669
664 // set a CUSTOM submit handler for inline comments.
670 // set a CUSTOM submit handler for inline comments.
665 commentForm.setHandleFormSubmit(function(o) {
671 commentForm.setHandleFormSubmit(function(o) {
666 var text = commentForm.cm.getValue();
672 var text = commentForm.cm.getValue();
667 var commentType = commentForm.getCommentType();
673 var commentType = commentForm.getCommentType();
668 var resolvesCommentId = commentForm.getResolvesId();
674 var resolvesCommentId = commentForm.getResolvesId();
669
675
670 if (text === "") {
676 if (text === "") {
671 return;
677 return;
672 }
678 }
673
679
674 if (lineno === undefined) {
680 if (lineno === undefined) {
675 alert('missing line !');
681 alert('missing line !');
676 return;
682 return;
677 }
683 }
678 if (f_path === undefined) {
684 if (f_path === undefined) {
679 alert('missing file path !');
685 alert('missing file path !');
680 return;
686 return;
681 }
687 }
682
688
683 var excludeCancelBtn = false;
689 var excludeCancelBtn = false;
684 var submitEvent = true;
690 var submitEvent = true;
685 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
691 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
686 commentForm.cm.setOption("readOnly", true);
692 commentForm.cm.setOption("readOnly", true);
687 var postData = {
693 var postData = {
688 'text': text,
694 'text': text,
689 'f_path': f_path,
695 'f_path': f_path,
690 'line': lineno,
696 'line': lineno,
691 'comment_type': commentType,
697 'comment_type': commentType,
692 'csrf_token': CSRF_TOKEN
698 'csrf_token': CSRF_TOKEN
693 };
699 };
694 if (resolvesCommentId){
700 if (resolvesCommentId){
695 postData['resolves_comment_id'] = resolvesCommentId;
701 postData['resolves_comment_id'] = resolvesCommentId;
696 }
702 }
697
703
698 var submitSuccessCallback = function(json_data) {
704 var submitSuccessCallback = function(json_data) {
699 $form.remove();
705 $form.remove();
700 try {
706 try {
701 var html = json_data.rendered_text;
707 var html = json_data.rendered_text;
702 var lineno = json_data.line_no;
708 var lineno = json_data.line_no;
703 var target_id = json_data.target_id;
709 var target_id = json_data.target_id;
704
710
705 $comments.find('.cb-comment-add-button').before(html);
711 $comments.find('.cb-comment-add-button').before(html);
706
712
707 //mark visually which comment was resolved
713 //mark visually which comment was resolved
708 if (resolvesCommentId) {
714 if (resolvesCommentId) {
709 commentForm.markCommentResolved(resolvesCommentId);
715 commentForm.markCommentResolved(resolvesCommentId);
710 }
716 }
711
717
712 // run global callback on submit
718 // run global callback on submit
713 commentForm.globalSubmitSuccessCallback();
719 commentForm.globalSubmitSuccessCallback();
714
720
715 } catch (e) {
721 } catch (e) {
716 console.error(e);
722 console.error(e);
717 }
723 }
718
724
719 // re trigger the linkification of next/prev navigation
725 // re trigger the linkification of next/prev navigation
720 linkifyComments($('.inline-comment-injected'));
726 linkifyComments($('.inline-comment-injected'));
721 timeagoActivate();
727 timeagoActivate();
722 commentForm.setActionButtonsDisabled(false);
728 commentForm.setActionButtonsDisabled(false);
723
729
724 };
730 };
725 var submitFailCallback = function(){
731 var submitFailCallback = function(){
726 commentForm.resetCommentFormState(text)
732 commentForm.resetCommentFormState(text)
727 };
733 };
728 commentForm.submitAjaxPOST(
734 commentForm.submitAjaxPOST(
729 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
735 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
730 });
736 });
731 }
737 }
732
738
733 $form.addClass('comment-inline-form-open');
739 $form.addClass('comment-inline-form-open');
734 };
740 };
735
741
736 this.createResolutionComment = function(commentId){
742 this.createResolutionComment = function(commentId){
737 // hide the trigger text
743 // hide the trigger text
738 $('#resolve-comment-{0}'.format(commentId)).hide();
744 $('#resolve-comment-{0}'.format(commentId)).hide();
739
745
740 var comment = $('#comment-'+commentId);
746 var comment = $('#comment-'+commentId);
741 var commentData = comment.data();
747 var commentData = comment.data();
742 if (commentData.commentInline) {
748 if (commentData.commentInline) {
743 this.createComment(comment, commentId)
749 this.createComment(comment, commentId)
744 } else {
750 } else {
745 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
751 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
746 }
752 }
747
753
748 return false;
754 return false;
749 };
755 };
750
756
751 this.submitResolution = function(commentId){
757 this.submitResolution = function(commentId){
752 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
758 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
753 var commentForm = form.get(0).CommentForm;
759 var commentForm = form.get(0).CommentForm;
754
760
755 var cm = commentForm.getCmInstance();
761 var cm = commentForm.getCmInstance();
756 var renderer = templateContext.visual.default_renderer;
762 var renderer = templateContext.visual.default_renderer;
757 if (renderer == 'rst'){
763 if (renderer == 'rst'){
758 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
764 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
759 } else if (renderer == 'markdown') {
765 } else if (renderer == 'markdown') {
760 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
766 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
761 } else {
767 } else {
762 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
768 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
763 }
769 }
764
770
765 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
771 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
766 form.submit();
772 form.submit();
767 return false;
773 return false;
768 };
774 };
769
775
770 this.renderInlineComments = function(file_comments) {
776 this.renderInlineComments = function(file_comments) {
771 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
777 show_add_button = typeof show_add_button !== 'undefined' ? show_add_button : true;
772
778
773 for (var i = 0; i < file_comments.length; i++) {
779 for (var i = 0; i < file_comments.length; i++) {
774 var box = file_comments[i];
780 var box = file_comments[i];
775
781
776 var target_id = $(box).attr('target_id');
782 var target_id = $(box).attr('target_id');
777
783
778 // actually comments with line numbers
784 // actually comments with line numbers
779 var comments = box.children;
785 var comments = box.children;
780
786
781 for (var j = 0; j < comments.length; j++) {
787 for (var j = 0; j < comments.length; j++) {
782 var data = {
788 var data = {
783 'rendered_text': comments[j].outerHTML,
789 'rendered_text': comments[j].outerHTML,
784 'line_no': $(comments[j]).attr('line'),
790 'line_no': $(comments[j]).attr('line'),
785 'target_id': target_id
791 'target_id': target_id
786 };
792 };
787 }
793 }
788 }
794 }
789
795
790 // since order of injection is random, we're now re-iterating
796 // since order of injection is random, we're now re-iterating
791 // from correct order and filling in links
797 // from correct order and filling in links
792 linkifyComments($('.inline-comment-injected'));
798 linkifyComments($('.inline-comment-injected'));
793 firefoxAnchorFix();
799 firefoxAnchorFix();
794 };
800 };
795
801
796 };
802 };
@@ -1,408 +1,385 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6 <%namespace name="base" file="/base/base.mako"/>
6 <%namespace name="base" file="/base/base.mako"/>
7
7
8 <%def name="comment_block(comment, inline=False)">
8 <%def name="comment_block(comment, inline=False)">
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 % if inline:
10 % if inline:
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
11 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
12 % else:
12 % else:
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
13 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
14 % endif
14 % endif
15
15
16
16
17 <div class="comment
17 <div class="comment
18 ${'comment-inline' if inline else 'comment-general'}
18 ${'comment-inline' if inline else 'comment-general'}
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
19 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
20 id="comment-${comment.comment_id}"
20 id="comment-${comment.comment_id}"
21 line="${comment.line_no}"
21 line="${comment.line_no}"
22 data-comment-id="${comment.comment_id}"
22 data-comment-id="${comment.comment_id}"
23 data-comment-type="${comment.comment_type}"
23 data-comment-type="${comment.comment_type}"
24 data-comment-inline=${h.json.dumps(inline)}
24 data-comment-inline=${h.json.dumps(inline)}
25 style="${'display: none;' if outdated_at_ver else ''}">
25 style="${'display: none;' if outdated_at_ver else ''}">
26
26
27 <div class="meta">
27 <div class="meta">
28 <div class="comment-type-label">
28 <div class="comment-type-label">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
29 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
30 % if comment.comment_type == 'todo':
30 % if comment.comment_type == 'todo':
31 % if comment.resolved:
31 % if comment.resolved:
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
32 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
33 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
34 </div>
34 </div>
35 % else:
35 % else:
36 <div class="resolved tooltip" style="display: none">
36 <div class="resolved tooltip" style="display: none">
37 <span>${comment.comment_type}</span>
37 <span>${comment.comment_type}</span>
38 </div>
38 </div>
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
39 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
40 ${comment.comment_type}
40 ${comment.comment_type}
41 </div>
41 </div>
42 % endif
42 % endif
43 % else:
43 % else:
44 % if comment.resolved_comment:
44 % if comment.resolved_comment:
45 fix
45 fix
46 % else:
46 % else:
47 ${comment.comment_type or 'note'}
47 ${comment.comment_type or 'note'}
48 % endif
48 % endif
49 % endif
49 % endif
50 </div>
50 </div>
51 </div>
51 </div>
52
52
53 <div class="author ${'author-inline' if inline else 'author-general'}">
53 <div class="author ${'author-inline' if inline else 'author-general'}">
54 ${base.gravatar_with_user(comment.author.email, 16)}
54 ${base.gravatar_with_user(comment.author.email, 16)}
55 </div>
55 </div>
56 <div class="date">
56 <div class="date">
57 ${h.age_component(comment.modified_at, time_is_local=True)}
57 ${h.age_component(comment.modified_at, time_is_local=True)}
58 </div>
58 </div>
59 % if inline:
59 % if inline:
60 <span></span>
60 <span></span>
61 % else:
61 % else:
62 <div class="status-change">
62 <div class="status-change">
63 % if comment.pull_request:
63 % if comment.pull_request:
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
64 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
65 % if comment.status_change:
65 % if comment.status_change:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
66 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
67 % else:
67 % else:
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
68 ${_('pull request #%s') % comment.pull_request.pull_request_id}
69 % endif
69 % endif
70 </a>
70 </a>
71 % else:
71 % else:
72 % if comment.status_change:
72 % if comment.status_change:
73 ${_('Status change on commit')}:
73 ${_('Status change on commit')}:
74 % endif
74 % endif
75 % endif
75 % endif
76 </div>
76 </div>
77 % endif
77 % endif
78
78
79 % if comment.status_change:
79 % if comment.status_change:
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
80 <div class="${'flag_status %s' % comment.status_change[0].status}"></div>
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
81 <div title="${_('Commit status')}" class="changeset-status-lbl">
82 ${comment.status_change[0].status_lbl}
82 ${comment.status_change[0].status_lbl}
83 </div>
83 </div>
84 % endif
84 % endif
85
85
86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
86 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
87
87
88 <div class="comment-links-block">
88 <div class="comment-links-block">
89
89
90 % if inline:
90 % if inline:
91 <div class="pr-version-inline">
91 <div class="pr-version-inline">
92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
92 <a href="${h.url.current(version=comment.pull_request_version_id, anchor='comment-{}'.format(comment.comment_id))}">
93 % if outdated_at_ver:
93 % if outdated_at_ver:
94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
94 <code class="pr-version-num" title="${_('Outdated comment from pull request version {0}').format(pr_index_ver)}">
95 outdated ${'v{}'.format(pr_index_ver)} |
95 outdated ${'v{}'.format(pr_index_ver)} |
96 </code>
96 </code>
97 % elif pr_index_ver:
97 % elif pr_index_ver:
98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
98 <code class="pr-version-num" title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
99 ${'v{}'.format(pr_index_ver)} |
99 ${'v{}'.format(pr_index_ver)} |
100 </code>
100 </code>
101 % endif
101 % endif
102 </a>
102 </a>
103 </div>
103 </div>
104 % else:
104 % else:
105 % if comment.pull_request_version_id and pr_index_ver:
105 % if comment.pull_request_version_id and pr_index_ver:
106 |
106 |
107 <div class="pr-version">
107 <div class="pr-version">
108 % if comment.outdated:
108 % if comment.outdated:
109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
109 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
110 ${_('Outdated comment from pull request version {}').format(pr_index_ver)}
111 </a>
111 </a>
112 % else:
112 % else:
113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
113 <div title="${_('Comment from pull request version {0}').format(pr_index_ver)}">
114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
114 <a href="${h.url('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
115 <code class="pr-version-num">
115 <code class="pr-version-num">
116 ${'v{}'.format(pr_index_ver)}
116 ${'v{}'.format(pr_index_ver)}
117 </code>
117 </code>
118 </a>
118 </a>
119 </div>
119 </div>
120 % endif
120 % endif
121 </div>
121 </div>
122 % endif
122 % endif
123 % endif
123 % endif
124
124
125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
125 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
126 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
127 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
128 ## permissions to delete
128 ## permissions to delete
129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
129 %if h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
130 ## TODO: dan: add edit comment here
130 ## TODO: dan: add edit comment here
131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
131 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
132 %else:
132 %else:
133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
133 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
134 %endif
134 %endif
135 %else:
135 %else:
136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
136 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
137 %endif
137 %endif
138
138
139 %if not outdated_at_ver:
139 %if not outdated_at_ver:
140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
140 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
141 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
142 %endif
142 %endif
143
143
144 </div>
144 </div>
145 </div>
145 </div>
146 <div class="text">
146 <div class="text">
147 ${comment.render(mentions=True)|n}
147 ${comment.render(mentions=True)|n}
148 </div>
148 </div>
149
149
150 </div>
150 </div>
151 </%def>
151 </%def>
152
152
153 ## generate main comments
153 ## generate main comments
154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
154 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
155 <div id="comments">
155 <div id="comments">
156 %for comment in comments:
156 %for comment in comments:
157 <div id="comment-tr-${comment.comment_id}">
157 <div id="comment-tr-${comment.comment_id}">
158 ## only render comments that are not from pull request, or from
158 ## only render comments that are not from pull request, or from
159 ## pull request and a status change
159 ## pull request and a status change
160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
160 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
161 ${comment_block(comment)}
161 ${comment_block(comment)}
162 %endif
162 %endif
163 </div>
163 </div>
164 %endfor
164 %endfor
165 ## to anchor ajax comments
165 ## to anchor ajax comments
166 <div id="injected_page_comments"></div>
166 <div id="injected_page_comments"></div>
167 </div>
167 </div>
168 </%def>
168 </%def>
169
169
170
170
171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
171 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
172
172
173 ## merge status, and merge action
174 %if is_pull_request:
175 <div class="pull-request-merge">
176 %if c.allowed_to_merge:
177 <div class="pull-request-wrap">
178 <div class="pull-right">
179 ${h.secure_form(url('pullrequest_merge', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), id='merge_pull_request_form')}
180 <span data-role="merge-message">${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
181 <% merge_disabled = ' disabled' if c.pr_merge_status is False else '' %>
182 <input type="submit" id="merge_pull_request" value="${_('Merge Pull Request')}" class="btn${merge_disabled}"${merge_disabled}>
183 ${h.end_form()}
184 </div>
185 </div>
186 %else:
187 <div class="pull-request-wrap">
188 <div class="pull-right">
189 <span>${c.pr_merge_msg} ${c.approval_msg if c.approval_msg else ''}</span>
190 </div>
191 </div>
192 %endif
193 </div>
194 %endif
195
196 <div class="comments">
173 <div class="comments">
197 <%
174 <%
198 if is_pull_request:
175 if is_pull_request:
199 placeholder = _('Leave a comment on this Pull Request.')
176 placeholder = _('Leave a comment on this Pull Request.')
200 elif is_compare:
177 elif is_compare:
201 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
178 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
202 else:
179 else:
203 placeholder = _('Leave a comment on this Commit.')
180 placeholder = _('Leave a comment on this Commit.')
204 %>
181 %>
205
182
206 % if c.rhodecode_user.username != h.DEFAULT_USER:
183 % if c.rhodecode_user.username != h.DEFAULT_USER:
207 <div class="js-template" id="cb-comment-general-form-template">
184 <div class="js-template" id="cb-comment-general-form-template">
208 ## template generated for injection
185 ## template generated for injection
209 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
186 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
210 </div>
187 </div>
211
188
212 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
189 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
213 ## inject form here
190 ## inject form here
214 </div>
191 </div>
215 <script type="text/javascript">
192 <script type="text/javascript">
216 var lineNo = 'general';
193 var lineNo = 'general';
217 var resolvesCommentId = null;
194 var resolvesCommentId = null;
218 var generalCommentForm = Rhodecode.comments.createGeneralComment(
195 var generalCommentForm = Rhodecode.comments.createGeneralComment(
219 lineNo, "${placeholder}", resolvesCommentId);
196 lineNo, "${placeholder}", resolvesCommentId);
220
197
221 // set custom success callback on rangeCommit
198 // set custom success callback on rangeCommit
222 % if is_compare:
199 % if is_compare:
223 generalCommentForm.setHandleFormSubmit(function(o) {
200 generalCommentForm.setHandleFormSubmit(function(o) {
224 var self = generalCommentForm;
201 var self = generalCommentForm;
225
202
226 var text = self.cm.getValue();
203 var text = self.cm.getValue();
227 var status = self.getCommentStatus();
204 var status = self.getCommentStatus();
228 var commentType = self.getCommentType();
205 var commentType = self.getCommentType();
229
206
230 if (text === "" && !status) {
207 if (text === "" && !status) {
231 return;
208 return;
232 }
209 }
233
210
234 // we can pick which commits we want to make the comment by
211 // we can pick which commits we want to make the comment by
235 // selecting them via click on preview pane, this will alter the hidden inputs
212 // selecting them via click on preview pane, this will alter the hidden inputs
236 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
213 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
237
214
238 var commitIds = [];
215 var commitIds = [];
239 $('#changeset_compare_view_content .compare_select').each(function(el) {
216 $('#changeset_compare_view_content .compare_select').each(function(el) {
240 var commitId = this.id.replace('row-', '');
217 var commitId = this.id.replace('row-', '');
241 if ($(this).hasClass('hl') || !cherryPicked) {
218 if ($(this).hasClass('hl') || !cherryPicked) {
242 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
219 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
243 commitIds.push(commitId);
220 commitIds.push(commitId);
244 } else {
221 } else {
245 $("input[data-commit-id='{0}']".format(commitId)).val('')
222 $("input[data-commit-id='{0}']".format(commitId)).val('')
246 }
223 }
247 });
224 });
248
225
249 self.setActionButtonsDisabled(true);
226 self.setActionButtonsDisabled(true);
250 self.cm.setOption("readOnly", true);
227 self.cm.setOption("readOnly", true);
251 var postData = {
228 var postData = {
252 'text': text,
229 'text': text,
253 'changeset_status': status,
230 'changeset_status': status,
254 'comment_type': commentType,
231 'comment_type': commentType,
255 'commit_ids': commitIds,
232 'commit_ids': commitIds,
256 'csrf_token': CSRF_TOKEN
233 'csrf_token': CSRF_TOKEN
257 };
234 };
258
235
259 var submitSuccessCallback = function(o) {
236 var submitSuccessCallback = function(o) {
260 location.reload(true);
237 location.reload(true);
261 };
238 };
262 var submitFailCallback = function(){
239 var submitFailCallback = function(){
263 self.resetCommentFormState(text)
240 self.resetCommentFormState(text)
264 };
241 };
265 self.submitAjaxPOST(
242 self.submitAjaxPOST(
266 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
243 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
267 });
244 });
268 % endif
245 % endif
269
246
270
247
271 </script>
248 </script>
272 % else:
249 % else:
273 ## form state when not logged in
250 ## form state when not logged in
274 <div class="comment-form ac">
251 <div class="comment-form ac">
275
252
276 <div class="comment-area">
253 <div class="comment-area">
277 <div class="comment-area-header">
254 <div class="comment-area-header">
278 <ul class="nav-links clearfix">
255 <ul class="nav-links clearfix">
279 <li class="active">
256 <li class="active">
280 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
257 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
281 </li>
258 </li>
282 <li class="">
259 <li class="">
283 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
260 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
284 </li>
261 </li>
285 </ul>
262 </ul>
286 </div>
263 </div>
287
264
288 <div class="comment-area-write" style="display: block;">
265 <div class="comment-area-write" style="display: block;">
289 <div id="edit-container">
266 <div id="edit-container">
290 <div style="padding: 40px 0">
267 <div style="padding: 40px 0">
291 ${_('You need to be logged in to leave comments.')}
268 ${_('You need to be logged in to leave comments.')}
292 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
269 <a href="${h.route_path('login', _query={'came_from': h.url.current()})}">${_('Login now')}</a>
293 </div>
270 </div>
294 </div>
271 </div>
295 <div id="preview-container" class="clearfix" style="display: none;">
272 <div id="preview-container" class="clearfix" style="display: none;">
296 <div id="preview-box" class="preview-box"></div>
273 <div id="preview-box" class="preview-box"></div>
297 </div>
274 </div>
298 </div>
275 </div>
299
276
300 <div class="comment-area-footer">
277 <div class="comment-area-footer">
301 <div class="toolbar">
278 <div class="toolbar">
302 <div class="toolbar-text">
279 <div class="toolbar-text">
303 </div>
280 </div>
304 </div>
281 </div>
305 </div>
282 </div>
306 </div>
283 </div>
307
284
308 <div class="comment-footer">
285 <div class="comment-footer">
309 </div>
286 </div>
310
287
311 </div>
288 </div>
312 % endif
289 % endif
313
290
314 <script type="text/javascript">
291 <script type="text/javascript">
315 bindToggleButtons();
292 bindToggleButtons();
316 </script>
293 </script>
317 </div>
294 </div>
318 </%def>
295 </%def>
319
296
320
297
321 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
298 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
322 ## comment injected based on assumption that user is logged in
299 ## comment injected based on assumption that user is logged in
323
300
324 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
301 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
325
302
326 <div class="comment-area">
303 <div class="comment-area">
327 <div class="comment-area-header">
304 <div class="comment-area-header">
328 <ul class="nav-links clearfix">
305 <ul class="nav-links clearfix">
329 <li class="active">
306 <li class="active">
330 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
307 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
331 </li>
308 </li>
332 <li class="">
309 <li class="">
333 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
310 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
334 </li>
311 </li>
335 <li class="pull-right">
312 <li class="pull-right">
336 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
313 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
337 % for val in c.visual.comment_types:
314 % for val in c.visual.comment_types:
338 <option value="${val}">${val.upper()}</option>
315 <option value="${val}">${val.upper()}</option>
339 % endfor
316 % endfor
340 </select>
317 </select>
341 </li>
318 </li>
342 </ul>
319 </ul>
343 </div>
320 </div>
344
321
345 <div class="comment-area-write" style="display: block;">
322 <div class="comment-area-write" style="display: block;">
346 <div id="edit-container_${lineno_id}">
323 <div id="edit-container_${lineno_id}">
347 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
324 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
348 </div>
325 </div>
349 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
326 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
350 <div id="preview-box_${lineno_id}" class="preview-box"></div>
327 <div id="preview-box_${lineno_id}" class="preview-box"></div>
351 </div>
328 </div>
352 </div>
329 </div>
353
330
354 <div class="comment-area-footer">
331 <div class="comment-area-footer">
355 <div class="toolbar">
332 <div class="toolbar">
356 <div class="toolbar-text">
333 <div class="toolbar-text">
357 ${(_('Comments parsed using %s syntax with %s support.') % (
334 ${(_('Comments parsed using %s syntax with %s support.') % (
358 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
335 ('<a href="%s">%s</a>' % (h.url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
359 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
336 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user'))
360 )
337 )
361 )|n}
338 )|n}
362 </div>
339 </div>
363 </div>
340 </div>
364 </div>
341 </div>
365 </div>
342 </div>
366
343
367 <div class="comment-footer">
344 <div class="comment-footer">
368
345
369 % if review_statuses:
346 % if review_statuses:
370 <div class="status_box">
347 <div class="status_box">
371 <select id="change_status_${lineno_id}" name="changeset_status">
348 <select id="change_status_${lineno_id}" name="changeset_status">
372 <option></option> ## Placeholder
349 <option></option> ## Placeholder
373 % for status, lbl in review_statuses:
350 % for status, lbl in review_statuses:
374 <option value="${status}" data-status="${status}">${lbl}</option>
351 <option value="${status}" data-status="${status}">${lbl}</option>
375 %if is_pull_request and change_status and status in ('approved', 'rejected'):
352 %if is_pull_request and change_status and status in ('approved', 'rejected'):
376 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
353 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
377 %endif
354 %endif
378 % endfor
355 % endfor
379 </select>
356 </select>
380 </div>
357 </div>
381 % endif
358 % endif
382
359
383 ## inject extra inputs into the form
360 ## inject extra inputs into the form
384 % if form_extras and isinstance(form_extras, (list, tuple)):
361 % if form_extras and isinstance(form_extras, (list, tuple)):
385 <div id="comment_form_extras">
362 <div id="comment_form_extras">
386 % for form_ex_el in form_extras:
363 % for form_ex_el in form_extras:
387 ${form_ex_el|n}
364 ${form_ex_el|n}
388 % endfor
365 % endfor
389 </div>
366 </div>
390 % endif
367 % endif
391
368
392 <div class="action-buttons">
369 <div class="action-buttons">
393 ## inline for has a file, and line-number together with cancel hide button.
370 ## inline for has a file, and line-number together with cancel hide button.
394 % if form_type == 'inline':
371 % if form_type == 'inline':
395 <input type="hidden" name="f_path" value="{0}">
372 <input type="hidden" name="f_path" value="{0}">
396 <input type="hidden" name="line" value="${lineno_id}">
373 <input type="hidden" name="line" value="${lineno_id}">
397 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
374 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
398 ${_('Cancel')}
375 ${_('Cancel')}
399 </button>
376 </button>
400 % endif
377 % endif
401 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
378 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
402
379
403 </div>
380 </div>
404 </div>
381 </div>
405
382
406 </form>
383 </form>
407
384
408 </%def> No newline at end of file
385 </%def>
@@ -1,692 +1,713 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
4 ${_('%s Pull Request #%s') % (c.repo_name, c.pull_request.pull_request_id)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="breadcrumbs_links()">
10 <%def name="breadcrumbs_links()">
11 <span id="pr-title">
11 <span id="pr-title">
12 ${c.pull_request.title}
12 ${c.pull_request.title}
13 %if c.pull_request.is_closed():
13 %if c.pull_request.is_closed():
14 (${_('Closed')})
14 (${_('Closed')})
15 %endif
15 %endif
16 </span>
16 </span>
17 <div id="pr-title-edit" class="input" style="display: none;">
17 <div id="pr-title-edit" class="input" style="display: none;">
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
18 ${h.text('pullrequest_title', id_="pr-title-input", class_="large", value=c.pull_request.title)}
19 </div>
19 </div>
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_nav()">
22 <%def name="menu_bar_nav()">
23 ${self.menu_items(active='repositories')}
23 ${self.menu_items(active='repositories')}
24 </%def>
24 </%def>
25
25
26 <%def name="menu_bar_subnav()">
26 <%def name="menu_bar_subnav()">
27 ${self.repo_menu(active='showpullrequest')}
27 ${self.repo_menu(active='showpullrequest')}
28 </%def>
28 </%def>
29
29
30 <%def name="main()">
30 <%def name="main()">
31
31
32 <script type="text/javascript">
32 <script type="text/javascript">
33 // TODO: marcink switch this to pyroutes
33 // TODO: marcink switch this to pyroutes
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
34 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 </script>
36 </script>
37 <div class="box">
37 <div class="box">
38
38
39 <div class="title">
39 <div class="title">
40 ${self.repo_page_title(c.rhodecode_db_repo)}
40 ${self.repo_page_title(c.rhodecode_db_repo)}
41 </div>
41 </div>
42
42
43 ${self.breadcrumbs()}
43 ${self.breadcrumbs()}
44
44
45 <div class="box pr-summary">
45 <div class="box pr-summary">
46
46
47 <div class="summary-details block-left">
47 <div class="summary-details block-left">
48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
48 <% summary = lambda n:{False:'summary-short'}.get(n) %>
49 <div class="pr-details-title">
49 <div class="pr-details-title">
50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
50 <a href="${h.url('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request #%s') % c.pull_request.pull_request_id}</a> ${_('From')} ${h.format_date(c.pull_request.created_on)}
51 %if c.allowed_to_update:
51 %if c.allowed_to_update:
52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
52 <div id="delete_pullrequest" class="pull-right action_button ${'' if c.allowed_to_delete else 'disabled' }" style="clear:inherit;padding: 0">
53 % if c.allowed_to_delete:
53 % if c.allowed_to_delete:
54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
54 ${h.secure_form(url('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id),method='delete')}
55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
55 ${h.submit('remove_%s' % c.pull_request.pull_request_id, _('Delete'),
56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
56 class_="btn btn-link btn-danger",onclick="return confirm('"+_('Confirm to delete this pull request')+"');")}
57 ${h.end_form()}
57 ${h.end_form()}
58 % else:
58 % else:
59 ${_('Delete')}
59 ${_('Delete')}
60 % endif
60 % endif
61 </div>
61 </div>
62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
62 <div id="open_edit_pullrequest" class="pull-right action_button">${_('Edit')}</div>
63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
63 <div id="close_edit_pullrequest" class="pull-right action_button" style="display: none;padding: 0">${_('Cancel')}</div>
64 %endif
64 %endif
65 </div>
65 </div>
66
66
67 <div id="summary" class="fields pr-details-content">
67 <div id="summary" class="fields pr-details-content">
68 <div class="field">
68 <div class="field">
69 <div class="label-summary">
69 <div class="label-summary">
70 <label>${_('Origin')}:</label>
70 <label>${_('Origin')}:</label>
71 </div>
71 </div>
72 <div class="input">
72 <div class="input">
73 <div class="pr-origininfo">
73 <div class="pr-origininfo">
74 ## branch link is only valid if it is a branch
74 ## branch link is only valid if it is a branch
75 <span class="tag">
75 <span class="tag">
76 %if c.pull_request.source_ref_parts.type == 'branch':
76 %if c.pull_request.source_ref_parts.type == 'branch':
77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
77 <a href="${h.url('changelog_home', repo_name=c.pull_request.source_repo.repo_name, branch=c.pull_request.source_ref_parts.name)}">${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}</a>
78 %else:
78 %else:
79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
79 ${c.pull_request.source_ref_parts.type}: ${c.pull_request.source_ref_parts.name}
80 %endif
80 %endif
81 </span>
81 </span>
82 <span class="clone-url">
82 <span class="clone-url">
83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
83 <a href="${h.url('summary_home', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.clone_url()}</a>
84 </span>
84 </span>
85 </div>
85 </div>
86 <div class="pr-pullinfo">
86 <div class="pr-pullinfo">
87 %if h.is_hg(c.pull_request.source_repo):
87 %if h.is_hg(c.pull_request.source_repo):
88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
88 <input type="text" value="hg pull -r ${h.short_id(c.source_ref)} ${c.pull_request.source_repo.clone_url()}" readonly="readonly">
89 %elif h.is_git(c.pull_request.source_repo):
89 %elif h.is_git(c.pull_request.source_repo):
90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
90 <input type="text" value="git pull ${c.pull_request.source_repo.clone_url()} ${c.pull_request.source_ref_parts.name}" readonly="readonly">
91 %endif
91 %endif
92 </div>
92 </div>
93 </div>
93 </div>
94 </div>
94 </div>
95 <div class="field">
95 <div class="field">
96 <div class="label-summary">
96 <div class="label-summary">
97 <label>${_('Target')}:</label>
97 <label>${_('Target')}:</label>
98 </div>
98 </div>
99 <div class="input">
99 <div class="input">
100 <div class="pr-targetinfo">
100 <div class="pr-targetinfo">
101 ## branch link is only valid if it is a branch
101 ## branch link is only valid if it is a branch
102 <span class="tag">
102 <span class="tag">
103 %if c.pull_request.target_ref_parts.type == 'branch':
103 %if c.pull_request.target_ref_parts.type == 'branch':
104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
104 <a href="${h.url('changelog_home', repo_name=c.pull_request.target_repo.repo_name, branch=c.pull_request.target_ref_parts.name)}">${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}</a>
105 %else:
105 %else:
106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
106 ${c.pull_request.target_ref_parts.type}: ${c.pull_request.target_ref_parts.name}
107 %endif
107 %endif
108 </span>
108 </span>
109 <span class="clone-url">
109 <span class="clone-url">
110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
110 <a href="${h.url('summary_home', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.clone_url()}</a>
111 </span>
111 </span>
112 </div>
112 </div>
113 </div>
113 </div>
114 </div>
114 </div>
115
115
116 ## Link to the shadow repository.
116 ## Link to the shadow repository.
117 <div class="field">
117 <div class="field">
118 <div class="label-summary">
118 <div class="label-summary">
119 <label>${_('Merge')}:</label>
119 <label>${_('Merge')}:</label>
120 </div>
120 </div>
121 <div class="input">
121 <div class="input">
122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
122 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
123 <div class="pr-mergeinfo">
123 <div class="pr-mergeinfo">
124 %if h.is_hg(c.pull_request.target_repo):
124 %if h.is_hg(c.pull_request.target_repo):
125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
125 <input type="text" value="hg clone -u ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
126 %elif h.is_git(c.pull_request.target_repo):
126 %elif h.is_git(c.pull_request.target_repo):
127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
127 <input type="text" value="git clone --branch ${c.pull_request.shadow_merge_ref.name} ${c.shadow_clone_url} pull-request-${c.pull_request.pull_request_id}" readonly="readonly">
128 %endif
128 %endif
129 </div>
129 </div>
130 % else:
130 % else:
131 <div class="">
131 <div class="">
132 ${_('Shadow repository data not available')}.
132 ${_('Shadow repository data not available')}.
133 </div>
133 </div>
134 % endif
134 % endif
135 </div>
135 </div>
136 </div>
136 </div>
137
137
138 <div class="field">
138 <div class="field">
139 <div class="label-summary">
139 <div class="label-summary">
140 <label>${_('Review')}:</label>
140 <label>${_('Review')}:</label>
141 </div>
141 </div>
142 <div class="input">
142 <div class="input">
143 %if c.pull_request_review_status:
143 %if c.pull_request_review_status:
144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
144 <div class="${'flag_status %s' % c.pull_request_review_status} tooltip pull-left"></div>
145 <span class="changeset-status-lbl tooltip">
145 <span class="changeset-status-lbl tooltip">
146 %if c.pull_request.is_closed():
146 %if c.pull_request.is_closed():
147 ${_('Closed')},
147 ${_('Closed')},
148 %endif
148 %endif
149 ${h.commit_status_lbl(c.pull_request_review_status)}
149 ${h.commit_status_lbl(c.pull_request_review_status)}
150 </span>
150 </span>
151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
151 - ${ungettext('calculated based on %s reviewer vote', 'calculated based on %s reviewers votes', len(c.pull_request_reviewers)) % len(c.pull_request_reviewers)}
152 %endif
152 %endif
153 </div>
153 </div>
154 </div>
154 </div>
155 <div class="field">
155 <div class="field">
156 <div class="pr-description-label label-summary">
156 <div class="pr-description-label label-summary">
157 <label>${_('Description')}:</label>
157 <label>${_('Description')}:</label>
158 </div>
158 </div>
159 <div id="pr-desc" class="input">
159 <div id="pr-desc" class="input">
160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
160 <div class="pr-description">${h.urlify_commit_message(c.pull_request.description, c.repo_name)}</div>
161 </div>
161 </div>
162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
162 <div id="pr-desc-edit" class="input textarea editor" style="display: none;">
163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
163 <textarea id="pr-description-input" size="30">${c.pull_request.description}</textarea>
164 </div>
164 </div>
165 </div>
165 </div>
166
166
167 <div class="field">
167 <div class="field">
168 <div class="label-summary">
168 <div class="label-summary">
169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
169 <label>${_('Versions')} (${len(c.versions)+1}):</label>
170 </div>
170 </div>
171
171
172 <div class="pr-versions">
172 <div class="pr-versions">
173 % if c.show_version_changes:
173 % if c.show_version_changes:
174 <table>
174 <table>
175 ## CURRENTLY SELECT PR VERSION
175 ## CURRENTLY SELECT PR VERSION
176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
176 <tr class="version-pr" style="display: ${'' if c.at_version_num is None else 'none'}">
177 <td>
177 <td>
178 % if c.at_version_num is None:
178 % if c.at_version_num is None:
179 <i class="icon-ok link"></i>
179 <i class="icon-ok link"></i>
180 % else:
180 % else:
181 <i class="icon-comment"></i>
181 <i class="icon-comment"></i>
182 <code>
182 <code>
183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
183 ${len(c.comment_versions[None]['at'])}/${len(c.inline_versions[None]['at'])}
184 </code>
184 </code>
185 % endif
185 % endif
186 </td>
186 </td>
187 <td>
187 <td>
188 <code>
188 <code>
189 % if c.versions:
189 % if c.versions:
190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
190 <a href="${h.url.current(version='latest')}">${_('latest')}</a>
191 % else:
191 % else:
192 ${_('initial')}
192 ${_('initial')}
193 % endif
193 % endif
194 </code>
194 </code>
195 </td>
195 </td>
196 <td>
196 <td>
197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
197 <code>${c.pull_request_latest.source_ref_parts.commit_id[:6]}</code>
198 </td>
198 </td>
199 <td>
199 <td>
200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
200 ${_('created')} ${h.age_component(c.pull_request_latest.updated_on)}
201 </td>
201 </td>
202 <td align="right">
202 <td align="right">
203 % if c.versions and c.at_version_num in [None, 'latest']:
203 % if c.versions and c.at_version_num in [None, 'latest']:
204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
204 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
205 % endif
205 % endif
206 </td>
206 </td>
207 </tr>
207 </tr>
208
208
209 ## SHOW ALL VERSIONS OF PR
209 ## SHOW ALL VERSIONS OF PR
210 <% ver_pr = None %>
210 <% ver_pr = None %>
211
211
212 % for data in reversed(list(enumerate(c.versions, 1))):
212 % for data in reversed(list(enumerate(c.versions, 1))):
213 <% ver_pos = data[0] %>
213 <% ver_pos = data[0] %>
214 <% ver = data[1] %>
214 <% ver = data[1] %>
215 <% ver_pr = ver.pull_request_version_id %>
215 <% ver_pr = ver.pull_request_version_id %>
216
216
217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
217 <tr class="version-pr" style="display: ${'' if c.at_version_num == ver_pr else 'none'}">
218 <td>
218 <td>
219 % if c.at_version_num == ver_pr:
219 % if c.at_version_num == ver_pr:
220 <i class="icon-ok link"></i>
220 <i class="icon-ok link"></i>
221 % else:
221 % else:
222 <i class="icon-comment"></i>
222 <i class="icon-comment"></i>
223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
223 <code class="tooltip" title="${_('Comment from pull request version {0}, general:{1} inline{2}').format(ver_pos, len(c.comment_versions[ver_pr]['at']), len(c.inline_versions[ver_pr]['at']))}">
224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
224 ${len(c.comment_versions[ver_pr]['at'])}/${len(c.inline_versions[ver_pr]['at'])}
225 </code>
225 </code>
226 % endif
226 % endif
227 </td>
227 </td>
228 <td>
228 <td>
229 <code>
229 <code>
230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
230 <a href="${h.url.current(version=ver_pr)}">v${ver_pos}</a>
231 </code>
231 </code>
232 </td>
232 </td>
233 <td>
233 <td>
234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
234 <code>${ver.source_ref_parts.commit_id[:6]}</code>
235 </td>
235 </td>
236 <td>
236 <td>
237 ${_('created')} ${h.age_component(ver.updated_on)}
237 ${_('created')} ${h.age_component(ver.updated_on)}
238 </td>
238 </td>
239 <td align="right">
239 <td align="right">
240 % if c.at_version_num == ver_pr:
240 % if c.at_version_num == ver_pr:
241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
241 <span id="show-pr-versions" class="btn btn-link" onclick="$('.version-pr').show(); $(this).hide(); return false">${_('Show all versions')}</span>
242 % endif
242 % endif
243 </td>
243 </td>
244 </tr>
244 </tr>
245 % endfor
245 % endfor
246
246
247 ## show comment/inline comments summary
247 ## show comment/inline comments summary
248 <tr>
248 <tr>
249 <td>
249 <td>
250 </td>
250 </td>
251
251
252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
252 <td colspan="4" style="border-top: 1px dashed #dbd9da">
253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
253 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
254 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
255
255
256
256
257 % if c.at_version:
257 % if c.at_version:
258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
258 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['display']) %>
259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
259 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['display']) %>
260 ${_('Comments at this version')}:
260 ${_('Comments at this version')}:
261 % else:
261 % else:
262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
262 <% inline_comm_count_ver = len(c.inline_versions[c.at_version_num]['until']) %>
263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
263 <% general_comm_count_ver = len(c.comment_versions[c.at_version_num]['until']) %>
264 ${_('Comments for this pull request')}:
264 ${_('Comments for this pull request')}:
265 % endif
265 % endif
266
266
267 %if general_comm_count_ver:
267 %if general_comm_count_ver:
268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
268 <a href="#comments">${_("%d General ") % general_comm_count_ver}</a>
269 %else:
269 %else:
270 ${_("%d General ") % general_comm_count_ver}
270 ${_("%d General ") % general_comm_count_ver}
271 %endif
271 %endif
272
272
273 %if inline_comm_count_ver:
273 %if inline_comm_count_ver:
274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
274 , <a href="#" onclick="return Rhodecode.comments.nextComment();" id="inline-comments-counter">${_("%d Inline") % inline_comm_count_ver}</a>
275 %else:
275 %else:
276 , ${_("%d Inline") % inline_comm_count_ver}
276 , ${_("%d Inline") % inline_comm_count_ver}
277 %endif
277 %endif
278
278
279 %if outdated_comm_count_ver:
279 %if outdated_comm_count_ver:
280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
280 , <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">${_("%d Outdated") % outdated_comm_count_ver}</a>
281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
281 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated comments')}</a>
282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
282 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated comments')}</a>
283 %else:
283 %else:
284 , ${_("%d Outdated") % outdated_comm_count_ver}
284 , ${_("%d Outdated") % outdated_comm_count_ver}
285 %endif
285 %endif
286 </td>
286 </td>
287 </tr>
287 </tr>
288
288
289 <tr>
289 <tr>
290 <td></td>
290 <td></td>
291 <td colspan="4">
291 <td colspan="4">
292 % if c.at_version:
292 % if c.at_version:
293 <pre>
293 <pre>
294 Changed commits:
294 Changed commits:
295 * added: ${len(c.changes.added)}
295 * added: ${len(c.changes.added)}
296 * removed: ${len(c.changes.removed)}
296 * removed: ${len(c.changes.removed)}
297
297
298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
298 % if not (c.file_changes.added+c.file_changes.modified+c.file_changes.removed):
299 No file changes found
299 No file changes found
300 % else:
300 % else:
301 Changed files:
301 Changed files:
302 %for file_name in c.file_changes.added:
302 %for file_name in c.file_changes.added:
303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
303 * A <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
304 %endfor
304 %endfor
305 %for file_name in c.file_changes.modified:
305 %for file_name in c.file_changes.modified:
306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
306 * M <a href="#${'a_' + h.FID('', file_name)}">${file_name}</a>
307 %endfor
307 %endfor
308 %for file_name in c.file_changes.removed:
308 %for file_name in c.file_changes.removed:
309 * R ${file_name}
309 * R ${file_name}
310 %endfor
310 %endfor
311 % endif
311 % endif
312 </pre>
312 </pre>
313 % endif
313 % endif
314 </td>
314 </td>
315 </tr>
315 </tr>
316 </table>
316 </table>
317 % else:
317 % else:
318 ${_('Pull request versions not available')}.
318 ${_('Pull request versions not available')}.
319 % endif
319 % endif
320 </div>
320 </div>
321 </div>
321 </div>
322
322
323 <div id="pr-save" class="field" style="display: none;">
323 <div id="pr-save" class="field" style="display: none;">
324 <div class="label-summary"></div>
324 <div class="label-summary"></div>
325 <div class="input">
325 <div class="input">
326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
326 <span id="edit_pull_request" class="btn btn-small">${_('Save Changes')}</span>
327 </div>
327 </div>
328 </div>
328 </div>
329 </div>
329 </div>
330 </div>
330 </div>
331 <div>
331 <div>
332 ## AUTHOR
332 ## AUTHOR
333 <div class="reviewers-title block-right">
333 <div class="reviewers-title block-right">
334 <div class="pr-details-title">
334 <div class="pr-details-title">
335 ${_('Author')}
335 ${_('Author')}
336 </div>
336 </div>
337 </div>
337 </div>
338 <div class="block-right pr-details-content reviewers">
338 <div class="block-right pr-details-content reviewers">
339 <ul class="group_members">
339 <ul class="group_members">
340 <li>
340 <li>
341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
341 ${self.gravatar_with_user(c.pull_request.author.email, 16)}
342 </li>
342 </li>
343 </ul>
343 </ul>
344 </div>
344 </div>
345 ## REVIEWERS
345 ## REVIEWERS
346 <div class="reviewers-title block-right">
346 <div class="reviewers-title block-right">
347 <div class="pr-details-title">
347 <div class="pr-details-title">
348 ${_('Pull request reviewers')}
348 ${_('Pull request reviewers')}
349 %if c.allowed_to_update:
349 %if c.allowed_to_update:
350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
350 <span id="open_edit_reviewers" class="block-right action_button">${_('Edit')}</span>
351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
351 <span id="close_edit_reviewers" class="block-right action_button" style="display: none;">${_('Close')}</span>
352 %endif
352 %endif
353 </div>
353 </div>
354 </div>
354 </div>
355 <div id="reviewers" class="block-right pr-details-content reviewers">
355 <div id="reviewers" class="block-right pr-details-content reviewers">
356 ## members goes here !
356 ## members goes here !
357 <input type="hidden" name="__start__" value="review_members:sequence">
357 <input type="hidden" name="__start__" value="review_members:sequence">
358 <ul id="review_members" class="group_members">
358 <ul id="review_members" class="group_members">
359 %for member,reasons,status in c.pull_request_reviewers:
359 %for member,reasons,status in c.pull_request_reviewers:
360 <li id="reviewer_${member.user_id}">
360 <li id="reviewer_${member.user_id}">
361 <div class="reviewers_member">
361 <div class="reviewers_member">
362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
362 <div class="reviewer_status tooltip" title="${h.tooltip(h.commit_status_lbl(status[0][1].status if status else 'not_reviewed'))}">
363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
363 <div class="${'flag_status %s' % (status[0][1].status if status else 'not_reviewed')} pull-left reviewer_member_status"></div>
364 </div>
364 </div>
365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
365 <div id="reviewer_${member.user_id}_name" class="reviewer_name">
366 ${self.gravatar_with_user(member.email, 16)}
366 ${self.gravatar_with_user(member.email, 16)}
367 </div>
367 </div>
368 <input type="hidden" name="__start__" value="reviewer:mapping">
368 <input type="hidden" name="__start__" value="reviewer:mapping">
369 <input type="hidden" name="__start__" value="reasons:sequence">
369 <input type="hidden" name="__start__" value="reasons:sequence">
370 %for reason in reasons:
370 %for reason in reasons:
371 <div class="reviewer_reason">- ${reason}</div>
371 <div class="reviewer_reason">- ${reason}</div>
372 <input type="hidden" name="reason" value="${reason}">
372 <input type="hidden" name="reason" value="${reason}">
373
373
374 %endfor
374 %endfor
375 <input type="hidden" name="__end__" value="reasons:sequence">
375 <input type="hidden" name="__end__" value="reasons:sequence">
376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
376 <input id="reviewer_${member.user_id}_input" type="hidden" value="${member.user_id}" name="user_id" />
377 <input type="hidden" name="__end__" value="reviewer:mapping">
377 <input type="hidden" name="__end__" value="reviewer:mapping">
378 %if c.allowed_to_update:
378 %if c.allowed_to_update:
379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
379 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(${member.user_id}, true)" style="visibility: hidden;">
380 <i class="icon-remove-sign" ></i>
380 <i class="icon-remove-sign" ></i>
381 </div>
381 </div>
382 %endif
382 %endif
383 </div>
383 </div>
384 </li>
384 </li>
385 %endfor
385 %endfor
386 </ul>
386 </ul>
387 <input type="hidden" name="__end__" value="review_members:sequence">
387 <input type="hidden" name="__end__" value="review_members:sequence">
388 %if not c.pull_request.is_closed():
388 %if not c.pull_request.is_closed():
389 <div id="add_reviewer_input" class='ac' style="display: none;">
389 <div id="add_reviewer_input" class='ac' style="display: none;">
390 %if c.allowed_to_update:
390 %if c.allowed_to_update:
391 <div class="reviewer_ac">
391 <div class="reviewer_ac">
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
392 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
393 <div id="reviewers_container"></div>
393 <div id="reviewers_container"></div>
394 </div>
394 </div>
395 <div>
395 <div>
396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
396 <span id="update_pull_request" class="btn btn-small">${_('Save Changes')}</span>
397 </div>
397 </div>
398 %endif
398 %endif
399 </div>
399 </div>
400 %endif
400 %endif
401 </div>
401 </div>
402 </div>
402 </div>
403 </div>
403 </div>
404 <div class="box">
404 <div class="box">
405 ##DIFF
405 ##DIFF
406 <div class="table" >
406 <div class="table" >
407 <div id="changeset_compare_view_content">
407 <div id="changeset_compare_view_content">
408 ##CS
408 ##CS
409 % if c.missing_requirements:
409 % if c.missing_requirements:
410 <div class="box">
410 <div class="box">
411 <div class="alert alert-warning">
411 <div class="alert alert-warning">
412 <div>
412 <div>
413 <strong>${_('Missing requirements:')}</strong>
413 <strong>${_('Missing requirements:')}</strong>
414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
414 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
415 </div>
415 </div>
416 </div>
416 </div>
417 </div>
417 </div>
418 % elif c.missing_commits:
418 % elif c.missing_commits:
419 <div class="box">
419 <div class="box">
420 <div class="alert alert-warning">
420 <div class="alert alert-warning">
421 <div>
421 <div>
422 <strong>${_('Missing commits')}:</strong>
422 <strong>${_('Missing commits')}:</strong>
423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
423 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}
424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
424 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}
425 </div>
425 </div>
426 </div>
426 </div>
427 </div>
427 </div>
428 % endif
428 % endif
429 <div class="compare_view_commits_title">
429 <div class="compare_view_commits_title">
430
430
431 <div class="pull-left">
431 <div class="pull-left">
432 <div class="btn-group">
432 <div class="btn-group">
433 <a
433 <a
434 class="btn"
434 class="btn"
435 href="#"
435 href="#"
436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
436 onclick="$('.compare_select').show();$('.compare_select_hidden').hide(); return false">
437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
437 ${ungettext('Expand %s commit','Expand %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
438 </a>
438 </a>
439 <a
439 <a
440 class="btn"
440 class="btn"
441 href="#"
441 href="#"
442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
442 onclick="$('.compare_select').hide();$('.compare_select_hidden').show(); return false">
443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
443 ${ungettext('Collapse %s commit','Collapse %s commits', len(c.commit_ranges)) % len(c.commit_ranges)}
444 </a>
444 </a>
445 </div>
445 </div>
446 </div>
446 </div>
447
447
448 <div class="pull-right">
448 <div class="pull-right">
449 % if c.allowed_to_update and not c.pull_request.is_closed():
449 % if c.allowed_to_update and not c.pull_request.is_closed():
450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
450 <a id="update_commits" class="btn btn-primary pull-right">${_('Update commits')}</a>
451 % else:
451 % else:
452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
452 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
453 % endif
453 % endif
454
454
455 </div>
455 </div>
456
456
457 </div>
457 </div>
458
458
459 % if not c.missing_commits:
459 % if not c.missing_commits:
460 <%include file="/compare/compare_commits.mako" />
460 <%include file="/compare/compare_commits.mako" />
461 <div class="cs_files">
461 <div class="cs_files">
462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
462 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
463 ${cbdiffs.render_diffset_menu()}
463 ${cbdiffs.render_diffset_menu()}
464 ${cbdiffs.render_diffset(
464 ${cbdiffs.render_diffset(
465 c.diffset, use_comments=True,
465 c.diffset, use_comments=True,
466 collapse_when_files_over=30,
466 collapse_when_files_over=30,
467 disable_new_comments=not c.allowed_to_comment,
467 disable_new_comments=not c.allowed_to_comment,
468 deleted_files_comments=c.deleted_files_comments)}
468 deleted_files_comments=c.deleted_files_comments)}
469 </div>
469 </div>
470 % else:
470 % else:
471 ## skipping commits we need to clear the view for missing commits
471 ## skipping commits we need to clear the view for missing commits
472 <div style="clear:both;"></div>
472 <div style="clear:both;"></div>
473 % endif
473 % endif
474
474
475 </div>
475 </div>
476 </div>
476 </div>
477
477
478 ## template for inline comment form
478 ## template for inline comment form
479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
479 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
480
480
481 ## render general comments
481 ## render general comments
482
482
483 <div id="comment-tr-show">
483 <div id="comment-tr-show">
484 <div class="comment">
484 <div class="comment">
485 % if general_outdated_comm_count_ver:
485 <div class="meta">
486 <div class="meta">
486 % if general_outdated_comm_count_ver:
487 % if general_outdated_comm_count_ver == 1:
487 % if general_outdated_comm_count_ver == 1:
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
488 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
489 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
490 % else:
490 % else:
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
491 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
492 <a href="#" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
493 % endif
494 % endif
493 % endif
495 </div>
494 </div>
495 % endif
496 </div>
496 </div>
497 </div>
497 </div>
498
498
499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
499 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
500
500
501 % if not c.pull_request.is_closed():
501 % if not c.pull_request.is_closed():
502 ## merge status, and merge action
503 <div class="pull-request-merge">
504 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
505 </div>
506
502 ## main comment form and it status
507 ## main comment form and it status
503 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
508 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name,
504 pull_request_id=c.pull_request.pull_request_id),
509 pull_request_id=c.pull_request.pull_request_id),
505 c.pull_request_review_status,
510 c.pull_request_review_status,
506 is_pull_request=True, change_status=c.allowed_to_change_status)}
511 is_pull_request=True, change_status=c.allowed_to_change_status)}
507 %endif
512 %endif
508
513
509 <script type="text/javascript">
514 <script type="text/javascript">
510 if (location.hash) {
515 if (location.hash) {
511 var result = splitDelimitedHash(location.hash);
516 var result = splitDelimitedHash(location.hash);
512 var line = $('html').find(result.loc);
517 var line = $('html').find(result.loc);
513 // show hidden comments if we use location.hash
518 // show hidden comments if we use location.hash
514 if (line.hasClass('comment-general')) {
519 if (line.hasClass('comment-general')) {
515 $(line).show();
520 $(line).show();
516 } else if (line.hasClass('comment-inline')) {
521 } else if (line.hasClass('comment-inline')) {
517 $(line).show();
522 $(line).show();
518 var $cb = $(line).closest('.cb');
523 var $cb = $(line).closest('.cb');
519 $cb.removeClass('cb-collapsed')
524 $cb.removeClass('cb-collapsed')
520 }
525 }
521 if (line.length > 0){
526 if (line.length > 0){
522 offsetScroll(line, 70);
527 offsetScroll(line, 70);
523 }
528 }
524 }
529 }
525
530
526 $(function(){
531 $(function(){
527 ReviewerAutoComplete('user');
532 ReviewerAutoComplete('user');
528 // custom code mirror
533 // custom code mirror
529 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
534 var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input');
530
535
531 var PRDetails = {
536 var PRDetails = {
532 editButton: $('#open_edit_pullrequest'),
537 editButton: $('#open_edit_pullrequest'),
533 closeButton: $('#close_edit_pullrequest'),
538 closeButton: $('#close_edit_pullrequest'),
534 deleteButton: $('#delete_pullrequest'),
539 deleteButton: $('#delete_pullrequest'),
535 viewFields: $('#pr-desc, #pr-title'),
540 viewFields: $('#pr-desc, #pr-title'),
536 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
541 editFields: $('#pr-desc-edit, #pr-title-edit, #pr-save'),
537
542
538 init: function() {
543 init: function() {
539 var that = this;
544 var that = this;
540 this.editButton.on('click', function(e) { that.edit(); });
545 this.editButton.on('click', function(e) { that.edit(); });
541 this.closeButton.on('click', function(e) { that.view(); });
546 this.closeButton.on('click', function(e) { that.view(); });
542 },
547 },
543
548
544 edit: function(event) {
549 edit: function(event) {
545 this.viewFields.hide();
550 this.viewFields.hide();
546 this.editButton.hide();
551 this.editButton.hide();
547 this.deleteButton.hide();
552 this.deleteButton.hide();
548 this.closeButton.show();
553 this.closeButton.show();
549 this.editFields.show();
554 this.editFields.show();
550 codeMirrorInstance.refresh();
555 codeMirrorInstance.refresh();
551 },
556 },
552
557
553 view: function(event) {
558 view: function(event) {
554 this.editButton.show();
559 this.editButton.show();
555 this.deleteButton.show();
560 this.deleteButton.show();
556 this.editFields.hide();
561 this.editFields.hide();
557 this.closeButton.hide();
562 this.closeButton.hide();
558 this.viewFields.show();
563 this.viewFields.show();
559 }
564 }
560 };
565 };
561
566
562 var ReviewersPanel = {
567 var ReviewersPanel = {
563 editButton: $('#open_edit_reviewers'),
568 editButton: $('#open_edit_reviewers'),
564 closeButton: $('#close_edit_reviewers'),
569 closeButton: $('#close_edit_reviewers'),
565 addButton: $('#add_reviewer_input'),
570 addButton: $('#add_reviewer_input'),
566 removeButtons: $('.reviewer_member_remove'),
571 removeButtons: $('.reviewer_member_remove'),
567
572
568 init: function() {
573 init: function() {
569 var that = this;
574 var that = this;
570 this.editButton.on('click', function(e) { that.edit(); });
575 this.editButton.on('click', function(e) { that.edit(); });
571 this.closeButton.on('click', function(e) { that.close(); });
576 this.closeButton.on('click', function(e) { that.close(); });
572 },
577 },
573
578
574 edit: function(event) {
579 edit: function(event) {
575 this.editButton.hide();
580 this.editButton.hide();
576 this.closeButton.show();
581 this.closeButton.show();
577 this.addButton.show();
582 this.addButton.show();
578 this.removeButtons.css('visibility', 'visible');
583 this.removeButtons.css('visibility', 'visible');
579 },
584 },
580
585
581 close: function(event) {
586 close: function(event) {
582 this.editButton.show();
587 this.editButton.show();
583 this.closeButton.hide();
588 this.closeButton.hide();
584 this.addButton.hide();
589 this.addButton.hide();
585 this.removeButtons.css('visibility', 'hidden');
590 this.removeButtons.css('visibility', 'hidden');
586 }
591 }
587 };
592 };
588
593
589 PRDetails.init();
594 PRDetails.init();
590 ReviewersPanel.init();
595 ReviewersPanel.init();
591
596
592 showOutdated = function(self){
597 showOutdated = function(self){
593 $('.comment-inline.comment-outdated').show();
598 $('.comment-inline.comment-outdated').show();
594 $('.filediff-outdated').show();
599 $('.filediff-outdated').show();
595 $('.showOutdatedComments').hide();
600 $('.showOutdatedComments').hide();
596 $('.hideOutdatedComments').show();
601 $('.hideOutdatedComments').show();
597 };
602 };
598
603
599 hideOutdated = function(self){
604 hideOutdated = function(self){
600 $('.comment-inline.comment-outdated').hide();
605 $('.comment-inline.comment-outdated').hide();
601 $('.filediff-outdated').hide();
606 $('.filediff-outdated').hide();
602 $('.hideOutdatedComments').hide();
607 $('.hideOutdatedComments').hide();
603 $('.showOutdatedComments').show();
608 $('.showOutdatedComments').show();
604 };
609 };
605
610
611 refreshMergeChecks = function(){
612 var loadUrl = "${h.url.current(merge_checks=1)}";
613 $('.pull-request-merge').css('opacity', 0.3);
614 $('.pull-request-merge').load(
615 loadUrl,function() {
616 $('.pull-request-merge').css('opacity', 1);
617 }
618 );
619 };
620
606 $('#show-outdated-comments').on('click', function(e){
621 $('#show-outdated-comments').on('click', function(e){
607 var button = $(this);
622 var button = $(this);
608 var outdated = $('.comment-outdated');
623 var outdated = $('.comment-outdated');
609
624
610 if (button.html() === "(Show)") {
625 if (button.html() === "(Show)") {
611 button.html("(Hide)");
626 button.html("(Hide)");
612 outdated.show();
627 outdated.show();
613 } else {
628 } else {
614 button.html("(Show)");
629 button.html("(Show)");
615 outdated.hide();
630 outdated.hide();
616 }
631 }
617 });
632 });
618
633
619 $('.show-inline-comments').on('change', function(e){
634 $('.show-inline-comments').on('change', function(e){
620 var show = 'none';
635 var show = 'none';
621 var target = e.currentTarget;
636 var target = e.currentTarget;
622 if(target.checked){
637 if(target.checked){
623 show = ''
638 show = ''
624 }
639 }
625 var boxid = $(target).attr('id_for');
640 var boxid = $(target).attr('id_for');
626 var comments = $('#{0} .inline-comments'.format(boxid));
641 var comments = $('#{0} .inline-comments'.format(boxid));
627 var fn_display = function(idx){
642 var fn_display = function(idx){
628 $(this).css('display', show);
643 $(this).css('display', show);
629 };
644 };
630 $(comments).each(fn_display);
645 $(comments).each(fn_display);
631 var btns = $('#{0} .inline-comments-button'.format(boxid));
646 var btns = $('#{0} .inline-comments-button'.format(boxid));
632 $(btns).each(fn_display);
647 $(btns).each(fn_display);
633 });
648 });
634
649
635 $('#merge_pull_request_form').submit(function() {
650 $('#merge_pull_request_form').submit(function() {
636 if (!$('#merge_pull_request').attr('disabled')) {
651 if (!$('#merge_pull_request').attr('disabled')) {
637 $('#merge_pull_request').attr('disabled', 'disabled');
652 $('#merge_pull_request').attr('disabled', 'disabled');
638 }
653 }
639 return true;
654 return true;
640 });
655 });
641
656
642 $('#edit_pull_request').on('click', function(e){
657 $('#edit_pull_request').on('click', function(e){
643 var title = $('#pr-title-input').val();
658 var title = $('#pr-title-input').val();
644 var description = codeMirrorInstance.getValue();
659 var description = codeMirrorInstance.getValue();
645 editPullRequest(
660 editPullRequest(
646 "${c.repo_name}", "${c.pull_request.pull_request_id}",
661 "${c.repo_name}", "${c.pull_request.pull_request_id}",
647 title, description);
662 title, description);
648 });
663 });
649
664
650 $('#update_pull_request').on('click', function(e){
665 $('#update_pull_request').on('click', function(e){
651 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
666 updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}");
652 });
667 });
653
668
654 $('#update_commits').on('click', function(e){
669 $('#update_commits').on('click', function(e){
655 var isDisabled = !$(e.currentTarget).attr('disabled');
670 var isDisabled = !$(e.currentTarget).attr('disabled');
656 $(e.currentTarget).text(_gettext('Updating...'));
671 $(e.currentTarget).text(_gettext('Updating...'));
657 $(e.currentTarget).attr('disabled', 'disabled');
672 $(e.currentTarget).attr('disabled', 'disabled');
658 if(isDisabled){
673 if(isDisabled){
659 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
674 updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}");
660 }
675 }
661
676
662 });
677 });
663 // fixing issue with caches on firefox
678 // fixing issue with caches on firefox
664 $('#update_commits').removeAttr("disabled");
679 $('#update_commits').removeAttr("disabled");
665
680
666 $('#close_pull_request').on('click', function(e){
681 $('#close_pull_request').on('click', function(e){
667 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
682 closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}");
668 });
683 });
669
684
670 $('.show-inline-comments').on('click', function(e){
685 $('.show-inline-comments').on('click', function(e){
671 var boxid = $(this).attr('data-comment-id');
686 var boxid = $(this).attr('data-comment-id');
672 var button = $(this);
687 var button = $(this);
673
688
674 if(button.hasClass("comments-visible")) {
689 if(button.hasClass("comments-visible")) {
675 $('#{0} .inline-comments'.format(boxid)).each(function(index){
690 $('#{0} .inline-comments'.format(boxid)).each(function(index){
676 $(this).hide();
691 $(this).hide();
677 });
692 });
678 button.removeClass("comments-visible");
693 button.removeClass("comments-visible");
679 } else {
694 } else {
680 $('#{0} .inline-comments'.format(boxid)).each(function(index){
695 $('#{0} .inline-comments'.format(boxid)).each(function(index){
681 $(this).show();
696 $(this).show();
682 });
697 });
683 button.addClass("comments-visible");
698 button.addClass("comments-visible");
684 }
699 }
685 });
700 });
701
702 // register submit callback on commentForm form to track TODOs
703 window.commentFormGlobalSubmitSuccessCallback = function(){
704 refreshMergeChecks();
705 };
706
686 })
707 })
687 </script>
708 </script>
688
709
689 </div>
710 </div>
690 </div>
711 </div>
691
712
692 </%def>
713 </%def>
General Comments 0
You need to be logged in to leave comments. Login now