##// END OF EJS Templates
Adde pull request voting recalculation
marcink -
r2481:4d303243 beta
parent child Browse files
Show More
@@ -1,277 +1,297 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.pullrequests
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 pull requests controller for rhodecode for initializing pull requests
7 7
8 8 :created_on: May 7, 2012
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 26 import traceback
27 27
28 28 from webob.exc import HTTPNotFound
29 from collections import defaultdict
30 from itertools import groupby
29 31
30 32 from pylons import request, response, session, tmpl_context as c, url
31 33 from pylons.controllers.util import abort, redirect
32 34 from pylons.i18n.translation import _
33 35 from pylons.decorators import jsonify
34 36
35 37 from rhodecode.lib.base import BaseRepoController, render
36 38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 39 from rhodecode.lib import helpers as h
38 40 from rhodecode.lib import diffs
39 41 from rhodecode.lib.utils import action_logger
40 42 from rhodecode.model.db import User, PullRequest, ChangesetStatus
41 43 from rhodecode.model.pull_request import PullRequestModel
42 44 from rhodecode.model.meta import Session
43 45 from rhodecode.model.repo import RepoModel
44 46 from rhodecode.model.comment import ChangesetCommentsModel
45 47 from rhodecode.model.changeset_status import ChangesetStatusModel
46 48
47 49 log = logging.getLogger(__name__)
48 50
49 51
50 52 class PullrequestsController(BaseRepoController):
51 53
52 54 @LoginRequired()
53 55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
54 56 'repository.admin')
55 57 def __before__(self):
56 58 super(PullrequestsController, self).__before__()
57 59
58 60 def _get_repo_refs(self, repo):
59 61 hist_l = []
60 62
61 63 branches_group = ([('branch:%s:%s' % (k, v), k) for
62 64 k, v in repo.branches.iteritems()], _("Branches"))
63 65 bookmarks_group = ([('book:%s:%s' % (k, v), k) for
64 66 k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
65 67 tags_group = ([('tag:%s:%s' % (k, v), k) for
66 68 k, v in repo.tags.iteritems()], _("Tags"))
67 69
68 70 hist_l.append(bookmarks_group)
69 71 hist_l.append(branches_group)
70 72 hist_l.append(tags_group)
71 73
72 74 return hist_l
73 75
74 76 def show_all(self, repo_name):
75 77 c.pull_requests = PullRequestModel().get_all(repo_name)
76 78 c.repo_name = repo_name
77 79 return render('/pullrequests/pullrequest_show_all.html')
78 80
79 81 def index(self):
80 82 org_repo = c.rhodecode_db_repo
81 83
82 84 if org_repo.scm_instance.alias != 'hg':
83 85 log.error('Review not available for GIT REPOS')
84 86 raise HTTPNotFound
85 87
86 88 c.org_refs = self._get_repo_refs(c.rhodecode_repo)
87 89 c.org_repos = []
88 90 c.other_repos = []
89 91 c.org_repos.append((org_repo.repo_name, '%s/%s' % (
90 92 org_repo.user.username, c.repo_name))
91 93 )
92 94
93 95 c.other_refs = c.org_refs
94 96 c.other_repos.extend(c.org_repos)
95 97 c.default_pull_request = org_repo.repo_name
96 98 #gather forks and add to this list
97 99 for fork in org_repo.forks:
98 100 c.other_repos.append((fork.repo_name, '%s/%s' % (
99 101 fork.user.username, fork.repo_name))
100 102 )
101 103 #add parents of this fork also
102 104 if org_repo.parent:
103 105 c.default_pull_request = org_repo.parent.repo_name
104 106 c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
105 107 org_repo.parent.user.username,
106 108 org_repo.parent.repo_name))
107 109 )
108 110
109 111 c.review_members = []
110 112 c.available_members = []
111 113 for u in User.query().filter(User.username != 'default').all():
112 114 uname = u.username
113 115 if org_repo.user == u:
114 116 uname = _('%s (owner)' % u.username)
115 117 # auto add owner to pull-request recipients
116 118 c.review_members.append([u.user_id, uname])
117 119 c.available_members.append([u.user_id, uname])
118 120 return render('/pullrequests/pullrequest.html')
119 121
120 122 def create(self, repo_name):
121 123 req_p = request.POST
122 124 org_repo = req_p['org_repo']
123 125 org_ref = req_p['org_ref']
124 126 other_repo = req_p['other_repo']
125 127 other_ref = req_p['other_ref']
126 128 revisions = req_p.getall('revisions')
127 129 reviewers = req_p.getall('review_members')
128 130 #TODO: wrap this into a FORM !!!
129 131
130 132 title = req_p['pullrequest_title']
131 133 description = req_p['pullrequest_desc']
132 134
133 135 try:
134 136 model = PullRequestModel()
135 137 model.create(self.rhodecode_user.user_id, org_repo,
136 138 org_ref, other_repo, other_ref, revisions,
137 139 reviewers, title, description)
138 140 Session.commit()
139 141 h.flash(_('Pull request send'), category='success')
140 142 except Exception:
141 143 raise
142 144 h.flash(_('Error occured during sending pull request'),
143 145 category='error')
144 146 log.error(traceback.format_exc())
145 147
146 148 return redirect(url('changelog_home', repo_name=repo_name))
147 149
148 150 def _load_compare_data(self, pull_request):
149 151 """
150 152 Load context data needed for generating compare diff
151 153
152 154 :param pull_request:
153 155 :type pull_request:
154 156 """
155 157
156 158 org_repo = pull_request.org_repo
157 159 org_ref_type, org_ref_, org_ref = pull_request.org_ref.split(':')
158 160 other_repo = pull_request.other_repo
159 161 other_ref_type, other_ref, other_ref_ = pull_request.other_ref.split(':')
160 162
161 163 org_ref = (org_ref_type, org_ref)
162 164 other_ref = (other_ref_type, other_ref)
163 165
164 166 c.org_repo = org_repo
165 167 c.other_repo = other_repo
166 168
167 169 c.cs_ranges, discovery_data = PullRequestModel().get_compare_data(
168 170 org_repo, org_ref, other_repo, other_ref
169 171 )
170 172
171 173 c.statuses = c.rhodecode_db_repo.statuses([x.raw_id for x in
172 174 c.cs_ranges])
173 175 # defines that we need hidden inputs with changesets
174 176 c.as_form = request.GET.get('as_form', False)
175 177
176 178 c.org_ref = org_ref[1]
177 179 c.other_ref = other_ref[1]
178 180 # diff needs to have swapped org with other to generate proper diff
179 181 _diff = diffs.differ(other_repo, other_ref, org_repo, org_ref,
180 182 discovery_data)
181 183 diff_processor = diffs.DiffProcessor(_diff, format='gitdiff')
182 184 _parsed = diff_processor.prepare()
183 185
184 186 c.files = []
185 187 c.changes = {}
186 188
187 189 for f in _parsed:
188 190 fid = h.FID('', f['filename'])
189 191 c.files.append([fid, f['operation'], f['filename'], f['stats']])
190 192 diff = diff_processor.as_html(enable_comments=False, diff_lines=[f])
191 193 c.changes[fid] = [f['operation'], f['filename'], diff]
192 194
193 195 def show(self, repo_name, pull_request_id):
194 196 repo_model = RepoModel()
195 197 c.users_array = repo_model.get_users_js()
196 198 c.users_groups_array = repo_model.get_users_groups_js()
197 199 c.pull_request = PullRequest.get(pull_request_id)
198 200
199 201 # valid ID
200 202 if not c.pull_request:
201 203 raise HTTPNotFound
204 cc_model = ChangesetCommentsModel()
205 cs_model = ChangesetStatusModel()
206 _cs_statuses = cs_model.get_statuses(c.pull_request.org_repo,
207 pull_request=c.pull_request,
208 with_revisions=True)
209
210 cs_statuses = defaultdict(list)
211 for st in _cs_statuses:
212 cs_statuses[st.author.username] += [st]
213
214 c.pull_request_reviewers = []
215 for o in c.pull_request.reviewers:
216 st = cs_statuses.get(o.user.username, None)
217 if st:
218 sorter = lambda k: k.version
219 st = [(x, list(y)[0])
220 for x, y in (groupby(sorted(st, key=sorter), sorter))]
221 c.pull_request_reviewers.append([o.user, st])
202 222
203 223 # pull_requests repo_name we opened it against
204 224 # ie. other_repo must match
205 225 if repo_name != c.pull_request.other_repo.repo_name:
206 226 raise HTTPNotFound
207 227
208 228 # load compare data into template context
209 229 self._load_compare_data(c.pull_request)
210 230
211 231 # inline comments
212 232 c.inline_cnt = 0
213 c.inline_comments = ChangesetCommentsModel()\
214 .get_inline_comments(c.rhodecode_db_repo.repo_id,
215 pull_request=pull_request_id)
233 c.inline_comments = cc_model.get_inline_comments(
234 c.rhodecode_db_repo.repo_id,
235 pull_request=pull_request_id)
216 236 # count inline comments
217 237 for __, lines in c.inline_comments:
218 238 for comments in lines.values():
219 239 c.inline_cnt += len(comments)
220 240 # comments
221 c.comments = ChangesetCommentsModel()\
222 .get_comments(c.rhodecode_db_repo.repo_id,
223 pull_request=pull_request_id)
241 c.comments = cc_model.get_comments(c.rhodecode_db_repo.repo_id,
242 pull_request=pull_request_id)
224 243
225 244 # changeset(pull-request) status
226 c.current_changeset_status = ChangesetStatusModel()\
227 .get_status(c.pull_request.org_repo,
228 pull_request=c.pull_request)
245 c.current_changeset_status = cs_model.calculate_status(
246 c.pull_request_reviewers
247 )
229 248 c.changeset_statuses = ChangesetStatus.STATUSES
249
230 250 return render('/pullrequests/pullrequest_show.html')
231 251
232 252 @jsonify
233 253 def comment(self, repo_name, pull_request_id):
234 254
235 255 status = request.POST.get('changeset_status')
236 256 change_status = request.POST.get('change_changeset_status')
237 257
238 258 comm = ChangesetCommentsModel().create(
239 259 text=request.POST.get('text'),
240 260 repo_id=c.rhodecode_db_repo.repo_id,
241 261 user_id=c.rhodecode_user.user_id,
242 262 pull_request=pull_request_id,
243 263 f_path=request.POST.get('f_path'),
244 264 line_no=request.POST.get('line'),
245 265 status_change=(ChangesetStatus.get_status_lbl(status)
246 266 if status and change_status else None)
247 267 )
248 268
249 269 # get status if set !
250 270 if status and change_status:
251 271 ChangesetStatusModel().set_status(
252 272 c.rhodecode_db_repo.repo_id,
253 273 status,
254 274 c.rhodecode_user.user_id,
255 275 comm,
256 276 pull_request=pull_request_id
257 277 )
258 278 action_logger(self.rhodecode_user,
259 279 'user_commented_pull_request:%s' % pull_request_id,
260 280 c.rhodecode_db_repo, self.ip_addr, self.sa)
261 281
262 282 Session.commit()
263 283
264 284 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
265 285 return redirect(h.url('pullrequest_show', repo_name=repo_name,
266 286 pull_request_id=pull_request_id))
267 287
268 288 data = {
269 289 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
270 290 }
271 291 if comm:
272 292 c.co = comm
273 293 data.update(comm.get_dict())
274 294 data.update({'rendered_text':
275 295 render('changeset/changeset_comment_block.html')})
276 296
277 297 return data
@@ -1,139 +1,174 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.changeset_status
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6
7 7 :created_on: Apr 30, 2012
8 8 :author: marcink
9 9 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
10 10 :license: GPLv3, see COPYING for more details.
11 11 """
12 12 # This program is free software: you can redistribute it and/or modify
13 13 # it under the terms of the GNU General Public License as published by
14 14 # the Free Software Foundation, either version 3 of the License, or
15 15 # (at your option) any later version.
16 16 #
17 17 # This program is distributed in the hope that it will be useful,
18 18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 20 # GNU General Public License for more details.
21 21 #
22 22 # You should have received a copy of the GNU General Public License
23 23 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 24
25 25
26 26 import logging
27 from collections import defaultdict
27 28
28 29 from rhodecode.model import BaseModel
29 30 from rhodecode.model.db import ChangesetStatus, PullRequest
30 31
31 32 log = logging.getLogger(__name__)
32 33
33 34
34 35 class ChangesetStatusModel(BaseModel):
35 36
36 37 def __get_changeset_status(self, changeset_status):
37 38 return self._get_instance(ChangesetStatus, changeset_status)
38 39
39 40 def __get_pull_request(self, pull_request):
40 41 return self._get_instance(PullRequest, pull_request)
41 42
43 def _get_status_query(self, repo, revision, pull_request,
44 with_revisions=False):
45 repo = self._get_repo(repo)
46
47 q = ChangesetStatus.query()\
48 .filter(ChangesetStatus.repo == repo)
49 if not with_revisions:
50 q = q.filter(ChangesetStatus.version == 0)
51
52 if revision:
53 q = q.filter(ChangesetStatus.revision == revision)
54 elif pull_request:
55 pull_request = self.__get_pull_request(pull_request)
56 q = q.filter(ChangesetStatus.pull_request == pull_request)
57 else:
58 raise Exception('Please specify revision or pull_request')
59 q.order_by(ChangesetStatus.version.asc())
60 return q
61
62 def calculate_status(self, statuses_by_reviewers):
63 """
64 leading one wins, if number of occurences are equal than weaker wins
65
66 :param statuses_by_reviewers:
67 """
68 status = None
69 votes = defaultdict(int)
70 reviewers_number = len(statuses_by_reviewers)
71 for user, statuses in statuses_by_reviewers:
72 if statuses:
73 ver, latest = statuses[0]
74 votes[latest.status] += 1
75 else:
76 votes[ChangesetStatus.DEFAULT] += 1
77
78 if votes.get(ChangesetStatus.STATUS_APPROVED) == reviewers_number:
79 return ChangesetStatus.STATUS_APPROVED
80 else:
81 return ChangesetStatus.STATUS_UNDER_REVIEW
82
83 def get_statuses(self, repo, revision=None, pull_request=None,
84 with_revisions=False):
85 q = self._get_status_query(repo, revision, pull_request,
86 with_revisions)
87 return q.all()
88
42 89 def get_status(self, repo, revision=None, pull_request=None):
43 90 """
44 91 Returns latest status of changeset for given revision or for given
45 92 pull request. Statuses are versioned inside a table itself and
46 93 version == 0 is always the current one
47 94
48 95 :param repo:
49 96 :type repo:
50 97 :param revision: 40char hash or None
51 98 :type revision: str
52 99 :param pull_request: pull_request reference
53 100 :type:
54 101 """
55 repo = self._get_repo(repo)
56
57 q = ChangesetStatus.query()\
58 .filter(ChangesetStatus.repo == repo)\
59 .filter(ChangesetStatus.version == 0)
60
61 if revision:
62 q = q.filter(ChangesetStatus.revision == revision)
63 elif pull_request:
64 pull_request = self.__get_pull_request(pull_request)
65 q = q.filter(ChangesetStatus.pull_request == pull_request)
66 else:
67 raise Exception('Please specify revision or pull_request')
102 q = self._get_status_query(repo, revision, pull_request)
68 103
69 104 # need to use first here since there can be multiple statuses
70 105 # returned from pull_request
71 106 status = q.first()
72 107 status = status.status if status else status
73 108 st = status or ChangesetStatus.DEFAULT
74 109 return str(st)
75 110
76 111 def set_status(self, repo, status, user, comment, revision=None,
77 112 pull_request=None):
78 113 """
79 114 Creates new status for changeset or updates the old ones bumping their
80 115 version, leaving the current status at
81 116
82 117 :param repo:
83 118 :type repo:
84 119 :param revision:
85 120 :type revision:
86 121 :param status:
87 122 :type status:
88 123 :param user:
89 124 :type user:
90 125 :param comment:
91 126 :type comment:
92 127 """
93 128 repo = self._get_repo(repo)
94 129
95 130 q = ChangesetStatus.query()
96 131
97 132 if revision:
98 133 q = q.filter(ChangesetStatus.repo == repo)
99 134 q = q.filter(ChangesetStatus.revision == revision)
100 135 elif pull_request:
101 136 pull_request = self.__get_pull_request(pull_request)
102 137 q = q.filter(ChangesetStatus.repo == pull_request.org_repo)
103 138 q = q.filter(ChangesetStatus.pull_request == pull_request)
104 139 cur_statuses = q.all()
105 140
106 141 if cur_statuses:
107 142 for st in cur_statuses:
108 143 st.version += 1
109 144 self.sa.add(st)
110 145
111 146 def _create_status(user, repo, status, comment, revision, pull_request):
112 147 new_status = ChangesetStatus()
113 148 new_status.author = self._get_user(user)
114 149 new_status.repo = self._get_repo(repo)
115 150 new_status.status = status
116 151 new_status.comment = comment
117 152 new_status.revision = revision
118 153 new_status.pull_request = pull_request
119 154 return new_status
120 155
121 156 if revision:
122 157 new_status = _create_status(user=user, repo=repo, status=status,
123 158 comment=comment, revision=revision,
124 159 pull_request=None)
125 160 self.sa.add(new_status)
126 161 return new_status
127 162 elif pull_request:
128 163 #pull request can have more than one revision associated to it
129 164 #we need to create new version for each one
130 165 new_statuses = []
131 166 repo = pull_request.org_repo
132 167 for rev in pull_request.revisions:
133 168 new_status = _create_status(user=user, repo=repo,
134 169 status=status, comment=comment,
135 170 revision=rev,
136 171 pull_request=pull_request)
137 172 new_statuses.append(new_status)
138 173 self.sa.add(new_status)
139 174 return new_statuses
@@ -1,1549 +1,1558 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 from collections import defaultdict
32 32
33 33 from sqlalchemy import *
34 34 from sqlalchemy.ext.hybrid import hybrid_property
35 35 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
36 36 from sqlalchemy.exc import DatabaseError
37 37 from beaker.cache import cache_region, region_invalidate
38 38
39 39 from pylons.i18n.translation import lazy_ugettext as _
40 40
41 41 from rhodecode.lib.vcs import get_backend
42 42 from rhodecode.lib.vcs.utils.helpers import get_scm
43 43 from rhodecode.lib.vcs.exceptions import VCSError
44 44 from rhodecode.lib.vcs.utils.lazy import LazyProperty
45 45
46 46 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
47 47 safe_unicode
48 48 from rhodecode.lib.compat import json
49 49 from rhodecode.lib.caching_query import FromCache
50 50
51 51 from rhodecode.model.meta import Base, Session
52 52
53 53
54 54 URL_SEP = '/'
55 55 log = logging.getLogger(__name__)
56 56
57 57 #==============================================================================
58 58 # BASE CLASSES
59 59 #==============================================================================
60 60
61 61 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
62 62
63 63
64 64 class ModelSerializer(json.JSONEncoder):
65 65 """
66 66 Simple Serializer for JSON,
67 67
68 68 usage::
69 69
70 70 to make object customized for serialization implement a __json__
71 71 method that will return a dict for serialization into json
72 72
73 73 example::
74 74
75 75 class Task(object):
76 76
77 77 def __init__(self, name, value):
78 78 self.name = name
79 79 self.value = value
80 80
81 81 def __json__(self):
82 82 return dict(name=self.name,
83 83 value=self.value)
84 84
85 85 """
86 86
87 87 def default(self, obj):
88 88
89 89 if hasattr(obj, '__json__'):
90 90 return obj.__json__()
91 91 else:
92 92 return json.JSONEncoder.default(self, obj)
93 93
94 94
95 95 class BaseModel(object):
96 96 """
97 97 Base Model for all classess
98 98 """
99 99
100 100 @classmethod
101 101 def _get_keys(cls):
102 102 """return column names for this model """
103 103 return class_mapper(cls).c.keys()
104 104
105 105 def get_dict(self):
106 106 """
107 107 return dict with keys and values corresponding
108 108 to this model data """
109 109
110 110 d = {}
111 111 for k in self._get_keys():
112 112 d[k] = getattr(self, k)
113 113
114 114 # also use __json__() if present to get additional fields
115 115 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
116 116 d[k] = val
117 117 return d
118 118
119 119 def get_appstruct(self):
120 120 """return list with keys and values tupples corresponding
121 121 to this model data """
122 122
123 123 l = []
124 124 for k in self._get_keys():
125 125 l.append((k, getattr(self, k),))
126 126 return l
127 127
128 128 def populate_obj(self, populate_dict):
129 129 """populate model with data from given populate_dict"""
130 130
131 131 for k in self._get_keys():
132 132 if k in populate_dict:
133 133 setattr(self, k, populate_dict[k])
134 134
135 135 @classmethod
136 136 def query(cls):
137 137 return Session.query(cls)
138 138
139 139 @classmethod
140 140 def get(cls, id_):
141 141 if id_:
142 142 return cls.query().get(id_)
143 143
144 144 @classmethod
145 145 def getAll(cls):
146 146 return cls.query().all()
147 147
148 148 @classmethod
149 149 def delete(cls, id_):
150 150 obj = cls.query().get(id_)
151 151 Session.delete(obj)
152 152
153 153 def __repr__(self):
154 154 if hasattr(self, '__unicode__'):
155 155 # python repr needs to return str
156 156 return safe_str(self.__unicode__())
157 157 return '<DB:%s>' % (self.__class__.__name__)
158 158
159 159
160 160 class RhodeCodeSetting(Base, BaseModel):
161 161 __tablename__ = 'rhodecode_settings'
162 162 __table_args__ = (
163 163 UniqueConstraint('app_settings_name'),
164 164 {'extend_existing': True, 'mysql_engine': 'InnoDB',
165 165 'mysql_charset': 'utf8'}
166 166 )
167 167 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
168 168 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
169 169 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
170 170
171 171 def __init__(self, k='', v=''):
172 172 self.app_settings_name = k
173 173 self.app_settings_value = v
174 174
175 175 @validates('_app_settings_value')
176 176 def validate_settings_value(self, key, val):
177 177 assert type(val) == unicode
178 178 return val
179 179
180 180 @hybrid_property
181 181 def app_settings_value(self):
182 182 v = self._app_settings_value
183 183 if self.app_settings_name == 'ldap_active':
184 184 v = str2bool(v)
185 185 return v
186 186
187 187 @app_settings_value.setter
188 188 def app_settings_value(self, val):
189 189 """
190 190 Setter that will always make sure we use unicode in app_settings_value
191 191
192 192 :param val:
193 193 """
194 194 self._app_settings_value = safe_unicode(val)
195 195
196 196 def __unicode__(self):
197 197 return u"<%s('%s:%s')>" % (
198 198 self.__class__.__name__,
199 199 self.app_settings_name, self.app_settings_value
200 200 )
201 201
202 202 @classmethod
203 203 def get_by_name(cls, ldap_key):
204 204 return cls.query()\
205 205 .filter(cls.app_settings_name == ldap_key).scalar()
206 206
207 207 @classmethod
208 208 def get_app_settings(cls, cache=False):
209 209
210 210 ret = cls.query()
211 211
212 212 if cache:
213 213 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
214 214
215 215 if not ret:
216 216 raise Exception('Could not get application settings !')
217 217 settings = {}
218 218 for each in ret:
219 219 settings['rhodecode_' + each.app_settings_name] = \
220 220 each.app_settings_value
221 221
222 222 return settings
223 223
224 224 @classmethod
225 225 def get_ldap_settings(cls, cache=False):
226 226 ret = cls.query()\
227 227 .filter(cls.app_settings_name.startswith('ldap_')).all()
228 228 fd = {}
229 229 for row in ret:
230 230 fd.update({row.app_settings_name: row.app_settings_value})
231 231
232 232 return fd
233 233
234 234
235 235 class RhodeCodeUi(Base, BaseModel):
236 236 __tablename__ = 'rhodecode_ui'
237 237 __table_args__ = (
238 238 UniqueConstraint('ui_key'),
239 239 {'extend_existing': True, 'mysql_engine': 'InnoDB',
240 240 'mysql_charset': 'utf8'}
241 241 )
242 242
243 243 HOOK_UPDATE = 'changegroup.update'
244 244 HOOK_REPO_SIZE = 'changegroup.repo_size'
245 245 HOOK_PUSH = 'changegroup.push_logger'
246 246 HOOK_PULL = 'preoutgoing.pull_logger'
247 247
248 248 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
249 249 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
250 250 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
251 251 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
252 252 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
253 253
254 254 @classmethod
255 255 def get_by_key(cls, key):
256 256 return cls.query().filter(cls.ui_key == key)
257 257
258 258 @classmethod
259 259 def get_builtin_hooks(cls):
260 260 q = cls.query()
261 261 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
262 262 cls.HOOK_REPO_SIZE,
263 263 cls.HOOK_PUSH, cls.HOOK_PULL]))
264 264 return q.all()
265 265
266 266 @classmethod
267 267 def get_custom_hooks(cls):
268 268 q = cls.query()
269 269 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
270 270 cls.HOOK_REPO_SIZE,
271 271 cls.HOOK_PUSH, cls.HOOK_PULL]))
272 272 q = q.filter(cls.ui_section == 'hooks')
273 273 return q.all()
274 274
275 275 @classmethod
276 276 def get_repos_location(cls):
277 277 return cls.get_by_key('/').one().ui_value
278 278
279 279 @classmethod
280 280 def create_or_update_hook(cls, key, val):
281 281 new_ui = cls.get_by_key(key).scalar() or cls()
282 282 new_ui.ui_section = 'hooks'
283 283 new_ui.ui_active = True
284 284 new_ui.ui_key = key
285 285 new_ui.ui_value = val
286 286
287 287 Session.add(new_ui)
288 288
289 289
290 290 class User(Base, BaseModel):
291 291 __tablename__ = 'users'
292 292 __table_args__ = (
293 293 UniqueConstraint('username'), UniqueConstraint('email'),
294 294 {'extend_existing': True, 'mysql_engine': 'InnoDB',
295 295 'mysql_charset': 'utf8'}
296 296 )
297 297 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
298 298 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
299 299 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 300 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
301 301 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
302 302 name = Column("firstname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
303 303 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
304 304 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
305 305 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
306 306 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
307 307 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
308 308
309 309 user_log = relationship('UserLog', cascade='all')
310 310 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
311 311
312 312 repositories = relationship('Repository')
313 313 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
314 314 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
315 315 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
316 316
317 317 group_member = relationship('UsersGroupMember', cascade='all')
318 318
319 319 notifications = relationship('UserNotification', cascade='all')
320 320 # notifications assigned to this user
321 321 user_created_notifications = relationship('Notification', cascade='all')
322 322 # comments created by this user
323 323 user_comments = relationship('ChangesetComment', cascade='all')
324 324
325 325 @hybrid_property
326 326 def email(self):
327 327 return self._email
328 328
329 329 @email.setter
330 330 def email(self, val):
331 331 self._email = val.lower() if val else None
332 332
333 333 @property
334 334 def full_name(self):
335 335 return '%s %s' % (self.name, self.lastname)
336 336
337 337 @property
338 338 def full_name_or_username(self):
339 339 return ('%s %s' % (self.name, self.lastname)
340 340 if (self.name and self.lastname) else self.username)
341 341
342 342 @property
343 343 def full_contact(self):
344 344 return '%s %s <%s>' % (self.name, self.lastname, self.email)
345 345
346 346 @property
347 347 def short_contact(self):
348 348 return '%s %s' % (self.name, self.lastname)
349 349
350 350 @property
351 351 def is_admin(self):
352 352 return self.admin
353 353
354 354 def __unicode__(self):
355 355 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
356 356 self.user_id, self.username)
357 357
358 358 @classmethod
359 359 def get_by_username(cls, username, case_insensitive=False, cache=False):
360 360 if case_insensitive:
361 361 q = cls.query().filter(cls.username.ilike(username))
362 362 else:
363 363 q = cls.query().filter(cls.username == username)
364 364
365 365 if cache:
366 366 q = q.options(FromCache(
367 367 "sql_cache_short",
368 368 "get_user_%s" % _hash_key(username)
369 369 )
370 370 )
371 371 return q.scalar()
372 372
373 373 @classmethod
374 374 def get_by_api_key(cls, api_key, cache=False):
375 375 q = cls.query().filter(cls.api_key == api_key)
376 376
377 377 if cache:
378 378 q = q.options(FromCache("sql_cache_short",
379 379 "get_api_key_%s" % api_key))
380 380 return q.scalar()
381 381
382 382 @classmethod
383 383 def get_by_email(cls, email, case_insensitive=False, cache=False):
384 384 if case_insensitive:
385 385 q = cls.query().filter(cls.email.ilike(email))
386 386 else:
387 387 q = cls.query().filter(cls.email == email)
388 388
389 389 if cache:
390 390 q = q.options(FromCache("sql_cache_short",
391 391 "get_email_key_%s" % email))
392 392
393 393 ret = q.scalar()
394 394 if ret is None:
395 395 q = UserEmailMap.query()
396 396 # try fetching in alternate email map
397 397 if case_insensitive:
398 398 q = q.filter(UserEmailMap.email.ilike(email))
399 399 else:
400 400 q = q.filter(UserEmailMap.email == email)
401 401 q = q.options(joinedload(UserEmailMap.user))
402 402 if cache:
403 403 q = q.options(FromCache("sql_cache_short",
404 404 "get_email_map_key_%s" % email))
405 405 ret = getattr(q.scalar(), 'user', None)
406 406
407 407 return ret
408 408
409 409 def update_lastlogin(self):
410 410 """Update user lastlogin"""
411 411 self.last_login = datetime.datetime.now()
412 412 Session.add(self)
413 413 log.debug('updated user %s lastlogin' % self.username)
414 414
415 415 def __json__(self):
416 416 return dict(
417 417 user_id=self.user_id,
418 418 first_name=self.name,
419 419 last_name=self.lastname,
420 420 email=self.email,
421 421 full_name=self.full_name,
422 422 full_name_or_username=self.full_name_or_username,
423 423 short_contact=self.short_contact,
424 424 full_contact=self.full_contact
425 425 )
426 426
427 427
428 428 class UserEmailMap(Base, BaseModel):
429 429 __tablename__ = 'user_email_map'
430 430 __table_args__ = (
431 431 Index('uem_email_idx', 'email'),
432 432 UniqueConstraint('email'),
433 433 {'extend_existing': True, 'mysql_engine': 'InnoDB',
434 434 'mysql_charset': 'utf8'}
435 435 )
436 436 __mapper_args__ = {}
437 437
438 438 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
439 439 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
440 440 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
441 441
442 442 user = relationship('User', lazy='joined')
443 443
444 444 @validates('_email')
445 445 def validate_email(self, key, email):
446 446 # check if this email is not main one
447 447 main_email = Session.query(User).filter(User.email == email).scalar()
448 448 if main_email is not None:
449 449 raise AttributeError('email %s is present is user table' % email)
450 450 return email
451 451
452 452 @hybrid_property
453 453 def email(self):
454 454 return self._email
455 455
456 456 @email.setter
457 457 def email(self, val):
458 458 self._email = val.lower() if val else None
459 459
460 460
461 461 class UserLog(Base, BaseModel):
462 462 __tablename__ = 'user_logs'
463 463 __table_args__ = (
464 464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 465 'mysql_charset': 'utf8'},
466 466 )
467 467 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
468 468 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
469 469 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
470 470 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
471 471 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
472 472 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
473 473 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
474 474
475 475 @property
476 476 def action_as_day(self):
477 477 return datetime.date(*self.action_date.timetuple()[:3])
478 478
479 479 user = relationship('User')
480 480 repository = relationship('Repository', cascade='')
481 481
482 482
483 483 class UsersGroup(Base, BaseModel):
484 484 __tablename__ = 'users_groups'
485 485 __table_args__ = (
486 486 {'extend_existing': True, 'mysql_engine': 'InnoDB',
487 487 'mysql_charset': 'utf8'},
488 488 )
489 489
490 490 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
491 491 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
492 492 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
493 493
494 494 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
495 495 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
496 496 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
497 497
498 498 def __unicode__(self):
499 499 return u'<userGroup(%s)>' % (self.users_group_name)
500 500
501 501 @classmethod
502 502 def get_by_group_name(cls, group_name, cache=False,
503 503 case_insensitive=False):
504 504 if case_insensitive:
505 505 q = cls.query().filter(cls.users_group_name.ilike(group_name))
506 506 else:
507 507 q = cls.query().filter(cls.users_group_name == group_name)
508 508 if cache:
509 509 q = q.options(FromCache(
510 510 "sql_cache_short",
511 511 "get_user_%s" % _hash_key(group_name)
512 512 )
513 513 )
514 514 return q.scalar()
515 515
516 516 @classmethod
517 517 def get(cls, users_group_id, cache=False):
518 518 users_group = cls.query()
519 519 if cache:
520 520 users_group = users_group.options(FromCache("sql_cache_short",
521 521 "get_users_group_%s" % users_group_id))
522 522 return users_group.get(users_group_id)
523 523
524 524
525 525 class UsersGroupMember(Base, BaseModel):
526 526 __tablename__ = 'users_groups_members'
527 527 __table_args__ = (
528 528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
529 529 'mysql_charset': 'utf8'},
530 530 )
531 531
532 532 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
533 533 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
534 534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
535 535
536 536 user = relationship('User', lazy='joined')
537 537 users_group = relationship('UsersGroup')
538 538
539 539 def __init__(self, gr_id='', u_id=''):
540 540 self.users_group_id = gr_id
541 541 self.user_id = u_id
542 542
543 543
544 544 class Repository(Base, BaseModel):
545 545 __tablename__ = 'repositories'
546 546 __table_args__ = (
547 547 UniqueConstraint('repo_name'),
548 548 {'extend_existing': True, 'mysql_engine': 'InnoDB',
549 549 'mysql_charset': 'utf8'},
550 550 )
551 551
552 552 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
553 553 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
554 554 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
555 555 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
556 556 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
557 557 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
558 558 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
559 559 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
560 560 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
561 561 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
562 562 landing_rev = Column("landing_revision", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
563 563
564 564 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
565 565 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
566 566
567 567 user = relationship('User')
568 568 fork = relationship('Repository', remote_side=repo_id)
569 569 group = relationship('RepoGroup')
570 570 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
571 571 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
572 572 stats = relationship('Statistics', cascade='all', uselist=False)
573 573
574 574 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
575 575
576 576 logs = relationship('UserLog')
577 577 comments = relationship('ChangesetComment')
578 578
579 579 def __unicode__(self):
580 580 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
581 581 self.repo_name)
582 582
583 583 @classmethod
584 584 def url_sep(cls):
585 585 return URL_SEP
586 586
587 587 @classmethod
588 588 def get_by_repo_name(cls, repo_name):
589 589 q = Session.query(cls).filter(cls.repo_name == repo_name)
590 590 q = q.options(joinedload(Repository.fork))\
591 591 .options(joinedload(Repository.user))\
592 592 .options(joinedload(Repository.group))
593 593 return q.scalar()
594 594
595 595 @classmethod
596 596 def get_by_full_path(cls, repo_full_path):
597 597 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
598 598 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
599 599
600 600 @classmethod
601 601 def get_repo_forks(cls, repo_id):
602 602 return cls.query().filter(Repository.fork_id == repo_id)
603 603
604 604 @classmethod
605 605 def base_path(cls):
606 606 """
607 607 Returns base path when all repos are stored
608 608
609 609 :param cls:
610 610 """
611 611 q = Session.query(RhodeCodeUi)\
612 612 .filter(RhodeCodeUi.ui_key == cls.url_sep())
613 613 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
614 614 return q.one().ui_value
615 615
616 616 @property
617 617 def forks(self):
618 618 """
619 619 Return forks of this repo
620 620 """
621 621 return Repository.get_repo_forks(self.repo_id)
622 622
623 623 @property
624 624 def parent(self):
625 625 """
626 626 Returns fork parent
627 627 """
628 628 return self.fork
629 629
630 630 @property
631 631 def just_name(self):
632 632 return self.repo_name.split(Repository.url_sep())[-1]
633 633
634 634 @property
635 635 def groups_with_parents(self):
636 636 groups = []
637 637 if self.group is None:
638 638 return groups
639 639
640 640 cur_gr = self.group
641 641 groups.insert(0, cur_gr)
642 642 while 1:
643 643 gr = getattr(cur_gr, 'parent_group', None)
644 644 cur_gr = cur_gr.parent_group
645 645 if gr is None:
646 646 break
647 647 groups.insert(0, gr)
648 648
649 649 return groups
650 650
651 651 @property
652 652 def groups_and_repo(self):
653 653 return self.groups_with_parents, self.just_name
654 654
655 655 @LazyProperty
656 656 def repo_path(self):
657 657 """
658 658 Returns base full path for that repository means where it actually
659 659 exists on a filesystem
660 660 """
661 661 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
662 662 Repository.url_sep())
663 663 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
664 664 return q.one().ui_value
665 665
666 666 @property
667 667 def repo_full_path(self):
668 668 p = [self.repo_path]
669 669 # we need to split the name by / since this is how we store the
670 670 # names in the database, but that eventually needs to be converted
671 671 # into a valid system path
672 672 p += self.repo_name.split(Repository.url_sep())
673 673 return os.path.join(*p)
674 674
675 675 def get_new_name(self, repo_name):
676 676 """
677 677 returns new full repository name based on assigned group and new new
678 678
679 679 :param group_name:
680 680 """
681 681 path_prefix = self.group.full_path_splitted if self.group else []
682 682 return Repository.url_sep().join(path_prefix + [repo_name])
683 683
684 684 @property
685 685 def _ui(self):
686 686 """
687 687 Creates an db based ui object for this repository
688 688 """
689 689 from mercurial import ui
690 690 from mercurial import config
691 691 baseui = ui.ui()
692 692
693 693 #clean the baseui object
694 694 baseui._ocfg = config.config()
695 695 baseui._ucfg = config.config()
696 696 baseui._tcfg = config.config()
697 697
698 698 ret = RhodeCodeUi.query()\
699 699 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
700 700
701 701 hg_ui = ret
702 702 for ui_ in hg_ui:
703 703 if ui_.ui_active:
704 704 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
705 705 ui_.ui_key, ui_.ui_value)
706 706 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
707 707
708 708 return baseui
709 709
710 710 @classmethod
711 711 def is_valid(cls, repo_name):
712 712 """
713 713 returns True if given repo name is a valid filesystem repository
714 714
715 715 :param cls:
716 716 :param repo_name:
717 717 """
718 718 from rhodecode.lib.utils import is_valid_repo
719 719
720 720 return is_valid_repo(repo_name, cls.base_path())
721 721
722 722 #==========================================================================
723 723 # SCM PROPERTIES
724 724 #==========================================================================
725 725
726 726 def get_changeset(self, rev=None):
727 727 return get_changeset_safe(self.scm_instance, rev)
728 728
729 729 @property
730 730 def tip(self):
731 731 return self.get_changeset('tip')
732 732
733 733 @property
734 734 def author(self):
735 735 return self.tip.author
736 736
737 737 @property
738 738 def last_change(self):
739 739 return self.scm_instance.last_change
740 740
741 741 def comments(self, revisions=None):
742 742 """
743 743 Returns comments for this repository grouped by revisions
744 744
745 745 :param revisions: filter query by revisions only
746 746 """
747 747 cmts = ChangesetComment.query()\
748 748 .filter(ChangesetComment.repo == self)
749 749 if revisions:
750 750 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
751 751 grouped = defaultdict(list)
752 752 for cmt in cmts.all():
753 753 grouped[cmt.revision].append(cmt)
754 754 return grouped
755 755
756 756 def statuses(self, revisions=None):
757 757 """
758 758 Returns statuses for this repository
759 759
760 760 :param revisions: list of revisions to get statuses for
761 761 :type revisions: list
762 762 """
763 763
764 764 statuses = ChangesetStatus.query()\
765 765 .filter(ChangesetStatus.repo == self)\
766 766 .filter(ChangesetStatus.version == 0)
767 767 if revisions:
768 768 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
769 769 grouped = {}
770 770 for stat in statuses.all():
771 771 pr_id = pr_repo = None
772 772 if stat.pull_request:
773 773 pr_id = stat.pull_request.pull_request_id
774 774 pr_repo = stat.pull_request.other_repo.repo_name
775 775 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
776 776 pr_id, pr_repo]
777 777 return grouped
778 778
779 779 #==========================================================================
780 780 # SCM CACHE INSTANCE
781 781 #==========================================================================
782 782
783 783 @property
784 784 def invalidate(self):
785 785 return CacheInvalidation.invalidate(self.repo_name)
786 786
787 787 def set_invalidate(self):
788 788 """
789 789 set a cache for invalidation for this instance
790 790 """
791 791 CacheInvalidation.set_invalidate(self.repo_name)
792 792
793 793 @LazyProperty
794 794 def scm_instance(self):
795 795 return self.__get_instance()
796 796
797 797 def scm_instance_cached(self, cache_map=None):
798 798 @cache_region('long_term')
799 799 def _c(repo_name):
800 800 return self.__get_instance()
801 801 rn = self.repo_name
802 802 log.debug('Getting cached instance of repo')
803 803
804 804 if cache_map:
805 805 # get using prefilled cache_map
806 806 invalidate_repo = cache_map[self.repo_name]
807 807 if invalidate_repo:
808 808 invalidate_repo = (None if invalidate_repo.cache_active
809 809 else invalidate_repo)
810 810 else:
811 811 # get from invalidate
812 812 invalidate_repo = self.invalidate
813 813
814 814 if invalidate_repo is not None:
815 815 region_invalidate(_c, None, rn)
816 816 # update our cache
817 817 CacheInvalidation.set_valid(invalidate_repo.cache_key)
818 818 return _c(rn)
819 819
820 820 def __get_instance(self):
821 821 repo_full_path = self.repo_full_path
822 822 try:
823 823 alias = get_scm(repo_full_path)[0]
824 824 log.debug('Creating instance of %s repository' % alias)
825 825 backend = get_backend(alias)
826 826 except VCSError:
827 827 log.error(traceback.format_exc())
828 828 log.error('Perhaps this repository is in db and not in '
829 829 'filesystem run rescan repositories with '
830 830 '"destroy old data " option from admin panel')
831 831 return
832 832
833 833 if alias == 'hg':
834 834
835 835 repo = backend(safe_str(repo_full_path), create=False,
836 836 baseui=self._ui)
837 837 # skip hidden web repository
838 838 if repo._get_hidden():
839 839 return
840 840 else:
841 841 repo = backend(repo_full_path, create=False)
842 842
843 843 return repo
844 844
845 845
846 846 class RepoGroup(Base, BaseModel):
847 847 __tablename__ = 'groups'
848 848 __table_args__ = (
849 849 UniqueConstraint('group_name', 'group_parent_id'),
850 850 CheckConstraint('group_id != group_parent_id'),
851 851 {'extend_existing': True, 'mysql_engine': 'InnoDB',
852 852 'mysql_charset': 'utf8'},
853 853 )
854 854 __mapper_args__ = {'order_by': 'group_name'}
855 855
856 856 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
857 857 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
858 858 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
859 859 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
860 860
861 861 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
862 862 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
863 863
864 864 parent_group = relationship('RepoGroup', remote_side=group_id)
865 865
866 866 def __init__(self, group_name='', parent_group=None):
867 867 self.group_name = group_name
868 868 self.parent_group = parent_group
869 869
870 870 def __unicode__(self):
871 871 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
872 872 self.group_name)
873 873
874 874 @classmethod
875 875 def groups_choices(cls):
876 876 from webhelpers.html import literal as _literal
877 877 repo_groups = [('', '')]
878 878 sep = ' &raquo; '
879 879 _name = lambda k: _literal(sep.join(k))
880 880
881 881 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
882 882 for x in cls.query().all()])
883 883
884 884 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
885 885 return repo_groups
886 886
887 887 @classmethod
888 888 def url_sep(cls):
889 889 return URL_SEP
890 890
891 891 @classmethod
892 892 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
893 893 if case_insensitive:
894 894 gr = cls.query()\
895 895 .filter(cls.group_name.ilike(group_name))
896 896 else:
897 897 gr = cls.query()\
898 898 .filter(cls.group_name == group_name)
899 899 if cache:
900 900 gr = gr.options(FromCache(
901 901 "sql_cache_short",
902 902 "get_group_%s" % _hash_key(group_name)
903 903 )
904 904 )
905 905 return gr.scalar()
906 906
907 907 @property
908 908 def parents(self):
909 909 parents_recursion_limit = 5
910 910 groups = []
911 911 if self.parent_group is None:
912 912 return groups
913 913 cur_gr = self.parent_group
914 914 groups.insert(0, cur_gr)
915 915 cnt = 0
916 916 while 1:
917 917 cnt += 1
918 918 gr = getattr(cur_gr, 'parent_group', None)
919 919 cur_gr = cur_gr.parent_group
920 920 if gr is None:
921 921 break
922 922 if cnt == parents_recursion_limit:
923 923 # this will prevent accidental infinit loops
924 924 log.error('group nested more than %s' %
925 925 parents_recursion_limit)
926 926 break
927 927
928 928 groups.insert(0, gr)
929 929 return groups
930 930
931 931 @property
932 932 def children(self):
933 933 return RepoGroup.query().filter(RepoGroup.parent_group == self)
934 934
935 935 @property
936 936 def name(self):
937 937 return self.group_name.split(RepoGroup.url_sep())[-1]
938 938
939 939 @property
940 940 def full_path(self):
941 941 return self.group_name
942 942
943 943 @property
944 944 def full_path_splitted(self):
945 945 return self.group_name.split(RepoGroup.url_sep())
946 946
947 947 @property
948 948 def repositories(self):
949 949 return Repository.query()\
950 950 .filter(Repository.group == self)\
951 951 .order_by(Repository.repo_name)
952 952
953 953 @property
954 954 def repositories_recursive_count(self):
955 955 cnt = self.repositories.count()
956 956
957 957 def children_count(group):
958 958 cnt = 0
959 959 for child in group.children:
960 960 cnt += child.repositories.count()
961 961 cnt += children_count(child)
962 962 return cnt
963 963
964 964 return cnt + children_count(self)
965 965
966 966 def get_new_name(self, group_name):
967 967 """
968 968 returns new full group name based on parent and new name
969 969
970 970 :param group_name:
971 971 """
972 972 path_prefix = (self.parent_group.full_path_splitted if
973 973 self.parent_group else [])
974 974 return RepoGroup.url_sep().join(path_prefix + [group_name])
975 975
976 976
977 977 class Permission(Base, BaseModel):
978 978 __tablename__ = 'permissions'
979 979 __table_args__ = (
980 980 Index('p_perm_name_idx', 'permission_name'),
981 981 {'extend_existing': True, 'mysql_engine': 'InnoDB',
982 982 'mysql_charset': 'utf8'},
983 983 )
984 984 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
985 985 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
986 986 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
987 987
988 988 def __unicode__(self):
989 989 return u"<%s('%s:%s')>" % (
990 990 self.__class__.__name__, self.permission_id, self.permission_name
991 991 )
992 992
993 993 @classmethod
994 994 def get_by_key(cls, key):
995 995 return cls.query().filter(cls.permission_name == key).scalar()
996 996
997 997 @classmethod
998 998 def get_default_perms(cls, default_user_id):
999 999 q = Session.query(UserRepoToPerm, Repository, cls)\
1000 1000 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1001 1001 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1002 1002 .filter(UserRepoToPerm.user_id == default_user_id)
1003 1003
1004 1004 return q.all()
1005 1005
1006 1006 @classmethod
1007 1007 def get_default_group_perms(cls, default_user_id):
1008 1008 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
1009 1009 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1010 1010 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1011 1011 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1012 1012
1013 1013 return q.all()
1014 1014
1015 1015
1016 1016 class UserRepoToPerm(Base, BaseModel):
1017 1017 __tablename__ = 'repo_to_perm'
1018 1018 __table_args__ = (
1019 1019 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1020 1020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1021 1021 'mysql_charset': 'utf8'}
1022 1022 )
1023 1023 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1024 1024 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1025 1025 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1026 1026 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1027 1027
1028 1028 user = relationship('User')
1029 1029 repository = relationship('Repository')
1030 1030 permission = relationship('Permission')
1031 1031
1032 1032 @classmethod
1033 1033 def create(cls, user, repository, permission):
1034 1034 n = cls()
1035 1035 n.user = user
1036 1036 n.repository = repository
1037 1037 n.permission = permission
1038 1038 Session.add(n)
1039 1039 return n
1040 1040
1041 1041 def __unicode__(self):
1042 1042 return u'<user:%s => %s >' % (self.user, self.repository)
1043 1043
1044 1044
1045 1045 class UserToPerm(Base, BaseModel):
1046 1046 __tablename__ = 'user_to_perm'
1047 1047 __table_args__ = (
1048 1048 UniqueConstraint('user_id', 'permission_id'),
1049 1049 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1050 1050 'mysql_charset': 'utf8'}
1051 1051 )
1052 1052 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1053 1053 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1054 1054 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1055 1055
1056 1056 user = relationship('User')
1057 1057 permission = relationship('Permission', lazy='joined')
1058 1058
1059 1059
1060 1060 class UsersGroupRepoToPerm(Base, BaseModel):
1061 1061 __tablename__ = 'users_group_repo_to_perm'
1062 1062 __table_args__ = (
1063 1063 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1064 1064 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1065 1065 'mysql_charset': 'utf8'}
1066 1066 )
1067 1067 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1068 1068 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1069 1069 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1070 1070 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1071 1071
1072 1072 users_group = relationship('UsersGroup')
1073 1073 permission = relationship('Permission')
1074 1074 repository = relationship('Repository')
1075 1075
1076 1076 @classmethod
1077 1077 def create(cls, users_group, repository, permission):
1078 1078 n = cls()
1079 1079 n.users_group = users_group
1080 1080 n.repository = repository
1081 1081 n.permission = permission
1082 1082 Session.add(n)
1083 1083 return n
1084 1084
1085 1085 def __unicode__(self):
1086 1086 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1087 1087
1088 1088
1089 1089 class UsersGroupToPerm(Base, BaseModel):
1090 1090 __tablename__ = 'users_group_to_perm'
1091 1091 __table_args__ = (
1092 1092 UniqueConstraint('users_group_id', 'permission_id',),
1093 1093 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1094 1094 'mysql_charset': 'utf8'}
1095 1095 )
1096 1096 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1097 1097 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1098 1098 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1099 1099
1100 1100 users_group = relationship('UsersGroup')
1101 1101 permission = relationship('Permission')
1102 1102
1103 1103
1104 1104 class UserRepoGroupToPerm(Base, BaseModel):
1105 1105 __tablename__ = 'user_repo_group_to_perm'
1106 1106 __table_args__ = (
1107 1107 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1108 1108 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1109 1109 'mysql_charset': 'utf8'}
1110 1110 )
1111 1111
1112 1112 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 1113 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1114 1114 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1115 1115 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1116 1116
1117 1117 user = relationship('User')
1118 1118 group = relationship('RepoGroup')
1119 1119 permission = relationship('Permission')
1120 1120
1121 1121
1122 1122 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1123 1123 __tablename__ = 'users_group_repo_group_to_perm'
1124 1124 __table_args__ = (
1125 1125 UniqueConstraint('users_group_id', 'group_id'),
1126 1126 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1127 1127 'mysql_charset': 'utf8'}
1128 1128 )
1129 1129
1130 1130 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)
1131 1131 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1132 1132 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1133 1133 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1134 1134
1135 1135 users_group = relationship('UsersGroup')
1136 1136 permission = relationship('Permission')
1137 1137 group = relationship('RepoGroup')
1138 1138
1139 1139
1140 1140 class Statistics(Base, BaseModel):
1141 1141 __tablename__ = 'statistics'
1142 1142 __table_args__ = (
1143 1143 UniqueConstraint('repository_id'),
1144 1144 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1145 1145 'mysql_charset': 'utf8'}
1146 1146 )
1147 1147 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 1148 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1149 1149 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1150 1150 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1151 1151 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1152 1152 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1153 1153
1154 1154 repository = relationship('Repository', single_parent=True)
1155 1155
1156 1156
1157 1157 class UserFollowing(Base, BaseModel):
1158 1158 __tablename__ = 'user_followings'
1159 1159 __table_args__ = (
1160 1160 UniqueConstraint('user_id', 'follows_repository_id'),
1161 1161 UniqueConstraint('user_id', 'follows_user_id'),
1162 1162 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1163 1163 'mysql_charset': 'utf8'}
1164 1164 )
1165 1165
1166 1166 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1167 1167 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1168 1168 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1169 1169 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1170 1170 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1171 1171
1172 1172 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1173 1173
1174 1174 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1175 1175 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1176 1176
1177 1177 @classmethod
1178 1178 def get_repo_followers(cls, repo_id):
1179 1179 return cls.query().filter(cls.follows_repo_id == repo_id)
1180 1180
1181 1181
1182 1182 class CacheInvalidation(Base, BaseModel):
1183 1183 __tablename__ = 'cache_invalidation'
1184 1184 __table_args__ = (
1185 1185 UniqueConstraint('cache_key'),
1186 1186 Index('key_idx', 'cache_key'),
1187 1187 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1188 1188 'mysql_charset': 'utf8'},
1189 1189 )
1190 1190 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1191 1191 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1192 1192 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1193 1193 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1194 1194
1195 1195 def __init__(self, cache_key, cache_args=''):
1196 1196 self.cache_key = cache_key
1197 1197 self.cache_args = cache_args
1198 1198 self.cache_active = False
1199 1199
1200 1200 def __unicode__(self):
1201 1201 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1202 1202 self.cache_id, self.cache_key)
1203 1203
1204 1204 @classmethod
1205 1205 def clear_cache(cls):
1206 1206 cls.query().delete()
1207 1207
1208 1208 @classmethod
1209 1209 def _get_key(cls, key):
1210 1210 """
1211 1211 Wrapper for generating a key, together with a prefix
1212 1212
1213 1213 :param key:
1214 1214 """
1215 1215 import rhodecode
1216 1216 prefix = ''
1217 1217 iid = rhodecode.CONFIG.get('instance_id')
1218 1218 if iid:
1219 1219 prefix = iid
1220 1220 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1221 1221
1222 1222 @classmethod
1223 1223 def get_by_key(cls, key):
1224 1224 return cls.query().filter(cls.cache_key == key).scalar()
1225 1225
1226 1226 @classmethod
1227 1227 def _get_or_create_key(cls, key, prefix, org_key):
1228 1228 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1229 1229 if not inv_obj:
1230 1230 try:
1231 1231 inv_obj = CacheInvalidation(key, org_key)
1232 1232 Session.add(inv_obj)
1233 1233 Session.commit()
1234 1234 except Exception:
1235 1235 log.error(traceback.format_exc())
1236 1236 Session.rollback()
1237 1237 return inv_obj
1238 1238
1239 1239 @classmethod
1240 1240 def invalidate(cls, key):
1241 1241 """
1242 1242 Returns Invalidation object if this given key should be invalidated
1243 1243 None otherwise. `cache_active = False` means that this cache
1244 1244 state is not valid and needs to be invalidated
1245 1245
1246 1246 :param key:
1247 1247 """
1248 1248
1249 1249 key, _prefix, _org_key = cls._get_key(key)
1250 1250 inv = cls._get_or_create_key(key, _prefix, _org_key)
1251 1251
1252 1252 if inv and inv.cache_active is False:
1253 1253 return inv
1254 1254
1255 1255 @classmethod
1256 1256 def set_invalidate(cls, key):
1257 1257 """
1258 1258 Mark this Cache key for invalidation
1259 1259
1260 1260 :param key:
1261 1261 """
1262 1262
1263 1263 key, _prefix, _org_key = cls._get_key(key)
1264 1264 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1265 1265 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1266 1266 _org_key))
1267 1267 try:
1268 1268 for inv_obj in inv_objs:
1269 1269 if inv_obj:
1270 1270 inv_obj.cache_active = False
1271 1271
1272 1272 Session.add(inv_obj)
1273 1273 Session.commit()
1274 1274 except Exception:
1275 1275 log.error(traceback.format_exc())
1276 1276 Session.rollback()
1277 1277
1278 1278 @classmethod
1279 1279 def set_valid(cls, key):
1280 1280 """
1281 1281 Mark this cache key as active and currently cached
1282 1282
1283 1283 :param key:
1284 1284 """
1285 1285 inv_obj = cls.get_by_key(key)
1286 1286 inv_obj.cache_active = True
1287 1287 Session.add(inv_obj)
1288 1288 Session.commit()
1289 1289
1290 1290 @classmethod
1291 1291 def get_cache_map(cls):
1292 1292
1293 1293 class cachemapdict(dict):
1294 1294
1295 1295 def __init__(self, *args, **kwargs):
1296 1296 fixkey = kwargs.get('fixkey')
1297 1297 if fixkey:
1298 1298 del kwargs['fixkey']
1299 1299 self.fixkey = fixkey
1300 1300 super(cachemapdict, self).__init__(*args, **kwargs)
1301 1301
1302 1302 def __getattr__(self, name):
1303 1303 key = name
1304 1304 if self.fixkey:
1305 1305 key, _prefix, _org_key = cls._get_key(key)
1306 1306 if key in self.__dict__:
1307 1307 return self.__dict__[key]
1308 1308 else:
1309 1309 return self[key]
1310 1310
1311 1311 def __getitem__(self, key):
1312 1312 if self.fixkey:
1313 1313 key, _prefix, _org_key = cls._get_key(key)
1314 1314 try:
1315 1315 return super(cachemapdict, self).__getitem__(key)
1316 1316 except KeyError:
1317 1317 return
1318 1318
1319 1319 cache_map = cachemapdict(fixkey=True)
1320 1320 for obj in cls.query().all():
1321 1321 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1322 1322 return cache_map
1323 1323
1324 1324
1325 1325 class ChangesetComment(Base, BaseModel):
1326 1326 __tablename__ = 'changeset_comments'
1327 1327 __table_args__ = (
1328 1328 Index('cc_revision_idx', 'revision'),
1329 1329 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1330 1330 'mysql_charset': 'utf8'},
1331 1331 )
1332 1332 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1333 1333 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1334 1334 revision = Column('revision', String(40), nullable=True)
1335 1335 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1336 1336 line_no = Column('line_no', Unicode(10), nullable=True)
1337 1337 f_path = Column('f_path', Unicode(1000), nullable=True)
1338 1338 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1339 1339 text = Column('text', Unicode(25000), nullable=False)
1340 1340 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1341 1341
1342 1342 author = relationship('User', lazy='joined')
1343 1343 repo = relationship('Repository')
1344 1344 status_change = relationship('ChangesetStatus', uselist=False)
1345 1345 pull_request = relationship('PullRequest', lazy='joined')
1346 1346
1347 1347 @classmethod
1348 1348 def get_users(cls, revision=None, pull_request_id=None):
1349 1349 """
1350 1350 Returns user associated with this ChangesetComment. ie those
1351 1351 who actually commented
1352 1352
1353 1353 :param cls:
1354 1354 :param revision:
1355 1355 """
1356 1356 q = Session.query(User)\
1357 1357 .join(ChangesetComment.author)
1358 1358 if revision:
1359 1359 q = q.filter(cls.revision == revision)
1360 1360 elif pull_request_id:
1361 1361 q = q.filter(cls.pull_request_id == pull_request_id)
1362 1362 return q.all()
1363 1363
1364 1364
1365 1365 class ChangesetStatus(Base, BaseModel):
1366 1366 __tablename__ = 'changeset_statuses'
1367 1367 __table_args__ = (
1368 1368 Index('cs_revision_idx', 'revision'),
1369 1369 Index('cs_version_idx', 'version'),
1370 1370 UniqueConstraint('repo_id', 'revision', 'version'),
1371 1371 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1372 1372 'mysql_charset': 'utf8'}
1373 1373 )
1374 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1375 STATUS_APPROVED = 'approved'
1376 STATUS_REJECTED = 'rejected'
1377 STATUS_UNDER_REVIEW = 'under_review'
1374 1378
1375 1379 STATUSES = [
1376 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1377 ('approved', _("Approved")),
1378 ('rejected', _("Rejected")),
1379 ('under_review', _("Under Review")),
1380 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1381 (STATUS_APPROVED, _("Approved")),
1382 (STATUS_REJECTED, _("Rejected")),
1383 (STATUS_UNDER_REVIEW, _("Under Review")),
1380 1384 ]
1381 DEFAULT = STATUSES[0][0]
1382 1385
1383 1386 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1384 1387 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1385 1388 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1386 1389 revision = Column('revision', String(40), nullable=False)
1387 1390 status = Column('status', String(128), nullable=False, default=DEFAULT)
1388 1391 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1389 1392 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1390 1393 version = Column('version', Integer(), nullable=False, default=0)
1391 1394 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1392 1395
1393 1396 author = relationship('User', lazy='joined')
1394 1397 repo = relationship('Repository')
1395 1398 comment = relationship('ChangesetComment', lazy='joined')
1396 1399 pull_request = relationship('PullRequest', lazy='joined')
1397 1400
1401 def __unicode__(self):
1402 return u"<%s('%s:%s')>" % (
1403 self.__class__.__name__,
1404 self.status, self.author
1405 )
1406
1398 1407 @classmethod
1399 1408 def get_status_lbl(cls, value):
1400 1409 return dict(cls.STATUSES).get(value)
1401 1410
1402 1411 @property
1403 1412 def status_lbl(self):
1404 1413 return ChangesetStatus.get_status_lbl(self.status)
1405 1414
1406 1415
1407 1416 class PullRequest(Base, BaseModel):
1408 1417 __tablename__ = 'pull_requests'
1409 1418 __table_args__ = (
1410 1419 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1411 1420 'mysql_charset': 'utf8'},
1412 1421 )
1413 1422
1414 1423 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1415 1424 title = Column('title', Unicode(256), nullable=True)
1416 1425 description = Column('description', Unicode(10240), nullable=True)
1417 1426 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1418 1427 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1419 1428 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1420 1429 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1421 1430 org_ref = Column('org_ref', Unicode(256), nullable=False)
1422 1431 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1423 1432 other_ref = Column('other_ref', Unicode(256), nullable=False)
1424 1433
1425 1434 @hybrid_property
1426 1435 def revisions(self):
1427 1436 return self._revisions.split(':')
1428 1437
1429 1438 @revisions.setter
1430 1439 def revisions(self, val):
1431 1440 self._revisions = ':'.join(val)
1432 1441
1433 1442 author = relationship('User', lazy='joined')
1434 1443 reviewers = relationship('PullRequestReviewers')
1435 1444 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1436 1445 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1437 1446
1438 1447 def __json__(self):
1439 1448 return dict(
1440 1449 revisions=self.revisions
1441 1450 )
1442 1451
1443 1452
1444 1453 class PullRequestReviewers(Base, BaseModel):
1445 1454 __tablename__ = 'pull_request_reviewers'
1446 1455 __table_args__ = (
1447 1456 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1448 1457 'mysql_charset': 'utf8'},
1449 1458 )
1450 1459
1451 1460 def __init__(self, user=None, pull_request=None):
1452 1461 self.user = user
1453 1462 self.pull_request = pull_request
1454 1463
1455 1464 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1456 1465 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1457 1466 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1458 1467
1459 1468 user = relationship('User')
1460 1469 pull_request = relationship('PullRequest')
1461 1470
1462 1471
1463 1472 class Notification(Base, BaseModel):
1464 1473 __tablename__ = 'notifications'
1465 1474 __table_args__ = (
1466 1475 Index('notification_type_idx', 'type'),
1467 1476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1468 1477 'mysql_charset': 'utf8'},
1469 1478 )
1470 1479
1471 1480 TYPE_CHANGESET_COMMENT = u'cs_comment'
1472 1481 TYPE_MESSAGE = u'message'
1473 1482 TYPE_MENTION = u'mention'
1474 1483 TYPE_REGISTRATION = u'registration'
1475 1484 TYPE_PULL_REQUEST = u'pull_request'
1476 1485 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1477 1486
1478 1487 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1479 1488 subject = Column('subject', Unicode(512), nullable=True)
1480 1489 body = Column('body', Unicode(50000), nullable=True)
1481 1490 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1482 1491 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1483 1492 type_ = Column('type', Unicode(256))
1484 1493
1485 1494 created_by_user = relationship('User')
1486 1495 notifications_to_users = relationship('UserNotification', lazy='joined',
1487 1496 cascade="all, delete, delete-orphan")
1488 1497
1489 1498 @property
1490 1499 def recipients(self):
1491 1500 return [x.user for x in UserNotification.query()\
1492 1501 .filter(UserNotification.notification == self)\
1493 1502 .order_by(UserNotification.user).all()]
1494 1503
1495 1504 @classmethod
1496 1505 def create(cls, created_by, subject, body, recipients, type_=None):
1497 1506 if type_ is None:
1498 1507 type_ = Notification.TYPE_MESSAGE
1499 1508
1500 1509 notification = cls()
1501 1510 notification.created_by_user = created_by
1502 1511 notification.subject = subject
1503 1512 notification.body = body
1504 1513 notification.type_ = type_
1505 1514 notification.created_on = datetime.datetime.now()
1506 1515
1507 1516 for u in recipients:
1508 1517 assoc = UserNotification()
1509 1518 assoc.notification = notification
1510 1519 u.notifications.append(assoc)
1511 1520 Session.add(notification)
1512 1521 return notification
1513 1522
1514 1523 @property
1515 1524 def description(self):
1516 1525 from rhodecode.model.notification import NotificationModel
1517 1526 return NotificationModel().make_description(self)
1518 1527
1519 1528
1520 1529 class UserNotification(Base, BaseModel):
1521 1530 __tablename__ = 'user_to_notification'
1522 1531 __table_args__ = (
1523 1532 UniqueConstraint('user_id', 'notification_id'),
1524 1533 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1525 1534 'mysql_charset': 'utf8'}
1526 1535 )
1527 1536 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1528 1537 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1529 1538 read = Column('read', Boolean, default=False)
1530 1539 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1531 1540
1532 1541 user = relationship('User', lazy="joined")
1533 1542 notification = relationship('Notification', lazy="joined",
1534 1543 order_by=lambda: Notification.created_on.desc(),)
1535 1544
1536 1545 def mark_as_read(self):
1537 1546 self.read = True
1538 1547 Session.add(self)
1539 1548
1540 1549
1541 1550 class DbMigrateVersion(Base, BaseModel):
1542 1551 __tablename__ = 'db_migrate_version'
1543 1552 __table_args__ = (
1544 1553 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1545 1554 'mysql_charset': 'utf8'},
1546 1555 )
1547 1556 repository_id = Column('repository_id', String(250), primary_key=True)
1548 1557 repository_path = Column('repository_path', Text)
1549 1558 version = Column('version', Integer)
@@ -1,83 +1,107 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('Pull request #%s') % c.pull_request.pull_request_id}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${h.link_to(u'Home',h.url('/'))}
9 9 &raquo;
10 10 ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
11 11 &raquo;
12 12 ${_('Pull request #%s') % c.pull_request.pull_request_id}
13 13 </%def>
14 14
15 15 <%def name="main()">
16 16
17 17 <div class="box">
18 18 <!-- box / title -->
19 19 <div class="title">
20 20 ${self.breadcrumbs()}
21 21 </div>
22 22
23 23 <h3>${_('Title')}: ${c.pull_request.title}</h3>
24 <div class="changeset-status-container" style="float:left;padding:0px 20px 20px 20px">
24 <div class="changeset-status-container" style="float:left;padding:0px 20px 0px 20px">
25 25 %if c.current_changeset_status:
26 26 <div title="${_('Changeset status')}" class="changeset-status-lbl">[${h.changeset_status_lbl(c.current_changeset_status)}]</div>
27 27 <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.current_changeset_status)}" /></div>
28 28 %endif
29 29 </div>
30 <div style="padding:4px">
30 <div style="padding:4px 4px 10px 4px">
31 31 <div>${h.fmt_date(c.pull_request.created_on)}</div>
32 32 </div>
33 33
34 ##DIFF
35
36 <div class="table">
37 <div id="body" class="diffblock">
38 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
39 </div>
40 <div id="changeset_compare_view_content">
41 ##CS
42 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
43 <%include file="/compare/compare_cs.html" />
44
45 ## FILES
46 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
47 <div class="cs_files">
48 %for fid, change, f, stat in c.files:
49 <div class="cs_${change}">
50 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
51 <div class="changes">${h.fancy_file_stats(stat)}</div>
34 ## REVIEWERS
35 <div>
36 <div class="table" style="float:right;width:46%;clear:none">
37 <div id="body" class="diffblock">
38 <div style="white-space:pre-wrap;padding:5px">${_('Pull request reviewers')}</div>
39 </div>
40 <div style="border: 1px solid #CCC">
41 <div class="group_members_wrap">
42 <ul class="group_members">
43 %for user,status in c.pull_request_reviewers:
44 <li>
45 <div class="group_member">
46 <div style="float:left;padding:3px">
47 <img src="${h.url(str('/images/icons/flag_status_%s.png' % (status[0][1].status if status else 'not_reviewed')))}"/>
48 </div>
49 <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(user.email,20)}"/> </div>
50 <div style="float:left">${user.username}</div>
52 51 </div>
52 </li>
53 53 %endfor
54 </div>
55 </div>
54 </ul>
55 </div>
56 </div>
57 </div>
58 ##DIFF
59 <div class="table" style="float:left;width:46%;clear:none">
60 <div id="body" class="diffblock">
61 <div style="white-space:pre-wrap;padding:5px">${h.literal(c.pull_request.description)}</div>
62 </div>
63 <div id="changeset_compare_view_content">
64 ##CS
65 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Incoming changesets')}</div>
66 <%include file="/compare/compare_cs.html" />
67
68 ## FILES
69 <div style="font-size:1.1em;font-weight: bold;clear:both;padding-top:10px">${_('Files affected')}</div>
70 <div class="cs_files">
71 %for fid, change, f, stat in c.files:
72 <div class="cs_${change}">
73 <div class="node">${h.link_to(h.safe_unicode(f),h.url.current(anchor=fid))}</div>
74 <div class="changes">${h.fancy_file_stats(stat)}</div>
75 </div>
76 %endfor
77 </div>
78 </div>
79 </div>
56 80 </div>
57 81 <script>
58 82 var _USERS_AC_DATA = ${c.users_array|n};
59 83 var _GROUPS_AC_DATA = ${c.users_groups_array|n};
60 84 </script>
61 85
62 86 ## diff block
63 87 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
64 88 %for fid, change, f, stat in c.files:
65 89 ${diff_block.diff_block_simple([c.changes[fid]])}
66 90 %endfor
67 91
68 92 ## template for inline comment form
69 93 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
70 94 ##${comment.comment_inline_form(c.changeset)}
71 95
72 96 ## render comments main comments form and it status
73 97 ${comment.comments(h.url('pullrequest_comment', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id),
74 98 c.current_changeset_status)}
75 99
76 100 </div>
77 101
78 102 <script type="text/javascript">
79 103
80 104
81 105 </script>
82 106
83 107 </%def>
General Comments 0
You need to be logged in to leave comments. Login now