##// END OF EJS Templates
Add changeset status change into emails
marcink -
r2296:e5c0f201 codereview
parent child Browse files
Show More
@@ -1,416 +1,420
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.changeset
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 changeset controller for pylons showoing changes beetween
7 7 revisions
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 28 from collections import defaultdict
29 29 from webob.exc import HTTPForbidden
30 30
31 31 from pylons import tmpl_context as c, url, request, response
32 32 from pylons.i18n.translation import _
33 33 from pylons.controllers.util import redirect
34 34 from pylons.decorators import jsonify
35 35
36 36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 37 ChangesetDoesNotExistError
38 38 from rhodecode.lib.vcs.nodes import FileNode
39 39
40 40 import rhodecode.lib.helpers as h
41 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 42 from rhodecode.lib.base import BaseRepoController, render
43 43 from rhodecode.lib.utils import EmptyChangeset
44 44 from rhodecode.lib.compat import OrderedDict
45 45 from rhodecode.lib import diffs
46 46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
47 47 from rhodecode.model.comment import ChangesetCommentsModel
48 48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.lib.diffs import wrapped_diff
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 def _update_with_GET(params, GET):
56 56 for k in ['diff1', 'diff2', 'diff']:
57 57 params[k] += GET.getall(k)
58 58
59 59
60 60 def anchor_url(revision, path, GET):
61 61 fid = h.FID(revision, path)
62 62 return h.url.current(anchor=fid, **dict(GET))
63 63
64 64
65 65 def get_ignore_ws(fid, GET):
66 66 ig_ws_global = GET.get('ignorews')
67 67 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
68 68 if ig_ws:
69 69 try:
70 70 return int(ig_ws[0].split(':')[-1])
71 71 except:
72 72 pass
73 73 return ig_ws_global
74 74
75 75
76 76 def _ignorews_url(GET, fileid=None):
77 77 fileid = str(fileid) if fileid else None
78 78 params = defaultdict(list)
79 79 _update_with_GET(params, GET)
80 80 lbl = _('show white space')
81 81 ig_ws = get_ignore_ws(fileid, GET)
82 82 ln_ctx = get_line_ctx(fileid, GET)
83 83 # global option
84 84 if fileid is None:
85 85 if ig_ws is None:
86 86 params['ignorews'] += [1]
87 87 lbl = _('ignore white space')
88 88 ctx_key = 'context'
89 89 ctx_val = ln_ctx
90 90 # per file options
91 91 else:
92 92 if ig_ws is None:
93 93 params[fileid] += ['WS:1']
94 94 lbl = _('ignore white space')
95 95
96 96 ctx_key = fileid
97 97 ctx_val = 'C:%s' % ln_ctx
98 98 # if we have passed in ln_ctx pass it along to our params
99 99 if ln_ctx:
100 100 params[ctx_key] += [ctx_val]
101 101
102 102 params['anchor'] = fileid
103 103 img = h.image(h.url('/images/icons/text_strikethrough.png'), lbl, class_='icon')
104 104 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
105 105
106 106
107 107 def get_line_ctx(fid, GET):
108 108 ln_ctx_global = GET.get('context')
109 109 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
110 110
111 111 if ln_ctx:
112 112 retval = ln_ctx[0].split(':')[-1]
113 113 else:
114 114 retval = ln_ctx_global
115 115
116 116 try:
117 117 return int(retval)
118 118 except:
119 119 return
120 120
121 121
122 122 def _context_url(GET, fileid=None):
123 123 """
124 124 Generates url for context lines
125 125
126 126 :param fileid:
127 127 """
128 128
129 129 fileid = str(fileid) if fileid else None
130 130 ig_ws = get_ignore_ws(fileid, GET)
131 131 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
132 132
133 133 params = defaultdict(list)
134 134 _update_with_GET(params, GET)
135 135
136 136 # global option
137 137 if fileid is None:
138 138 if ln_ctx > 0:
139 139 params['context'] += [ln_ctx]
140 140
141 141 if ig_ws:
142 142 ig_ws_key = 'ignorews'
143 143 ig_ws_val = 1
144 144
145 145 # per file option
146 146 else:
147 147 params[fileid] += ['C:%s' % ln_ctx]
148 148 ig_ws_key = fileid
149 149 ig_ws_val = 'WS:%s' % 1
150 150
151 151 if ig_ws:
152 152 params[ig_ws_key] += [ig_ws_val]
153 153
154 154 lbl = _('%s line context') % ln_ctx
155 155
156 156 params['anchor'] = fileid
157 157 img = h.image(h.url('/images/icons/table_add.png'), lbl, class_='icon')
158 158 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
159 159
160 160
161 161 class ChangesetController(BaseRepoController):
162 162
163 163 @LoginRequired()
164 164 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
165 165 'repository.admin')
166 166 def __before__(self):
167 167 super(ChangesetController, self).__before__()
168 168 c.affected_files_cut_off = 60
169 169
170 170 def index(self, revision):
171 171
172 172 c.anchor_url = anchor_url
173 173 c.ignorews_url = _ignorews_url
174 174 c.context_url = _context_url
175 175 limit_off = request.GET.get('fulldiff')
176 176 #get ranges of revisions if preset
177 177 rev_range = revision.split('...')[:2]
178 178 enable_comments = True
179 179 try:
180 180 if len(rev_range) == 2:
181 181 enable_comments = False
182 182 rev_start = rev_range[0]
183 183 rev_end = rev_range[1]
184 184 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
185 185 end=rev_end)
186 186 else:
187 187 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
188 188
189 189 c.cs_ranges = list(rev_ranges)
190 190 if not c.cs_ranges:
191 191 raise RepositoryError('Changeset range returned empty result')
192 192
193 193 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
194 194 log.error(traceback.format_exc())
195 195 h.flash(str(e), category='warning')
196 196 return redirect(url('home'))
197 197
198 198 c.changes = OrderedDict()
199 199
200 200 c.lines_added = 0 # count of lines added
201 201 c.lines_deleted = 0 # count of lines removes
202 202
203 203 cumulative_diff = 0
204 204 c.cut_off = False # defines if cut off limit is reached
205 205 c.changeset_statuses = ChangesetStatus.STATUSES
206 206 c.comments = []
207 207 c.statuses = []
208 208 c.inline_comments = []
209 209 c.inline_cnt = 0
210 210 # Iterate over ranges (default changeset view is always one changeset)
211 211 for changeset in c.cs_ranges:
212 212
213 213 c.statuses.extend([ChangesetStatusModel()\
214 214 .get_status(c.rhodecode_db_repo.repo_id,
215 215 changeset.raw_id)])
216 216
217 217 c.comments.extend(ChangesetCommentsModel()\
218 218 .get_comments(c.rhodecode_db_repo.repo_id,
219 219 changeset.raw_id))
220 220 inlines = ChangesetCommentsModel()\
221 221 .get_inline_comments(c.rhodecode_db_repo.repo_id,
222 222 changeset.raw_id)
223 223 c.inline_comments.extend(inlines)
224 224 c.changes[changeset.raw_id] = []
225 225 try:
226 226 changeset_parent = changeset.parents[0]
227 227 except IndexError:
228 228 changeset_parent = None
229 229
230 230 #==================================================================
231 231 # ADDED FILES
232 232 #==================================================================
233 233 for node in changeset.added:
234 234 fid = h.FID(revision, node.path)
235 235 line_context_lcl = get_line_ctx(fid, request.GET)
236 236 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
237 237 lim = self.cut_off_limit
238 238 if cumulative_diff > self.cut_off_limit:
239 239 lim = -1 if limit_off is None else None
240 240 size, cs1, cs2, diff, st = wrapped_diff(
241 241 filenode_old=None,
242 242 filenode_new=node,
243 243 cut_off_limit=lim,
244 244 ignore_whitespace=ign_whitespace_lcl,
245 245 line_context=line_context_lcl,
246 246 enable_comments=enable_comments
247 247 )
248 248 cumulative_diff += size
249 249 c.lines_added += st[0]
250 250 c.lines_deleted += st[1]
251 251 c.changes[changeset.raw_id].append(
252 252 ('added', node, diff, cs1, cs2, st)
253 253 )
254 254
255 255 #==================================================================
256 256 # CHANGED FILES
257 257 #==================================================================
258 258 for node in changeset.changed:
259 259 try:
260 260 filenode_old = changeset_parent.get_node(node.path)
261 261 except ChangesetError:
262 262 log.warning('Unable to fetch parent node for diff')
263 263 filenode_old = FileNode(node.path, '', EmptyChangeset())
264 264
265 265 fid = h.FID(revision, node.path)
266 266 line_context_lcl = get_line_ctx(fid, request.GET)
267 267 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
268 268 lim = self.cut_off_limit
269 269 if cumulative_diff > self.cut_off_limit:
270 270 lim = -1 if limit_off is None else None
271 271 size, cs1, cs2, diff, st = wrapped_diff(
272 272 filenode_old=filenode_old,
273 273 filenode_new=node,
274 274 cut_off_limit=lim,
275 275 ignore_whitespace=ign_whitespace_lcl,
276 276 line_context=line_context_lcl,
277 277 enable_comments=enable_comments
278 278 )
279 279 cumulative_diff += size
280 280 c.lines_added += st[0]
281 281 c.lines_deleted += st[1]
282 282 c.changes[changeset.raw_id].append(
283 283 ('changed', node, diff, cs1, cs2, st)
284 284 )
285 285 #==================================================================
286 286 # REMOVED FILES
287 287 #==================================================================
288 288 for node in changeset.removed:
289 289 c.changes[changeset.raw_id].append(
290 290 ('removed', node, None, None, None, (0, 0))
291 291 )
292 292
293 293 # count inline comments
294 294 for path, lines in c.inline_comments:
295 295 for comments in lines.values():
296 296 c.inline_cnt += len(comments)
297 297
298 298 if len(c.cs_ranges) == 1:
299 299 c.changeset = c.cs_ranges[0]
300 300 c.changes = c.changes[c.changeset.raw_id]
301 301
302 302 return render('changeset/changeset.html')
303 303 else:
304 304 return render('changeset/changeset_range.html')
305 305
306 306 def raw_changeset(self, revision):
307 307
308 308 method = request.GET.get('diff', 'show')
309 309 ignore_whitespace = request.GET.get('ignorews') == '1'
310 310 line_context = request.GET.get('context', 3)
311 311 try:
312 312 c.scm_type = c.rhodecode_repo.alias
313 313 c.changeset = c.rhodecode_repo.get_changeset(revision)
314 314 except RepositoryError:
315 315 log.error(traceback.format_exc())
316 316 return redirect(url('home'))
317 317 else:
318 318 try:
319 319 c.changeset_parent = c.changeset.parents[0]
320 320 except IndexError:
321 321 c.changeset_parent = None
322 322 c.changes = []
323 323
324 324 for node in c.changeset.added:
325 325 filenode_old = FileNode(node.path, '')
326 326 if filenode_old.is_binary or node.is_binary:
327 327 diff = _('binary file') + '\n'
328 328 else:
329 329 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
330 330 ignore_whitespace=ignore_whitespace,
331 331 context=line_context)
332 332 diff = diffs.DiffProcessor(f_gitdiff,
333 333 format='gitdiff').raw_diff()
334 334
335 335 cs1 = None
336 336 cs2 = node.changeset.raw_id
337 337 c.changes.append(('added', node, diff, cs1, cs2))
338 338
339 339 for node in c.changeset.changed:
340 340 filenode_old = c.changeset_parent.get_node(node.path)
341 341 if filenode_old.is_binary or node.is_binary:
342 342 diff = _('binary file')
343 343 else:
344 344 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
345 345 ignore_whitespace=ignore_whitespace,
346 346 context=line_context)
347 347 diff = diffs.DiffProcessor(f_gitdiff,
348 348 format='gitdiff').raw_diff()
349 349
350 350 cs1 = filenode_old.changeset.raw_id
351 351 cs2 = node.changeset.raw_id
352 352 c.changes.append(('changed', node, diff, cs1, cs2))
353 353
354 354 response.content_type = 'text/plain'
355 355
356 356 if method == 'download':
357 357 response.content_disposition = 'attachment; filename=%s.patch' \
358 358 % revision
359 359
360 360 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
361 361 for x in c.changeset.parents])
362 362
363 363 c.diffs = ''
364 364 for x in c.changes:
365 365 c.diffs += x[2]
366 366
367 367 return render('changeset/raw_changeset.html')
368 368
369 369 @jsonify
370 370 def comment(self, repo_name, revision):
371 status = request.POST.get('changeset_status')
372 change_status = request.POST.get('change_changeset_status')
373
371 374 comm = ChangesetCommentsModel().create(
372 375 text=request.POST.get('text'),
373 376 repo_id=c.rhodecode_db_repo.repo_id,
374 377 user_id=c.rhodecode_user.user_id,
375 378 revision=revision,
376 379 f_path=request.POST.get('f_path'),
377 line_no=request.POST.get('line')
380 line_no=request.POST.get('line'),
381 status_change=(ChangesetStatus.get_status_lbl(status)
382 if status and change_status else None)
378 383 )
379 384
380 385 # get status if set !
381 status = request.POST.get('changeset_status')
382 if status and request.POST.get('change_changeset_status'):
386 if status and change_status:
383 387 ChangesetStatusModel().set_status(
384 388 c.rhodecode_db_repo.repo_id,
385 389 revision,
386 390 status,
387 391 c.rhodecode_user.user_id,
388 392 comm,
389 393 )
390 394
391 395 Session.commit()
392 396 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
393 397 return redirect(h.url('changeset_home', repo_name=repo_name,
394 398 revision=revision))
395 399
396 400 data = {
397 401 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
398 402 }
399 403 if comm:
400 404 c.co = comm
401 405 data.update(comm.get_dict())
402 406 data.update({'rendered_text':
403 407 render('changeset/changeset_comment_block.html')})
404 408
405 409 return data
406 410
407 411 @jsonify
408 412 def delete_comment(self, repo_name, comment_id):
409 413 co = ChangesetComment.get(comment_id)
410 414 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
411 415 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
412 416 ChangesetCommentsModel().delete(comment=co)
413 417 Session.commit()
414 418 return True
415 419 else:
416 420 raise HTTPForbidden()
@@ -1,153 +1,156
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.comment
4 4 ~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 comments model for RhodeCode
7 7
8 8 :created_on: Nov 11, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-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 logging
27 27 import traceback
28 28
29 29 from pylons.i18n.translation import _
30 30 from sqlalchemy.util.compat import defaultdict
31 31
32 32 from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import ChangesetComment, User, Repository, Notification
36 36 from rhodecode.model.notification import NotificationModel
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 class ChangesetCommentsModel(BaseModel):
42 42
43 43 def __get_changeset_comment(self, changeset_comment):
44 44 return self._get_instance(ChangesetComment, changeset_comment)
45 45
46 46 def _extract_mentions(self, s):
47 47 user_objects = []
48 48 for username in extract_mentioned_users(s):
49 49 user_obj = User.get_by_username(username, case_insensitive=True)
50 50 if user_obj:
51 51 user_objects.append(user_obj)
52 52 return user_objects
53 53
54 54 def create(self, text, repo_id, user_id, revision, f_path=None,
55 line_no=None):
55 line_no=None, status_change=None):
56 56 """
57 Creates new comment for changeset
57 Creates new comment for changeset. IF status_change is not none
58 this comment is associated with a status change of changeset
58 59
59 60 :param text:
60 61 :param repo_id:
61 62 :param user_id:
62 63 :param revision:
63 64 :param f_path:
64 65 :param line_no:
66 :param status_change:
65 67 """
66 68
67 69 if text:
68 70 repo = Repository.get(repo_id)
69 71 cs = repo.scm_instance.get_changeset(revision)
70 72 desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256))
71 73 author_email = cs.author_email
72 74 comment = ChangesetComment()
73 75 comment.repo = repo
74 76 comment.user_id = user_id
75 77 comment.revision = revision
76 78 comment.text = text
77 79 comment.f_path = f_path
78 80 comment.line_no = line_no
79 81
80 82 self.sa.add(comment)
81 83 self.sa.flush()
82 84 # make notification
83 85 line = ''
84 86 if line_no:
85 87 line = _('on line %s') % line_no
86 88 subj = safe_unicode(
87 89 h.link_to('Re commit: %(commit_desc)s %(line)s' % \
88 90 {'commit_desc': desc, 'line': line},
89 91 h.url('changeset_home', repo_name=repo.repo_name,
90 92 revision=revision,
91 93 anchor='comment-%s' % comment.comment_id,
92 94 qualified=True,
93 95 )
94 96 )
95 97 )
96 98
97 99 body = text
98 100
99 101 # get the current participants of this changeset
100 102 recipients = ChangesetComment.get_users(revision=revision)
101 103
102 104 # add changeset author if it's in rhodecode system
103 105 recipients += [User.get_by_email(author_email)]
104 106
105 107 NotificationModel().create(
106 108 created_by=user_id, subject=subj, body=body,
107 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT
109 recipients=recipients, type_=Notification.TYPE_CHANGESET_COMMENT,
110 email_kwargs={'status_change': status_change}
108 111 )
109 112
110 113 mention_recipients = set(self._extract_mentions(body))\
111 114 .difference(recipients)
112 115 if mention_recipients:
113 116 subj = _('[Mention]') + ' ' + subj
114 117 NotificationModel().create(
115 118 created_by=user_id, subject=subj, body=body,
116 119 recipients=mention_recipients,
117 120 type_=Notification.TYPE_CHANGESET_COMMENT
118 121 )
119 122
120 123 return comment
121 124
122 125 def delete(self, comment):
123 126 """
124 127 Deletes given comment
125 128
126 129 :param comment_id:
127 130 """
128 131 comment = self.__get_changeset_comment(comment)
129 132 self.sa.delete(comment)
130 133
131 134 return comment
132 135
133 136 def get_comments(self, repo_id, revision):
134 137 return ChangesetComment.query()\
135 138 .filter(ChangesetComment.repo_id == repo_id)\
136 139 .filter(ChangesetComment.revision == revision)\
137 140 .filter(ChangesetComment.line_no == None)\
138 141 .filter(ChangesetComment.f_path == None).all()
139 142
140 143 def get_inline_comments(self, repo_id, revision):
141 144 comments = self.sa.query(ChangesetComment)\
142 145 .filter(ChangesetComment.repo_id == repo_id)\
143 146 .filter(ChangesetComment.revision == revision)\
144 147 .filter(ChangesetComment.line_no != None)\
145 148 .filter(ChangesetComment.f_path != None)\
146 149 .order_by(ChangesetComment.comment_id.asc())\
147 150 .all()
148 151
149 152 paths = defaultdict(lambda: defaultdict(list))
150 153
151 154 for co in comments:
152 155 paths[co.f_path][co.line_no].append(co)
153 156 return paths.items()
@@ -1,1348 +1,1352
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 from collections import defaultdict
31 31
32 32 from sqlalchemy import *
33 33 from sqlalchemy.ext.hybrid import hybrid_property
34 34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 35 from beaker.cache import cache_region, region_invalidate
36 36
37 37 from pylons.i18n.translation import lazy_ugettext as _
38 38
39 39 from rhodecode.lib.vcs import get_backend
40 40 from rhodecode.lib.vcs.utils.helpers import get_scm
41 41 from rhodecode.lib.vcs.exceptions import VCSError
42 42 from rhodecode.lib.vcs.utils.lazy import LazyProperty
43 43
44 44 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
45 45 safe_unicode
46 46 from rhodecode.lib.compat import json
47 47 from rhodecode.lib.caching_query import FromCache
48 48
49 49 from rhodecode.model.meta import Base, Session
50 50 import hashlib
51 51
52 52
53 53 log = logging.getLogger(__name__)
54 54
55 55 #==============================================================================
56 56 # BASE CLASSES
57 57 #==============================================================================
58 58
59 59 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
60 60
61 61
62 62 class ModelSerializer(json.JSONEncoder):
63 63 """
64 64 Simple Serializer for JSON,
65 65
66 66 usage::
67 67
68 68 to make object customized for serialization implement a __json__
69 69 method that will return a dict for serialization into json
70 70
71 71 example::
72 72
73 73 class Task(object):
74 74
75 75 def __init__(self, name, value):
76 76 self.name = name
77 77 self.value = value
78 78
79 79 def __json__(self):
80 80 return dict(name=self.name,
81 81 value=self.value)
82 82
83 83 """
84 84
85 85 def default(self, obj):
86 86
87 87 if hasattr(obj, '__json__'):
88 88 return obj.__json__()
89 89 else:
90 90 return json.JSONEncoder.default(self, obj)
91 91
92 92
93 93 class BaseModel(object):
94 94 """
95 95 Base Model for all classess
96 96 """
97 97
98 98 @classmethod
99 99 def _get_keys(cls):
100 100 """return column names for this model """
101 101 return class_mapper(cls).c.keys()
102 102
103 103 def get_dict(self):
104 104 """
105 105 return dict with keys and values corresponding
106 106 to this model data """
107 107
108 108 d = {}
109 109 for k in self._get_keys():
110 110 d[k] = getattr(self, k)
111 111
112 112 # also use __json__() if present to get additional fields
113 113 for k, val in getattr(self, '__json__', lambda: {})().iteritems():
114 114 d[k] = val
115 115 return d
116 116
117 117 def get_appstruct(self):
118 118 """return list with keys and values tupples corresponding
119 119 to this model data """
120 120
121 121 l = []
122 122 for k in self._get_keys():
123 123 l.append((k, getattr(self, k),))
124 124 return l
125 125
126 126 def populate_obj(self, populate_dict):
127 127 """populate model with data from given populate_dict"""
128 128
129 129 for k in self._get_keys():
130 130 if k in populate_dict:
131 131 setattr(self, k, populate_dict[k])
132 132
133 133 @classmethod
134 134 def query(cls):
135 135 return Session.query(cls)
136 136
137 137 @classmethod
138 138 def get(cls, id_):
139 139 if id_:
140 140 return cls.query().get(id_)
141 141
142 142 @classmethod
143 143 def getAll(cls):
144 144 return cls.query().all()
145 145
146 146 @classmethod
147 147 def delete(cls, id_):
148 148 obj = cls.query().get(id_)
149 149 Session.delete(obj)
150 150
151 151 def __repr__(self):
152 152 if hasattr(self, '__unicode__'):
153 153 # python repr needs to return str
154 154 return safe_str(self.__unicode__())
155 155 return '<DB:%s>' % (self.__class__.__name__)
156 156
157 157 class RhodeCodeSetting(Base, BaseModel):
158 158 __tablename__ = 'rhodecode_settings'
159 159 __table_args__ = (
160 160 UniqueConstraint('app_settings_name'),
161 161 {'extend_existing': True, 'mysql_engine': 'InnoDB',
162 162 'mysql_charset': 'utf8'}
163 163 )
164 164 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
165 165 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
166 166 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
167 167
168 168 def __init__(self, k='', v=''):
169 169 self.app_settings_name = k
170 170 self.app_settings_value = v
171 171
172 172 @validates('_app_settings_value')
173 173 def validate_settings_value(self, key, val):
174 174 assert type(val) == unicode
175 175 return val
176 176
177 177 @hybrid_property
178 178 def app_settings_value(self):
179 179 v = self._app_settings_value
180 180 if self.app_settings_name == 'ldap_active':
181 181 v = str2bool(v)
182 182 return v
183 183
184 184 @app_settings_value.setter
185 185 def app_settings_value(self, val):
186 186 """
187 187 Setter that will always make sure we use unicode in app_settings_value
188 188
189 189 :param val:
190 190 """
191 191 self._app_settings_value = safe_unicode(val)
192 192
193 193 def __unicode__(self):
194 194 return u"<%s('%s:%s')>" % (
195 195 self.__class__.__name__,
196 196 self.app_settings_name, self.app_settings_value
197 197 )
198 198
199 199 @classmethod
200 200 def get_by_name(cls, ldap_key):
201 201 return cls.query()\
202 202 .filter(cls.app_settings_name == ldap_key).scalar()
203 203
204 204 @classmethod
205 205 def get_app_settings(cls, cache=False):
206 206
207 207 ret = cls.query()
208 208
209 209 if cache:
210 210 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
211 211
212 212 if not ret:
213 213 raise Exception('Could not get application settings !')
214 214 settings = {}
215 215 for each in ret:
216 216 settings['rhodecode_' + each.app_settings_name] = \
217 217 each.app_settings_value
218 218
219 219 return settings
220 220
221 221 @classmethod
222 222 def get_ldap_settings(cls, cache=False):
223 223 ret = cls.query()\
224 224 .filter(cls.app_settings_name.startswith('ldap_')).all()
225 225 fd = {}
226 226 for row in ret:
227 227 fd.update({row.app_settings_name:row.app_settings_value})
228 228
229 229 return fd
230 230
231 231
232 232 class RhodeCodeUi(Base, BaseModel):
233 233 __tablename__ = 'rhodecode_ui'
234 234 __table_args__ = (
235 235 UniqueConstraint('ui_key'),
236 236 {'extend_existing': True, 'mysql_engine': 'InnoDB',
237 237 'mysql_charset': 'utf8'}
238 238 )
239 239
240 240 HOOK_UPDATE = 'changegroup.update'
241 241 HOOK_REPO_SIZE = 'changegroup.repo_size'
242 242 HOOK_PUSH = 'pretxnchangegroup.push_logger'
243 243 HOOK_PULL = 'preoutgoing.pull_logger'
244 244
245 245 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
246 246 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 247 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 248 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
249 249 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
250 250
251 251 @classmethod
252 252 def get_by_key(cls, key):
253 253 return cls.query().filter(cls.ui_key == key)
254 254
255 255 @classmethod
256 256 def get_builtin_hooks(cls):
257 257 q = cls.query()
258 258 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
259 259 cls.HOOK_REPO_SIZE,
260 260 cls.HOOK_PUSH, cls.HOOK_PULL]))
261 261 return q.all()
262 262
263 263 @classmethod
264 264 def get_custom_hooks(cls):
265 265 q = cls.query()
266 266 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
267 267 cls.HOOK_REPO_SIZE,
268 268 cls.HOOK_PUSH, cls.HOOK_PULL]))
269 269 q = q.filter(cls.ui_section == 'hooks')
270 270 return q.all()
271 271
272 272 @classmethod
273 273 def create_or_update_hook(cls, key, val):
274 274 new_ui = cls.get_by_key(key).scalar() or cls()
275 275 new_ui.ui_section = 'hooks'
276 276 new_ui.ui_active = True
277 277 new_ui.ui_key = key
278 278 new_ui.ui_value = val
279 279
280 280 Session.add(new_ui)
281 281
282 282
283 283 class User(Base, BaseModel):
284 284 __tablename__ = 'users'
285 285 __table_args__ = (
286 286 UniqueConstraint('username'), UniqueConstraint('email'),
287 287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
288 288 'mysql_charset': 'utf8'}
289 289 )
290 290 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
291 291 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
292 292 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
293 293 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
294 294 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
295 295 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
296 296 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
297 297 _email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
298 298 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
299 299 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
300 300 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
301 301
302 302 user_log = relationship('UserLog', cascade='all')
303 303 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
304 304
305 305 repositories = relationship('Repository')
306 306 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
307 307 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
308 308 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
309 309
310 310 group_member = relationship('UsersGroupMember', cascade='all')
311 311
312 312 notifications = relationship('UserNotification', cascade='all')
313 313 # notifications assigned to this user
314 314 user_created_notifications = relationship('Notification', cascade='all')
315 315 # comments created by this user
316 316 user_comments = relationship('ChangesetComment', cascade='all')
317 317
318 318 @hybrid_property
319 319 def email(self):
320 320 return self._email
321 321
322 322 @email.setter
323 323 def email(self, val):
324 324 self._email = val.lower() if val else None
325 325
326 326 @property
327 327 def full_name(self):
328 328 return '%s %s' % (self.name, self.lastname)
329 329
330 330 @property
331 331 def full_name_or_username(self):
332 332 return ('%s %s' % (self.name, self.lastname)
333 333 if (self.name and self.lastname) else self.username)
334 334
335 335 @property
336 336 def full_contact(self):
337 337 return '%s %s <%s>' % (self.name, self.lastname, self.email)
338 338
339 339 @property
340 340 def short_contact(self):
341 341 return '%s %s' % (self.name, self.lastname)
342 342
343 343 @property
344 344 def is_admin(self):
345 345 return self.admin
346 346
347 347 def __unicode__(self):
348 348 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
349 349 self.user_id, self.username)
350 350
351 351 @classmethod
352 352 def get_by_username(cls, username, case_insensitive=False, cache=False):
353 353 if case_insensitive:
354 354 q = cls.query().filter(cls.username.ilike(username))
355 355 else:
356 356 q = cls.query().filter(cls.username == username)
357 357
358 358 if cache:
359 359 q = q.options(FromCache(
360 360 "sql_cache_short",
361 361 "get_user_%s" % _hash_key(username)
362 362 )
363 363 )
364 364 return q.scalar()
365 365
366 366 @classmethod
367 367 def get_by_api_key(cls, api_key, cache=False):
368 368 q = cls.query().filter(cls.api_key == api_key)
369 369
370 370 if cache:
371 371 q = q.options(FromCache("sql_cache_short",
372 372 "get_api_key_%s" % api_key))
373 373 return q.scalar()
374 374
375 375 @classmethod
376 376 def get_by_email(cls, email, case_insensitive=False, cache=False):
377 377 if case_insensitive:
378 378 q = cls.query().filter(cls.email.ilike(email))
379 379 else:
380 380 q = cls.query().filter(cls.email == email)
381 381
382 382 if cache:
383 383 q = q.options(FromCache("sql_cache_short",
384 384 "get_api_key_%s" % email))
385 385 return q.scalar()
386 386
387 387 def update_lastlogin(self):
388 388 """Update user lastlogin"""
389 389 self.last_login = datetime.datetime.now()
390 390 Session.add(self)
391 391 log.debug('updated user %s lastlogin' % self.username)
392 392
393 393 def __json__(self):
394 394 return dict(
395 395 user_id=self.user_id,
396 396 first_name=self.name,
397 397 last_name=self.lastname,
398 398 email=self.email,
399 399 full_name=self.full_name,
400 400 full_name_or_username=self.full_name_or_username,
401 401 short_contact=self.short_contact,
402 402 full_contact=self.full_contact
403 403 )
404 404
405 405
406 406 class UserLog(Base, BaseModel):
407 407 __tablename__ = 'user_logs'
408 408 __table_args__ = (
409 409 {'extend_existing': True, 'mysql_engine': 'InnoDB',
410 410 'mysql_charset': 'utf8'},
411 411 )
412 412 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
413 413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
414 414 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
415 415 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
416 416 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
417 417 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
418 418 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
419 419
420 420 @property
421 421 def action_as_day(self):
422 422 return datetime.date(*self.action_date.timetuple()[:3])
423 423
424 424 user = relationship('User')
425 425 repository = relationship('Repository', cascade='')
426 426
427 427
428 428 class UsersGroup(Base, BaseModel):
429 429 __tablename__ = 'users_groups'
430 430 __table_args__ = (
431 431 {'extend_existing': True, 'mysql_engine': 'InnoDB',
432 432 'mysql_charset': 'utf8'},
433 433 )
434 434
435 435 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
436 436 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
437 437 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
438 438
439 439 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
440 440 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
441 441 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
442 442
443 443 def __unicode__(self):
444 444 return u'<userGroup(%s)>' % (self.users_group_name)
445 445
446 446 @classmethod
447 447 def get_by_group_name(cls, group_name, cache=False,
448 448 case_insensitive=False):
449 449 if case_insensitive:
450 450 q = cls.query().filter(cls.users_group_name.ilike(group_name))
451 451 else:
452 452 q = cls.query().filter(cls.users_group_name == group_name)
453 453 if cache:
454 454 q = q.options(FromCache(
455 455 "sql_cache_short",
456 456 "get_user_%s" % _hash_key(group_name)
457 457 )
458 458 )
459 459 return q.scalar()
460 460
461 461 @classmethod
462 462 def get(cls, users_group_id, cache=False):
463 463 users_group = cls.query()
464 464 if cache:
465 465 users_group = users_group.options(FromCache("sql_cache_short",
466 466 "get_users_group_%s" % users_group_id))
467 467 return users_group.get(users_group_id)
468 468
469 469
470 470 class UsersGroupMember(Base, BaseModel):
471 471 __tablename__ = 'users_groups_members'
472 472 __table_args__ = (
473 473 {'extend_existing': True, 'mysql_engine': 'InnoDB',
474 474 'mysql_charset': 'utf8'},
475 475 )
476 476
477 477 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
478 478 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
479 479 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
480 480
481 481 user = relationship('User', lazy='joined')
482 482 users_group = relationship('UsersGroup')
483 483
484 484 def __init__(self, gr_id='', u_id=''):
485 485 self.users_group_id = gr_id
486 486 self.user_id = u_id
487 487
488 488
489 489 class Repository(Base, BaseModel):
490 490 __tablename__ = 'repositories'
491 491 __table_args__ = (
492 492 UniqueConstraint('repo_name'),
493 493 {'extend_existing': True, 'mysql_engine': 'InnoDB',
494 494 'mysql_charset': 'utf8'},
495 495 )
496 496
497 497 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
498 498 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
499 499 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
500 500 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
501 501 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
502 502 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
503 503 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
504 504 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
505 505 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
506 506 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
507 507
508 508 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
509 509 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
510 510
511 511 user = relationship('User')
512 512 fork = relationship('Repository', remote_side=repo_id)
513 513 group = relationship('RepoGroup')
514 514 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
515 515 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
516 516 stats = relationship('Statistics', cascade='all', uselist=False)
517 517
518 518 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
519 519
520 520 logs = relationship('UserLog')
521 521
522 522 def __unicode__(self):
523 523 return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
524 524 self.repo_name)
525 525
526 526 @classmethod
527 527 def url_sep(cls):
528 528 return '/'
529 529
530 530 @classmethod
531 531 def get_by_repo_name(cls, repo_name):
532 532 q = Session.query(cls).filter(cls.repo_name == repo_name)
533 533 q = q.options(joinedload(Repository.fork))\
534 534 .options(joinedload(Repository.user))\
535 535 .options(joinedload(Repository.group))
536 536 return q.scalar()
537 537
538 538 @classmethod
539 539 def get_repo_forks(cls, repo_id):
540 540 return cls.query().filter(Repository.fork_id == repo_id)
541 541
542 542 @classmethod
543 543 def base_path(cls):
544 544 """
545 545 Returns base path when all repos are stored
546 546
547 547 :param cls:
548 548 """
549 549 q = Session.query(RhodeCodeUi)\
550 550 .filter(RhodeCodeUi.ui_key == cls.url_sep())
551 551 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
552 552 return q.one().ui_value
553 553
554 554 @property
555 555 def just_name(self):
556 556 return self.repo_name.split(Repository.url_sep())[-1]
557 557
558 558 @property
559 559 def groups_with_parents(self):
560 560 groups = []
561 561 if self.group is None:
562 562 return groups
563 563
564 564 cur_gr = self.group
565 565 groups.insert(0, cur_gr)
566 566 while 1:
567 567 gr = getattr(cur_gr, 'parent_group', None)
568 568 cur_gr = cur_gr.parent_group
569 569 if gr is None:
570 570 break
571 571 groups.insert(0, gr)
572 572
573 573 return groups
574 574
575 575 @property
576 576 def groups_and_repo(self):
577 577 return self.groups_with_parents, self.just_name
578 578
579 579 @LazyProperty
580 580 def repo_path(self):
581 581 """
582 582 Returns base full path for that repository means where it actually
583 583 exists on a filesystem
584 584 """
585 585 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
586 586 Repository.url_sep())
587 587 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
588 588 return q.one().ui_value
589 589
590 590 @property
591 591 def repo_full_path(self):
592 592 p = [self.repo_path]
593 593 # we need to split the name by / since this is how we store the
594 594 # names in the database, but that eventually needs to be converted
595 595 # into a valid system path
596 596 p += self.repo_name.split(Repository.url_sep())
597 597 return os.path.join(*p)
598 598
599 599 def get_new_name(self, repo_name):
600 600 """
601 601 returns new full repository name based on assigned group and new new
602 602
603 603 :param group_name:
604 604 """
605 605 path_prefix = self.group.full_path_splitted if self.group else []
606 606 return Repository.url_sep().join(path_prefix + [repo_name])
607 607
608 608 @property
609 609 def _ui(self):
610 610 """
611 611 Creates an db based ui object for this repository
612 612 """
613 613 from mercurial import ui
614 614 from mercurial import config
615 615 baseui = ui.ui()
616 616
617 617 #clean the baseui object
618 618 baseui._ocfg = config.config()
619 619 baseui._ucfg = config.config()
620 620 baseui._tcfg = config.config()
621 621
622 622 ret = RhodeCodeUi.query()\
623 623 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
624 624
625 625 hg_ui = ret
626 626 for ui_ in hg_ui:
627 627 if ui_.ui_active:
628 628 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
629 629 ui_.ui_key, ui_.ui_value)
630 630 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
631 631
632 632 return baseui
633 633
634 634 @classmethod
635 635 def is_valid(cls, repo_name):
636 636 """
637 637 returns True if given repo name is a valid filesystem repository
638 638
639 639 :param cls:
640 640 :param repo_name:
641 641 """
642 642 from rhodecode.lib.utils import is_valid_repo
643 643
644 644 return is_valid_repo(repo_name, cls.base_path())
645 645
646 646 #==========================================================================
647 647 # SCM PROPERTIES
648 648 #==========================================================================
649 649
650 650 def get_changeset(self, rev=None):
651 651 return get_changeset_safe(self.scm_instance, rev)
652 652
653 653 @property
654 654 def tip(self):
655 655 return self.get_changeset('tip')
656 656
657 657 @property
658 658 def author(self):
659 659 return self.tip.author
660 660
661 661 @property
662 662 def last_change(self):
663 663 return self.scm_instance.last_change
664 664
665 665 def comments(self, revisions=None):
666 666 """
667 667 Returns comments for this repository grouped by revisions
668 668
669 669 :param revisions: filter query by revisions only
670 670 """
671 671 cmts = ChangesetComment.query()\
672 672 .filter(ChangesetComment.repo == self)
673 673 if revisions:
674 674 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
675 675 grouped = defaultdict(list)
676 676 for cmt in cmts.all():
677 677 grouped[cmt.revision].append(cmt)
678 678 return grouped
679 679
680 680 def statuses(self, revisions=None):
681 681 """
682 682 Returns statuses for this repository
683 683
684 684 :param revisions: list of revisions to get statuses for
685 685 :type revisions: list
686 686 """
687 687
688 688 statuses = ChangesetStatus.query()\
689 689 .filter(ChangesetStatus.repo == self)\
690 690 .filter(ChangesetStatus.version == 0)
691 691 if revisions:
692 692 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
693 693 grouped = {}
694 694 for stat in statuses.all():
695 695 grouped[stat.revision] = [str(stat.status), stat.status_lbl]
696 696 return grouped
697 697
698 698 #==========================================================================
699 699 # SCM CACHE INSTANCE
700 700 #==========================================================================
701 701
702 702 @property
703 703 def invalidate(self):
704 704 return CacheInvalidation.invalidate(self.repo_name)
705 705
706 706 def set_invalidate(self):
707 707 """
708 708 set a cache for invalidation for this instance
709 709 """
710 710 CacheInvalidation.set_invalidate(self.repo_name)
711 711
712 712 @LazyProperty
713 713 def scm_instance(self):
714 714 return self.__get_instance()
715 715
716 716 @property
717 717 def scm_instance_cached(self):
718 718 @cache_region('long_term')
719 719 def _c(repo_name):
720 720 return self.__get_instance()
721 721 rn = self.repo_name
722 722 log.debug('Getting cached instance of repo')
723 723 inv = self.invalidate
724 724 if inv is not None:
725 725 region_invalidate(_c, None, rn)
726 726 # update our cache
727 727 CacheInvalidation.set_valid(inv.cache_key)
728 728 return _c(rn)
729 729
730 730 def __get_instance(self):
731 731 repo_full_path = self.repo_full_path
732 732 try:
733 733 alias = get_scm(repo_full_path)[0]
734 734 log.debug('Creating instance of %s repository' % alias)
735 735 backend = get_backend(alias)
736 736 except VCSError:
737 737 log.error(traceback.format_exc())
738 738 log.error('Perhaps this repository is in db and not in '
739 739 'filesystem run rescan repositories with '
740 740 '"destroy old data " option from admin panel')
741 741 return
742 742
743 743 if alias == 'hg':
744 744
745 745 repo = backend(safe_str(repo_full_path), create=False,
746 746 baseui=self._ui)
747 747 # skip hidden web repository
748 748 if repo._get_hidden():
749 749 return
750 750 else:
751 751 repo = backend(repo_full_path, create=False)
752 752
753 753 return repo
754 754
755 755
756 756 class RepoGroup(Base, BaseModel):
757 757 __tablename__ = 'groups'
758 758 __table_args__ = (
759 759 UniqueConstraint('group_name', 'group_parent_id'),
760 760 CheckConstraint('group_id != group_parent_id'),
761 761 {'extend_existing': True, 'mysql_engine': 'InnoDB',
762 762 'mysql_charset': 'utf8'},
763 763 )
764 764 __mapper_args__ = {'order_by': 'group_name'}
765 765
766 766 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
767 767 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
768 768 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
769 769 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
770 770
771 771 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
772 772 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
773 773
774 774 parent_group = relationship('RepoGroup', remote_side=group_id)
775 775
776 776 def __init__(self, group_name='', parent_group=None):
777 777 self.group_name = group_name
778 778 self.parent_group = parent_group
779 779
780 780 def __unicode__(self):
781 781 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
782 782 self.group_name)
783 783
784 784 @classmethod
785 785 def groups_choices(cls):
786 786 from webhelpers.html import literal as _literal
787 787 repo_groups = [('', '')]
788 788 sep = ' &raquo; '
789 789 _name = lambda k: _literal(sep.join(k))
790 790
791 791 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
792 792 for x in cls.query().all()])
793 793
794 794 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
795 795 return repo_groups
796 796
797 797 @classmethod
798 798 def url_sep(cls):
799 799 return '/'
800 800
801 801 @classmethod
802 802 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
803 803 if case_insensitive:
804 804 gr = cls.query()\
805 805 .filter(cls.group_name.ilike(group_name))
806 806 else:
807 807 gr = cls.query()\
808 808 .filter(cls.group_name == group_name)
809 809 if cache:
810 810 gr = gr.options(FromCache(
811 811 "sql_cache_short",
812 812 "get_group_%s" % _hash_key(group_name)
813 813 )
814 814 )
815 815 return gr.scalar()
816 816
817 817 @property
818 818 def parents(self):
819 819 parents_recursion_limit = 5
820 820 groups = []
821 821 if self.parent_group is None:
822 822 return groups
823 823 cur_gr = self.parent_group
824 824 groups.insert(0, cur_gr)
825 825 cnt = 0
826 826 while 1:
827 827 cnt += 1
828 828 gr = getattr(cur_gr, 'parent_group', None)
829 829 cur_gr = cur_gr.parent_group
830 830 if gr is None:
831 831 break
832 832 if cnt == parents_recursion_limit:
833 833 # this will prevent accidental infinit loops
834 834 log.error('group nested more than %s' %
835 835 parents_recursion_limit)
836 836 break
837 837
838 838 groups.insert(0, gr)
839 839 return groups
840 840
841 841 @property
842 842 def children(self):
843 843 return RepoGroup.query().filter(RepoGroup.parent_group == self)
844 844
845 845 @property
846 846 def name(self):
847 847 return self.group_name.split(RepoGroup.url_sep())[-1]
848 848
849 849 @property
850 850 def full_path(self):
851 851 return self.group_name
852 852
853 853 @property
854 854 def full_path_splitted(self):
855 855 return self.group_name.split(RepoGroup.url_sep())
856 856
857 857 @property
858 858 def repositories(self):
859 859 return Repository.query()\
860 860 .filter(Repository.group == self)\
861 861 .order_by(Repository.repo_name)
862 862
863 863 @property
864 864 def repositories_recursive_count(self):
865 865 cnt = self.repositories.count()
866 866
867 867 def children_count(group):
868 868 cnt = 0
869 869 for child in group.children:
870 870 cnt += child.repositories.count()
871 871 cnt += children_count(child)
872 872 return cnt
873 873
874 874 return cnt + children_count(self)
875 875
876 876 def get_new_name(self, group_name):
877 877 """
878 878 returns new full group name based on parent and new name
879 879
880 880 :param group_name:
881 881 """
882 882 path_prefix = (self.parent_group.full_path_splitted if
883 883 self.parent_group else [])
884 884 return RepoGroup.url_sep().join(path_prefix + [group_name])
885 885
886 886
887 887 class Permission(Base, BaseModel):
888 888 __tablename__ = 'permissions'
889 889 __table_args__ = (
890 890 {'extend_existing': True, 'mysql_engine': 'InnoDB',
891 891 'mysql_charset': 'utf8'},
892 892 )
893 893 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
894 894 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
895 895 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
896 896
897 897 def __unicode__(self):
898 898 return u"<%s('%s:%s')>" % (
899 899 self.__class__.__name__, self.permission_id, self.permission_name
900 900 )
901 901
902 902 @classmethod
903 903 def get_by_key(cls, key):
904 904 return cls.query().filter(cls.permission_name == key).scalar()
905 905
906 906 @classmethod
907 907 def get_default_perms(cls, default_user_id):
908 908 q = Session.query(UserRepoToPerm, Repository, cls)\
909 909 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
910 910 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
911 911 .filter(UserRepoToPerm.user_id == default_user_id)
912 912
913 913 return q.all()
914 914
915 915 @classmethod
916 916 def get_default_group_perms(cls, default_user_id):
917 917 q = Session.query(UserRepoGroupToPerm, RepoGroup, cls)\
918 918 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
919 919 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
920 920 .filter(UserRepoGroupToPerm.user_id == default_user_id)
921 921
922 922 return q.all()
923 923
924 924
925 925 class UserRepoToPerm(Base, BaseModel):
926 926 __tablename__ = 'repo_to_perm'
927 927 __table_args__ = (
928 928 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
929 929 {'extend_existing': True, 'mysql_engine': 'InnoDB',
930 930 'mysql_charset': 'utf8'}
931 931 )
932 932 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
933 933 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
934 934 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
935 935 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
936 936
937 937 user = relationship('User')
938 938 repository = relationship('Repository')
939 939 permission = relationship('Permission')
940 940
941 941 @classmethod
942 942 def create(cls, user, repository, permission):
943 943 n = cls()
944 944 n.user = user
945 945 n.repository = repository
946 946 n.permission = permission
947 947 Session.add(n)
948 948 return n
949 949
950 950 def __unicode__(self):
951 951 return u'<user:%s => %s >' % (self.user, self.repository)
952 952
953 953
954 954 class UserToPerm(Base, BaseModel):
955 955 __tablename__ = 'user_to_perm'
956 956 __table_args__ = (
957 957 UniqueConstraint('user_id', 'permission_id'),
958 958 {'extend_existing': True, 'mysql_engine': 'InnoDB',
959 959 'mysql_charset': 'utf8'}
960 960 )
961 961 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
962 962 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
963 963 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
964 964
965 965 user = relationship('User')
966 966 permission = relationship('Permission', lazy='joined')
967 967
968 968
969 969 class UsersGroupRepoToPerm(Base, BaseModel):
970 970 __tablename__ = 'users_group_repo_to_perm'
971 971 __table_args__ = (
972 972 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
973 973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
974 974 'mysql_charset': 'utf8'}
975 975 )
976 976 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
977 977 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
978 978 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
979 979 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
980 980
981 981 users_group = relationship('UsersGroup')
982 982 permission = relationship('Permission')
983 983 repository = relationship('Repository')
984 984
985 985 @classmethod
986 986 def create(cls, users_group, repository, permission):
987 987 n = cls()
988 988 n.users_group = users_group
989 989 n.repository = repository
990 990 n.permission = permission
991 991 Session.add(n)
992 992 return n
993 993
994 994 def __unicode__(self):
995 995 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
996 996
997 997
998 998 class UsersGroupToPerm(Base, BaseModel):
999 999 __tablename__ = 'users_group_to_perm'
1000 1000 __table_args__ = (
1001 1001 UniqueConstraint('users_group_id', 'permission_id',),
1002 1002 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1003 1003 'mysql_charset': 'utf8'}
1004 1004 )
1005 1005 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1006 1006 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1007 1007 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1008 1008
1009 1009 users_group = relationship('UsersGroup')
1010 1010 permission = relationship('Permission')
1011 1011
1012 1012
1013 1013 class UserRepoGroupToPerm(Base, BaseModel):
1014 1014 __tablename__ = 'user_repo_group_to_perm'
1015 1015 __table_args__ = (
1016 1016 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1017 1017 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1018 1018 'mysql_charset': 'utf8'}
1019 1019 )
1020 1020
1021 1021 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1022 1022 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1023 1023 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1024 1024 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1025 1025
1026 1026 user = relationship('User')
1027 1027 group = relationship('RepoGroup')
1028 1028 permission = relationship('Permission')
1029 1029
1030 1030
1031 1031 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1032 1032 __tablename__ = 'users_group_repo_group_to_perm'
1033 1033 __table_args__ = (
1034 1034 UniqueConstraint('users_group_id', 'group_id'),
1035 1035 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1036 1036 'mysql_charset': 'utf8'}
1037 1037 )
1038 1038
1039 1039 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)
1040 1040 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1041 1041 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1042 1042 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1043 1043
1044 1044 users_group = relationship('UsersGroup')
1045 1045 permission = relationship('Permission')
1046 1046 group = relationship('RepoGroup')
1047 1047
1048 1048
1049 1049 class Statistics(Base, BaseModel):
1050 1050 __tablename__ = 'statistics'
1051 1051 __table_args__ = (
1052 1052 UniqueConstraint('repository_id'),
1053 1053 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1054 1054 'mysql_charset': 'utf8'}
1055 1055 )
1056 1056 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1057 1057 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1058 1058 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1059 1059 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1060 1060 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1061 1061 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1062 1062
1063 1063 repository = relationship('Repository', single_parent=True)
1064 1064
1065 1065
1066 1066 class UserFollowing(Base, BaseModel):
1067 1067 __tablename__ = 'user_followings'
1068 1068 __table_args__ = (
1069 1069 UniqueConstraint('user_id', 'follows_repository_id'),
1070 1070 UniqueConstraint('user_id', 'follows_user_id'),
1071 1071 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1072 1072 'mysql_charset': 'utf8'}
1073 1073 )
1074 1074
1075 1075 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1076 1076 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1077 1077 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1078 1078 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1079 1079 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1080 1080
1081 1081 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1082 1082
1083 1083 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1084 1084 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1085 1085
1086 1086 @classmethod
1087 1087 def get_repo_followers(cls, repo_id):
1088 1088 return cls.query().filter(cls.follows_repo_id == repo_id)
1089 1089
1090 1090
1091 1091 class CacheInvalidation(Base, BaseModel):
1092 1092 __tablename__ = 'cache_invalidation'
1093 1093 __table_args__ = (
1094 1094 UniqueConstraint('cache_key'),
1095 1095 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1096 1096 'mysql_charset': 'utf8'},
1097 1097 )
1098 1098 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1099 1099 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1100 1100 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1101 1101 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1102 1102
1103 1103 def __init__(self, cache_key, cache_args=''):
1104 1104 self.cache_key = cache_key
1105 1105 self.cache_args = cache_args
1106 1106 self.cache_active = False
1107 1107
1108 1108 def __unicode__(self):
1109 1109 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1110 1110 self.cache_id, self.cache_key)
1111 1111 @classmethod
1112 1112 def clear_cache(cls):
1113 1113 cls.query().delete()
1114 1114
1115 1115 @classmethod
1116 1116 def _get_key(cls, key):
1117 1117 """
1118 1118 Wrapper for generating a key, together with a prefix
1119 1119
1120 1120 :param key:
1121 1121 """
1122 1122 import rhodecode
1123 1123 prefix = ''
1124 1124 iid = rhodecode.CONFIG.get('instance_id')
1125 1125 if iid:
1126 1126 prefix = iid
1127 1127 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1128 1128
1129 1129 @classmethod
1130 1130 def get_by_key(cls, key):
1131 1131 return cls.query().filter(cls.cache_key == key).scalar()
1132 1132
1133 1133 @classmethod
1134 1134 def _get_or_create_key(cls, key, prefix, org_key):
1135 1135 inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
1136 1136 if not inv_obj:
1137 1137 try:
1138 1138 inv_obj = CacheInvalidation(key, org_key)
1139 1139 Session.add(inv_obj)
1140 1140 Session.commit()
1141 1141 except Exception:
1142 1142 log.error(traceback.format_exc())
1143 1143 Session.rollback()
1144 1144 return inv_obj
1145 1145
1146 1146 @classmethod
1147 1147 def invalidate(cls, key):
1148 1148 """
1149 1149 Returns Invalidation object if this given key should be invalidated
1150 1150 None otherwise. `cache_active = False` means that this cache
1151 1151 state is not valid and needs to be invalidated
1152 1152
1153 1153 :param key:
1154 1154 """
1155 1155
1156 1156 key, _prefix, _org_key = cls._get_key(key)
1157 1157 inv = cls._get_or_create_key(key, _prefix, _org_key)
1158 1158
1159 1159 if inv and inv.cache_active is False:
1160 1160 return inv
1161 1161
1162 1162 @classmethod
1163 1163 def set_invalidate(cls, key):
1164 1164 """
1165 1165 Mark this Cache key for invalidation
1166 1166
1167 1167 :param key:
1168 1168 """
1169 1169
1170 1170 key, _prefix, _org_key = cls._get_key(key)
1171 1171 inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
1172 1172 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1173 1173 _org_key))
1174 1174 try:
1175 1175 for inv_obj in inv_objs:
1176 1176 if inv_obj:
1177 1177 inv_obj.cache_active = False
1178 1178
1179 1179 Session.add(inv_obj)
1180 1180 Session.commit()
1181 1181 except Exception:
1182 1182 log.error(traceback.format_exc())
1183 1183 Session.rollback()
1184 1184
1185 1185 @classmethod
1186 1186 def set_valid(cls, key):
1187 1187 """
1188 1188 Mark this cache key as active and currently cached
1189 1189
1190 1190 :param key:
1191 1191 """
1192 1192 inv_obj = cls.get_by_key(key)
1193 1193 inv_obj.cache_active = True
1194 1194 Session.add(inv_obj)
1195 1195 Session.commit()
1196 1196
1197 1197
1198 1198 class ChangesetComment(Base, BaseModel):
1199 1199 __tablename__ = 'changeset_comments'
1200 1200 __table_args__ = (
1201 1201 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1202 1202 'mysql_charset': 'utf8'},
1203 1203 )
1204 1204 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1205 1205 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1206 1206 revision = Column('revision', String(40), nullable=False)
1207 1207 line_no = Column('line_no', Unicode(10), nullable=True)
1208 1208 f_path = Column('f_path', Unicode(1000), nullable=True)
1209 1209 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1210 1210 text = Column('text', Unicode(25000), nullable=False)
1211 1211 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1212 1212
1213 1213 author = relationship('User', lazy='joined')
1214 1214 repo = relationship('Repository')
1215 1215 status_change = relationship('ChangesetStatus', uselist=False)
1216 1216
1217 1217 @classmethod
1218 1218 def get_users(cls, revision):
1219 1219 """
1220 1220 Returns user associated with this changesetComment. ie those
1221 1221 who actually commented
1222 1222
1223 1223 :param cls:
1224 1224 :param revision:
1225 1225 """
1226 1226 return Session.query(User)\
1227 1227 .filter(cls.revision == revision)\
1228 1228 .join(ChangesetComment.author).all()
1229 1229
1230 1230
1231 1231 class ChangesetStatus(Base, BaseModel):
1232 1232 __tablename__ = 'changeset_statuses'
1233 1233 __table_args__ = (
1234 1234 UniqueConstraint('repo_id', 'revision', 'version'),
1235 1235 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1236 1236 'mysql_charset': 'utf8'}
1237 1237 )
1238 1238
1239 1239 STATUSES = [
1240 1240 ('not_reviewed', _("Not Reviewed")), # (no icon) and default
1241 1241 ('approved', _("Approved")),
1242 1242 ('rejected', _("Rejected")),
1243 1243 ('under_review', _("Under Review")),
1244 1244 ]
1245 1245 DEFAULT = STATUSES[0][0]
1246 1246
1247 1247 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1248 1248 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1249 1249 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1250 1250 revision = Column('revision', String(40), nullable=False)
1251 1251 status = Column('status', String(128), nullable=False, default=DEFAULT)
1252 1252 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1253 1253 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1254 1254 version = Column('version', Integer(), nullable=False, default=0)
1255 1255 author = relationship('User', lazy='joined')
1256 1256 repo = relationship('Repository')
1257 1257 comment = relationship('ChangesetComment', lazy='joined')
1258 1258
1259 @classmethod
1260 def get_status_lbl(cls, value):
1261 return dict(cls.STATUSES).get(value)
1262
1259 1263 @property
1260 1264 def status_lbl(self):
1261 return dict(self.STATUSES).get(self.status)
1265 return ChangesetStatus.get_status_lbl(self.status)
1262 1266
1263 1267
1264 1268 class Notification(Base, BaseModel):
1265 1269 __tablename__ = 'notifications'
1266 1270 __table_args__ = (
1267 1271 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1268 1272 'mysql_charset': 'utf8'},
1269 1273 )
1270 1274
1271 1275 TYPE_CHANGESET_COMMENT = u'cs_comment'
1272 1276 TYPE_MESSAGE = u'message'
1273 1277 TYPE_MENTION = u'mention'
1274 1278 TYPE_REGISTRATION = u'registration'
1275 1279 TYPE_PULL_REQUEST = u'pull_request'
1276 1280
1277 1281 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1278 1282 subject = Column('subject', Unicode(512), nullable=True)
1279 1283 body = Column('body', Unicode(50000), nullable=True)
1280 1284 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1281 1285 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1282 1286 type_ = Column('type', Unicode(256))
1283 1287
1284 1288 created_by_user = relationship('User')
1285 1289 notifications_to_users = relationship('UserNotification', lazy='joined',
1286 1290 cascade="all, delete, delete-orphan")
1287 1291
1288 1292 @property
1289 1293 def recipients(self):
1290 1294 return [x.user for x in UserNotification.query()\
1291 1295 .filter(UserNotification.notification == self)\
1292 1296 .order_by(UserNotification.user).all()]
1293 1297
1294 1298 @classmethod
1295 1299 def create(cls, created_by, subject, body, recipients, type_=None):
1296 1300 if type_ is None:
1297 1301 type_ = Notification.TYPE_MESSAGE
1298 1302
1299 1303 notification = cls()
1300 1304 notification.created_by_user = created_by
1301 1305 notification.subject = subject
1302 1306 notification.body = body
1303 1307 notification.type_ = type_
1304 1308 notification.created_on = datetime.datetime.now()
1305 1309
1306 1310 for u in recipients:
1307 1311 assoc = UserNotification()
1308 1312 assoc.notification = notification
1309 1313 u.notifications.append(assoc)
1310 1314 Session.add(notification)
1311 1315 return notification
1312 1316
1313 1317 @property
1314 1318 def description(self):
1315 1319 from rhodecode.model.notification import NotificationModel
1316 1320 return NotificationModel().make_description(self)
1317 1321
1318 1322
1319 1323 class UserNotification(Base, BaseModel):
1320 1324 __tablename__ = 'user_to_notification'
1321 1325 __table_args__ = (
1322 1326 UniqueConstraint('user_id', 'notification_id'),
1323 1327 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1324 1328 'mysql_charset': 'utf8'}
1325 1329 )
1326 1330 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1327 1331 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1328 1332 read = Column('read', Boolean, default=False)
1329 1333 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1330 1334
1331 1335 user = relationship('User', lazy="joined")
1332 1336 notification = relationship('Notification', lazy="joined",
1333 1337 order_by=lambda: Notification.created_on.desc(),)
1334 1338
1335 1339 def mark_as_read(self):
1336 1340 self.read = True
1337 1341 Session.add(self)
1338 1342
1339 1343
1340 1344 class DbMigrateVersion(Base, BaseModel):
1341 1345 __tablename__ = 'db_migrate_version'
1342 1346 __table_args__ = (
1343 1347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1344 1348 'mysql_charset': 'utf8'},
1345 1349 )
1346 1350 repository_id = Column('repository_id', String(250), primary_key=True)
1347 1351 repository_path = Column('repository_path', Text)
1348 1352 version = Column('version', Integer)
@@ -1,226 +1,227
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.notification
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Model for notifications
7 7
8 8
9 9 :created_on: Nov 20, 2011
10 10 :author: marcink
11 11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26
27 27 import os
28 28 import logging
29 29 import traceback
30 30 import datetime
31 31
32 32 from pylons.i18n.translation import _
33 33
34 34 import rhodecode
35 35 from rhodecode.config.conf import DATETIME_FORMAT
36 36 from rhodecode.lib import helpers as h
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import Notification, User, UserNotification
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class NotificationModel(BaseModel):
44 44
45 45 def __get_user(self, user):
46 46 return self._get_instance(User, user, callback=User.get_by_username)
47 47
48 48 def __get_notification(self, notification):
49 49 if isinstance(notification, Notification):
50 50 return notification
51 51 elif isinstance(notification, (int, long)):
52 52 return Notification.get(notification)
53 53 else:
54 54 if notification:
55 55 raise Exception('notification must be int, long or Instance'
56 56 ' of Notification got %s' % type(notification))
57 57
58 58 def create(self, created_by, subject, body, recipients=None,
59 59 type_=Notification.TYPE_MESSAGE, with_email=True,
60 60 email_kwargs={}):
61 61 """
62 62
63 63 Creates notification of given type
64 64
65 65 :param created_by: int, str or User instance. User who created this
66 66 notification
67 67 :param subject:
68 68 :param body:
69 69 :param recipients: list of int, str or User objects, when None
70 70 is given send to all admins
71 71 :param type_: type of notification
72 72 :param with_email: send email with this notification
73 73 :param email_kwargs: additional dict to pass as args to email template
74 74 """
75 75 from rhodecode.lib.celerylib import tasks, run_task
76 76
77 77 if recipients and not getattr(recipients, '__iter__', False):
78 78 raise Exception('recipients must be a list of iterable')
79 79
80 80 created_by_obj = self.__get_user(created_by)
81 81
82 82 if recipients:
83 83 recipients_objs = []
84 84 for u in recipients:
85 85 obj = self.__get_user(u)
86 86 if obj:
87 87 recipients_objs.append(obj)
88 88 recipients_objs = set(recipients_objs)
89 89 log.debug('sending notifications %s to %s' % (
90 90 type_, recipients_objs)
91 91 )
92 92 else:
93 93 # empty recipients means to all admins
94 94 recipients_objs = User.query().filter(User.admin == True).all()
95 95 log.debug('sending notifications %s to admins: %s' % (
96 96 type_, recipients_objs)
97 97 )
98 98 notif = Notification.create(
99 99 created_by=created_by_obj, subject=subject,
100 100 body=body, recipients=recipients_objs, type_=type_
101 101 )
102 102
103 103 if with_email is False:
104 104 return notif
105 105
106 106 # send email with notification
107 107 for rec in recipients_objs:
108 108 email_subject = NotificationModel().make_description(notif, False)
109 109 type_ = type_
110 110 email_body = body
111 ## this is passed into template
111 112 kwargs = {'subject': subject, 'body': h.rst_w_mentions(body)}
112 113 kwargs.update(email_kwargs)
113 114 email_body_html = EmailNotificationModel()\
114 115 .get_email_tmpl(type_, **kwargs)
115 116
116 117 run_task(tasks.send_email, rec.email, email_subject, email_body,
117 118 email_body_html)
118 119
119 120 return notif
120 121
121 122 def delete(self, user, notification):
122 123 # we don't want to remove actual notification just the assignment
123 124 try:
124 125 notification = self.__get_notification(notification)
125 126 user = self.__get_user(user)
126 127 if notification and user:
127 128 obj = UserNotification.query()\
128 129 .filter(UserNotification.user == user)\
129 130 .filter(UserNotification.notification
130 131 == notification)\
131 132 .one()
132 133 self.sa.delete(obj)
133 134 return True
134 135 except Exception:
135 136 log.error(traceback.format_exc())
136 137 raise
137 138
138 139 def get_for_user(self, user):
139 140 user = self.__get_user(user)
140 141 return user.notifications
141 142
142 143 def mark_all_read_for_user(self, user):
143 144 user = self.__get_user(user)
144 145 UserNotification.query()\
145 146 .filter(UserNotification.read == False)\
146 147 .update({'read': True})
147 148
148 149 def get_unread_cnt_for_user(self, user):
149 150 user = self.__get_user(user)
150 151 return UserNotification.query()\
151 152 .filter(UserNotification.read == False)\
152 153 .filter(UserNotification.user == user).count()
153 154
154 155 def get_unread_for_user(self, user):
155 156 user = self.__get_user(user)
156 157 return [x.notification for x in UserNotification.query()\
157 158 .filter(UserNotification.read == False)\
158 159 .filter(UserNotification.user == user).all()]
159 160
160 161 def get_user_notification(self, user, notification):
161 162 user = self.__get_user(user)
162 163 notification = self.__get_notification(notification)
163 164
164 165 return UserNotification.query()\
165 166 .filter(UserNotification.notification == notification)\
166 167 .filter(UserNotification.user == user).scalar()
167 168
168 169 def make_description(self, notification, show_age=True):
169 170 """
170 171 Creates a human readable description based on properties
171 172 of notification object
172 173 """
173 174
174 175 _map = {
175 176 notification.TYPE_CHANGESET_COMMENT: _('commented on commit'),
176 177 notification.TYPE_MESSAGE: _('sent message'),
177 178 notification.TYPE_MENTION: _('mentioned you'),
178 179 notification.TYPE_REGISTRATION: _('registered in RhodeCode')
179 180 }
180 181
181 182 tmpl = "%(user)s %(action)s %(when)s"
182 183 if show_age:
183 184 when = h.age(notification.created_on)
184 185 else:
185 186 DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT)
186 187 when = DTF(notification.created_on)
187 188
188 189 data = dict(
189 190 user=notification.created_by_user.username,
190 191 action=_map[notification.type_], when=when,
191 192 )
192 193 return tmpl % data
193 194
194 195
195 196 class EmailNotificationModel(BaseModel):
196 197
197 198 TYPE_CHANGESET_COMMENT = Notification.TYPE_CHANGESET_COMMENT
198 199 TYPE_PASSWORD_RESET = 'passoword_link'
199 200 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
200 201 TYPE_DEFAULT = 'default'
201 202
202 203 def __init__(self):
203 204 self._template_root = rhodecode.CONFIG['pylons.paths']['templates'][0]
204 205 self._tmpl_lookup = rhodecode.CONFIG['pylons.app_globals'].mako_lookup
205 206
206 207 self.email_types = {
207 208 self.TYPE_CHANGESET_COMMENT: 'email_templates/changeset_comment.html',
208 209 self.TYPE_PASSWORD_RESET: 'email_templates/password_reset.html',
209 210 self.TYPE_REGISTRATION: 'email_templates/registration.html',
210 211 self.TYPE_DEFAULT: 'email_templates/default.html'
211 212 }
212 213
213 214 def get_email_tmpl(self, type_, **kwargs):
214 215 """
215 216 return generated template for email based on given type
216 217
217 218 :param type_:
218 219 """
219 220
220 221 base = self.email_types.get(type_, self.email_types[self.TYPE_DEFAULT])
221 222 email_template = self._tmpl_lookup.get_template(base)
222 223 # translator inject
223 224 _kwargs = {'_': _}
224 225 _kwargs.update(kwargs)
225 226 log.debug('rendering tmpl %s with kwargs %s' % (base, _kwargs))
226 227 return email_template.render(**_kwargs)
@@ -1,6 +1,12
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="main.html"/>
3 3
4 4 <h4>${subject}</h4>
5 5
6 6 ${body}
7
8 % if status_change is not None:
9 <div>
10 New status -> ${status_change}
11 </div>
12 % endif No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now