##// END OF EJS Templates
store download archive actions in journal
marcink -
r3868:e262494c beta
parent child Browse files
Show More
@@ -1,658 +1,661 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.files
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Files controller for RhodeCode
7 7
8 8 :created_on: Apr 21, 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 from __future__ import with_statement
26 26 import os
27 27 import logging
28 28 import traceback
29 29 import tempfile
30 30 import shutil
31 31
32 32 from pylons import request, response, tmpl_context as c, url
33 33 from pylons.i18n.translation import _
34 34 from pylons.controllers.util import redirect
35 from rhodecode.lib.utils import jsonify
35 from rhodecode.lib.utils import jsonify, action_logger
36 36
37 37 from rhodecode.lib import diffs
38 38 from rhodecode.lib import helpers as h
39 39
40 40 from rhodecode.lib.compat import OrderedDict
41 41 from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str,\
42 42 str2bool
43 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
44 44 from rhodecode.lib.base import BaseRepoController, render
45 45 from rhodecode.lib.vcs.backends.base import EmptyChangeset
46 46 from rhodecode.lib.vcs.conf import settings
47 47 from rhodecode.lib.vcs.exceptions import RepositoryError, \
48 48 ChangesetDoesNotExistError, EmptyRepositoryError, \
49 49 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError,\
50 50 NodeDoesNotExistError, ChangesetError, NodeError
51 51 from rhodecode.lib.vcs.nodes import FileNode
52 52
53 53 from rhodecode.model.repo import RepoModel
54 54 from rhodecode.model.scm import ScmModel
55 55 from rhodecode.model.db import Repository
56 56
57 57 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
58 58 _context_url, get_line_ctx, get_ignore_ws
59 59 from webob.exc import HTTPNotFound
60 60 from rhodecode.lib.exceptions import NonRelativePathError
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class FilesController(BaseRepoController):
67 67
68 68 def __before__(self):
69 69 super(FilesController, self).__before__()
70 70 c.cut_off_limit = self.cut_off_limit
71 71
72 72 def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
73 73 """
74 74 Safe way to get changeset if error occur it redirects to tip with
75 75 proper message
76 76
77 77 :param rev: revision to fetch
78 78 :param repo_name: repo name to redirect after
79 79 """
80 80
81 81 try:
82 82 return c.rhodecode_repo.get_changeset(rev)
83 83 except EmptyRepositoryError, e:
84 84 if not redirect_after:
85 85 return None
86 86 url_ = url('files_add_home',
87 87 repo_name=c.repo_name,
88 88 revision=0, f_path='')
89 89 add_new = h.link_to(_('Click here to add new file'), url_)
90 90 h.flash(h.literal(_('There are no files yet %s') % add_new),
91 91 category='warning')
92 92 redirect(h.url('summary_home', repo_name=repo_name))
93 93
94 94 except RepositoryError, e: # including ChangesetDoesNotExistError
95 95 h.flash(str(e), category='error')
96 96 raise HTTPNotFound()
97 97
98 98 def __get_filenode_or_redirect(self, repo_name, cs, path):
99 99 """
100 100 Returns file_node, if error occurs or given path is directory,
101 101 it'll redirect to top level path
102 102
103 103 :param repo_name: repo_name
104 104 :param cs: given changeset
105 105 :param path: path to lookup
106 106 """
107 107
108 108 try:
109 109 file_node = cs.get_node(path)
110 110 if file_node.is_dir():
111 111 raise RepositoryError('given path is a directory')
112 112 except RepositoryError, e:
113 113 h.flash(str(e), category='error')
114 114 raise HTTPNotFound()
115 115
116 116 return file_node
117 117
118 118 @LoginRequired()
119 119 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
120 120 'repository.admin')
121 121 def index(self, repo_name, revision, f_path, annotate=False):
122 122 # redirect to given revision from form if given
123 123 post_revision = request.POST.get('at_rev', None)
124 124 if post_revision:
125 125 cs = self.__get_cs_or_redirect(post_revision, repo_name)
126 126
127 127 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
128 128 c.branch = request.GET.get('branch', None)
129 129 c.f_path = f_path
130 130 c.annotate = annotate
131 131 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
132 132 cur_rev = c.changeset.revision
133 133
134 134 # prev link
135 135 try:
136 136 prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
137 137 c.url_prev = url('files_home', repo_name=c.repo_name,
138 138 revision=prev_rev.raw_id, f_path=f_path)
139 139 if c.branch:
140 140 c.url_prev += '?branch=%s' % c.branch
141 141 except (ChangesetDoesNotExistError, VCSError):
142 142 c.url_prev = '#'
143 143
144 144 # next link
145 145 try:
146 146 next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
147 147 c.url_next = url('files_home', repo_name=c.repo_name,
148 148 revision=next_rev.raw_id, f_path=f_path)
149 149 if c.branch:
150 150 c.url_next += '?branch=%s' % c.branch
151 151 except (ChangesetDoesNotExistError, VCSError):
152 152 c.url_next = '#'
153 153
154 154 # files or dirs
155 155 try:
156 156 c.file = c.changeset.get_node(f_path)
157 157
158 158 if c.file.is_file():
159 159 c.load_full_history = False
160 160 file_last_cs = c.file.last_changeset
161 161 c.file_changeset = (c.changeset
162 162 if c.changeset.revision < file_last_cs.revision
163 163 else file_last_cs)
164 164 #determine if we're on branch head
165 165 _branches = c.rhodecode_repo.branches
166 166 c.on_branch_head = revision in _branches.keys() + _branches.values()
167 167 _hist = []
168 168 c.file_history = []
169 169 if c.load_full_history:
170 170 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
171 171
172 172 c.authors = []
173 173 for a in set([x.author for x in _hist]):
174 174 c.authors.append((h.email(a), h.person(a)))
175 175 else:
176 176 c.authors = c.file_history = []
177 177 except RepositoryError, e:
178 178 h.flash(str(e), category='error')
179 179 raise HTTPNotFound()
180 180
181 181 if request.environ.get('HTTP_X_PARTIAL_XHR'):
182 182 return render('files/files_ypjax.html')
183 183
184 184 return render('files/files.html')
185 185
186 186 @LoginRequired()
187 187 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
188 188 'repository.admin')
189 189 def history(self, repo_name, revision, f_path, annotate=False):
190 190 if request.environ.get('HTTP_X_PARTIAL_XHR'):
191 191 c.changeset = self.__get_cs_or_redirect(revision, repo_name)
192 192 c.f_path = f_path
193 193 c.annotate = annotate
194 194 c.file = c.changeset.get_node(f_path)
195 195 if c.file.is_file():
196 196 file_last_cs = c.file.last_changeset
197 197 c.file_changeset = (c.changeset
198 198 if c.changeset.revision < file_last_cs.revision
199 199 else file_last_cs)
200 200 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
201 201 c.authors = []
202 202 for a in set([x.author for x in _hist]):
203 203 c.authors.append((h.email(a), h.person(a)))
204 204 return render('files/files_history_box.html')
205 205
206 206 @LoginRequired()
207 207 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
208 208 'repository.admin')
209 209 def rawfile(self, repo_name, revision, f_path):
210 210 cs = self.__get_cs_or_redirect(revision, repo_name)
211 211 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
212 212
213 213 response.content_disposition = 'attachment; filename=%s' % \
214 214 safe_str(f_path.split(Repository.url_sep())[-1])
215 215
216 216 response.content_type = file_node.mimetype
217 217 return file_node.content
218 218
219 219 @LoginRequired()
220 220 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
221 221 'repository.admin')
222 222 def raw(self, repo_name, revision, f_path):
223 223 cs = self.__get_cs_or_redirect(revision, repo_name)
224 224 file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
225 225
226 226 raw_mimetype_mapping = {
227 227 # map original mimetype to a mimetype used for "show as raw"
228 228 # you can also provide a content-disposition to override the
229 229 # default "attachment" disposition.
230 230 # orig_type: (new_type, new_dispo)
231 231
232 232 # show images inline:
233 233 'image/x-icon': ('image/x-icon', 'inline'),
234 234 'image/png': ('image/png', 'inline'),
235 235 'image/gif': ('image/gif', 'inline'),
236 236 'image/jpeg': ('image/jpeg', 'inline'),
237 237 'image/svg+xml': ('image/svg+xml', 'inline'),
238 238 }
239 239
240 240 mimetype = file_node.mimetype
241 241 try:
242 242 mimetype, dispo = raw_mimetype_mapping[mimetype]
243 243 except KeyError:
244 244 # we don't know anything special about this, handle it safely
245 245 if file_node.is_binary:
246 246 # do same as download raw for binary files
247 247 mimetype, dispo = 'application/octet-stream', 'attachment'
248 248 else:
249 249 # do not just use the original mimetype, but force text/plain,
250 250 # otherwise it would serve text/html and that might be unsafe.
251 251 # Note: underlying vcs library fakes text/plain mimetype if the
252 252 # mimetype can not be determined and it thinks it is not
253 253 # binary.This might lead to erroneous text display in some
254 254 # cases, but helps in other cases, like with text files
255 255 # without extension.
256 256 mimetype, dispo = 'text/plain', 'inline'
257 257
258 258 if dispo == 'attachment':
259 259 dispo = 'attachment; filename=%s' % \
260 260 safe_str(f_path.split(os.sep)[-1])
261 261
262 262 response.content_disposition = dispo
263 263 response.content_type = mimetype
264 264 return file_node.content
265 265
266 266 @LoginRequired()
267 267 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
268 268 def edit(self, repo_name, revision, f_path):
269 269 repo = c.rhodecode_db_repo
270 270 if repo.enable_locking and repo.locked[0]:
271 271 h.flash(_('This repository is has been locked by %s on %s')
272 272 % (h.person_by_id(repo.locked[0]),
273 273 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
274 274 'warning')
275 275 return redirect(h.url('files_home',
276 276 repo_name=repo_name, revision='tip'))
277 277
278 278 # check if revision is a branch identifier- basically we cannot
279 279 # create multiple heads via file editing
280 280 _branches = repo.scm_instance.branches
281 281 # check if revision is a branch name or branch hash
282 282 if revision not in _branches.keys() + _branches.values():
283 283 h.flash(_('You can only edit files with revision '
284 284 'being a valid branch '), category='warning')
285 285 return redirect(h.url('files_home',
286 286 repo_name=repo_name, revision='tip',
287 287 f_path=f_path))
288 288
289 289 r_post = request.POST
290 290
291 291 c.cs = self.__get_cs_or_redirect(revision, repo_name)
292 292 c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
293 293
294 294 if c.file.is_binary:
295 295 return redirect(url('files_home', repo_name=c.repo_name,
296 296 revision=c.cs.raw_id, f_path=f_path))
297 297 c.default_message = _('Edited file %s via RhodeCode') % (f_path)
298 298 c.f_path = f_path
299 299
300 300 if r_post:
301 301
302 302 old_content = c.file.content
303 303 sl = old_content.splitlines(1)
304 304 first_line = sl[0] if sl else ''
305 305 # modes: 0 - Unix, 1 - Mac, 2 - DOS
306 306 mode = detect_mode(first_line, 0)
307 307 content = convert_line_endings(r_post.get('content', ''), mode)
308 308
309 309 message = r_post.get('message') or c.default_message
310 310 author = self.rhodecode_user.full_contact
311 311
312 312 if content == old_content:
313 313 h.flash(_('No changes'), category='warning')
314 314 return redirect(url('changeset_home', repo_name=c.repo_name,
315 315 revision='tip'))
316 316 try:
317 317 self.scm_model.commit_change(repo=c.rhodecode_repo,
318 318 repo_name=repo_name, cs=c.cs,
319 319 user=self.rhodecode_user.user_id,
320 320 author=author, message=message,
321 321 content=content, f_path=f_path)
322 322 h.flash(_('Successfully committed to %s') % f_path,
323 323 category='success')
324 324
325 325 except Exception:
326 326 log.error(traceback.format_exc())
327 327 h.flash(_('Error occurred during commit'), category='error')
328 328 return redirect(url('changeset_home',
329 329 repo_name=c.repo_name, revision='tip'))
330 330
331 331 return render('files/files_edit.html')
332 332
333 333 @LoginRequired()
334 334 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
335 335 def add(self, repo_name, revision, f_path):
336 336
337 337 repo = Repository.get_by_repo_name(repo_name)
338 338 if repo.enable_locking and repo.locked[0]:
339 339 h.flash(_('This repository is has been locked by %s on %s')
340 340 % (h.person_by_id(repo.locked[0]),
341 341 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
342 342 'warning')
343 343 return redirect(h.url('files_home',
344 344 repo_name=repo_name, revision='tip'))
345 345
346 346 r_post = request.POST
347 347 c.cs = self.__get_cs_or_redirect(revision, repo_name,
348 348 redirect_after=False)
349 349 if c.cs is None:
350 350 c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
351 351 c.default_message = (_('Added file via RhodeCode'))
352 352 c.f_path = f_path
353 353
354 354 if r_post:
355 355 unix_mode = 0
356 356 content = convert_line_endings(r_post.get('content', ''), unix_mode)
357 357
358 358 message = r_post.get('message') or c.default_message
359 359 filename = r_post.get('filename')
360 360 location = r_post.get('location', '')
361 361 file_obj = r_post.get('upload_file', None)
362 362
363 363 if file_obj is not None and hasattr(file_obj, 'filename'):
364 364 filename = file_obj.filename
365 365 content = file_obj.file
366 366
367 367 if not content:
368 368 h.flash(_('No content'), category='warning')
369 369 return redirect(url('changeset_home', repo_name=c.repo_name,
370 370 revision='tip'))
371 371 if not filename:
372 372 h.flash(_('No filename'), category='warning')
373 373 return redirect(url('changeset_home', repo_name=c.repo_name,
374 374 revision='tip'))
375 375 #strip all crap out of file, just leave the basename
376 376 filename = os.path.basename(filename)
377 377 node_path = os.path.join(location, filename)
378 378 author = self.rhodecode_user.full_contact
379 379
380 380 try:
381 381 nodes = {
382 382 node_path: {
383 383 'content': content
384 384 }
385 385 }
386 386 self.scm_model.create_nodes(
387 387 user=c.rhodecode_user.user_id, repo=c.rhodecode_db_repo,
388 388 message=message,
389 389 nodes=nodes,
390 390 parent_cs=c.cs,
391 391 author=author,
392 392 )
393 393
394 394 h.flash(_('Successfully committed to %s') % node_path,
395 395 category='success')
396 396 except NonRelativePathError, e:
397 397 h.flash(_('Location must be relative path and must not '
398 398 'contain .. in path'), category='warning')
399 399 return redirect(url('changeset_home', repo_name=c.repo_name,
400 400 revision='tip'))
401 401 except (NodeError, NodeAlreadyExistsError), e:
402 402 h.flash(_(e), category='error')
403 403 except Exception:
404 404 log.error(traceback.format_exc())
405 405 h.flash(_('Error occurred during commit'), category='error')
406 406 return redirect(url('changeset_home',
407 407 repo_name=c.repo_name, revision='tip'))
408 408
409 409 return render('files/files_add.html')
410 410
411 411 @LoginRequired()
412 412 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
413 413 'repository.admin')
414 414 def archivefile(self, repo_name, fname):
415 415
416 416 fileformat = None
417 417 revision = None
418 418 ext = None
419 419 subrepos = request.GET.get('subrepos') == 'true'
420 420
421 421 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
422 422 archive_spec = fname.split(ext_data[1])
423 423 if len(archive_spec) == 2 and archive_spec[1] == '':
424 424 fileformat = a_type or ext_data[1]
425 425 revision = archive_spec[0]
426 426 ext = ext_data[1]
427 427
428 428 try:
429 429 dbrepo = RepoModel().get_by_repo_name(repo_name)
430 430 if not dbrepo.enable_downloads:
431 431 return _('Downloads disabled')
432 432
433 433 if c.rhodecode_repo.alias == 'hg':
434 434 # patch and reset hooks section of UI config to not run any
435 435 # hooks on fetching archives with subrepos
436 436 for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
437 437 c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
438 438
439 439 cs = c.rhodecode_repo.get_changeset(revision)
440 440 content_type = settings.ARCHIVE_SPECS[fileformat][0]
441 441 except ChangesetDoesNotExistError:
442 442 return _('Unknown revision %s') % revision
443 443 except EmptyRepositoryError:
444 444 return _('Empty repository')
445 445 except (ImproperArchiveTypeError, KeyError):
446 446 return _('Unknown archive type')
447 447 # archive cache
448 448 from rhodecode import CONFIG
449 449 rev_name = cs.raw_id[:12]
450 450 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
451 451 safe_str(rev_name), ext)
452 452
453 453 use_cached_archive = False # defines if we use cached version of archive
454 454 archive_cache_enabled = CONFIG.get('archive_cache_dir')
455 455 if not subrepos and archive_cache_enabled:
456 456 #check if we it's ok to write
457 457 if not os.path.isdir(CONFIG['archive_cache_dir']):
458 458 os.makedirs(CONFIG['archive_cache_dir'])
459 459 cached_archive_path = os.path.join(CONFIG['archive_cache_dir'], archive_name)
460 460 if os.path.isfile(cached_archive_path):
461 461 log.debug('Found cached archive in %s' % cached_archive_path)
462 462 fd, archive = None, cached_archive_path
463 463 use_cached_archive = True
464 464 else:
465 465 log.debug('Archive %s is not yet cached' % (archive_name))
466 466
467 467 if not use_cached_archive:
468 468 #generate new archive
469 469 try:
470 470 fd, archive = tempfile.mkstemp()
471 471 t = open(archive, 'wb')
472 472 log.debug('Creating new temp archive in %s' % archive)
473 473 cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
474 474 if archive_cache_enabled:
475 475 #if we generated the archive and use cache rename that
476 476 log.debug('Storing new archive in %s' % cached_archive_path)
477 477 shutil.move(archive, cached_archive_path)
478 478 archive = cached_archive_path
479 479 finally:
480 480 t.close()
481 481
482 482 def get_chunked_archive(archive):
483 483 stream = open(archive, 'rb')
484 484 while True:
485 485 data = stream.read(16 * 1024)
486 486 if not data:
487 487 stream.close()
488 488 if fd: # fd means we used temporary file
489 489 os.close(fd)
490 490 if not archive_cache_enabled:
491 491 log.debug('Destroing temp archive %s' % archive)
492 492 os.remove(archive)
493 493 break
494 494 yield data
495
495 # store download action
496 action_logger(user=c.rhodecode_user,
497 action='user_downloaded_archive:%s' % (archive_name),
498 repo=repo_name, ipaddr=self.ip_addr, commit=True)
496 499 response.content_disposition = str('attachment; filename=%s' % (archive_name))
497 500 response.content_type = str(content_type)
498 501 return get_chunked_archive(archive)
499 502
500 503 @LoginRequired()
501 504 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
502 505 'repository.admin')
503 506 def diff(self, repo_name, f_path):
504 507 ignore_whitespace = request.GET.get('ignorews') == '1'
505 508 line_context = request.GET.get('context', 3)
506 509 diff1 = request.GET.get('diff1', '')
507 510 diff2 = request.GET.get('diff2', '')
508 511 c.action = request.GET.get('diff')
509 512 c.no_changes = diff1 == diff2
510 513 c.f_path = f_path
511 514 c.big_diff = False
512 515 c.anchor_url = anchor_url
513 516 c.ignorews_url = _ignorews_url
514 517 c.context_url = _context_url
515 518 c.changes = OrderedDict()
516 519 c.changes[diff2] = []
517 520
518 521 #special case if we want a show rev only, it's impl here
519 522 #to reduce JS and callbacks
520 523
521 524 if request.GET.get('show_rev'):
522 525 if str2bool(request.GET.get('annotate', 'False')):
523 526 _url = url('files_annotate_home', repo_name=c.repo_name,
524 527 revision=diff1, f_path=c.f_path)
525 528 else:
526 529 _url = url('files_home', repo_name=c.repo_name,
527 530 revision=diff1, f_path=c.f_path)
528 531
529 532 return redirect(_url)
530 533 try:
531 534 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
532 535 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
533 536 try:
534 537 node1 = c.changeset_1.get_node(f_path)
535 538 if node1.is_dir():
536 539 raise NodeError('%s path is a %s not a file'
537 540 % (node1, type(node1)))
538 541 except NodeDoesNotExistError:
539 542 c.changeset_1 = EmptyChangeset(cs=diff1,
540 543 revision=c.changeset_1.revision,
541 544 repo=c.rhodecode_repo)
542 545 node1 = FileNode(f_path, '', changeset=c.changeset_1)
543 546 else:
544 547 c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
545 548 node1 = FileNode(f_path, '', changeset=c.changeset_1)
546 549
547 550 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
548 551 c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
549 552 try:
550 553 node2 = c.changeset_2.get_node(f_path)
551 554 if node2.is_dir():
552 555 raise NodeError('%s path is a %s not a file'
553 556 % (node2, type(node2)))
554 557 except NodeDoesNotExistError:
555 558 c.changeset_2 = EmptyChangeset(cs=diff2,
556 559 revision=c.changeset_2.revision,
557 560 repo=c.rhodecode_repo)
558 561 node2 = FileNode(f_path, '', changeset=c.changeset_2)
559 562 else:
560 563 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
561 564 node2 = FileNode(f_path, '', changeset=c.changeset_2)
562 565 except (RepositoryError, NodeError):
563 566 log.error(traceback.format_exc())
564 567 return redirect(url('files_home', repo_name=c.repo_name,
565 568 f_path=f_path))
566 569
567 570 if c.action == 'download':
568 571 _diff = diffs.get_gitdiff(node1, node2,
569 572 ignore_whitespace=ignore_whitespace,
570 573 context=line_context)
571 574 diff = diffs.DiffProcessor(_diff, format='gitdiff')
572 575
573 576 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
574 577 response.content_type = 'text/plain'
575 578 response.content_disposition = (
576 579 'attachment; filename=%s' % diff_name
577 580 )
578 581 return diff.as_raw()
579 582
580 583 elif c.action == 'raw':
581 584 _diff = diffs.get_gitdiff(node1, node2,
582 585 ignore_whitespace=ignore_whitespace,
583 586 context=line_context)
584 587 diff = diffs.DiffProcessor(_diff, format='gitdiff')
585 588 response.content_type = 'text/plain'
586 589 return diff.as_raw()
587 590
588 591 else:
589 592 fid = h.FID(diff2, node2.path)
590 593 line_context_lcl = get_line_ctx(fid, request.GET)
591 594 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
592 595
593 596 lim = request.GET.get('fulldiff') or self.cut_off_limit
594 597 _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
595 598 filenode_new=node2,
596 599 cut_off_limit=lim,
597 600 ignore_whitespace=ign_whitespace_lcl,
598 601 line_context=line_context_lcl,
599 602 enable_comments=False)
600 603 op = ''
601 604 filename = node1.path
602 605 cs_changes = {
603 606 'fid': [cs1, cs2, op, filename, diff, st]
604 607 }
605 608 c.changes = cs_changes
606 609
607 610 return render('files/file_diff.html')
608 611
609 612 def _get_node_history(self, cs, f_path, changesets=None):
610 613 """
611 614 get changesets history for given node
612 615
613 616 :param cs: changeset to calculate history
614 617 :param f_path: path for node to calculate history for
615 618 :param changesets: if passed don't calculate history and take
616 619 changesets defined in this list
617 620 """
618 621 # calculate history based on tip
619 622 tip_cs = c.rhodecode_repo.get_changeset()
620 623 if changesets is None:
621 624 try:
622 625 changesets = tip_cs.get_file_history(f_path)
623 626 except (NodeDoesNotExistError, ChangesetError):
624 627 #this node is not present at tip !
625 628 changesets = cs.get_file_history(f_path)
626 629 hist_l = []
627 630
628 631 changesets_group = ([], _("Changesets"))
629 632 branches_group = ([], _("Branches"))
630 633 tags_group = ([], _("Tags"))
631 634 _hg = cs.repository.alias == 'hg'
632 635 for chs in changesets:
633 636 #_branch = '(%s)' % chs.branch if _hg else ''
634 637 _branch = chs.branch
635 638 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, _branch)
636 639 changesets_group[0].append((chs.raw_id, n_desc,))
637 640 hist_l.append(changesets_group)
638 641
639 642 for name, chs in c.rhodecode_repo.branches.items():
640 643 branches_group[0].append((chs, name),)
641 644 hist_l.append(branches_group)
642 645
643 646 for name, chs in c.rhodecode_repo.tags.items():
644 647 tags_group[0].append((chs, name),)
645 648 hist_l.append(tags_group)
646 649
647 650 return hist_l, changesets
648 651
649 652 @LoginRequired()
650 653 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
651 654 'repository.admin')
652 655 @jsonify
653 656 def nodelist(self, repo_name, revision, f_path):
654 657 if request.environ.get('HTTP_X_PARTIAL_XHR'):
655 658 cs = self.__get_cs_or_redirect(revision, repo_name)
656 659 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
657 660 flat=False)
658 661 return {'nodes': _d + _f}
@@ -1,1371 +1,1377 b''
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11 import logging
12 12 import re
13 13 import urlparse
14 14 import textwrap
15 15
16 16 from datetime import datetime
17 17 from pygments.formatters.html import HtmlFormatter
18 18 from pygments import highlight as code_highlight
19 19 from pylons import url, request, config
20 20 from pylons.i18n.translation import _, ungettext
21 21 from hashlib import md5
22 22
23 23 from webhelpers.html import literal, HTML, escape
24 24 from webhelpers.html.tools import *
25 25 from webhelpers.html.builder import make_tag
26 26 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
27 27 end_form, file, form, hidden, image, javascript_link, link_to, \
28 28 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
29 29 submit, text, password, textarea, title, ul, xml_declaration, radio
30 30 from webhelpers.html.tools import auto_link, button_to, highlight, \
31 31 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
32 32 from webhelpers.number import format_byte_size, format_bit_size
33 33 from webhelpers.pylonslib import Flash as _Flash
34 34 from webhelpers.pylonslib.secure_form import secure_form
35 35 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
36 36 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
37 37 replace_whitespace, urlify, truncate, wrap_paragraphs
38 38 from webhelpers.date import time_ago_in_words
39 39 from webhelpers.paginate import Page as _Page
40 40 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
41 41 convert_boolean_attrs, NotGiven, _make_safe_id_component
42 42
43 43 from rhodecode.lib.annotate import annotate_highlight
44 44 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
45 45 from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 46 get_changeset_safe, datetime_to_time, time_to_datetime, AttributeDict,\
47 47 safe_int
48 48 from rhodecode.lib.markup_renderer import MarkupRenderer
49 49 from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError
50 50 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
51 51 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
52 52 from rhodecode.model.changeset_status import ChangesetStatusModel
53 53 from rhodecode.model.db import URL_SEP, Permission
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 html_escape_table = {
59 59 "&": "&amp;",
60 60 '"': "&quot;",
61 61 "'": "&apos;",
62 62 ">": "&gt;",
63 63 "<": "&lt;",
64 64 }
65 65
66 66
67 67 def html_escape(text):
68 68 """Produce entities within text."""
69 69 return "".join(html_escape_table.get(c, c) for c in text)
70 70
71 71
72 72 def shorter(text, size=20):
73 73 postfix = '...'
74 74 if len(text) > size:
75 75 return text[:size - len(postfix)] + postfix
76 76 return text
77 77
78 78
79 79 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
80 80 """
81 81 Reset button
82 82 """
83 83 _set_input_attrs(attrs, type, name, value)
84 84 _set_id_attr(attrs, id, name)
85 85 convert_boolean_attrs(attrs, ["disabled"])
86 86 return HTML.input(**attrs)
87 87
88 88 reset = _reset
89 89 safeid = _make_safe_id_component
90 90
91 91
92 92 def FID(raw_id, path):
93 93 """
94 94 Creates a uniqe ID for filenode based on it's hash of path and revision
95 95 it's safe to use in urls
96 96
97 97 :param raw_id:
98 98 :param path:
99 99 """
100 100
101 101 return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12])
102 102
103 103
104 104 def get_token():
105 105 """Return the current authentication token, creating one if one doesn't
106 106 already exist.
107 107 """
108 108 token_key = "_authentication_token"
109 109 from pylons import session
110 110 if not token_key in session:
111 111 try:
112 112 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
113 113 except AttributeError: # Python < 2.4
114 114 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
115 115 session[token_key] = token
116 116 if hasattr(session, 'save'):
117 117 session.save()
118 118 return session[token_key]
119 119
120 120
121 121 class _GetError(object):
122 122 """Get error from form_errors, and represent it as span wrapped error
123 123 message
124 124
125 125 :param field_name: field to fetch errors for
126 126 :param form_errors: form errors dict
127 127 """
128 128
129 129 def __call__(self, field_name, form_errors):
130 130 tmpl = """<span class="error_msg">%s</span>"""
131 131 if form_errors and field_name in form_errors:
132 132 return literal(tmpl % form_errors.get(field_name))
133 133
134 134 get_error = _GetError()
135 135
136 136
137 137 class _ToolTip(object):
138 138
139 139 def __call__(self, tooltip_title, trim_at=50):
140 140 """
141 141 Special function just to wrap our text into nice formatted
142 142 autowrapped text
143 143
144 144 :param tooltip_title:
145 145 """
146 146 tooltip_title = escape(tooltip_title)
147 147 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
148 148 return tooltip_title
149 149 tooltip = _ToolTip()
150 150
151 151
152 152 class _FilesBreadCrumbs(object):
153 153
154 154 def __call__(self, repo_name, rev, paths):
155 155 if isinstance(paths, str):
156 156 paths = safe_unicode(paths)
157 157 url_l = [link_to(repo_name, url('files_home',
158 158 repo_name=repo_name,
159 159 revision=rev, f_path=''),
160 160 class_='ypjax-link')]
161 161 paths_l = paths.split('/')
162 162 for cnt, p in enumerate(paths_l):
163 163 if p != '':
164 164 url_l.append(link_to(p,
165 165 url('files_home',
166 166 repo_name=repo_name,
167 167 revision=rev,
168 168 f_path='/'.join(paths_l[:cnt + 1])
169 169 ),
170 170 class_='ypjax-link'
171 171 )
172 172 )
173 173
174 174 return literal('/'.join(url_l))
175 175
176 176 files_breadcrumbs = _FilesBreadCrumbs()
177 177
178 178
179 179 class CodeHtmlFormatter(HtmlFormatter):
180 180 """
181 181 My code Html Formatter for source codes
182 182 """
183 183
184 184 def wrap(self, source, outfile):
185 185 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
186 186
187 187 def _wrap_code(self, source):
188 188 for cnt, it in enumerate(source):
189 189 i, t = it
190 190 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
191 191 yield i, t
192 192
193 193 def _wrap_tablelinenos(self, inner):
194 194 dummyoutfile = StringIO.StringIO()
195 195 lncount = 0
196 196 for t, line in inner:
197 197 if t:
198 198 lncount += 1
199 199 dummyoutfile.write(line)
200 200
201 201 fl = self.linenostart
202 202 mw = len(str(lncount + fl - 1))
203 203 sp = self.linenospecial
204 204 st = self.linenostep
205 205 la = self.lineanchors
206 206 aln = self.anchorlinenos
207 207 nocls = self.noclasses
208 208 if sp:
209 209 lines = []
210 210
211 211 for i in range(fl, fl + lncount):
212 212 if i % st == 0:
213 213 if i % sp == 0:
214 214 if aln:
215 215 lines.append('<a href="#%s%d" class="special">%*d</a>' %
216 216 (la, i, mw, i))
217 217 else:
218 218 lines.append('<span class="special">%*d</span>' % (mw, i))
219 219 else:
220 220 if aln:
221 221 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
222 222 else:
223 223 lines.append('%*d' % (mw, i))
224 224 else:
225 225 lines.append('')
226 226 ls = '\n'.join(lines)
227 227 else:
228 228 lines = []
229 229 for i in range(fl, fl + lncount):
230 230 if i % st == 0:
231 231 if aln:
232 232 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
233 233 else:
234 234 lines.append('%*d' % (mw, i))
235 235 else:
236 236 lines.append('')
237 237 ls = '\n'.join(lines)
238 238
239 239 # in case you wonder about the seemingly redundant <div> here: since the
240 240 # content in the other cell also is wrapped in a div, some browsers in
241 241 # some configurations seem to mess up the formatting...
242 242 if nocls:
243 243 yield 0, ('<table class="%stable">' % self.cssclass +
244 244 '<tr><td><div class="linenodiv" '
245 245 'style="background-color: #f0f0f0; padding-right: 10px">'
246 246 '<pre style="line-height: 125%">' +
247 247 ls + '</pre></div></td><td id="hlcode" class="code">')
248 248 else:
249 249 yield 0, ('<table class="%stable">' % self.cssclass +
250 250 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
251 251 ls + '</pre></div></td><td id="hlcode" class="code">')
252 252 yield 0, dummyoutfile.getvalue()
253 253 yield 0, '</td></tr></table>'
254 254
255 255
256 256 def pygmentize(filenode, **kwargs):
257 257 """
258 258 pygmentize function using pygments
259 259
260 260 :param filenode:
261 261 """
262 262 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
263 263 return literal(code_highlight(filenode.content, lexer,
264 264 CodeHtmlFormatter(**kwargs)))
265 265
266 266
267 267 def pygmentize_annotation(repo_name, filenode, **kwargs):
268 268 """
269 269 pygmentize function for annotation
270 270
271 271 :param filenode:
272 272 """
273 273
274 274 color_dict = {}
275 275
276 276 def gen_color(n=10000):
277 277 """generator for getting n of evenly distributed colors using
278 278 hsv color and golden ratio. It always return same order of colors
279 279
280 280 :returns: RGB tuple
281 281 """
282 282
283 283 def hsv_to_rgb(h, s, v):
284 284 if s == 0.0:
285 285 return v, v, v
286 286 i = int(h * 6.0) # XXX assume int() truncates!
287 287 f = (h * 6.0) - i
288 288 p = v * (1.0 - s)
289 289 q = v * (1.0 - s * f)
290 290 t = v * (1.0 - s * (1.0 - f))
291 291 i = i % 6
292 292 if i == 0:
293 293 return v, t, p
294 294 if i == 1:
295 295 return q, v, p
296 296 if i == 2:
297 297 return p, v, t
298 298 if i == 3:
299 299 return p, q, v
300 300 if i == 4:
301 301 return t, p, v
302 302 if i == 5:
303 303 return v, p, q
304 304
305 305 golden_ratio = 0.618033988749895
306 306 h = 0.22717784590367374
307 307
308 308 for _ in xrange(n):
309 309 h += golden_ratio
310 310 h %= 1
311 311 HSV_tuple = [h, 0.95, 0.95]
312 312 RGB_tuple = hsv_to_rgb(*HSV_tuple)
313 313 yield map(lambda x: str(int(x * 256)), RGB_tuple)
314 314
315 315 cgenerator = gen_color()
316 316
317 317 def get_color_string(cs):
318 318 if cs in color_dict:
319 319 col = color_dict[cs]
320 320 else:
321 321 col = color_dict[cs] = cgenerator.next()
322 322 return "color: rgb(%s)! important;" % (', '.join(col))
323 323
324 324 def url_func(repo_name):
325 325
326 326 def _url_func(changeset):
327 327 author = changeset.author
328 328 date = changeset.date
329 329 message = tooltip(changeset.message)
330 330
331 331 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
332 332 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
333 333 "</b> %s<br/></div>")
334 334
335 335 tooltip_html = tooltip_html % (author, date, message)
336 336 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
337 337 short_id(changeset.raw_id))
338 338 uri = link_to(
339 339 lnk_format,
340 340 url('changeset_home', repo_name=repo_name,
341 341 revision=changeset.raw_id),
342 342 style=get_color_string(changeset.raw_id),
343 343 class_='tooltip',
344 344 title=tooltip_html
345 345 )
346 346
347 347 uri += '\n'
348 348 return uri
349 349 return _url_func
350 350
351 351 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
352 352
353 353
354 354 def is_following_repo(repo_name, user_id):
355 355 from rhodecode.model.scm import ScmModel
356 356 return ScmModel().is_following_repo(repo_name, user_id)
357 357
358 358 flash = _Flash()
359 359
360 360 #==============================================================================
361 361 # SCM FILTERS available via h.
362 362 #==============================================================================
363 363 from rhodecode.lib.vcs.utils import author_name, author_email
364 364 from rhodecode.lib.utils2 import credentials_filter, age as _age
365 365 from rhodecode.model.db import User, ChangesetStatus
366 366
367 367 age = lambda x, y=False: _age(x, y)
368 368 capitalize = lambda x: x.capitalize()
369 369 email = author_email
370 370 short_id = lambda x: x[:12]
371 371 hide_credentials = lambda x: ''.join(credentials_filter(x))
372 372
373 373
374 374 def show_id(cs):
375 375 """
376 376 Configurable function that shows ID
377 377 by default it's r123:fffeeefffeee
378 378
379 379 :param cs: changeset instance
380 380 """
381 381 from rhodecode import CONFIG
382 382 def_len = safe_int(CONFIG.get('show_sha_length', 12))
383 383 show_rev = str2bool(CONFIG.get('show_revision_number', True))
384 384
385 385 raw_id = cs.raw_id[:def_len]
386 386 if show_rev:
387 387 return 'r%s:%s' % (cs.revision, raw_id)
388 388 else:
389 389 return '%s' % (raw_id)
390 390
391 391
392 392 def fmt_date(date):
393 393 if date:
394 394 _fmt = _(u"%a, %d %b %Y %H:%M:%S").encode('utf8')
395 395 return date.strftime(_fmt).decode('utf8')
396 396
397 397 return ""
398 398
399 399
400 400 def is_git(repository):
401 401 if hasattr(repository, 'alias'):
402 402 _type = repository.alias
403 403 elif hasattr(repository, 'repo_type'):
404 404 _type = repository.repo_type
405 405 else:
406 406 _type = repository
407 407 return _type == 'git'
408 408
409 409
410 410 def is_hg(repository):
411 411 if hasattr(repository, 'alias'):
412 412 _type = repository.alias
413 413 elif hasattr(repository, 'repo_type'):
414 414 _type = repository.repo_type
415 415 else:
416 416 _type = repository
417 417 return _type == 'hg'
418 418
419 419
420 420 def email_or_none(author):
421 421 # extract email from the commit string
422 422 _email = email(author)
423 423 if _email != '':
424 424 # check it against RhodeCode database, and use the MAIN email for this
425 425 # user
426 426 user = User.get_by_email(_email, case_insensitive=True, cache=True)
427 427 if user is not None:
428 428 return user.email
429 429 return _email
430 430
431 431 # See if it contains a username we can get an email from
432 432 user = User.get_by_username(author_name(author), case_insensitive=True,
433 433 cache=True)
434 434 if user is not None:
435 435 return user.email
436 436
437 437 # No valid email, not a valid user in the system, none!
438 438 return None
439 439
440 440
441 441 def person(author, show_attr="username_and_name"):
442 442 # attr to return from fetched user
443 443 person_getter = lambda usr: getattr(usr, show_attr)
444 444
445 445 # Valid email in the attribute passed, see if they're in the system
446 446 _email = email(author)
447 447 if _email != '':
448 448 user = User.get_by_email(_email, case_insensitive=True, cache=True)
449 449 if user is not None:
450 450 return person_getter(user)
451 451
452 452 # Maybe it's a username?
453 453 _author = author_name(author)
454 454 user = User.get_by_username(_author, case_insensitive=True,
455 455 cache=True)
456 456 if user is not None:
457 457 return person_getter(user)
458 458
459 459 # Still nothing? Just pass back the author name if any, else the email
460 460 return _author or _email
461 461
462 462
463 463 def person_by_id(id_, show_attr="username_and_name"):
464 464 # attr to return from fetched user
465 465 person_getter = lambda usr: getattr(usr, show_attr)
466 466
467 467 #maybe it's an ID ?
468 468 if str(id_).isdigit() or isinstance(id_, int):
469 469 id_ = int(id_)
470 470 user = User.get(id_)
471 471 if user is not None:
472 472 return person_getter(user)
473 473 return id_
474 474
475 475
476 476 def desc_stylize(value):
477 477 """
478 478 converts tags from value into html equivalent
479 479
480 480 :param value:
481 481 """
482 482 value = re.sub(r'\[see\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
483 483 '<div class="metatag" tag="see">see =&gt; \\1 </div>', value)
484 484 value = re.sub(r'\[license\ \=\>\ *([a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\]',
485 485 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>', value)
486 486 value = re.sub(r'\[(requires|recommends|conflicts|base)\ \=\>\ *([a-zA-Z0-9\-\/]*)\]',
487 487 '<div class="metatag" tag="\\1">\\1 =&gt; <a href="/\\2">\\2</a></div>', value)
488 488 value = re.sub(r'\[(lang|language)\ \=\>\ *([a-zA-Z\-\/\#\+]*)\]',
489 489 '<div class="metatag" tag="lang">\\2</div>', value)
490 490 value = re.sub(r'\[([a-z]+)\]',
491 491 '<div class="metatag" tag="\\1">\\1</div>', value)
492 492
493 493 return value
494 494
495 495
496 496 def boolicon(value):
497 497 """Returns boolean value of a value, represented as small html image of true/false
498 498 icons
499 499
500 500 :param value: value
501 501 """
502 502
503 503 if value:
504 504 return HTML.tag('img', src=url("/images/icons/accept.png"),
505 505 alt=_('True'))
506 506 else:
507 507 return HTML.tag('img', src=url("/images/icons/cancel.png"),
508 508 alt=_('False'))
509 509
510 510
511 511 def action_parser(user_log, feed=False, parse_cs=False):
512 512 """
513 513 This helper will action_map the specified string action into translated
514 514 fancy names with icons and links
515 515
516 516 :param user_log: user log instance
517 517 :param feed: use output for feeds (no html and fancy icons)
518 518 :param parse_cs: parse Changesets into VCS instances
519 519 """
520 520
521 521 action = user_log.action
522 522 action_params = ' '
523 523
524 524 x = action.split(':')
525 525
526 526 if len(x) > 1:
527 527 action, action_params = x
528 528
529 529 def get_cs_links():
530 530 revs_limit = 3 # display this amount always
531 531 revs_top_limit = 50 # show upto this amount of changesets hidden
532 532 revs_ids = action_params.split(',')
533 533 deleted = user_log.repository is None
534 534 if deleted:
535 535 return ','.join(revs_ids)
536 536
537 537 repo_name = user_log.repository.repo_name
538 538
539 539 def lnk(rev, repo_name):
540 540 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
541 541 lazy_cs = True
542 542 if getattr(rev, 'op', None) and getattr(rev, 'ref_name', None):
543 543 lazy_cs = False
544 544 lbl = '?'
545 545 if rev.op == 'delete_branch':
546 546 lbl = '%s' % _('Deleted branch: %s') % rev.ref_name
547 547 title = ''
548 548 elif rev.op == 'tag':
549 549 lbl = '%s' % _('Created tag: %s') % rev.ref_name
550 550 title = ''
551 551 _url = '#'
552 552
553 553 else:
554 554 lbl = '%s' % (rev.short_id[:8])
555 555 _url = url('changeset_home', repo_name=repo_name,
556 556 revision=rev.raw_id)
557 557 title = tooltip(rev.message)
558 558 else:
559 559 ## changeset cannot be found/striped/removed etc.
560 560 lbl = ('%s' % rev)[:12]
561 561 _url = '#'
562 562 title = _('Changeset not found')
563 563 if parse_cs:
564 564 return link_to(lbl, _url, title=title, class_='tooltip')
565 565 return link_to(lbl, _url, raw_id=rev.raw_id, repo_name=repo_name,
566 566 class_='lazy-cs' if lazy_cs else '')
567 567
568 568 def _get_op(rev_txt):
569 569 _op = None
570 570 _name = rev_txt
571 571 if len(rev_txt.split('=>')) == 2:
572 572 _op, _name = rev_txt.split('=>')
573 573 return _op, _name
574 574
575 575 revs = []
576 576 if len(filter(lambda v: v != '', revs_ids)) > 0:
577 577 repo = None
578 578 for rev in revs_ids[:revs_top_limit]:
579 579 _op, _name = _get_op(rev)
580 580
581 581 # we want parsed changesets, or new log store format is bad
582 582 if parse_cs:
583 583 try:
584 584 if repo is None:
585 585 repo = user_log.repository.scm_instance
586 586 _rev = repo.get_changeset(rev)
587 587 revs.append(_rev)
588 588 except ChangesetDoesNotExistError:
589 589 log.error('cannot find revision %s in this repo' % rev)
590 590 revs.append(rev)
591 591 continue
592 592 else:
593 593 _rev = AttributeDict({
594 594 'short_id': rev[:12],
595 595 'raw_id': rev,
596 596 'message': '',
597 597 'op': _op,
598 598 'ref_name': _name
599 599 })
600 600 revs.append(_rev)
601 601 cs_links = []
602 602 cs_links.append(" " + ', '.join(
603 603 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
604 604 )
605 605 )
606 606 _op1, _name1 = _get_op(revs_ids[0])
607 607 _op2, _name2 = _get_op(revs_ids[-1])
608 608
609 609 _rev = '%s...%s' % (_name1, _name2)
610 610
611 611 compare_view = (
612 612 ' <div class="compare_view tooltip" title="%s">'
613 613 '<a href="%s">%s</a> </div>' % (
614 614 _('Show all combined changesets %s->%s') % (
615 615 revs_ids[0][:12], revs_ids[-1][:12]
616 616 ),
617 617 url('changeset_home', repo_name=repo_name,
618 618 revision=_rev
619 619 ),
620 620 _('compare view')
621 621 )
622 622 )
623 623
624 624 # if we have exactly one more than normally displayed
625 625 # just display it, takes less space than displaying
626 626 # "and 1 more revisions"
627 627 if len(revs_ids) == revs_limit + 1:
628 628 rev = revs[revs_limit]
629 629 cs_links.append(", " + lnk(rev, repo_name))
630 630
631 631 # hidden-by-default ones
632 632 if len(revs_ids) > revs_limit + 1:
633 633 uniq_id = revs_ids[0]
634 634 html_tmpl = (
635 635 '<span> %s <a class="show_more" id="_%s" '
636 636 'href="#more">%s</a> %s</span>'
637 637 )
638 638 if not feed:
639 639 cs_links.append(html_tmpl % (
640 640 _('and'),
641 641 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
642 642 _('revisions')
643 643 )
644 644 )
645 645
646 646 if not feed:
647 647 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
648 648 else:
649 649 html_tmpl = '<span id="%s"> %s </span>'
650 650
651 651 morelinks = ', '.join(
652 652 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
653 653 )
654 654
655 655 if len(revs_ids) > revs_top_limit:
656 656 morelinks += ', ...'
657 657
658 658 cs_links.append(html_tmpl % (uniq_id, morelinks))
659 659 if len(revs) > 1:
660 660 cs_links.append(compare_view)
661 661 return ''.join(cs_links)
662 662
663 663 def get_fork_name():
664 664 repo_name = action_params
665 665 _url = url('summary_home', repo_name=repo_name)
666 666 return _('fork name %s') % link_to(action_params, _url)
667 667
668 668 def get_user_name():
669 669 user_name = action_params
670 670 return user_name
671 671
672 672 def get_users_group():
673 673 group_name = action_params
674 674 return group_name
675 675
676 676 def get_pull_request():
677 677 pull_request_id = action_params
678 678 deleted = user_log.repository is None
679 679 if deleted:
680 680 repo_name = user_log.repository_name
681 681 else:
682 682 repo_name = user_log.repository.repo_name
683 683 return link_to(_('Pull request #%s') % pull_request_id,
684 684 url('pullrequest_show', repo_name=repo_name,
685 685 pull_request_id=pull_request_id))
686 686
687 def get_archive_name():
688 archive_name = action_params
689 return archive_name
690
687 691 # action : translated str, callback(extractor), icon
688 692 action_map = {
689 693 'user_deleted_repo': (_('[deleted] repository'),
690 694 None, 'database_delete.png'),
691 695 'user_created_repo': (_('[created] repository'),
692 696 None, 'database_add.png'),
693 697 'user_created_fork': (_('[created] repository as fork'),
694 698 None, 'arrow_divide.png'),
695 699 'user_forked_repo': (_('[forked] repository'),
696 700 get_fork_name, 'arrow_divide.png'),
697 701 'user_updated_repo': (_('[updated] repository'),
698 702 None, 'database_edit.png'),
703 'user_downloaded_archive': (_('[downloaded] archive from repository'),
704 get_archive_name, 'page_white_compressed.png'),
699 705 'admin_deleted_repo': (_('[delete] repository'),
700 706 None, 'database_delete.png'),
701 707 'admin_created_repo': (_('[created] repository'),
702 708 None, 'database_add.png'),
703 709 'admin_forked_repo': (_('[forked] repository'),
704 710 None, 'arrow_divide.png'),
705 711 'admin_updated_repo': (_('[updated] repository'),
706 712 None, 'database_edit.png'),
707 713 'admin_created_user': (_('[created] user'),
708 714 get_user_name, 'user_add.png'),
709 715 'admin_updated_user': (_('[updated] user'),
710 716 get_user_name, 'user_edit.png'),
711 717 'admin_created_users_group': (_('[created] user group'),
712 718 get_users_group, 'group_add.png'),
713 719 'admin_updated_users_group': (_('[updated] user group'),
714 720 get_users_group, 'group_edit.png'),
715 721 'user_commented_revision': (_('[commented] on revision in repository'),
716 722 get_cs_links, 'comment_add.png'),
717 723 'user_commented_pull_request': (_('[commented] on pull request for'),
718 724 get_pull_request, 'comment_add.png'),
719 725 'user_closed_pull_request': (_('[closed] pull request for'),
720 726 get_pull_request, 'tick.png'),
721 727 'push': (_('[pushed] into'),
722 728 get_cs_links, 'script_add.png'),
723 729 'push_local': (_('[committed via RhodeCode] into repository'),
724 730 get_cs_links, 'script_edit.png'),
725 731 'push_remote': (_('[pulled from remote] into repository'),
726 732 get_cs_links, 'connect.png'),
727 733 'pull': (_('[pulled] from'),
728 734 None, 'down_16.png'),
729 735 'started_following_repo': (_('[started following] repository'),
730 736 None, 'heart_add.png'),
731 737 'stopped_following_repo': (_('[stopped following] repository'),
732 738 None, 'heart_delete.png'),
733 739 }
734 740
735 741 action_str = action_map.get(action, action)
736 742 if feed:
737 743 action = action_str[0].replace('[', '').replace(']', '')
738 744 else:
739 745 action = action_str[0]\
740 746 .replace('[', '<span class="journal_highlight">')\
741 747 .replace(']', '</span>')
742 748
743 749 action_params_func = lambda: ""
744 750
745 751 if callable(action_str[1]):
746 752 action_params_func = action_str[1]
747 753
748 754 def action_parser_icon():
749 755 action = user_log.action
750 756 action_params = None
751 757 x = action.split(':')
752 758
753 759 if len(x) > 1:
754 760 action, action_params = x
755 761
756 762 tmpl = """<img src="%s%s" alt="%s"/>"""
757 763 ico = action_map.get(action, ['', '', ''])[2]
758 764 return literal(tmpl % ((url('/images/icons/')), ico, action))
759 765
760 766 # returned callbacks we need to call to get
761 767 return [lambda: literal(action), action_params_func, action_parser_icon]
762 768
763 769
764 770
765 771 #==============================================================================
766 772 # PERMS
767 773 #==============================================================================
768 774 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
769 775 HasRepoPermissionAny, HasRepoPermissionAll, HasReposGroupPermissionAll, \
770 776 HasReposGroupPermissionAny
771 777
772 778
773 779 #==============================================================================
774 780 # GRAVATAR URL
775 781 #==============================================================================
776 782
777 783 def gravatar_url(email_address, size=30):
778 784 from pylons import url # doh, we need to re-import url to mock it later
779 785 _def = 'anonymous@rhodecode.org'
780 786 use_gravatar = str2bool(config['app_conf'].get('use_gravatar'))
781 787 email_address = email_address or _def
782 788 if (not use_gravatar or not email_address or email_address == _def):
783 789 f = lambda a, l: min(l, key=lambda x: abs(x - a))
784 790 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
785 791
786 792 if use_gravatar and config['app_conf'].get('alternative_gravatar_url'):
787 793 tmpl = config['app_conf'].get('alternative_gravatar_url', '')
788 794 parsed_url = urlparse.urlparse(url.current(qualified=True))
789 795 tmpl = tmpl.replace('{email}', email_address)\
790 796 .replace('{md5email}', hashlib.md5(email_address.lower()).hexdigest()) \
791 797 .replace('{netloc}', parsed_url.netloc)\
792 798 .replace('{scheme}', parsed_url.scheme)\
793 799 .replace('{size}', str(size))
794 800 return tmpl
795 801
796 802 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
797 803 default = 'identicon'
798 804 baseurl_nossl = "http://www.gravatar.com/avatar/"
799 805 baseurl_ssl = "https://secure.gravatar.com/avatar/"
800 806 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
801 807
802 808 if isinstance(email_address, unicode):
803 809 #hashlib crashes on unicode items
804 810 email_address = safe_str(email_address)
805 811 # construct the url
806 812 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
807 813 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
808 814
809 815 return gravatar_url
810 816
811 817
812 818 class Page(_Page):
813 819 """
814 820 Custom pager to match rendering style with YUI paginator
815 821 """
816 822
817 823 def _get_pos(self, cur_page, max_page, items):
818 824 edge = (items / 2) + 1
819 825 if (cur_page <= edge):
820 826 radius = max(items / 2, items - cur_page)
821 827 elif (max_page - cur_page) < edge:
822 828 radius = (items - 1) - (max_page - cur_page)
823 829 else:
824 830 radius = items / 2
825 831
826 832 left = max(1, (cur_page - (radius)))
827 833 right = min(max_page, cur_page + (radius))
828 834 return left, cur_page, right
829 835
830 836 def _range(self, regexp_match):
831 837 """
832 838 Return range of linked pages (e.g. '1 2 [3] 4 5 6 7 8').
833 839
834 840 Arguments:
835 841
836 842 regexp_match
837 843 A "re" (regular expressions) match object containing the
838 844 radius of linked pages around the current page in
839 845 regexp_match.group(1) as a string
840 846
841 847 This function is supposed to be called as a callable in
842 848 re.sub.
843 849
844 850 """
845 851 radius = int(regexp_match.group(1))
846 852
847 853 # Compute the first and last page number within the radius
848 854 # e.g. '1 .. 5 6 [7] 8 9 .. 12'
849 855 # -> leftmost_page = 5
850 856 # -> rightmost_page = 9
851 857 leftmost_page, _cur, rightmost_page = self._get_pos(self.page,
852 858 self.last_page,
853 859 (radius * 2) + 1)
854 860 nav_items = []
855 861
856 862 # Create a link to the first page (unless we are on the first page
857 863 # or there would be no need to insert '..' spacers)
858 864 if self.page != self.first_page and self.first_page < leftmost_page:
859 865 nav_items.append(self._pagerlink(self.first_page, self.first_page))
860 866
861 867 # Insert dots if there are pages between the first page
862 868 # and the currently displayed page range
863 869 if leftmost_page - self.first_page > 1:
864 870 # Wrap in a SPAN tag if nolink_attr is set
865 871 text = '..'
866 872 if self.dotdot_attr:
867 873 text = HTML.span(c=text, **self.dotdot_attr)
868 874 nav_items.append(text)
869 875
870 876 for thispage in xrange(leftmost_page, rightmost_page + 1):
871 877 # Hilight the current page number and do not use a link
872 878 if thispage == self.page:
873 879 text = '%s' % (thispage,)
874 880 # Wrap in a SPAN tag if nolink_attr is set
875 881 if self.curpage_attr:
876 882 text = HTML.span(c=text, **self.curpage_attr)
877 883 nav_items.append(text)
878 884 # Otherwise create just a link to that page
879 885 else:
880 886 text = '%s' % (thispage,)
881 887 nav_items.append(self._pagerlink(thispage, text))
882 888
883 889 # Insert dots if there are pages between the displayed
884 890 # page numbers and the end of the page range
885 891 if self.last_page - rightmost_page > 1:
886 892 text = '..'
887 893 # Wrap in a SPAN tag if nolink_attr is set
888 894 if self.dotdot_attr:
889 895 text = HTML.span(c=text, **self.dotdot_attr)
890 896 nav_items.append(text)
891 897
892 898 # Create a link to the very last page (unless we are on the last
893 899 # page or there would be no need to insert '..' spacers)
894 900 if self.page != self.last_page and rightmost_page < self.last_page:
895 901 nav_items.append(self._pagerlink(self.last_page, self.last_page))
896 902
897 903 return self.separator.join(nav_items)
898 904
899 905 def pager(self, format='~2~', page_param='page', partial_param='partial',
900 906 show_if_single_page=False, separator=' ', onclick=None,
901 907 symbol_first='<<', symbol_last='>>',
902 908 symbol_previous='<', symbol_next='>',
903 909 link_attr={'class': 'pager_link'},
904 910 curpage_attr={'class': 'pager_curpage'},
905 911 dotdot_attr={'class': 'pager_dotdot'}, **kwargs):
906 912
907 913 self.curpage_attr = curpage_attr
908 914 self.separator = separator
909 915 self.pager_kwargs = kwargs
910 916 self.page_param = page_param
911 917 self.partial_param = partial_param
912 918 self.onclick = onclick
913 919 self.link_attr = link_attr
914 920 self.dotdot_attr = dotdot_attr
915 921
916 922 # Don't show navigator if there is no more than one page
917 923 if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page):
918 924 return ''
919 925
920 926 from string import Template
921 927 # Replace ~...~ in token format by range of pages
922 928 result = re.sub(r'~(\d+)~', self._range, format)
923 929
924 930 # Interpolate '%' variables
925 931 result = Template(result).safe_substitute({
926 932 'first_page': self.first_page,
927 933 'last_page': self.last_page,
928 934 'page': self.page,
929 935 'page_count': self.page_count,
930 936 'items_per_page': self.items_per_page,
931 937 'first_item': self.first_item,
932 938 'last_item': self.last_item,
933 939 'item_count': self.item_count,
934 940 'link_first': self.page > self.first_page and \
935 941 self._pagerlink(self.first_page, symbol_first) or '',
936 942 'link_last': self.page < self.last_page and \
937 943 self._pagerlink(self.last_page, symbol_last) or '',
938 944 'link_previous': self.previous_page and \
939 945 self._pagerlink(self.previous_page, symbol_previous) \
940 946 or HTML.span(symbol_previous, class_="yui-pg-previous"),
941 947 'link_next': self.next_page and \
942 948 self._pagerlink(self.next_page, symbol_next) \
943 949 or HTML.span(symbol_next, class_="yui-pg-next")
944 950 })
945 951
946 952 return literal(result)
947 953
948 954
949 955 #==============================================================================
950 956 # REPO PAGER, PAGER FOR REPOSITORY
951 957 #==============================================================================
952 958 class RepoPage(Page):
953 959
954 960 def __init__(self, collection, page=1, items_per_page=20,
955 961 item_count=None, url=None, **kwargs):
956 962
957 963 """Create a "RepoPage" instance. special pager for paging
958 964 repository
959 965 """
960 966 self._url_generator = url
961 967
962 968 # Safe the kwargs class-wide so they can be used in the pager() method
963 969 self.kwargs = kwargs
964 970
965 971 # Save a reference to the collection
966 972 self.original_collection = collection
967 973
968 974 self.collection = collection
969 975
970 976 # The self.page is the number of the current page.
971 977 # The first page has the number 1!
972 978 try:
973 979 self.page = int(page) # make it int() if we get it as a string
974 980 except (ValueError, TypeError):
975 981 self.page = 1
976 982
977 983 self.items_per_page = items_per_page
978 984
979 985 # Unless the user tells us how many items the collections has
980 986 # we calculate that ourselves.
981 987 if item_count is not None:
982 988 self.item_count = item_count
983 989 else:
984 990 self.item_count = len(self.collection)
985 991
986 992 # Compute the number of the first and last available page
987 993 if self.item_count > 0:
988 994 self.first_page = 1
989 995 self.page_count = int(math.ceil(float(self.item_count) /
990 996 self.items_per_page))
991 997 self.last_page = self.first_page + self.page_count - 1
992 998
993 999 # Make sure that the requested page number is the range of
994 1000 # valid pages
995 1001 if self.page > self.last_page:
996 1002 self.page = self.last_page
997 1003 elif self.page < self.first_page:
998 1004 self.page = self.first_page
999 1005
1000 1006 # Note: the number of items on this page can be less than
1001 1007 # items_per_page if the last page is not full
1002 1008 self.first_item = max(0, (self.item_count) - (self.page *
1003 1009 items_per_page))
1004 1010 self.last_item = ((self.item_count - 1) - items_per_page *
1005 1011 (self.page - 1))
1006 1012
1007 1013 self.items = list(self.collection[self.first_item:self.last_item + 1])
1008 1014
1009 1015 # Links to previous and next page
1010 1016 if self.page > self.first_page:
1011 1017 self.previous_page = self.page - 1
1012 1018 else:
1013 1019 self.previous_page = None
1014 1020
1015 1021 if self.page < self.last_page:
1016 1022 self.next_page = self.page + 1
1017 1023 else:
1018 1024 self.next_page = None
1019 1025
1020 1026 # No items available
1021 1027 else:
1022 1028 self.first_page = None
1023 1029 self.page_count = 0
1024 1030 self.last_page = None
1025 1031 self.first_item = None
1026 1032 self.last_item = None
1027 1033 self.previous_page = None
1028 1034 self.next_page = None
1029 1035 self.items = []
1030 1036
1031 1037 # This is a subclass of the 'list' type. Initialise the list now.
1032 1038 list.__init__(self, reversed(self.items))
1033 1039
1034 1040
1035 1041 def changed_tooltip(nodes):
1036 1042 """
1037 1043 Generates a html string for changed nodes in changeset page.
1038 1044 It limits the output to 30 entries
1039 1045
1040 1046 :param nodes: LazyNodesGenerator
1041 1047 """
1042 1048 if nodes:
1043 1049 pref = ': <br/> '
1044 1050 suf = ''
1045 1051 if len(nodes) > 30:
1046 1052 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
1047 1053 return literal(pref + '<br/> '.join([safe_unicode(x.path)
1048 1054 for x in nodes[:30]]) + suf)
1049 1055 else:
1050 1056 return ': ' + _('No Files')
1051 1057
1052 1058
1053 1059 def repo_link(groups_and_repos):
1054 1060 """
1055 1061 Makes a breadcrumbs link to repo within a group
1056 1062 joins &raquo; on each group to create a fancy link
1057 1063
1058 1064 ex::
1059 1065 group >> subgroup >> repo
1060 1066
1061 1067 :param groups_and_repos:
1062 1068 :param last_url:
1063 1069 """
1064 1070 groups, just_name, repo_name = groups_and_repos
1065 1071 last_url = url('summary_home', repo_name=repo_name)
1066 1072 last_link = link_to(just_name, last_url)
1067 1073
1068 1074 def make_link(group):
1069 1075 return link_to(group.name,
1070 1076 url('repos_group_home', group_name=group.group_name))
1071 1077 return literal(' &raquo; '.join(map(make_link, groups) + ['<span>%s</span>' % last_link]))
1072 1078
1073 1079
1074 1080 def fancy_file_stats(stats):
1075 1081 """
1076 1082 Displays a fancy two colored bar for number of added/deleted
1077 1083 lines of code on file
1078 1084
1079 1085 :param stats: two element list of added/deleted lines of code
1080 1086 """
1081 1087 from rhodecode.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
1082 1088 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
1083 1089
1084 1090 def cgen(l_type, a_v, d_v):
1085 1091 mapping = {'tr': 'top-right-rounded-corner-mid',
1086 1092 'tl': 'top-left-rounded-corner-mid',
1087 1093 'br': 'bottom-right-rounded-corner-mid',
1088 1094 'bl': 'bottom-left-rounded-corner-mid'}
1089 1095 map_getter = lambda x: mapping[x]
1090 1096
1091 1097 if l_type == 'a' and d_v:
1092 1098 #case when added and deleted are present
1093 1099 return ' '.join(map(map_getter, ['tl', 'bl']))
1094 1100
1095 1101 if l_type == 'a' and not d_v:
1096 1102 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1097 1103
1098 1104 if l_type == 'd' and a_v:
1099 1105 return ' '.join(map(map_getter, ['tr', 'br']))
1100 1106
1101 1107 if l_type == 'd' and not a_v:
1102 1108 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
1103 1109
1104 1110 a, d = stats['added'], stats['deleted']
1105 1111 width = 100
1106 1112
1107 1113 if stats['binary']:
1108 1114 #binary mode
1109 1115 lbl = ''
1110 1116 bin_op = 1
1111 1117
1112 1118 if BIN_FILENODE in stats['ops']:
1113 1119 lbl = 'bin+'
1114 1120
1115 1121 if NEW_FILENODE in stats['ops']:
1116 1122 lbl += _('new file')
1117 1123 bin_op = NEW_FILENODE
1118 1124 elif MOD_FILENODE in stats['ops']:
1119 1125 lbl += _('mod')
1120 1126 bin_op = MOD_FILENODE
1121 1127 elif DEL_FILENODE in stats['ops']:
1122 1128 lbl += _('del')
1123 1129 bin_op = DEL_FILENODE
1124 1130 elif RENAMED_FILENODE in stats['ops']:
1125 1131 lbl += _('rename')
1126 1132 bin_op = RENAMED_FILENODE
1127 1133
1128 1134 #chmod can go with other operations
1129 1135 if CHMOD_FILENODE in stats['ops']:
1130 1136 _org_lbl = _('chmod')
1131 1137 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
1132 1138
1133 1139 #import ipdb;ipdb.set_trace()
1134 1140 b_d = '<div class="bin bin%s %s" style="width:100%%">%s</div>' % (bin_op, cgen('a', a_v='', d_v=0), lbl)
1135 1141 b_a = '<div class="bin bin1" style="width:0%%"></div>'
1136 1142 return literal('<div style="width:%spx">%s%s</div>' % (width, b_a, b_d))
1137 1143
1138 1144 t = stats['added'] + stats['deleted']
1139 1145 unit = float(width) / (t or 1)
1140 1146
1141 1147 # needs > 9% of width to be visible or 0 to be hidden
1142 1148 a_p = max(9, unit * a) if a > 0 else 0
1143 1149 d_p = max(9, unit * d) if d > 0 else 0
1144 1150 p_sum = a_p + d_p
1145 1151
1146 1152 if p_sum > width:
1147 1153 #adjust the percentage to be == 100% since we adjusted to 9
1148 1154 if a_p > d_p:
1149 1155 a_p = a_p - (p_sum - width)
1150 1156 else:
1151 1157 d_p = d_p - (p_sum - width)
1152 1158
1153 1159 a_v = a if a > 0 else ''
1154 1160 d_v = d if d > 0 else ''
1155 1161
1156 1162 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
1157 1163 cgen('a', a_v, d_v), a_p, a_v
1158 1164 )
1159 1165 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
1160 1166 cgen('d', a_v, d_v), d_p, d_v
1161 1167 )
1162 1168 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
1163 1169
1164 1170
1165 1171 def urlify_text(text_, safe=True):
1166 1172 """
1167 1173 Extrac urls from text and make html links out of them
1168 1174
1169 1175 :param text_:
1170 1176 """
1171 1177
1172 1178 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
1173 1179 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1174 1180
1175 1181 def url_func(match_obj):
1176 1182 url_full = match_obj.groups()[0]
1177 1183 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
1178 1184 _newtext = url_pat.sub(url_func, text_)
1179 1185 if safe:
1180 1186 return literal(_newtext)
1181 1187 return _newtext
1182 1188
1183 1189
1184 1190 def urlify_changesets(text_, repository):
1185 1191 """
1186 1192 Extract revision ids from changeset and make link from them
1187 1193
1188 1194 :param text_:
1189 1195 :param repository: repo name to build the URL with
1190 1196 """
1191 1197 from pylons import url # doh, we need to re-import url to mock it later
1192 1198 URL_PAT = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1193 1199
1194 1200 def url_func(match_obj):
1195 1201 rev = match_obj.groups()[1]
1196 1202 pref = match_obj.groups()[0]
1197 1203 suf = match_obj.groups()[2]
1198 1204
1199 1205 tmpl = (
1200 1206 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1201 1207 '%(rev)s</a>%(suf)s'
1202 1208 )
1203 1209 return tmpl % {
1204 1210 'pref': pref,
1205 1211 'cls': 'revision-link',
1206 1212 'url': url('changeset_home', repo_name=repository, revision=rev),
1207 1213 'rev': rev,
1208 1214 'suf': suf
1209 1215 }
1210 1216
1211 1217 newtext = URL_PAT.sub(url_func, text_)
1212 1218
1213 1219 return newtext
1214 1220
1215 1221
1216 1222 def urlify_commit(text_, repository=None, link_=None):
1217 1223 """
1218 1224 Parses given text message and makes proper links.
1219 1225 issues are linked to given issue-server, and rest is a changeset link
1220 1226 if link_ is given, in other case it's a plain text
1221 1227
1222 1228 :param text_:
1223 1229 :param repository:
1224 1230 :param link_: changeset link
1225 1231 """
1226 1232 import traceback
1227 1233 from pylons import url # doh, we need to re-import url to mock it later
1228 1234
1229 1235 def escaper(string):
1230 1236 return string.replace('<', '&lt;').replace('>', '&gt;')
1231 1237
1232 1238 def linkify_others(t, l):
1233 1239 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1234 1240 links = []
1235 1241 for e in urls.split(t):
1236 1242 if not urls.match(e):
1237 1243 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1238 1244 else:
1239 1245 links.append(e)
1240 1246
1241 1247 return ''.join(links)
1242 1248
1243 1249 # urlify changesets - extrac revisions and make link out of them
1244 1250 newtext = urlify_changesets(escaper(text_), repository)
1245 1251
1246 1252 # extract http/https links and make them real urls
1247 1253 newtext = urlify_text(newtext, safe=False)
1248 1254
1249 1255 try:
1250 1256 from rhodecode import CONFIG
1251 1257 conf = CONFIG
1252 1258
1253 1259 # allow multiple issue servers to be used
1254 1260 valid_indices = [
1255 1261 x.group(1)
1256 1262 for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
1257 1263 if x and 'issue_server_link%s' % x.group(1) in conf
1258 1264 and 'issue_prefix%s' % x.group(1) in conf
1259 1265 ]
1260 1266
1261 1267 log.debug('found issue server suffixes `%s` during valuation of: %s'
1262 1268 % (','.join(valid_indices), newtext))
1263 1269
1264 1270 for pattern_index in valid_indices:
1265 1271 ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
1266 1272 ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
1267 1273 ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
1268 1274
1269 1275 log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
1270 1276 % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
1271 1277 ISSUE_PREFIX))
1272 1278
1273 1279 URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
1274 1280
1275 1281 def url_func(match_obj):
1276 1282 pref = ''
1277 1283 if match_obj.group().startswith(' '):
1278 1284 pref = ' '
1279 1285
1280 1286 issue_id = ''.join(match_obj.groups())
1281 1287 tmpl = (
1282 1288 '%(pref)s<a class="%(cls)s" href="%(url)s">'
1283 1289 '%(issue-prefix)s%(id-repr)s'
1284 1290 '</a>'
1285 1291 )
1286 1292 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
1287 1293 if repository:
1288 1294 url = url.replace('{repo}', repository)
1289 1295 repo_name = repository.split(URL_SEP)[-1]
1290 1296 url = url.replace('{repo_name}', repo_name)
1291 1297
1292 1298 return tmpl % {
1293 1299 'pref': pref,
1294 1300 'cls': 'issue-tracker-link',
1295 1301 'url': url,
1296 1302 'id-repr': issue_id,
1297 1303 'issue-prefix': ISSUE_PREFIX,
1298 1304 'serv': ISSUE_SERVER_LNK,
1299 1305 }
1300 1306 newtext = URL_PAT.sub(url_func, newtext)
1301 1307 log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
1302 1308
1303 1309 # if we actually did something above
1304 1310 if link_:
1305 1311 # wrap not links into final link => link_
1306 1312 newtext = linkify_others(newtext, link_)
1307 1313 except Exception:
1308 1314 log.error(traceback.format_exc())
1309 1315 pass
1310 1316
1311 1317 return literal(newtext)
1312 1318
1313 1319
1314 1320 def rst(source):
1315 1321 return literal('<div class="rst-block">%s</div>' %
1316 1322 MarkupRenderer.rst(source))
1317 1323
1318 1324
1319 1325 def rst_w_mentions(source):
1320 1326 """
1321 1327 Wrapped rst renderer with @mention highlighting
1322 1328
1323 1329 :param source:
1324 1330 """
1325 1331 return literal('<div class="rst-block">%s</div>' %
1326 1332 MarkupRenderer.rst_with_mentions(source))
1327 1333
1328 1334
1329 1335 def changeset_status(repo, revision):
1330 1336 return ChangesetStatusModel().get_status(repo, revision)
1331 1337
1332 1338
1333 1339 def changeset_status_lbl(changeset_status):
1334 1340 return dict(ChangesetStatus.STATUSES).get(changeset_status)
1335 1341
1336 1342
1337 1343 def get_permission_name(key):
1338 1344 return dict(Permission.PERMS).get(key)
1339 1345
1340 1346
1341 1347 def journal_filter_help():
1342 1348 return _(textwrap.dedent('''
1343 1349 Example filter terms:
1344 1350 repository:vcs
1345 1351 username:marcin
1346 1352 action:*push*
1347 1353 ip:127.0.0.1
1348 1354 date:20120101
1349 1355 date:[20120101100000 TO 20120102]
1350 1356
1351 1357 Generate wildcards using '*' character:
1352 1358 "repositroy:vcs*" - search everything starting with 'vcs'
1353 1359 "repository:*vcs*" - search for repository containing 'vcs'
1354 1360
1355 1361 Optional AND / OR operators in queries
1356 1362 "repository:vcs OR repository:test"
1357 1363 "username:test AND repository:test*"
1358 1364 '''))
1359 1365
1360 1366
1361 1367 def not_mapped_error(repo_name):
1362 1368 flash(_('%s repository is not mapped to db perhaps'
1363 1369 ' it was created or renamed from the filesystem'
1364 1370 ' please run the application again'
1365 1371 ' in order to rescan repositories') % repo_name, category='error')
1366 1372
1367 1373
1368 1374 def ip_range(ip_addr):
1369 1375 from rhodecode.model.db import UserIpMap
1370 1376 s, e = UserIpMap._get_ip_range(ip_addr)
1371 1377 return '%s - %s' % (s, e)
@@ -1,2218 +1,2223 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 time
28 28 import logging
29 29 import datetime
30 30 import traceback
31 31 import hashlib
32 32 import collections
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47 from rhodecode.lib.vcs.backends.base import EmptyChangeset
48 48
49 49 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
50 50 safe_unicode, remove_suffix, remove_prefix, time_to_datetime, _set_extras
51 51 from rhodecode.lib.compat import json
52 52 from rhodecode.lib.caching_query import FromCache
53 53
54 54 from rhodecode.model.meta import Base, Session
55 55
56 56 URL_SEP = '/'
57 57 log = logging.getLogger(__name__)
58 58
59 59 #==============================================================================
60 60 # BASE CLASSES
61 61 #==============================================================================
62 62
63 63 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
64 64
65 65
66 66 class BaseModel(object):
67 67 """
68 68 Base Model for all classess
69 69 """
70 70
71 71 @classmethod
72 72 def _get_keys(cls):
73 73 """return column names for this model """
74 74 return class_mapper(cls).c.keys()
75 75
76 76 def get_dict(self):
77 77 """
78 78 return dict with keys and values corresponding
79 79 to this model data """
80 80
81 81 d = {}
82 82 for k in self._get_keys():
83 83 d[k] = getattr(self, k)
84 84
85 85 # also use __json__() if present to get additional fields
86 86 _json_attr = getattr(self, '__json__', None)
87 87 if _json_attr:
88 88 # update with attributes from __json__
89 89 if callable(_json_attr):
90 90 _json_attr = _json_attr()
91 91 for k, val in _json_attr.iteritems():
92 92 d[k] = val
93 93 return d
94 94
95 95 def get_appstruct(self):
96 96 """return list with keys and values tupples corresponding
97 97 to this model data """
98 98
99 99 l = []
100 100 for k in self._get_keys():
101 101 l.append((k, getattr(self, k),))
102 102 return l
103 103
104 104 def populate_obj(self, populate_dict):
105 105 """populate model with data from given populate_dict"""
106 106
107 107 for k in self._get_keys():
108 108 if k in populate_dict:
109 109 setattr(self, k, populate_dict[k])
110 110
111 111 @classmethod
112 112 def query(cls):
113 113 return Session().query(cls)
114 114
115 115 @classmethod
116 116 def get(cls, id_):
117 117 if id_:
118 118 return cls.query().get(id_)
119 119
120 120 @classmethod
121 121 def get_or_404(cls, id_):
122 122 try:
123 123 id_ = int(id_)
124 124 except (TypeError, ValueError):
125 125 raise HTTPNotFound
126 126
127 127 res = cls.query().get(id_)
128 128 if not res:
129 129 raise HTTPNotFound
130 130 return res
131 131
132 132 @classmethod
133 133 def getAll(cls):
134 134 # deprecated and left for backward compatibility
135 135 return cls.get_all()
136 136
137 137 @classmethod
138 138 def get_all(cls):
139 139 return cls.query().all()
140 140
141 141 @classmethod
142 142 def delete(cls, id_):
143 143 obj = cls.query().get(id_)
144 144 Session().delete(obj)
145 145
146 146 def __repr__(self):
147 147 if hasattr(self, '__unicode__'):
148 148 # python repr needs to return str
149 149 return safe_str(self.__unicode__())
150 150 return '<DB:%s>' % (self.__class__.__name__)
151 151
152 152
153 153 class RhodeCodeSetting(Base, BaseModel):
154 154 __tablename__ = 'rhodecode_settings'
155 155 __table_args__ = (
156 156 UniqueConstraint('app_settings_name'),
157 157 {'extend_existing': True, 'mysql_engine': 'InnoDB',
158 158 'mysql_charset': 'utf8'}
159 159 )
160 160 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
161 161 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
162 162 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
163 163
164 164 def __init__(self, k='', v=''):
165 165 self.app_settings_name = k
166 166 self.app_settings_value = v
167 167
168 168 @validates('_app_settings_value')
169 169 def validate_settings_value(self, key, val):
170 170 assert type(val) == unicode
171 171 return val
172 172
173 173 @hybrid_property
174 174 def app_settings_value(self):
175 175 v = self._app_settings_value
176 176 if self.app_settings_name in ["ldap_active",
177 177 "default_repo_enable_statistics",
178 178 "default_repo_enable_locking",
179 179 "default_repo_private",
180 180 "default_repo_enable_downloads"]:
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, key):
201 201 return cls.query()\
202 202 .filter(cls.app_settings_name == key).scalar()
203 203
204 204 @classmethod
205 205 def get_by_name_or_create(cls, key):
206 206 res = cls.get_by_name(key)
207 207 if not res:
208 208 res = cls(key)
209 209 return res
210 210
211 211 @classmethod
212 212 def get_app_settings(cls, cache=False):
213 213
214 214 ret = cls.query()
215 215
216 216 if cache:
217 217 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
218 218
219 219 if not ret:
220 220 raise Exception('Could not get application settings !')
221 221 settings = {}
222 222 for each in ret:
223 223 settings['rhodecode_' + each.app_settings_name] = \
224 224 each.app_settings_value
225 225
226 226 return settings
227 227
228 228 @classmethod
229 229 def get_ldap_settings(cls, cache=False):
230 230 ret = cls.query()\
231 231 .filter(cls.app_settings_name.startswith('ldap_')).all()
232 232 fd = {}
233 233 for row in ret:
234 234 fd.update({row.app_settings_name: row.app_settings_value})
235 235
236 236 return fd
237 237
238 238 @classmethod
239 239 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
240 240 ret = cls.query()\
241 241 .filter(cls.app_settings_name.startswith('default_')).all()
242 242 fd = {}
243 243 for row in ret:
244 244 key = row.app_settings_name
245 245 if strip_prefix:
246 246 key = remove_prefix(key, prefix='default_')
247 247 fd.update({key: row.app_settings_value})
248 248
249 249 return fd
250 250
251 251
252 252 class RhodeCodeUi(Base, BaseModel):
253 253 __tablename__ = 'rhodecode_ui'
254 254 __table_args__ = (
255 255 UniqueConstraint('ui_key'),
256 256 {'extend_existing': True, 'mysql_engine': 'InnoDB',
257 257 'mysql_charset': 'utf8'}
258 258 )
259 259
260 260 HOOK_UPDATE = 'changegroup.update'
261 261 HOOK_REPO_SIZE = 'changegroup.repo_size'
262 262 HOOK_PUSH = 'changegroup.push_logger'
263 263 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
264 264 HOOK_PULL = 'outgoing.pull_logger'
265 265 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
266 266
267 267 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
268 268 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
270 270 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
271 271 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
272 272
273 273 @classmethod
274 274 def get_by_key(cls, key):
275 275 return cls.query().filter(cls.ui_key == key).scalar()
276 276
277 277 @classmethod
278 278 def get_builtin_hooks(cls):
279 279 q = cls.query()
280 280 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
281 281 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
282 282 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
283 283 return q.all()
284 284
285 285 @classmethod
286 286 def get_custom_hooks(cls):
287 287 q = cls.query()
288 288 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
289 289 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
290 290 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
291 291 q = q.filter(cls.ui_section == 'hooks')
292 292 return q.all()
293 293
294 294 @classmethod
295 295 def get_repos_location(cls):
296 296 return cls.get_by_key('/').ui_value
297 297
298 298 @classmethod
299 299 def create_or_update_hook(cls, key, val):
300 300 new_ui = cls.get_by_key(key) or cls()
301 301 new_ui.ui_section = 'hooks'
302 302 new_ui.ui_active = True
303 303 new_ui.ui_key = key
304 304 new_ui.ui_value = val
305 305
306 306 Session().add(new_ui)
307 307
308 308 def __repr__(self):
309 309 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
310 310 self.ui_value)
311 311
312 312
313 313 class User(Base, BaseModel):
314 314 __tablename__ = 'users'
315 315 __table_args__ = (
316 316 UniqueConstraint('username'), UniqueConstraint('email'),
317 317 Index('u_username_idx', 'username'),
318 318 Index('u_email_idx', 'email'),
319 319 {'extend_existing': True, 'mysql_engine': 'InnoDB',
320 320 'mysql_charset': 'utf8'}
321 321 )
322 322 DEFAULT_USER = 'default'
323 323
324 324 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
325 325 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
326 326 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
327 327 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
328 328 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
329 329 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
330 330 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
331 331 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
332 332 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
333 333 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
334 334 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
335 335 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
336 336
337 337 user_log = relationship('UserLog')
338 338 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
339 339
340 340 repositories = relationship('Repository')
341 341 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
342 342 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
343 343
344 344 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
345 345 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
346 346
347 347 group_member = relationship('UserGroupMember', cascade='all')
348 348
349 349 notifications = relationship('UserNotification', cascade='all')
350 350 # notifications assigned to this user
351 351 user_created_notifications = relationship('Notification', cascade='all')
352 352 # comments created by this user
353 353 user_comments = relationship('ChangesetComment', cascade='all')
354 354 #extra emails for this user
355 355 user_emails = relationship('UserEmailMap', cascade='all')
356 356
357 357 @hybrid_property
358 358 def email(self):
359 359 return self._email
360 360
361 361 @email.setter
362 362 def email(self, val):
363 363 self._email = val.lower() if val else None
364 364
365 365 @property
366 366 def firstname(self):
367 367 # alias for future
368 368 return self.name
369 369
370 370 @property
371 371 def emails(self):
372 372 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
373 373 return [self.email] + [x.email for x in other]
374 374
375 375 @property
376 376 def ip_addresses(self):
377 377 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
378 378 return [x.ip_addr for x in ret]
379 379
380 380 @property
381 381 def username_and_name(self):
382 382 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
383 383
384 384 @property
385 385 def full_name(self):
386 386 return '%s %s' % (self.firstname, self.lastname)
387 387
388 388 @property
389 389 def full_name_or_username(self):
390 390 return ('%s %s' % (self.firstname, self.lastname)
391 391 if (self.firstname and self.lastname) else self.username)
392 392
393 393 @property
394 394 def full_contact(self):
395 395 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
396 396
397 397 @property
398 398 def short_contact(self):
399 399 return '%s %s' % (self.firstname, self.lastname)
400 400
401 401 @property
402 402 def is_admin(self):
403 403 return self.admin
404 404
405 405 @property
406 406 def AuthUser(self):
407 407 """
408 408 Returns instance of AuthUser for this user
409 409 """
410 410 from rhodecode.lib.auth import AuthUser
411 411 return AuthUser(user_id=self.user_id, api_key=self.api_key,
412 412 username=self.username)
413 413
414 414 def __unicode__(self):
415 415 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
416 416 self.user_id, self.username)
417 417
418 418 @classmethod
419 419 def get_by_username(cls, username, case_insensitive=False, cache=False):
420 420 if case_insensitive:
421 421 q = cls.query().filter(cls.username.ilike(username))
422 422 else:
423 423 q = cls.query().filter(cls.username == username)
424 424
425 425 if cache:
426 426 q = q.options(FromCache(
427 427 "sql_cache_short",
428 428 "get_user_%s" % _hash_key(username)
429 429 )
430 430 )
431 431 return q.scalar()
432 432
433 433 @classmethod
434 434 def get_by_api_key(cls, api_key, cache=False):
435 435 q = cls.query().filter(cls.api_key == api_key)
436 436
437 437 if cache:
438 438 q = q.options(FromCache("sql_cache_short",
439 439 "get_api_key_%s" % api_key))
440 440 return q.scalar()
441 441
442 442 @classmethod
443 443 def get_by_email(cls, email, case_insensitive=False, cache=False):
444 444 if case_insensitive:
445 445 q = cls.query().filter(cls.email.ilike(email))
446 446 else:
447 447 q = cls.query().filter(cls.email == email)
448 448
449 449 if cache:
450 450 q = q.options(FromCache("sql_cache_short",
451 451 "get_email_key_%s" % email))
452 452
453 453 ret = q.scalar()
454 454 if ret is None:
455 455 q = UserEmailMap.query()
456 456 # try fetching in alternate email map
457 457 if case_insensitive:
458 458 q = q.filter(UserEmailMap.email.ilike(email))
459 459 else:
460 460 q = q.filter(UserEmailMap.email == email)
461 461 q = q.options(joinedload(UserEmailMap.user))
462 462 if cache:
463 463 q = q.options(FromCache("sql_cache_short",
464 464 "get_email_map_key_%s" % email))
465 465 ret = getattr(q.scalar(), 'user', None)
466 466
467 467 return ret
468 468
469 469 @classmethod
470 470 def get_from_cs_author(cls, author):
471 471 """
472 472 Tries to get User objects out of commit author string
473 473
474 474 :param author:
475 475 """
476 476 from rhodecode.lib.helpers import email, author_name
477 477 # Valid email in the attribute passed, see if they're in the system
478 478 _email = email(author)
479 479 if _email:
480 480 user = cls.get_by_email(_email, case_insensitive=True)
481 481 if user:
482 482 return user
483 483 # Maybe we can match by username?
484 484 _author = author_name(author)
485 485 user = cls.get_by_username(_author, case_insensitive=True)
486 486 if user:
487 487 return user
488 488
489 489 def update_lastlogin(self):
490 490 """Update user lastlogin"""
491 491 self.last_login = datetime.datetime.now()
492 492 Session().add(self)
493 493 log.debug('updated user %s lastlogin' % self.username)
494 494
495 495 @classmethod
496 496 def get_first_admin(cls):
497 497 user = User.query().filter(User.admin == True).first()
498 498 if user is None:
499 499 raise Exception('Missing administrative account!')
500 500 return user
501 501
502 502 @classmethod
503 503 def get_default_user(cls, cache=False):
504 504 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
505 505 if user is None:
506 506 raise Exception('Missing default account!')
507 507 return user
508 508
509 509 def get_api_data(self):
510 510 """
511 511 Common function for generating user related data for API
512 512 """
513 513 user = self
514 514 data = dict(
515 515 user_id=user.user_id,
516 516 username=user.username,
517 517 firstname=user.name,
518 518 lastname=user.lastname,
519 519 email=user.email,
520 520 emails=user.emails,
521 521 api_key=user.api_key,
522 522 active=user.active,
523 523 admin=user.admin,
524 524 ldap_dn=user.ldap_dn,
525 525 last_login=user.last_login,
526 526 ip_addresses=user.ip_addresses
527 527 )
528 528 return data
529 529
530 530 def __json__(self):
531 531 data = dict(
532 532 full_name=self.full_name,
533 533 full_name_or_username=self.full_name_or_username,
534 534 short_contact=self.short_contact,
535 535 full_contact=self.full_contact
536 536 )
537 537 data.update(self.get_api_data())
538 538 return data
539 539
540 540
541 541 class UserEmailMap(Base, BaseModel):
542 542 __tablename__ = 'user_email_map'
543 543 __table_args__ = (
544 544 Index('uem_email_idx', 'email'),
545 545 UniqueConstraint('email'),
546 546 {'extend_existing': True, 'mysql_engine': 'InnoDB',
547 547 'mysql_charset': 'utf8'}
548 548 )
549 549 __mapper_args__ = {}
550 550
551 551 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
552 552 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
553 553 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
554 554 user = relationship('User', lazy='joined')
555 555
556 556 @validates('_email')
557 557 def validate_email(self, key, email):
558 558 # check if this email is not main one
559 559 main_email = Session().query(User).filter(User.email == email).scalar()
560 560 if main_email is not None:
561 561 raise AttributeError('email %s is present is user table' % email)
562 562 return email
563 563
564 564 @hybrid_property
565 565 def email(self):
566 566 return self._email
567 567
568 568 @email.setter
569 569 def email(self, val):
570 570 self._email = val.lower() if val else None
571 571
572 572
573 573 class UserIpMap(Base, BaseModel):
574 574 __tablename__ = 'user_ip_map'
575 575 __table_args__ = (
576 576 UniqueConstraint('user_id', 'ip_addr'),
577 577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
578 578 'mysql_charset': 'utf8'}
579 579 )
580 580 __mapper_args__ = {}
581 581
582 582 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
583 583 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
584 584 ip_addr = Column("ip_addr", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
585 585 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
586 586 user = relationship('User', lazy='joined')
587 587
588 588 @classmethod
589 589 def _get_ip_range(cls, ip_addr):
590 590 from rhodecode.lib import ipaddr
591 591 net = ipaddr.IPNetwork(address=ip_addr)
592 592 return [str(net.network), str(net.broadcast)]
593 593
594 594 def __json__(self):
595 595 return dict(
596 596 ip_addr=self.ip_addr,
597 597 ip_range=self._get_ip_range(self.ip_addr)
598 598 )
599 599
600 600
601 601 class UserLog(Base, BaseModel):
602 602 __tablename__ = 'user_logs'
603 603 __table_args__ = (
604 604 {'extend_existing': True, 'mysql_engine': 'InnoDB',
605 605 'mysql_charset': 'utf8'},
606 606 )
607 607 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
608 608 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
609 609 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
610 610 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
611 611 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
612 612 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
613 613 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
614 614 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
615 615
616 def __unicode__(self):
617 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
618 self.repository_name,
619 self.action)
620
616 621 @property
617 622 def action_as_day(self):
618 623 return datetime.date(*self.action_date.timetuple()[:3])
619 624
620 625 user = relationship('User')
621 626 repository = relationship('Repository', cascade='')
622 627
623 628
624 629 class UserGroup(Base, BaseModel):
625 630 __tablename__ = 'users_groups'
626 631 __table_args__ = (
627 632 {'extend_existing': True, 'mysql_engine': 'InnoDB',
628 633 'mysql_charset': 'utf8'},
629 634 )
630 635
631 636 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
632 637 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
633 638 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
634 639 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
635 640 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
636 641
637 642 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
638 643 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
639 644 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
640 645 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
641 646 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
642 647 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
643 648
644 649 user = relationship('User')
645 650
646 651 def __unicode__(self):
647 652 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
648 653 self.users_group_id,
649 654 self.users_group_name)
650 655
651 656 @classmethod
652 657 def get_by_group_name(cls, group_name, cache=False,
653 658 case_insensitive=False):
654 659 if case_insensitive:
655 660 q = cls.query().filter(cls.users_group_name.ilike(group_name))
656 661 else:
657 662 q = cls.query().filter(cls.users_group_name == group_name)
658 663 if cache:
659 664 q = q.options(FromCache(
660 665 "sql_cache_short",
661 666 "get_user_%s" % _hash_key(group_name)
662 667 )
663 668 )
664 669 return q.scalar()
665 670
666 671 @classmethod
667 672 def get(cls, users_group_id, cache=False):
668 673 users_group = cls.query()
669 674 if cache:
670 675 users_group = users_group.options(FromCache("sql_cache_short",
671 676 "get_users_group_%s" % users_group_id))
672 677 return users_group.get(users_group_id)
673 678
674 679 def get_api_data(self):
675 680 users_group = self
676 681
677 682 data = dict(
678 683 users_group_id=users_group.users_group_id,
679 684 group_name=users_group.users_group_name,
680 685 active=users_group.users_group_active,
681 686 )
682 687
683 688 return data
684 689
685 690
686 691 class UserGroupMember(Base, BaseModel):
687 692 __tablename__ = 'users_groups_members'
688 693 __table_args__ = (
689 694 {'extend_existing': True, 'mysql_engine': 'InnoDB',
690 695 'mysql_charset': 'utf8'},
691 696 )
692 697
693 698 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
694 699 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
695 700 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
696 701
697 702 user = relationship('User', lazy='joined')
698 703 users_group = relationship('UserGroup')
699 704
700 705 def __init__(self, gr_id='', u_id=''):
701 706 self.users_group_id = gr_id
702 707 self.user_id = u_id
703 708
704 709
705 710 class RepositoryField(Base, BaseModel):
706 711 __tablename__ = 'repositories_fields'
707 712 __table_args__ = (
708 713 UniqueConstraint('repository_id', 'field_key'), # no-multi field
709 714 {'extend_existing': True, 'mysql_engine': 'InnoDB',
710 715 'mysql_charset': 'utf8'},
711 716 )
712 717 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
713 718
714 719 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
715 720 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
716 721 field_key = Column("field_key", String(250, convert_unicode=False, assert_unicode=None))
717 722 field_label = Column("field_label", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
718 723 field_value = Column("field_value", String(10000, convert_unicode=False, assert_unicode=None), nullable=False)
719 724 field_desc = Column("field_desc", String(1024, convert_unicode=False, assert_unicode=None), nullable=False)
720 725 field_type = Column("field_type", String(256), nullable=False, unique=None)
721 726 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
722 727
723 728 repository = relationship('Repository')
724 729
725 730 @property
726 731 def field_key_prefixed(self):
727 732 return 'ex_%s' % self.field_key
728 733
729 734 @classmethod
730 735 def un_prefix_key(cls, key):
731 736 if key.startswith(cls.PREFIX):
732 737 return key[len(cls.PREFIX):]
733 738 return key
734 739
735 740 @classmethod
736 741 def get_by_key_name(cls, key, repo):
737 742 row = cls.query()\
738 743 .filter(cls.repository == repo)\
739 744 .filter(cls.field_key == key).scalar()
740 745 return row
741 746
742 747
743 748 class Repository(Base, BaseModel):
744 749 __tablename__ = 'repositories'
745 750 __table_args__ = (
746 751 UniqueConstraint('repo_name'),
747 752 Index('r_repo_name_idx', 'repo_name'),
748 753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
749 754 'mysql_charset': 'utf8'},
750 755 )
751 756
752 757 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
753 758 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
754 759 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
755 760 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
756 761 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
757 762 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
758 763 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
759 764 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
760 765 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
761 766 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
762 767 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
763 768 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
764 769 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
765 770 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
766 771 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
767 772
768 773 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
769 774 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
770 775
771 776 user = relationship('User')
772 777 fork = relationship('Repository', remote_side=repo_id)
773 778 group = relationship('RepoGroup')
774 779 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
775 780 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
776 781 stats = relationship('Statistics', cascade='all', uselist=False)
777 782
778 783 followers = relationship('UserFollowing',
779 784 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
780 785 cascade='all')
781 786 extra_fields = relationship('RepositoryField',
782 787 cascade="all, delete, delete-orphan")
783 788
784 789 logs = relationship('UserLog')
785 790 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
786 791
787 792 pull_requests_org = relationship('PullRequest',
788 793 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
789 794 cascade="all, delete, delete-orphan")
790 795
791 796 pull_requests_other = relationship('PullRequest',
792 797 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
793 798 cascade="all, delete, delete-orphan")
794 799
795 800 def __unicode__(self):
796 801 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
797 802 self.repo_name)
798 803
799 804 @hybrid_property
800 805 def locked(self):
801 806 # always should return [user_id, timelocked]
802 807 if self._locked:
803 808 _lock_info = self._locked.split(':')
804 809 return int(_lock_info[0]), _lock_info[1]
805 810 return [None, None]
806 811
807 812 @locked.setter
808 813 def locked(self, val):
809 814 if val and isinstance(val, (list, tuple)):
810 815 self._locked = ':'.join(map(str, val))
811 816 else:
812 817 self._locked = None
813 818
814 819 @hybrid_property
815 820 def changeset_cache(self):
816 821 from rhodecode.lib.vcs.backends.base import EmptyChangeset
817 822 dummy = EmptyChangeset().__json__()
818 823 if not self._changeset_cache:
819 824 return dummy
820 825 try:
821 826 return json.loads(self._changeset_cache)
822 827 except TypeError:
823 828 return dummy
824 829
825 830 @changeset_cache.setter
826 831 def changeset_cache(self, val):
827 832 try:
828 833 self._changeset_cache = json.dumps(val)
829 834 except Exception:
830 835 log.error(traceback.format_exc())
831 836
832 837 @classmethod
833 838 def url_sep(cls):
834 839 return URL_SEP
835 840
836 841 @classmethod
837 842 def normalize_repo_name(cls, repo_name):
838 843 """
839 844 Normalizes os specific repo_name to the format internally stored inside
840 845 dabatabase using URL_SEP
841 846
842 847 :param cls:
843 848 :param repo_name:
844 849 """
845 850 return cls.url_sep().join(repo_name.split(os.sep))
846 851
847 852 @classmethod
848 853 def get_by_repo_name(cls, repo_name):
849 854 q = Session().query(cls).filter(cls.repo_name == repo_name)
850 855 q = q.options(joinedload(Repository.fork))\
851 856 .options(joinedload(Repository.user))\
852 857 .options(joinedload(Repository.group))
853 858 return q.scalar()
854 859
855 860 @classmethod
856 861 def get_by_full_path(cls, repo_full_path):
857 862 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
858 863 repo_name = cls.normalize_repo_name(repo_name)
859 864 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
860 865
861 866 @classmethod
862 867 def get_repo_forks(cls, repo_id):
863 868 return cls.query().filter(Repository.fork_id == repo_id)
864 869
865 870 @classmethod
866 871 def base_path(cls):
867 872 """
868 873 Returns base path when all repos are stored
869 874
870 875 :param cls:
871 876 """
872 877 q = Session().query(RhodeCodeUi)\
873 878 .filter(RhodeCodeUi.ui_key == cls.url_sep())
874 879 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
875 880 return q.one().ui_value
876 881
877 882 @property
878 883 def forks(self):
879 884 """
880 885 Return forks of this repo
881 886 """
882 887 return Repository.get_repo_forks(self.repo_id)
883 888
884 889 @property
885 890 def parent(self):
886 891 """
887 892 Returns fork parent
888 893 """
889 894 return self.fork
890 895
891 896 @property
892 897 def just_name(self):
893 898 return self.repo_name.split(Repository.url_sep())[-1]
894 899
895 900 @property
896 901 def groups_with_parents(self):
897 902 groups = []
898 903 if self.group is None:
899 904 return groups
900 905
901 906 cur_gr = self.group
902 907 groups.insert(0, cur_gr)
903 908 while 1:
904 909 gr = getattr(cur_gr, 'parent_group', None)
905 910 cur_gr = cur_gr.parent_group
906 911 if gr is None:
907 912 break
908 913 groups.insert(0, gr)
909 914
910 915 return groups
911 916
912 917 @property
913 918 def groups_and_repo(self):
914 919 return self.groups_with_parents, self.just_name, self.repo_name
915 920
916 921 @LazyProperty
917 922 def repo_path(self):
918 923 """
919 924 Returns base full path for that repository means where it actually
920 925 exists on a filesystem
921 926 """
922 927 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
923 928 Repository.url_sep())
924 929 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
925 930 return q.one().ui_value
926 931
927 932 @property
928 933 def repo_full_path(self):
929 934 p = [self.repo_path]
930 935 # we need to split the name by / since this is how we store the
931 936 # names in the database, but that eventually needs to be converted
932 937 # into a valid system path
933 938 p += self.repo_name.split(Repository.url_sep())
934 939 return os.path.join(*map(safe_unicode, p))
935 940
936 941 @property
937 942 def cache_keys(self):
938 943 """
939 944 Returns associated cache keys for that repo
940 945 """
941 946 return CacheInvalidation.query()\
942 947 .filter(CacheInvalidation.cache_args == self.repo_name)\
943 948 .order_by(CacheInvalidation.cache_key)\
944 949 .all()
945 950
946 951 def get_new_name(self, repo_name):
947 952 """
948 953 returns new full repository name based on assigned group and new new
949 954
950 955 :param group_name:
951 956 """
952 957 path_prefix = self.group.full_path_splitted if self.group else []
953 958 return Repository.url_sep().join(path_prefix + [repo_name])
954 959
955 960 @property
956 961 def _ui(self):
957 962 """
958 963 Creates an db based ui object for this repository
959 964 """
960 965 from rhodecode.lib.utils import make_ui
961 966 return make_ui('db', clear_session=False)
962 967
963 968 @classmethod
964 969 def is_valid(cls, repo_name):
965 970 """
966 971 returns True if given repo name is a valid filesystem repository
967 972
968 973 :param cls:
969 974 :param repo_name:
970 975 """
971 976 from rhodecode.lib.utils import is_valid_repo
972 977
973 978 return is_valid_repo(repo_name, cls.base_path())
974 979
975 980 def get_api_data(self):
976 981 """
977 982 Common function for generating repo api data
978 983
979 984 """
980 985 repo = self
981 986 data = dict(
982 987 repo_id=repo.repo_id,
983 988 repo_name=repo.repo_name,
984 989 repo_type=repo.repo_type,
985 990 clone_uri=repo.clone_uri,
986 991 private=repo.private,
987 992 created_on=repo.created_on,
988 993 description=repo.description,
989 994 landing_rev=repo.landing_rev,
990 995 owner=repo.user.username,
991 996 fork_of=repo.fork.repo_name if repo.fork else None,
992 997 enable_statistics=repo.enable_statistics,
993 998 enable_locking=repo.enable_locking,
994 999 enable_downloads=repo.enable_downloads,
995 1000 last_changeset=repo.changeset_cache,
996 1001 locked_by=User.get(self.locked[0]).get_api_data() \
997 1002 if self.locked[0] else None,
998 1003 locked_date=time_to_datetime(self.locked[1]) \
999 1004 if self.locked[1] else None
1000 1005 )
1001 1006 rc_config = RhodeCodeSetting.get_app_settings()
1002 1007 repository_fields = str2bool(rc_config.get('rhodecode_repository_fields'))
1003 1008 if repository_fields:
1004 1009 for f in self.extra_fields:
1005 1010 data[f.field_key_prefixed] = f.field_value
1006 1011
1007 1012 return data
1008 1013
1009 1014 @classmethod
1010 1015 def lock(cls, repo, user_id, lock_time=None):
1011 1016 if not lock_time:
1012 1017 lock_time = time.time()
1013 1018 repo.locked = [user_id, lock_time]
1014 1019 Session().add(repo)
1015 1020 Session().commit()
1016 1021
1017 1022 @classmethod
1018 1023 def unlock(cls, repo):
1019 1024 repo.locked = None
1020 1025 Session().add(repo)
1021 1026 Session().commit()
1022 1027
1023 1028 @classmethod
1024 1029 def getlock(cls, repo):
1025 1030 return repo.locked
1026 1031
1027 1032 @property
1028 1033 def last_db_change(self):
1029 1034 return self.updated_on
1030 1035
1031 1036 def clone_url(self, **override):
1032 1037 from pylons import url
1033 1038 from urlparse import urlparse
1034 1039 import urllib
1035 1040 parsed_url = urlparse(url('home', qualified=True))
1036 1041 default_clone_uri = '%(scheme)s://%(user)s%(pass)s%(netloc)s%(prefix)s%(path)s'
1037 1042 decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
1038 1043 args = {
1039 1044 'user': '',
1040 1045 'pass': '',
1041 1046 'scheme': parsed_url.scheme,
1042 1047 'netloc': parsed_url.netloc,
1043 1048 'prefix': decoded_path,
1044 1049 'path': self.repo_name
1045 1050 }
1046 1051
1047 1052 args.update(override)
1048 1053 return default_clone_uri % args
1049 1054
1050 1055 #==========================================================================
1051 1056 # SCM PROPERTIES
1052 1057 #==========================================================================
1053 1058
1054 1059 def get_changeset(self, rev=None):
1055 1060 return get_changeset_safe(self.scm_instance, rev)
1056 1061
1057 1062 def get_landing_changeset(self):
1058 1063 """
1059 1064 Returns landing changeset, or if that doesn't exist returns the tip
1060 1065 """
1061 1066 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
1062 1067 return cs
1063 1068
1064 1069 def update_changeset_cache(self, cs_cache=None):
1065 1070 """
1066 1071 Update cache of last changeset for repository, keys should be::
1067 1072
1068 1073 short_id
1069 1074 raw_id
1070 1075 revision
1071 1076 message
1072 1077 date
1073 1078 author
1074 1079
1075 1080 :param cs_cache:
1076 1081 """
1077 1082 from rhodecode.lib.vcs.backends.base import BaseChangeset
1078 1083 if cs_cache is None:
1079 1084 cs_cache = EmptyChangeset()
1080 1085 # use no-cache version here
1081 1086 scm_repo = self.scm_instance_no_cache()
1082 1087 if scm_repo:
1083 1088 cs_cache = scm_repo.get_changeset()
1084 1089
1085 1090 if isinstance(cs_cache, BaseChangeset):
1086 1091 cs_cache = cs_cache.__json__()
1087 1092
1088 1093 if (cs_cache != self.changeset_cache or not self.changeset_cache):
1089 1094 _default = datetime.datetime.fromtimestamp(0)
1090 1095 last_change = cs_cache.get('date') or _default
1091 1096 log.debug('updated repo %s with new cs cache %s'
1092 1097 % (self.repo_name, cs_cache))
1093 1098 self.updated_on = last_change
1094 1099 self.changeset_cache = cs_cache
1095 1100 Session().add(self)
1096 1101 Session().commit()
1097 1102 else:
1098 1103 log.debug('Skipping repo:%s already with latest changes'
1099 1104 % self.repo_name)
1100 1105
1101 1106 @property
1102 1107 def tip(self):
1103 1108 return self.get_changeset('tip')
1104 1109
1105 1110 @property
1106 1111 def author(self):
1107 1112 return self.tip.author
1108 1113
1109 1114 @property
1110 1115 def last_change(self):
1111 1116 return self.scm_instance.last_change
1112 1117
1113 1118 def get_comments(self, revisions=None):
1114 1119 """
1115 1120 Returns comments for this repository grouped by revisions
1116 1121
1117 1122 :param revisions: filter query by revisions only
1118 1123 """
1119 1124 cmts = ChangesetComment.query()\
1120 1125 .filter(ChangesetComment.repo == self)
1121 1126 if revisions:
1122 1127 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1123 1128 grouped = collections.defaultdict(list)
1124 1129 for cmt in cmts.all():
1125 1130 grouped[cmt.revision].append(cmt)
1126 1131 return grouped
1127 1132
1128 1133 def statuses(self, revisions=None):
1129 1134 """
1130 1135 Returns statuses for this repository
1131 1136
1132 1137 :param revisions: list of revisions to get statuses for
1133 1138 """
1134 1139
1135 1140 statuses = ChangesetStatus.query()\
1136 1141 .filter(ChangesetStatus.repo == self)\
1137 1142 .filter(ChangesetStatus.version == 0)
1138 1143 if revisions:
1139 1144 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
1140 1145 grouped = {}
1141 1146
1142 1147 #maybe we have open new pullrequest without a status ?
1143 1148 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1144 1149 status_lbl = ChangesetStatus.get_status_lbl(stat)
1145 1150 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
1146 1151 for rev in pr.revisions:
1147 1152 pr_id = pr.pull_request_id
1148 1153 pr_repo = pr.other_repo.repo_name
1149 1154 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1150 1155
1151 1156 for stat in statuses.all():
1152 1157 pr_id = pr_repo = None
1153 1158 if stat.pull_request:
1154 1159 pr_id = stat.pull_request.pull_request_id
1155 1160 pr_repo = stat.pull_request.other_repo.repo_name
1156 1161 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1157 1162 pr_id, pr_repo]
1158 1163 return grouped
1159 1164
1160 1165 def _repo_size(self):
1161 1166 from rhodecode.lib import helpers as h
1162 1167 log.debug('calculating repository size...')
1163 1168 return h.format_byte_size(self.scm_instance.size)
1164 1169
1165 1170 #==========================================================================
1166 1171 # SCM CACHE INSTANCE
1167 1172 #==========================================================================
1168 1173
1169 1174 def set_invalidate(self):
1170 1175 """
1171 1176 Mark caches of this repo as invalid.
1172 1177 """
1173 1178 CacheInvalidation.set_invalidate(self.repo_name)
1174 1179
1175 1180 def scm_instance_no_cache(self):
1176 1181 return self.__get_instance()
1177 1182
1178 1183 @property
1179 1184 def scm_instance(self):
1180 1185 import rhodecode
1181 1186 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1182 1187 if full_cache:
1183 1188 return self.scm_instance_cached()
1184 1189 return self.__get_instance()
1185 1190
1186 1191 def scm_instance_cached(self, valid_cache_keys=None):
1187 1192 @cache_region('long_term')
1188 1193 def _c(repo_name):
1189 1194 return self.__get_instance()
1190 1195 rn = self.repo_name
1191 1196
1192 1197 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1193 1198 if not valid:
1194 1199 log.debug('Cache for %s invalidated, getting new object' % (rn))
1195 1200 region_invalidate(_c, None, rn)
1196 1201 else:
1197 1202 log.debug('Getting obj for %s from cache' % (rn))
1198 1203 return _c(rn)
1199 1204
1200 1205 def __get_instance(self):
1201 1206 repo_full_path = self.repo_full_path
1202 1207 try:
1203 1208 alias = get_scm(repo_full_path)[0]
1204 1209 log.debug('Creating instance of %s repository from %s'
1205 1210 % (alias, repo_full_path))
1206 1211 backend = get_backend(alias)
1207 1212 except VCSError:
1208 1213 log.error(traceback.format_exc())
1209 1214 log.error('Perhaps this repository is in db and not in '
1210 1215 'filesystem run rescan repositories with '
1211 1216 '"destroy old data " option from admin panel')
1212 1217 return
1213 1218
1214 1219 if alias == 'hg':
1215 1220
1216 1221 repo = backend(safe_str(repo_full_path), create=False,
1217 1222 baseui=self._ui)
1218 1223 # skip hidden web repository
1219 1224 if repo._get_hidden():
1220 1225 return
1221 1226 else:
1222 1227 repo = backend(repo_full_path, create=False)
1223 1228
1224 1229 return repo
1225 1230
1226 1231
1227 1232 class RepoGroup(Base, BaseModel):
1228 1233 __tablename__ = 'groups'
1229 1234 __table_args__ = (
1230 1235 UniqueConstraint('group_name', 'group_parent_id'),
1231 1236 CheckConstraint('group_id != group_parent_id'),
1232 1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1233 1238 'mysql_charset': 'utf8'},
1234 1239 )
1235 1240 __mapper_args__ = {'order_by': 'group_name'}
1236 1241
1237 1242 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1238 1243 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1239 1244 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1240 1245 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1241 1246 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1242 1247 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1243 1248
1244 1249 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1245 1250 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1246 1251 parent_group = relationship('RepoGroup', remote_side=group_id)
1247 1252 user = relationship('User')
1248 1253
1249 1254 def __init__(self, group_name='', parent_group=None):
1250 1255 self.group_name = group_name
1251 1256 self.parent_group = parent_group
1252 1257
1253 1258 def __unicode__(self):
1254 1259 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1255 1260 self.group_name)
1256 1261
1257 1262 @classmethod
1258 1263 def groups_choices(cls, groups=None, show_empty_group=True):
1259 1264 from webhelpers.html import literal as _literal
1260 1265 if not groups:
1261 1266 groups = cls.query().all()
1262 1267
1263 1268 repo_groups = []
1264 1269 if show_empty_group:
1265 1270 repo_groups = [('-1', '-- %s --' % _('top level'))]
1266 1271 sep = ' &raquo; '
1267 1272 _name = lambda k: _literal(sep.join(k))
1268 1273
1269 1274 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1270 1275 for x in groups])
1271 1276
1272 1277 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1273 1278 return repo_groups
1274 1279
1275 1280 @classmethod
1276 1281 def url_sep(cls):
1277 1282 return URL_SEP
1278 1283
1279 1284 @classmethod
1280 1285 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1281 1286 if case_insensitive:
1282 1287 gr = cls.query()\
1283 1288 .filter(cls.group_name.ilike(group_name))
1284 1289 else:
1285 1290 gr = cls.query()\
1286 1291 .filter(cls.group_name == group_name)
1287 1292 if cache:
1288 1293 gr = gr.options(FromCache(
1289 1294 "sql_cache_short",
1290 1295 "get_group_%s" % _hash_key(group_name)
1291 1296 )
1292 1297 )
1293 1298 return gr.scalar()
1294 1299
1295 1300 @property
1296 1301 def parents(self):
1297 1302 parents_recursion_limit = 5
1298 1303 groups = []
1299 1304 if self.parent_group is None:
1300 1305 return groups
1301 1306 cur_gr = self.parent_group
1302 1307 groups.insert(0, cur_gr)
1303 1308 cnt = 0
1304 1309 while 1:
1305 1310 cnt += 1
1306 1311 gr = getattr(cur_gr, 'parent_group', None)
1307 1312 cur_gr = cur_gr.parent_group
1308 1313 if gr is None:
1309 1314 break
1310 1315 if cnt == parents_recursion_limit:
1311 1316 # this will prevent accidental infinit loops
1312 1317 log.error('group nested more than %s' %
1313 1318 parents_recursion_limit)
1314 1319 break
1315 1320
1316 1321 groups.insert(0, gr)
1317 1322 return groups
1318 1323
1319 1324 @property
1320 1325 def children(self):
1321 1326 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1322 1327
1323 1328 @property
1324 1329 def name(self):
1325 1330 return self.group_name.split(RepoGroup.url_sep())[-1]
1326 1331
1327 1332 @property
1328 1333 def full_path(self):
1329 1334 return self.group_name
1330 1335
1331 1336 @property
1332 1337 def full_path_splitted(self):
1333 1338 return self.group_name.split(RepoGroup.url_sep())
1334 1339
1335 1340 @property
1336 1341 def repositories(self):
1337 1342 return Repository.query()\
1338 1343 .filter(Repository.group == self)\
1339 1344 .order_by(Repository.repo_name)
1340 1345
1341 1346 @property
1342 1347 def repositories_recursive_count(self):
1343 1348 cnt = self.repositories.count()
1344 1349
1345 1350 def children_count(group):
1346 1351 cnt = 0
1347 1352 for child in group.children:
1348 1353 cnt += child.repositories.count()
1349 1354 cnt += children_count(child)
1350 1355 return cnt
1351 1356
1352 1357 return cnt + children_count(self)
1353 1358
1354 1359 def _recursive_objects(self, include_repos=True):
1355 1360 all_ = []
1356 1361
1357 1362 def _get_members(root_gr):
1358 1363 if include_repos:
1359 1364 for r in root_gr.repositories:
1360 1365 all_.append(r)
1361 1366 childs = root_gr.children.all()
1362 1367 if childs:
1363 1368 for gr in childs:
1364 1369 all_.append(gr)
1365 1370 _get_members(gr)
1366 1371
1367 1372 _get_members(self)
1368 1373 return [self] + all_
1369 1374
1370 1375 def recursive_groups_and_repos(self):
1371 1376 """
1372 1377 Recursive return all groups, with repositories in those groups
1373 1378 """
1374 1379 return self._recursive_objects()
1375 1380
1376 1381 def recursive_groups(self):
1377 1382 """
1378 1383 Returns all children groups for this group including children of children
1379 1384 """
1380 1385 return self._recursive_objects(include_repos=False)
1381 1386
1382 1387 def get_new_name(self, group_name):
1383 1388 """
1384 1389 returns new full group name based on parent and new name
1385 1390
1386 1391 :param group_name:
1387 1392 """
1388 1393 path_prefix = (self.parent_group.full_path_splitted if
1389 1394 self.parent_group else [])
1390 1395 return RepoGroup.url_sep().join(path_prefix + [group_name])
1391 1396
1392 1397
1393 1398 class Permission(Base, BaseModel):
1394 1399 __tablename__ = 'permissions'
1395 1400 __table_args__ = (
1396 1401 Index('p_perm_name_idx', 'permission_name'),
1397 1402 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1398 1403 'mysql_charset': 'utf8'},
1399 1404 )
1400 1405 PERMS = [
1401 1406 ('hg.admin', _('RhodeCode Administrator')),
1402 1407
1403 1408 ('repository.none', _('Repository no access')),
1404 1409 ('repository.read', _('Repository read access')),
1405 1410 ('repository.write', _('Repository write access')),
1406 1411 ('repository.admin', _('Repository admin access')),
1407 1412
1408 1413 ('group.none', _('Repository group no access')),
1409 1414 ('group.read', _('Repository group read access')),
1410 1415 ('group.write', _('Repository group write access')),
1411 1416 ('group.admin', _('Repository group admin access')),
1412 1417
1413 1418 ('usergroup.none', _('User group no access')),
1414 1419 ('usergroup.read', _('User group read access')),
1415 1420 ('usergroup.write', _('User group write access')),
1416 1421 ('usergroup.admin', _('User group admin access')),
1417 1422
1418 1423 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
1419 1424 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
1420 1425
1421 1426 ('hg.usergroup.create.false', _('User Group creation disabled')),
1422 1427 ('hg.usergroup.create.true', _('User Group creation enabled')),
1423 1428
1424 1429 ('hg.create.none', _('Repository creation disabled')),
1425 1430 ('hg.create.repository', _('Repository creation enabled')),
1426 1431
1427 1432 ('hg.fork.none', _('Repository forking disabled')),
1428 1433 ('hg.fork.repository', _('Repository forking enabled')),
1429 1434
1430 1435 ('hg.register.none', _('Registration disabled')),
1431 1436 ('hg.register.manual_activate', _('User Registration with manual account activation')),
1432 1437 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
1433 1438
1434 1439 ('hg.extern_activate.manual', _('Manual activation of external account')),
1435 1440 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1436 1441
1437 1442 ]
1438 1443
1439 1444 #definition of system default permissions for DEFAULT user
1440 1445 DEFAULT_USER_PERMISSIONS = [
1441 1446 'repository.read',
1442 1447 'group.read',
1443 1448 'usergroup.read',
1444 1449 'hg.create.repository',
1445 1450 'hg.fork.repository',
1446 1451 'hg.register.manual_activate',
1447 1452 'hg.extern_activate.auto',
1448 1453 ]
1449 1454
1450 1455 # defines which permissions are more important higher the more important
1451 1456 # Weight defines which permissions are more important.
1452 1457 # The higher number the more important.
1453 1458 PERM_WEIGHTS = {
1454 1459 'repository.none': 0,
1455 1460 'repository.read': 1,
1456 1461 'repository.write': 3,
1457 1462 'repository.admin': 4,
1458 1463
1459 1464 'group.none': 0,
1460 1465 'group.read': 1,
1461 1466 'group.write': 3,
1462 1467 'group.admin': 4,
1463 1468
1464 1469 'usergroup.none': 0,
1465 1470 'usergroup.read': 1,
1466 1471 'usergroup.write': 3,
1467 1472 'usergroup.admin': 4,
1468 1473 'hg.repogroup.create.false': 0,
1469 1474 'hg.repogroup.create.true': 1,
1470 1475
1471 1476 'hg.usergroup.create.false': 0,
1472 1477 'hg.usergroup.create.true': 1,
1473 1478
1474 1479 'hg.fork.none': 0,
1475 1480 'hg.fork.repository': 1,
1476 1481 'hg.create.none': 0,
1477 1482 'hg.create.repository': 1
1478 1483 }
1479 1484
1480 1485 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1481 1486 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1482 1487 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1483 1488
1484 1489 def __unicode__(self):
1485 1490 return u"<%s('%s:%s')>" % (
1486 1491 self.__class__.__name__, self.permission_id, self.permission_name
1487 1492 )
1488 1493
1489 1494 @classmethod
1490 1495 def get_by_key(cls, key):
1491 1496 return cls.query().filter(cls.permission_name == key).scalar()
1492 1497
1493 1498 @classmethod
1494 1499 def get_default_perms(cls, default_user_id):
1495 1500 q = Session().query(UserRepoToPerm, Repository, cls)\
1496 1501 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1497 1502 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1498 1503 .filter(UserRepoToPerm.user_id == default_user_id)
1499 1504
1500 1505 return q.all()
1501 1506
1502 1507 @classmethod
1503 1508 def get_default_group_perms(cls, default_user_id):
1504 1509 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1505 1510 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1506 1511 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1507 1512 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1508 1513
1509 1514 return q.all()
1510 1515
1511 1516 @classmethod
1512 1517 def get_default_user_group_perms(cls, default_user_id):
1513 1518 q = Session().query(UserUserGroupToPerm, UserGroup, cls)\
1514 1519 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
1515 1520 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id))\
1516 1521 .filter(UserUserGroupToPerm.user_id == default_user_id)
1517 1522
1518 1523 return q.all()
1519 1524
1520 1525
1521 1526 class UserRepoToPerm(Base, BaseModel):
1522 1527 __tablename__ = 'repo_to_perm'
1523 1528 __table_args__ = (
1524 1529 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1525 1530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1526 1531 'mysql_charset': 'utf8'}
1527 1532 )
1528 1533 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1529 1534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1530 1535 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1531 1536 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1532 1537
1533 1538 user = relationship('User')
1534 1539 repository = relationship('Repository')
1535 1540 permission = relationship('Permission')
1536 1541
1537 1542 @classmethod
1538 1543 def create(cls, user, repository, permission):
1539 1544 n = cls()
1540 1545 n.user = user
1541 1546 n.repository = repository
1542 1547 n.permission = permission
1543 1548 Session().add(n)
1544 1549 return n
1545 1550
1546 1551 def __unicode__(self):
1547 1552 return u'<%s => %s >' % (self.user, self.repository)
1548 1553
1549 1554
1550 1555 class UserUserGroupToPerm(Base, BaseModel):
1551 1556 __tablename__ = 'user_user_group_to_perm'
1552 1557 __table_args__ = (
1553 1558 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1554 1559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1555 1560 'mysql_charset': 'utf8'}
1556 1561 )
1557 1562 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1558 1563 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1559 1564 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1560 1565 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1561 1566
1562 1567 user = relationship('User')
1563 1568 user_group = relationship('UserGroup')
1564 1569 permission = relationship('Permission')
1565 1570
1566 1571 @classmethod
1567 1572 def create(cls, user, user_group, permission):
1568 1573 n = cls()
1569 1574 n.user = user
1570 1575 n.user_group = user_group
1571 1576 n.permission = permission
1572 1577 Session().add(n)
1573 1578 return n
1574 1579
1575 1580 def __unicode__(self):
1576 1581 return u'<%s => %s >' % (self.user, self.user_group)
1577 1582
1578 1583
1579 1584 class UserToPerm(Base, BaseModel):
1580 1585 __tablename__ = 'user_to_perm'
1581 1586 __table_args__ = (
1582 1587 UniqueConstraint('user_id', 'permission_id'),
1583 1588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1584 1589 'mysql_charset': 'utf8'}
1585 1590 )
1586 1591 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1587 1592 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1588 1593 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1589 1594
1590 1595 user = relationship('User')
1591 1596 permission = relationship('Permission', lazy='joined')
1592 1597
1593 1598 def __unicode__(self):
1594 1599 return u'<%s => %s >' % (self.user, self.permission)
1595 1600
1596 1601
1597 1602 class UserGroupRepoToPerm(Base, BaseModel):
1598 1603 __tablename__ = 'users_group_repo_to_perm'
1599 1604 __table_args__ = (
1600 1605 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1601 1606 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1602 1607 'mysql_charset': 'utf8'}
1603 1608 )
1604 1609 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1605 1610 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1606 1611 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1607 1612 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1608 1613
1609 1614 users_group = relationship('UserGroup')
1610 1615 permission = relationship('Permission')
1611 1616 repository = relationship('Repository')
1612 1617
1613 1618 @classmethod
1614 1619 def create(cls, users_group, repository, permission):
1615 1620 n = cls()
1616 1621 n.users_group = users_group
1617 1622 n.repository = repository
1618 1623 n.permission = permission
1619 1624 Session().add(n)
1620 1625 return n
1621 1626
1622 1627 def __unicode__(self):
1623 1628 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1624 1629
1625 1630
1626 1631 class UserGroupUserGroupToPerm(Base, BaseModel):
1627 1632 __tablename__ = 'user_group_user_group_to_perm'
1628 1633 __table_args__ = (
1629 1634 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1630 1635 CheckConstraint('target_user_group_id != user_group_id'),
1631 1636 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1632 1637 'mysql_charset': 'utf8'}
1633 1638 )
1634 1639 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 1640 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1636 1641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1637 1642 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1638 1643
1639 1644 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1640 1645 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1641 1646 permission = relationship('Permission')
1642 1647
1643 1648 @classmethod
1644 1649 def create(cls, target_user_group, user_group, permission):
1645 1650 n = cls()
1646 1651 n.target_user_group = target_user_group
1647 1652 n.user_group = user_group
1648 1653 n.permission = permission
1649 1654 Session().add(n)
1650 1655 return n
1651 1656
1652 1657 def __unicode__(self):
1653 1658 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1654 1659
1655 1660
1656 1661 class UserGroupToPerm(Base, BaseModel):
1657 1662 __tablename__ = 'users_group_to_perm'
1658 1663 __table_args__ = (
1659 1664 UniqueConstraint('users_group_id', 'permission_id',),
1660 1665 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1661 1666 'mysql_charset': 'utf8'}
1662 1667 )
1663 1668 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1664 1669 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1665 1670 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1666 1671
1667 1672 users_group = relationship('UserGroup')
1668 1673 permission = relationship('Permission')
1669 1674
1670 1675
1671 1676 class UserRepoGroupToPerm(Base, BaseModel):
1672 1677 __tablename__ = 'user_repo_group_to_perm'
1673 1678 __table_args__ = (
1674 1679 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1675 1680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1676 1681 'mysql_charset': 'utf8'}
1677 1682 )
1678 1683
1679 1684 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1680 1685 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1681 1686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1682 1687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1683 1688
1684 1689 user = relationship('User')
1685 1690 group = relationship('RepoGroup')
1686 1691 permission = relationship('Permission')
1687 1692
1688 1693
1689 1694 class UserGroupRepoGroupToPerm(Base, BaseModel):
1690 1695 __tablename__ = 'users_group_repo_group_to_perm'
1691 1696 __table_args__ = (
1692 1697 UniqueConstraint('users_group_id', 'group_id'),
1693 1698 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1694 1699 'mysql_charset': 'utf8'}
1695 1700 )
1696 1701
1697 1702 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)
1698 1703 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1699 1704 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1700 1705 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1701 1706
1702 1707 users_group = relationship('UserGroup')
1703 1708 permission = relationship('Permission')
1704 1709 group = relationship('RepoGroup')
1705 1710
1706 1711
1707 1712 class Statistics(Base, BaseModel):
1708 1713 __tablename__ = 'statistics'
1709 1714 __table_args__ = (
1710 1715 UniqueConstraint('repository_id'),
1711 1716 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1712 1717 'mysql_charset': 'utf8'}
1713 1718 )
1714 1719 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1715 1720 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1716 1721 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1717 1722 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1718 1723 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1719 1724 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1720 1725
1721 1726 repository = relationship('Repository', single_parent=True)
1722 1727
1723 1728
1724 1729 class UserFollowing(Base, BaseModel):
1725 1730 __tablename__ = 'user_followings'
1726 1731 __table_args__ = (
1727 1732 UniqueConstraint('user_id', 'follows_repository_id'),
1728 1733 UniqueConstraint('user_id', 'follows_user_id'),
1729 1734 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1730 1735 'mysql_charset': 'utf8'}
1731 1736 )
1732 1737
1733 1738 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1734 1739 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1735 1740 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1736 1741 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1737 1742 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1738 1743
1739 1744 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1740 1745
1741 1746 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1742 1747 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1743 1748
1744 1749 @classmethod
1745 1750 def get_repo_followers(cls, repo_id):
1746 1751 return cls.query().filter(cls.follows_repo_id == repo_id)
1747 1752
1748 1753
1749 1754 class CacheInvalidation(Base, BaseModel):
1750 1755 __tablename__ = 'cache_invalidation'
1751 1756 __table_args__ = (
1752 1757 UniqueConstraint('cache_key'),
1753 1758 Index('key_idx', 'cache_key'),
1754 1759 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1755 1760 'mysql_charset': 'utf8'},
1756 1761 )
1757 1762 # cache_id, not used
1758 1763 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1759 1764 # cache_key as created by _get_cache_key
1760 1765 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1761 1766 # cache_args is a repo_name
1762 1767 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1763 1768 # instance sets cache_active True when it is caching,
1764 1769 # other instances set cache_active to False to indicate that this cache is invalid
1765 1770 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1766 1771
1767 1772 def __init__(self, cache_key, repo_name=''):
1768 1773 self.cache_key = cache_key
1769 1774 self.cache_args = repo_name
1770 1775 self.cache_active = False
1771 1776
1772 1777 def __unicode__(self):
1773 1778 return u"<%s('%s:%s[%s]')>" % (self.__class__.__name__,
1774 1779 self.cache_id, self.cache_key, self.cache_active)
1775 1780
1776 1781 def _cache_key_partition(self):
1777 1782 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
1778 1783 return prefix, repo_name, suffix
1779 1784
1780 1785 def get_prefix(self):
1781 1786 """
1782 1787 get prefix that might have been used in _get_cache_key to
1783 1788 generate self.cache_key. Only used for informational purposes
1784 1789 in repo_edit.html.
1785 1790 """
1786 1791 # prefix, repo_name, suffix
1787 1792 return self._cache_key_partition()[0]
1788 1793
1789 1794 def get_suffix(self):
1790 1795 """
1791 1796 get suffix that might have been used in _get_cache_key to
1792 1797 generate self.cache_key. Only used for informational purposes
1793 1798 in repo_edit.html.
1794 1799 """
1795 1800 # prefix, repo_name, suffix
1796 1801 return self._cache_key_partition()[2]
1797 1802
1798 1803 @classmethod
1799 1804 def clear_cache(cls):
1800 1805 """
1801 1806 Delete all cache keys from database.
1802 1807 Should only be run when all instances are down and all entries thus stale.
1803 1808 """
1804 1809 cls.query().delete()
1805 1810 Session().commit()
1806 1811
1807 1812 @classmethod
1808 1813 def _get_cache_key(cls, key):
1809 1814 """
1810 1815 Wrapper for generating a unique cache key for this instance and "key".
1811 1816 key must / will start with a repo_name which will be stored in .cache_args .
1812 1817 """
1813 1818 import rhodecode
1814 1819 prefix = rhodecode.CONFIG.get('instance_id', '')
1815 1820 return "%s%s" % (prefix, key)
1816 1821
1817 1822 @classmethod
1818 1823 def set_invalidate(cls, repo_name):
1819 1824 """
1820 1825 Mark all caches of a repo as invalid in the database.
1821 1826 """
1822 1827 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
1823 1828
1824 1829 try:
1825 1830 for inv_obj in inv_objs:
1826 1831 log.debug('marking %s key for invalidation based on repo_name=%s'
1827 1832 % (inv_obj, safe_str(repo_name)))
1828 1833 inv_obj.cache_active = False
1829 1834 Session().add(inv_obj)
1830 1835 Session().commit()
1831 1836 except Exception:
1832 1837 log.error(traceback.format_exc())
1833 1838 Session().rollback()
1834 1839
1835 1840 @classmethod
1836 1841 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
1837 1842 """
1838 1843 Mark this cache key as active and currently cached.
1839 1844 Return True if the existing cache registration still was valid.
1840 1845 Return False to indicate that it had been invalidated and caches should be refreshed.
1841 1846 """
1842 1847
1843 1848 key = (repo_name + '_' + kind) if kind else repo_name
1844 1849 cache_key = cls._get_cache_key(key)
1845 1850
1846 1851 if valid_cache_keys and cache_key in valid_cache_keys:
1847 1852 return True
1848 1853
1849 1854 try:
1850 1855 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
1851 1856 if not inv_obj:
1852 1857 inv_obj = CacheInvalidation(cache_key, repo_name)
1853 1858 was_valid = inv_obj.cache_active
1854 1859 inv_obj.cache_active = True
1855 1860 Session().add(inv_obj)
1856 1861 Session().commit()
1857 1862 return was_valid
1858 1863 except Exception:
1859 1864 log.error(traceback.format_exc())
1860 1865 Session().rollback()
1861 1866 return False
1862 1867
1863 1868 @classmethod
1864 1869 def get_valid_cache_keys(cls):
1865 1870 """
1866 1871 Return opaque object with information of which caches still are valid
1867 1872 and can be used without checking for invalidation.
1868 1873 """
1869 1874 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
1870 1875
1871 1876
1872 1877 class ChangesetComment(Base, BaseModel):
1873 1878 __tablename__ = 'changeset_comments'
1874 1879 __table_args__ = (
1875 1880 Index('cc_revision_idx', 'revision'),
1876 1881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1877 1882 'mysql_charset': 'utf8'},
1878 1883 )
1879 1884 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1880 1885 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1881 1886 revision = Column('revision', String(40), nullable=True)
1882 1887 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1883 1888 line_no = Column('line_no', Unicode(10), nullable=True)
1884 1889 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1885 1890 f_path = Column('f_path', Unicode(1000), nullable=True)
1886 1891 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1887 1892 text = Column('text', UnicodeText(25000), nullable=False)
1888 1893 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1889 1894 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1890 1895
1891 1896 author = relationship('User', lazy='joined')
1892 1897 repo = relationship('Repository')
1893 1898 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1894 1899 pull_request = relationship('PullRequest', lazy='joined')
1895 1900
1896 1901 @classmethod
1897 1902 def get_users(cls, revision=None, pull_request_id=None):
1898 1903 """
1899 1904 Returns user associated with this ChangesetComment. ie those
1900 1905 who actually commented
1901 1906
1902 1907 :param cls:
1903 1908 :param revision:
1904 1909 """
1905 1910 q = Session().query(User)\
1906 1911 .join(ChangesetComment.author)
1907 1912 if revision:
1908 1913 q = q.filter(cls.revision == revision)
1909 1914 elif pull_request_id:
1910 1915 q = q.filter(cls.pull_request_id == pull_request_id)
1911 1916 return q.all()
1912 1917
1913 1918
1914 1919 class ChangesetStatus(Base, BaseModel):
1915 1920 __tablename__ = 'changeset_statuses'
1916 1921 __table_args__ = (
1917 1922 Index('cs_revision_idx', 'revision'),
1918 1923 Index('cs_version_idx', 'version'),
1919 1924 UniqueConstraint('repo_id', 'revision', 'version'),
1920 1925 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1921 1926 'mysql_charset': 'utf8'}
1922 1927 )
1923 1928 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1924 1929 STATUS_APPROVED = 'approved'
1925 1930 STATUS_REJECTED = 'rejected'
1926 1931 STATUS_UNDER_REVIEW = 'under_review'
1927 1932
1928 1933 STATUSES = [
1929 1934 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1930 1935 (STATUS_APPROVED, _("Approved")),
1931 1936 (STATUS_REJECTED, _("Rejected")),
1932 1937 (STATUS_UNDER_REVIEW, _("Under Review")),
1933 1938 ]
1934 1939
1935 1940 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1936 1941 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1937 1942 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1938 1943 revision = Column('revision', String(40), nullable=False)
1939 1944 status = Column('status', String(128), nullable=False, default=DEFAULT)
1940 1945 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1941 1946 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1942 1947 version = Column('version', Integer(), nullable=False, default=0)
1943 1948 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1944 1949
1945 1950 author = relationship('User', lazy='joined')
1946 1951 repo = relationship('Repository')
1947 1952 comment = relationship('ChangesetComment', lazy='joined')
1948 1953 pull_request = relationship('PullRequest', lazy='joined')
1949 1954
1950 1955 def __unicode__(self):
1951 1956 return u"<%s('%s:%s')>" % (
1952 1957 self.__class__.__name__,
1953 1958 self.status, self.author
1954 1959 )
1955 1960
1956 1961 @classmethod
1957 1962 def get_status_lbl(cls, value):
1958 1963 return dict(cls.STATUSES).get(value)
1959 1964
1960 1965 @property
1961 1966 def status_lbl(self):
1962 1967 return ChangesetStatus.get_status_lbl(self.status)
1963 1968
1964 1969
1965 1970 class PullRequest(Base, BaseModel):
1966 1971 __tablename__ = 'pull_requests'
1967 1972 __table_args__ = (
1968 1973 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1969 1974 'mysql_charset': 'utf8'},
1970 1975 )
1971 1976
1972 1977 STATUS_NEW = u'new'
1973 1978 STATUS_OPEN = u'open'
1974 1979 STATUS_CLOSED = u'closed'
1975 1980
1976 1981 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1977 1982 title = Column('title', Unicode(256), nullable=True)
1978 1983 description = Column('description', UnicodeText(10240), nullable=True)
1979 1984 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1980 1985 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1981 1986 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1982 1987 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1983 1988 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1984 1989 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1985 1990 org_ref = Column('org_ref', Unicode(256), nullable=False)
1986 1991 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1987 1992 other_ref = Column('other_ref', Unicode(256), nullable=False)
1988 1993
1989 1994 @hybrid_property
1990 1995 def revisions(self):
1991 1996 return self._revisions.split(':')
1992 1997
1993 1998 @revisions.setter
1994 1999 def revisions(self, val):
1995 2000 self._revisions = ':'.join(val)
1996 2001
1997 2002 @property
1998 2003 def org_ref_parts(self):
1999 2004 return self.org_ref.split(':')
2000 2005
2001 2006 @property
2002 2007 def other_ref_parts(self):
2003 2008 return self.other_ref.split(':')
2004 2009
2005 2010 author = relationship('User', lazy='joined')
2006 2011 reviewers = relationship('PullRequestReviewers',
2007 2012 cascade="all, delete, delete-orphan")
2008 2013 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2009 2014 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2010 2015 statuses = relationship('ChangesetStatus')
2011 2016 comments = relationship('ChangesetComment',
2012 2017 cascade="all, delete, delete-orphan")
2013 2018
2014 2019 def is_closed(self):
2015 2020 return self.status == self.STATUS_CLOSED
2016 2021
2017 2022 @property
2018 2023 def last_review_status(self):
2019 2024 return self.statuses[-1].status if self.statuses else ''
2020 2025
2021 2026 def __json__(self):
2022 2027 return dict(
2023 2028 revisions=self.revisions
2024 2029 )
2025 2030
2026 2031
2027 2032 class PullRequestReviewers(Base, BaseModel):
2028 2033 __tablename__ = 'pull_request_reviewers'
2029 2034 __table_args__ = (
2030 2035 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2031 2036 'mysql_charset': 'utf8'},
2032 2037 )
2033 2038
2034 2039 def __init__(self, user=None, pull_request=None):
2035 2040 self.user = user
2036 2041 self.pull_request = pull_request
2037 2042
2038 2043 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
2039 2044 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2040 2045 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
2041 2046
2042 2047 user = relationship('User')
2043 2048 pull_request = relationship('PullRequest')
2044 2049
2045 2050
2046 2051 class Notification(Base, BaseModel):
2047 2052 __tablename__ = 'notifications'
2048 2053 __table_args__ = (
2049 2054 Index('notification_type_idx', 'type'),
2050 2055 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2051 2056 'mysql_charset': 'utf8'},
2052 2057 )
2053 2058
2054 2059 TYPE_CHANGESET_COMMENT = u'cs_comment'
2055 2060 TYPE_MESSAGE = u'message'
2056 2061 TYPE_MENTION = u'mention'
2057 2062 TYPE_REGISTRATION = u'registration'
2058 2063 TYPE_PULL_REQUEST = u'pull_request'
2059 2064 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
2060 2065
2061 2066 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
2062 2067 subject = Column('subject', Unicode(512), nullable=True)
2063 2068 body = Column('body', UnicodeText(50000), nullable=True)
2064 2069 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
2065 2070 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2066 2071 type_ = Column('type', Unicode(256))
2067 2072
2068 2073 created_by_user = relationship('User')
2069 2074 notifications_to_users = relationship('UserNotification', lazy='joined',
2070 2075 cascade="all, delete, delete-orphan")
2071 2076
2072 2077 @property
2073 2078 def recipients(self):
2074 2079 return [x.user for x in UserNotification.query()\
2075 2080 .filter(UserNotification.notification == self)\
2076 2081 .order_by(UserNotification.user_id.asc()).all()]
2077 2082
2078 2083 @classmethod
2079 2084 def create(cls, created_by, subject, body, recipients, type_=None):
2080 2085 if type_ is None:
2081 2086 type_ = Notification.TYPE_MESSAGE
2082 2087
2083 2088 notification = cls()
2084 2089 notification.created_by_user = created_by
2085 2090 notification.subject = subject
2086 2091 notification.body = body
2087 2092 notification.type_ = type_
2088 2093 notification.created_on = datetime.datetime.now()
2089 2094
2090 2095 for u in recipients:
2091 2096 assoc = UserNotification()
2092 2097 assoc.notification = notification
2093 2098 u.notifications.append(assoc)
2094 2099 Session().add(notification)
2095 2100 return notification
2096 2101
2097 2102 @property
2098 2103 def description(self):
2099 2104 from rhodecode.model.notification import NotificationModel
2100 2105 return NotificationModel().make_description(self)
2101 2106
2102 2107
2103 2108 class UserNotification(Base, BaseModel):
2104 2109 __tablename__ = 'user_to_notification'
2105 2110 __table_args__ = (
2106 2111 UniqueConstraint('user_id', 'notification_id'),
2107 2112 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2108 2113 'mysql_charset': 'utf8'}
2109 2114 )
2110 2115 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2111 2116 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2112 2117 read = Column('read', Boolean, default=False)
2113 2118 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2114 2119
2115 2120 user = relationship('User', lazy="joined")
2116 2121 notification = relationship('Notification', lazy="joined",
2117 2122 order_by=lambda: Notification.created_on.desc(),)
2118 2123
2119 2124 def mark_as_read(self):
2120 2125 self.read = True
2121 2126 Session().add(self)
2122 2127
2123 2128
2124 2129 class Gist(Base, BaseModel):
2125 2130 __tablename__ = 'gists'
2126 2131 __table_args__ = (
2127 2132 Index('g_gist_access_id_idx', 'gist_access_id'),
2128 2133 Index('g_created_on_idx', 'created_on'),
2129 2134 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2130 2135 'mysql_charset': 'utf8'}
2131 2136 )
2132 2137 GIST_PUBLIC = u'public'
2133 2138 GIST_PRIVATE = u'private'
2134 2139
2135 2140 gist_id = Column('gist_id', Integer(), primary_key=True)
2136 2141 gist_access_id = Column('gist_access_id', UnicodeText(1024))
2137 2142 gist_description = Column('gist_description', UnicodeText(1024))
2138 2143 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
2139 2144 gist_expires = Column('gist_expires', Float(), nullable=False)
2140 2145 gist_type = Column('gist_type', Unicode(128), nullable=False)
2141 2146 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2142 2147 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2143 2148
2144 2149 owner = relationship('User')
2145 2150
2146 2151 @classmethod
2147 2152 def get_or_404(cls, id_):
2148 2153 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2149 2154 if not res:
2150 2155 raise HTTPNotFound
2151 2156 return res
2152 2157
2153 2158 @classmethod
2154 2159 def get_by_access_id(cls, gist_access_id):
2155 2160 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2156 2161
2157 2162 def gist_url(self):
2158 2163 import rhodecode
2159 2164 alias_url = rhodecode.CONFIG.get('gist_alias_url')
2160 2165 if alias_url:
2161 2166 return alias_url.replace('{gistid}', self.gist_access_id)
2162 2167
2163 2168 from pylons import url
2164 2169 return url('gist', gist_id=self.gist_access_id, qualified=True)
2165 2170
2166 2171 @classmethod
2167 2172 def base_path(cls):
2168 2173 """
2169 2174 Returns base path when all gists are stored
2170 2175
2171 2176 :param cls:
2172 2177 """
2173 2178 from rhodecode.model.gist import GIST_STORE_LOC
2174 2179 q = Session().query(RhodeCodeUi)\
2175 2180 .filter(RhodeCodeUi.ui_key == URL_SEP)
2176 2181 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2177 2182 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2178 2183
2179 2184 def get_api_data(self):
2180 2185 """
2181 2186 Common function for generating gist related data for API
2182 2187 """
2183 2188 gist = self
2184 2189 data = dict(
2185 2190 gist_id=gist.gist_id,
2186 2191 type=gist.gist_type,
2187 2192 access_id=gist.gist_access_id,
2188 2193 description=gist.gist_description,
2189 2194 url=gist.gist_url(),
2190 2195 expires=gist.gist_expires,
2191 2196 created_on=gist.created_on,
2192 2197 )
2193 2198 return data
2194 2199
2195 2200 def __json__(self):
2196 2201 data = dict(
2197 2202 )
2198 2203 data.update(self.get_api_data())
2199 2204 return data
2200 2205 ## SCM functions
2201 2206
2202 2207 @property
2203 2208 def scm_instance(self):
2204 2209 from rhodecode.lib.vcs import get_repo
2205 2210 base_path = self.base_path()
2206 2211 return get_repo(os.path.join(*map(safe_str,
2207 2212 [base_path, self.gist_access_id])))
2208 2213
2209 2214
2210 2215 class DbMigrateVersion(Base, BaseModel):
2211 2216 __tablename__ = 'db_migrate_version'
2212 2217 __table_args__ = (
2213 2218 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2214 2219 'mysql_charset': 'utf8'},
2215 2220 )
2216 2221 repository_id = Column('repository_id', String(250), primary_key=True)
2217 2222 repository_path = Column('repository_path', Text)
2218 2223 version = Column('version', Integer)
General Comments 0
You need to be logged in to leave comments. Login now