##// END OF EJS Templates
db: drop __mapper_args__ that cause SQLAlchemy deprecation warnings...
Mads Kiilerich -
r7642:b0ed41df default
parent child Browse files
Show More
@@ -1,2535 +1,2531 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.db
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 Database Models for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 08, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import time
30 30 import logging
31 31 import datetime
32 32 import traceback
33 33 import hashlib
34 34 import collections
35 35 import functools
36 36
37 37 import sqlalchemy
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.hybrid import hybrid_property
40 40 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
41 41 from beaker.cache import cache_region, region_invalidate
42 42 from webob.exc import HTTPNotFound
43 43
44 44 from tg.i18n import lazy_ugettext as _
45 45
46 46 from kallithea.lib.exceptions import DefaultUserException
47 47 from kallithea.lib.vcs import get_backend
48 48 from kallithea.lib.vcs.utils.helpers import get_scm
49 49 from kallithea.lib.vcs.utils.lazy import LazyProperty
50 50 from kallithea.lib.vcs.backends.base import EmptyChangeset
51 51
52 52 from kallithea.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
53 53 safe_unicode, remove_prefix, time_to_datetime, aslist, Optional, safe_int, \
54 54 get_clone_url, urlreadable
55 55 from kallithea.lib.compat import json
56 56 from kallithea.lib.caching_query import FromCache
57 57
58 58 from kallithea.model.meta import Base, Session
59 59
60 60 URL_SEP = '/'
61 61 log = logging.getLogger(__name__)
62 62
63 63 #==============================================================================
64 64 # BASE CLASSES
65 65 #==============================================================================
66 66
67 67 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
68 68
69 69
70 70 class BaseDbModel(object):
71 71 """
72 72 Base Model for all classes
73 73 """
74 74
75 75 @classmethod
76 76 def _get_keys(cls):
77 77 """return column names for this model """
78 78 return class_mapper(cls).c.keys()
79 79
80 80 def get_dict(self):
81 81 """
82 82 return dict with keys and values corresponding
83 83 to this model data """
84 84
85 85 d = {}
86 86 for k in self._get_keys():
87 87 d[k] = getattr(self, k)
88 88
89 89 # also use __json__() if present to get additional fields
90 90 _json_attr = getattr(self, '__json__', None)
91 91 if _json_attr:
92 92 # update with attributes from __json__
93 93 if callable(_json_attr):
94 94 _json_attr = _json_attr()
95 95 for k, val in _json_attr.iteritems():
96 96 d[k] = val
97 97 return d
98 98
99 99 def get_appstruct(self):
100 100 """return list with keys and values tuples corresponding
101 101 to this model data """
102 102
103 103 return [
104 104 (k, getattr(self, k))
105 105 for k in self._get_keys()
106 106 ]
107 107
108 108 def populate_obj(self, populate_dict):
109 109 """populate model with data from given populate_dict"""
110 110
111 111 for k in self._get_keys():
112 112 if k in populate_dict:
113 113 setattr(self, k, populate_dict[k])
114 114
115 115 @classmethod
116 116 def query(cls):
117 117 return Session().query(cls)
118 118
119 119 @classmethod
120 120 def get(cls, id_):
121 121 if id_:
122 122 return cls.query().get(id_)
123 123
124 124 @classmethod
125 125 def guess_instance(cls, value, callback=None):
126 126 """Haphazardly attempt to convert `value` to a `cls` instance.
127 127
128 128 If `value` is None or already a `cls` instance, return it. If `value`
129 129 is a number (or looks like one if you squint just right), assume it's
130 130 a database primary key and let SQLAlchemy sort things out. Otherwise,
131 131 fall back to resolving it using `callback` (if specified); this could
132 132 e.g. be a function that looks up instances by name (though that won't
133 133 work if the name begins with a digit). Otherwise, raise Exception.
134 134 """
135 135
136 136 if value is None:
137 137 return None
138 138 if isinstance(value, cls):
139 139 return value
140 140 if isinstance(value, (int, long)) or safe_str(value).isdigit():
141 141 return cls.get(value)
142 142 if callback is not None:
143 143 return callback(value)
144 144
145 145 raise Exception(
146 146 'given object must be int, long or Instance of %s '
147 147 'got %s, no callback provided' % (cls, type(value))
148 148 )
149 149
150 150 @classmethod
151 151 def get_or_404(cls, id_):
152 152 try:
153 153 id_ = int(id_)
154 154 except (TypeError, ValueError):
155 155 raise HTTPNotFound
156 156
157 157 res = cls.query().get(id_)
158 158 if res is None:
159 159 raise HTTPNotFound
160 160 return res
161 161
162 162 @classmethod
163 163 def delete(cls, id_):
164 164 obj = cls.query().get(id_)
165 165 Session().delete(obj)
166 166
167 167 def __repr__(self):
168 168 if hasattr(self, '__unicode__'):
169 169 # python repr needs to return str
170 170 try:
171 171 return safe_str(self.__unicode__())
172 172 except UnicodeDecodeError:
173 173 pass
174 174 return '<DB:%s>' % (self.__class__.__name__)
175 175
176 176
177 177 _table_args_default_dict = {'extend_existing': True,
178 178 'mysql_engine': 'InnoDB',
179 179 'mysql_charset': 'utf8',
180 180 'sqlite_autoincrement': True,
181 181 }
182 182
183 183 class Setting(Base, BaseDbModel):
184 184 __tablename__ = 'settings'
185 185 __table_args__ = (
186 186 _table_args_default_dict,
187 187 )
188 188
189 189 SETTINGS_TYPES = {
190 190 'str': safe_str,
191 191 'int': safe_int,
192 192 'unicode': safe_unicode,
193 193 'bool': str2bool,
194 194 'list': functools.partial(aslist, sep=',')
195 195 }
196 196 DEFAULT_UPDATE_URL = ''
197 197
198 198 app_settings_id = Column(Integer(), primary_key=True)
199 199 app_settings_name = Column(String(255), nullable=False, unique=True)
200 200 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
201 201 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
202 202
203 203 def __init__(self, key='', val='', type='unicode'):
204 204 self.app_settings_name = key
205 205 self.app_settings_value = val
206 206 self.app_settings_type = type
207 207
208 208 @validates('_app_settings_value')
209 209 def validate_settings_value(self, key, val):
210 210 assert type(val) == unicode
211 211 return val
212 212
213 213 @hybrid_property
214 214 def app_settings_value(self):
215 215 v = self._app_settings_value
216 216 _type = self.app_settings_type
217 217 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
218 218 return converter(v)
219 219
220 220 @app_settings_value.setter
221 221 def app_settings_value(self, val):
222 222 """
223 223 Setter that will always make sure we use unicode in app_settings_value
224 224
225 225 :param val:
226 226 """
227 227 self._app_settings_value = safe_unicode(val)
228 228
229 229 @hybrid_property
230 230 def app_settings_type(self):
231 231 return self._app_settings_type
232 232
233 233 @app_settings_type.setter
234 234 def app_settings_type(self, val):
235 235 if val not in self.SETTINGS_TYPES:
236 236 raise Exception('type must be one of %s got %s'
237 237 % (self.SETTINGS_TYPES.keys(), val))
238 238 self._app_settings_type = val
239 239
240 240 def __unicode__(self):
241 241 return u"<%s('%s:%s[%s]')>" % (
242 242 self.__class__.__name__,
243 243 self.app_settings_name, self.app_settings_value, self.app_settings_type
244 244 )
245 245
246 246 @classmethod
247 247 def get_by_name(cls, key):
248 248 return cls.query() \
249 249 .filter(cls.app_settings_name == key).scalar()
250 250
251 251 @classmethod
252 252 def get_by_name_or_create(cls, key, val='', type='unicode'):
253 253 res = cls.get_by_name(key)
254 254 if res is None:
255 255 res = cls(key, val, type)
256 256 return res
257 257
258 258 @classmethod
259 259 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
260 260 """
261 261 Creates or updates Kallithea setting. If updates are triggered, it will only
262 262 update parameters that are explicitly set. Optional instance will be skipped.
263 263
264 264 :param key:
265 265 :param val:
266 266 :param type:
267 267 :return:
268 268 """
269 269 res = cls.get_by_name(key)
270 270 if res is None:
271 271 val = Optional.extract(val)
272 272 type = Optional.extract(type)
273 273 res = cls(key, val, type)
274 274 Session().add(res)
275 275 else:
276 276 res.app_settings_name = key
277 277 if not isinstance(val, Optional):
278 278 # update if set
279 279 res.app_settings_value = val
280 280 if not isinstance(type, Optional):
281 281 # update if set
282 282 res.app_settings_type = type
283 283 return res
284 284
285 285 @classmethod
286 286 def get_app_settings(cls, cache=False):
287 287
288 288 ret = cls.query()
289 289
290 290 if cache:
291 291 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
292 292
293 293 if ret is None:
294 294 raise Exception('Could not get application settings !')
295 295 settings = {}
296 296 for each in ret:
297 297 settings[each.app_settings_name] = \
298 298 each.app_settings_value
299 299
300 300 return settings
301 301
302 302 @classmethod
303 303 def get_auth_settings(cls, cache=False):
304 304 ret = cls.query() \
305 305 .filter(cls.app_settings_name.startswith('auth_')).all()
306 306 fd = {}
307 307 for row in ret:
308 308 fd[row.app_settings_name] = row.app_settings_value
309 309 return fd
310 310
311 311 @classmethod
312 312 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
313 313 ret = cls.query() \
314 314 .filter(cls.app_settings_name.startswith('default_')).all()
315 315 fd = {}
316 316 for row in ret:
317 317 key = row.app_settings_name
318 318 if strip_prefix:
319 319 key = remove_prefix(key, prefix='default_')
320 320 fd.update({key: row.app_settings_value})
321 321
322 322 return fd
323 323
324 324 @classmethod
325 325 def get_server_info(cls):
326 326 import pkg_resources
327 327 import platform
328 328 import kallithea
329 329 from kallithea.lib.utils import check_git_version
330 330 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
331 331 info = {
332 332 'modules': sorted(mods, key=lambda k: k[0].lower()),
333 333 'py_version': platform.python_version(),
334 334 'platform': safe_unicode(platform.platform()),
335 335 'kallithea_version': kallithea.__version__,
336 336 'git_version': safe_unicode(check_git_version()),
337 337 'git_path': kallithea.CONFIG.get('git_path')
338 338 }
339 339 return info
340 340
341 341
342 342 class Ui(Base, BaseDbModel):
343 343 __tablename__ = 'ui'
344 344 __table_args__ = (
345 345 # FIXME: ui_key as key is wrong and should be removed when the corresponding
346 346 # Ui.get_by_key has been replaced by the composite key
347 347 UniqueConstraint('ui_key'),
348 348 UniqueConstraint('ui_section', 'ui_key'),
349 349 _table_args_default_dict,
350 350 )
351 351
352 352 HOOK_UPDATE = 'changegroup.update'
353 353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 354
355 355 ui_id = Column(Integer(), primary_key=True)
356 356 ui_section = Column(String(255), nullable=False)
357 357 ui_key = Column(String(255), nullable=False)
358 358 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
359 359 ui_active = Column(Boolean(), nullable=False, default=True)
360 360
361 361 @classmethod
362 362 def get_by_key(cls, section, key):
363 363 """ Return specified Ui object, or None if not found. """
364 364 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
365 365
366 366 @classmethod
367 367 def get_or_create(cls, section, key):
368 368 """ Return specified Ui object, creating it if necessary. """
369 369 setting = cls.get_by_key(section, key)
370 370 if setting is None:
371 371 setting = cls(ui_section=section, ui_key=key)
372 372 Session().add(setting)
373 373 return setting
374 374
375 375 @classmethod
376 376 def get_builtin_hooks(cls):
377 377 q = cls.query()
378 378 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
379 379 q = q.filter(cls.ui_section == 'hooks')
380 380 return q.all()
381 381
382 382 @classmethod
383 383 def get_custom_hooks(cls):
384 384 q = cls.query()
385 385 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
386 386 q = q.filter(cls.ui_section == 'hooks')
387 387 return q.all()
388 388
389 389 @classmethod
390 390 def get_repos_location(cls):
391 391 return cls.get_by_key('paths', '/').ui_value
392 392
393 393 @classmethod
394 394 def create_or_update_hook(cls, key, val):
395 395 new_ui = cls.get_or_create('hooks', key)
396 396 new_ui.ui_active = True
397 397 new_ui.ui_value = val
398 398
399 399 def __repr__(self):
400 400 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
401 401 self.ui_key, self.ui_value)
402 402
403 403
404 404 class User(Base, BaseDbModel):
405 405 __tablename__ = 'users'
406 406 __table_args__ = (
407 407 Index('u_username_idx', 'username'),
408 408 Index('u_email_idx', 'email'),
409 409 _table_args_default_dict,
410 410 )
411 411
412 412 DEFAULT_USER = 'default'
413 413 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
414 414 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
415 415 DEFAULT_AUTH_TYPE = 'internal'
416 416
417 417 user_id = Column(Integer(), primary_key=True)
418 418 username = Column(String(255), nullable=False, unique=True)
419 419 password = Column(String(255), nullable=False)
420 420 active = Column(Boolean(), nullable=False, default=True)
421 421 admin = Column(Boolean(), nullable=False, default=False)
422 422 name = Column("firstname", Unicode(255), nullable=False)
423 423 lastname = Column(Unicode(255), nullable=False)
424 424 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
425 425 last_login = Column(DateTime(timezone=False), nullable=True)
426 426 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
427 427 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
428 428 api_key = Column(String(255), nullable=False)
429 429 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
430 430 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
431 431
432 432 user_log = relationship('UserLog')
433 433 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
434 434
435 435 repositories = relationship('Repository')
436 436 repo_groups = relationship('RepoGroup')
437 437 user_groups = relationship('UserGroup')
438 438 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
439 439 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
440 440
441 441 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
442 442 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
443 443
444 444 group_member = relationship('UserGroupMember', cascade='all')
445 445
446 446 # comments created by this user
447 447 user_comments = relationship('ChangesetComment', cascade='all')
448 448 # extra emails for this user
449 449 user_emails = relationship('UserEmailMap', cascade='all')
450 450 # extra API keys
451 451 user_api_keys = relationship('UserApiKeys', cascade='all')
452 452
453 453 @hybrid_property
454 454 def email(self):
455 455 return self._email
456 456
457 457 @email.setter
458 458 def email(self, val):
459 459 self._email = val.lower() if val else None
460 460
461 461 @property
462 462 def firstname(self):
463 463 # alias for future
464 464 return self.name
465 465
466 466 @property
467 467 def emails(self):
468 468 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
469 469 return [self.email] + [x.email for x in other]
470 470
471 471 @property
472 472 def api_keys(self):
473 473 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
474 474 return [self.api_key] + [x.api_key for x in other]
475 475
476 476 @property
477 477 def ip_addresses(self):
478 478 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
479 479 return [x.ip_addr for x in ret]
480 480
481 481 @property
482 482 def full_name(self):
483 483 return '%s %s' % (self.firstname, self.lastname)
484 484
485 485 @property
486 486 def full_name_or_username(self):
487 487 """
488 488 Show full name.
489 489 If full name is not set, fall back to username.
490 490 """
491 491 return ('%s %s' % (self.firstname, self.lastname)
492 492 if (self.firstname and self.lastname) else self.username)
493 493
494 494 @property
495 495 def full_name_and_username(self):
496 496 """
497 497 Show full name and username as 'Firstname Lastname (username)'.
498 498 If full name is not set, fall back to username.
499 499 """
500 500 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
501 501 if (self.firstname and self.lastname) else self.username)
502 502
503 503 @property
504 504 def full_contact(self):
505 505 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
506 506
507 507 @property
508 508 def short_contact(self):
509 509 return '%s %s' % (self.firstname, self.lastname)
510 510
511 511 @property
512 512 def is_admin(self):
513 513 return self.admin
514 514
515 515 @hybrid_property
516 516 def is_default_user(self):
517 517 return self.username == User.DEFAULT_USER
518 518
519 519 @hybrid_property
520 520 def user_data(self):
521 521 if not self._user_data:
522 522 return {}
523 523
524 524 try:
525 525 return json.loads(self._user_data)
526 526 except TypeError:
527 527 return {}
528 528
529 529 @user_data.setter
530 530 def user_data(self, val):
531 531 try:
532 532 self._user_data = json.dumps(val)
533 533 except Exception:
534 534 log.error(traceback.format_exc())
535 535
536 536 def __unicode__(self):
537 537 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
538 538 self.user_id, self.username)
539 539
540 540 @classmethod
541 541 def guess_instance(cls, value):
542 542 return super(User, cls).guess_instance(value, User.get_by_username)
543 543
544 544 @classmethod
545 545 def get_or_404(cls, id_, allow_default=True):
546 546 '''
547 547 Overridden version of BaseDbModel.get_or_404, with an extra check on
548 548 the default user.
549 549 '''
550 550 user = super(User, cls).get_or_404(id_)
551 551 if not allow_default and user.is_default_user:
552 552 raise DefaultUserException()
553 553 return user
554 554
555 555 @classmethod
556 556 def get_by_username_or_email(cls, username_or_email, case_insensitive=False, cache=False):
557 557 """
558 558 For anything that looks like an email address, look up by the email address (matching
559 559 case insensitively).
560 560 For anything else, try to look up by the user name.
561 561
562 562 This assumes no normal username can have '@' symbol.
563 563 """
564 564 if '@' in username_or_email:
565 565 return User.get_by_email(username_or_email, cache=cache)
566 566 else:
567 567 return User.get_by_username(username_or_email, case_insensitive=case_insensitive, cache=cache)
568 568
569 569 @classmethod
570 570 def get_by_username(cls, username, case_insensitive=False, cache=False):
571 571 if case_insensitive:
572 572 q = cls.query().filter(func.lower(cls.username) == func.lower(username))
573 573 else:
574 574 q = cls.query().filter(cls.username == username)
575 575
576 576 if cache:
577 577 q = q.options(FromCache(
578 578 "sql_cache_short",
579 579 "get_user_%s" % _hash_key(username)
580 580 )
581 581 )
582 582 return q.scalar()
583 583
584 584 @classmethod
585 585 def get_by_api_key(cls, api_key, cache=False, fallback=True):
586 586 if len(api_key) != 40 or not api_key.isalnum():
587 587 return None
588 588
589 589 q = cls.query().filter(cls.api_key == api_key)
590 590
591 591 if cache:
592 592 q = q.options(FromCache("sql_cache_short",
593 593 "get_api_key_%s" % api_key))
594 594 res = q.scalar()
595 595
596 596 if fallback and not res:
597 597 # fallback to additional keys
598 598 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
599 599 if _res:
600 600 res = _res.user
601 601 if res is None or not res.active or res.is_default_user:
602 602 return None
603 603 return res
604 604
605 605 @classmethod
606 606 def get_by_email(cls, email, cache=False):
607 607 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
608 608
609 609 if cache:
610 610 q = q.options(FromCache("sql_cache_short",
611 611 "get_email_key_%s" % email))
612 612
613 613 ret = q.scalar()
614 614 if ret is None:
615 615 q = UserEmailMap.query()
616 616 # try fetching in alternate email map
617 617 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
618 618 q = q.options(joinedload(UserEmailMap.user))
619 619 if cache:
620 620 q = q.options(FromCache("sql_cache_short",
621 621 "get_email_map_key_%s" % email))
622 622 ret = getattr(q.scalar(), 'user', None)
623 623
624 624 return ret
625 625
626 626 @classmethod
627 627 def get_from_cs_author(cls, author):
628 628 """
629 629 Tries to get User objects out of commit author string
630 630
631 631 :param author:
632 632 """
633 633 from kallithea.lib.helpers import email, author_name
634 634 # Valid email in the attribute passed, see if they're in the system
635 635 _email = email(author)
636 636 if _email:
637 637 user = cls.get_by_email(_email)
638 638 if user is not None:
639 639 return user
640 640 # Maybe we can match by username?
641 641 _author = author_name(author)
642 642 user = cls.get_by_username(_author, case_insensitive=True)
643 643 if user is not None:
644 644 return user
645 645
646 646 def update_lastlogin(self):
647 647 """Update user lastlogin"""
648 648 self.last_login = datetime.datetime.now()
649 649 log.debug('updated user %s lastlogin', self.username)
650 650
651 651 @classmethod
652 652 def get_first_admin(cls):
653 653 user = User.query().filter(User.admin == True).first()
654 654 if user is None:
655 655 raise Exception('Missing administrative account!')
656 656 return user
657 657
658 658 @classmethod
659 659 def get_default_user(cls, cache=False):
660 660 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
661 661 if user is None:
662 662 raise Exception('Missing default account!')
663 663 return user
664 664
665 665 def get_api_data(self, details=False):
666 666 """
667 667 Common function for generating user related data for API
668 668 """
669 669 user = self
670 670 data = dict(
671 671 user_id=user.user_id,
672 672 username=user.username,
673 673 firstname=user.name,
674 674 lastname=user.lastname,
675 675 email=user.email,
676 676 emails=user.emails,
677 677 active=user.active,
678 678 admin=user.admin,
679 679 )
680 680 if details:
681 681 data.update(dict(
682 682 extern_type=user.extern_type,
683 683 extern_name=user.extern_name,
684 684 api_key=user.api_key,
685 685 api_keys=user.api_keys,
686 686 last_login=user.last_login,
687 687 ip_addresses=user.ip_addresses
688 688 ))
689 689 return data
690 690
691 691 def __json__(self):
692 692 data = dict(
693 693 full_name=self.full_name,
694 694 full_name_or_username=self.full_name_or_username,
695 695 short_contact=self.short_contact,
696 696 full_contact=self.full_contact
697 697 )
698 698 data.update(self.get_api_data())
699 699 return data
700 700
701 701
702 702 class UserApiKeys(Base, BaseDbModel):
703 703 __tablename__ = 'user_api_keys'
704 704 __table_args__ = (
705 705 Index('uak_api_key_idx', 'api_key'),
706 706 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
707 707 _table_args_default_dict,
708 708 )
709 __mapper_args__ = {}
710 709
711 710 user_api_key_id = Column(Integer(), primary_key=True)
712 711 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
713 712 api_key = Column(String(255), nullable=False, unique=True)
714 713 description = Column(UnicodeText(), nullable=False)
715 714 expires = Column(Float(53), nullable=False)
716 715 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
717 716
718 717 user = relationship('User')
719 718
720 719 @hybrid_property
721 720 def is_expired(self):
722 721 return (self.expires != -1) & (time.time() > self.expires)
723 722
724 723
725 724 class UserEmailMap(Base, BaseDbModel):
726 725 __tablename__ = 'user_email_map'
727 726 __table_args__ = (
728 727 Index('uem_email_idx', 'email'),
729 728 _table_args_default_dict,
730 729 )
731 __mapper_args__ = {}
732 730
733 731 email_id = Column(Integer(), primary_key=True)
734 732 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
735 733 _email = Column("email", String(255), nullable=False, unique=True)
736 734 user = relationship('User')
737 735
738 736 @validates('_email')
739 737 def validate_email(self, key, email):
740 738 # check if this email is not main one
741 739 main_email = Session().query(User).filter(User.email == email).scalar()
742 740 if main_email is not None:
743 741 raise AttributeError('email %s is present is user table' % email)
744 742 return email
745 743
746 744 @hybrid_property
747 745 def email(self):
748 746 return self._email
749 747
750 748 @email.setter
751 749 def email(self, val):
752 750 self._email = val.lower() if val else None
753 751
754 752
755 753 class UserIpMap(Base, BaseDbModel):
756 754 __tablename__ = 'user_ip_map'
757 755 __table_args__ = (
758 756 UniqueConstraint('user_id', 'ip_addr'),
759 757 _table_args_default_dict,
760 758 )
761 __mapper_args__ = {}
762 759
763 760 ip_id = Column(Integer(), primary_key=True)
764 761 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
765 762 ip_addr = Column(String(255), nullable=False)
766 763 active = Column(Boolean(), nullable=False, default=True)
767 764 user = relationship('User')
768 765
769 766 @classmethod
770 767 def _get_ip_range(cls, ip_addr):
771 768 from kallithea.lib import ipaddr
772 769 net = ipaddr.IPNetwork(address=ip_addr)
773 770 return [str(net.network), str(net.broadcast)]
774 771
775 772 def __json__(self):
776 773 return dict(
777 774 ip_addr=self.ip_addr,
778 775 ip_range=self._get_ip_range(self.ip_addr)
779 776 )
780 777
781 778 def __unicode__(self):
782 779 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
783 780 self.user_id, self.ip_addr)
784 781
785 782
786 783 class UserLog(Base, BaseDbModel):
787 784 __tablename__ = 'user_logs'
788 785 __table_args__ = (
789 786 _table_args_default_dict,
790 787 )
791 788
792 789 user_log_id = Column(Integer(), primary_key=True)
793 790 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
794 791 username = Column(String(255), nullable=False)
795 792 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
796 793 repository_name = Column(Unicode(255), nullable=False)
797 794 user_ip = Column(String(255), nullable=True)
798 795 action = Column(UnicodeText(), nullable=False)
799 796 action_date = Column(DateTime(timezone=False), nullable=False)
800 797
801 798 def __unicode__(self):
802 799 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
803 800 self.repository_name,
804 801 self.action)
805 802
806 803 @property
807 804 def action_as_day(self):
808 805 return datetime.date(*self.action_date.timetuple()[:3])
809 806
810 807 user = relationship('User')
811 808 repository = relationship('Repository', cascade='')
812 809
813 810
814 811 class UserGroup(Base, BaseDbModel):
815 812 __tablename__ = 'users_groups'
816 813 __table_args__ = (
817 814 _table_args_default_dict,
818 815 )
819 816
820 817 users_group_id = Column(Integer(), primary_key=True)
821 818 users_group_name = Column(Unicode(255), nullable=False, unique=True)
822 819 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
823 820 users_group_active = Column(Boolean(), nullable=False)
824 821 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
825 822 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
826 823 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
827 824
828 825 members = relationship('UserGroupMember', cascade="all, delete-orphan")
829 826 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
830 827 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
831 828 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
832 829 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
833 830 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
834 831
835 832 owner = relationship('User')
836 833
837 834 @hybrid_property
838 835 def group_data(self):
839 836 if not self._group_data:
840 837 return {}
841 838
842 839 try:
843 840 return json.loads(self._group_data)
844 841 except TypeError:
845 842 return {}
846 843
847 844 @group_data.setter
848 845 def group_data(self, val):
849 846 try:
850 847 self._group_data = json.dumps(val)
851 848 except Exception:
852 849 log.error(traceback.format_exc())
853 850
854 851 def __unicode__(self):
855 852 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
856 853 self.users_group_id,
857 854 self.users_group_name)
858 855
859 856 @classmethod
860 857 def guess_instance(cls, value):
861 858 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
862 859
863 860 @classmethod
864 861 def get_by_group_name(cls, group_name, cache=False,
865 862 case_insensitive=False):
866 863 if case_insensitive:
867 864 q = cls.query().filter(func.lower(cls.users_group_name) == func.lower(group_name))
868 865 else:
869 866 q = cls.query().filter(cls.users_group_name == group_name)
870 867 if cache:
871 868 q = q.options(FromCache(
872 869 "sql_cache_short",
873 870 "get_group_%s" % _hash_key(group_name)
874 871 )
875 872 )
876 873 return q.scalar()
877 874
878 875 @classmethod
879 876 def get(cls, user_group_id, cache=False):
880 877 user_group = cls.query()
881 878 if cache:
882 879 user_group = user_group.options(FromCache("sql_cache_short",
883 880 "get_users_group_%s" % user_group_id))
884 881 return user_group.get(user_group_id)
885 882
886 883 def get_api_data(self, with_members=True):
887 884 user_group = self
888 885
889 886 data = dict(
890 887 users_group_id=user_group.users_group_id,
891 888 group_name=user_group.users_group_name,
892 889 group_description=user_group.user_group_description,
893 890 active=user_group.users_group_active,
894 891 owner=user_group.owner.username,
895 892 )
896 893 if with_members:
897 894 data['members'] = [
898 895 ugm.user.get_api_data()
899 896 for ugm in user_group.members
900 897 ]
901 898
902 899 return data
903 900
904 901
905 902 class UserGroupMember(Base, BaseDbModel):
906 903 __tablename__ = 'users_groups_members'
907 904 __table_args__ = (
908 905 _table_args_default_dict,
909 906 )
910 907
911 908 users_group_member_id = Column(Integer(), primary_key=True)
912 909 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
913 910 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
914 911
915 912 user = relationship('User')
916 913 users_group = relationship('UserGroup')
917 914
918 915 def __init__(self, gr_id='', u_id=''):
919 916 self.users_group_id = gr_id
920 917 self.user_id = u_id
921 918
922 919
923 920 class RepositoryField(Base, BaseDbModel):
924 921 __tablename__ = 'repositories_fields'
925 922 __table_args__ = (
926 923 UniqueConstraint('repository_id', 'field_key'), # no-multi field
927 924 _table_args_default_dict,
928 925 )
929 926
930 927 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
931 928
932 929 repo_field_id = Column(Integer(), primary_key=True)
933 930 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
934 931 field_key = Column(String(250), nullable=False)
935 932 field_label = Column(String(1024), nullable=False)
936 933 field_value = Column(String(10000), nullable=False)
937 934 field_desc = Column(String(1024), nullable=False)
938 935 field_type = Column(String(255), nullable=False)
939 936 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
940 937
941 938 repository = relationship('Repository')
942 939
943 940 @property
944 941 def field_key_prefixed(self):
945 942 return 'ex_%s' % self.field_key
946 943
947 944 @classmethod
948 945 def un_prefix_key(cls, key):
949 946 if key.startswith(cls.PREFIX):
950 947 return key[len(cls.PREFIX):]
951 948 return key
952 949
953 950 @classmethod
954 951 def get_by_key_name(cls, key, repo):
955 952 row = cls.query() \
956 953 .filter(cls.repository == repo) \
957 954 .filter(cls.field_key == key).scalar()
958 955 return row
959 956
960 957
961 958 class Repository(Base, BaseDbModel):
962 959 __tablename__ = 'repositories'
963 960 __table_args__ = (
964 961 Index('r_repo_name_idx', 'repo_name'),
965 962 _table_args_default_dict,
966 963 )
967 964
968 965 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
969 966 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
970 967
971 968 STATE_CREATED = u'repo_state_created'
972 969 STATE_PENDING = u'repo_state_pending'
973 970 STATE_ERROR = u'repo_state_error'
974 971
975 972 repo_id = Column(Integer(), primary_key=True)
976 973 repo_name = Column(Unicode(255), nullable=False, unique=True)
977 974 repo_state = Column(String(255), nullable=False)
978 975
979 976 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
980 977 repo_type = Column(String(255), nullable=False)
981 978 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
982 979 private = Column(Boolean(), nullable=False)
983 980 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
984 981 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
985 982 description = Column(Unicode(10000), nullable=False)
986 983 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
987 984 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
988 985 _landing_revision = Column("landing_revision", String(255), nullable=False)
989 986 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
990 987
991 988 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
992 989 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
993 990
994 991 owner = relationship('User')
995 992 fork = relationship('Repository', remote_side=repo_id)
996 993 group = relationship('RepoGroup')
997 994 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
998 995 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
999 996 stats = relationship('Statistics', cascade='all', uselist=False)
1000 997
1001 998 followers = relationship('UserFollowing',
1002 999 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
1003 1000 cascade='all')
1004 1001 extra_fields = relationship('RepositoryField',
1005 1002 cascade="all, delete-orphan")
1006 1003
1007 1004 logs = relationship('UserLog')
1008 1005 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
1009 1006
1010 1007 pull_requests_org = relationship('PullRequest',
1011 1008 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
1012 1009 cascade="all, delete-orphan")
1013 1010
1014 1011 pull_requests_other = relationship('PullRequest',
1015 1012 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
1016 1013 cascade="all, delete-orphan")
1017 1014
1018 1015 def __unicode__(self):
1019 1016 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1020 1017 safe_unicode(self.repo_name))
1021 1018
1022 1019 @hybrid_property
1023 1020 def landing_rev(self):
1024 1021 # always should return [rev_type, rev]
1025 1022 if self._landing_revision:
1026 1023 _rev_info = self._landing_revision.split(':')
1027 1024 if len(_rev_info) < 2:
1028 1025 _rev_info.insert(0, 'rev')
1029 1026 return [_rev_info[0], _rev_info[1]]
1030 1027 return [None, None]
1031 1028
1032 1029 @landing_rev.setter
1033 1030 def landing_rev(self, val):
1034 1031 if ':' not in val:
1035 1032 raise ValueError('value must be delimited with `:` and consist '
1036 1033 'of <rev_type>:<rev>, got %s instead' % val)
1037 1034 self._landing_revision = val
1038 1035
1039 1036 @hybrid_property
1040 1037 def changeset_cache(self):
1041 1038 try:
1042 1039 cs_cache = json.loads(self._changeset_cache) # might raise on bad data
1043 1040 cs_cache['raw_id'] # verify data, raise exception on error
1044 1041 return cs_cache
1045 1042 except (TypeError, KeyError, ValueError):
1046 1043 return EmptyChangeset().__json__()
1047 1044
1048 1045 @changeset_cache.setter
1049 1046 def changeset_cache(self, val):
1050 1047 try:
1051 1048 self._changeset_cache = json.dumps(val)
1052 1049 except Exception:
1053 1050 log.error(traceback.format_exc())
1054 1051
1055 1052 @classmethod
1056 1053 def query(cls, sorted=False):
1057 1054 """Add Repository-specific helpers for common query constructs.
1058 1055
1059 1056 sorted: if True, apply the default ordering (name, case insensitive).
1060 1057 """
1061 1058 q = super(Repository, cls).query()
1062 1059
1063 1060 if sorted:
1064 1061 q = q.order_by(func.lower(Repository.repo_name))
1065 1062
1066 1063 return q
1067 1064
1068 1065 @classmethod
1069 1066 def url_sep(cls):
1070 1067 return URL_SEP
1071 1068
1072 1069 @classmethod
1073 1070 def normalize_repo_name(cls, repo_name):
1074 1071 """
1075 1072 Normalizes os specific repo_name to the format internally stored inside
1076 1073 database using URL_SEP
1077 1074
1078 1075 :param cls:
1079 1076 :param repo_name:
1080 1077 """
1081 1078 return cls.url_sep().join(repo_name.split(os.sep))
1082 1079
1083 1080 @classmethod
1084 1081 def guess_instance(cls, value):
1085 1082 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1086 1083
1087 1084 @classmethod
1088 1085 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1089 1086 """Get the repo, defaulting to database case sensitivity.
1090 1087 case_insensitive will be slower and should only be specified if necessary."""
1091 1088 if case_insensitive:
1092 1089 q = Session().query(cls).filter(func.lower(cls.repo_name) == func.lower(repo_name))
1093 1090 else:
1094 1091 q = Session().query(cls).filter(cls.repo_name == repo_name)
1095 1092 q = q.options(joinedload(Repository.fork)) \
1096 1093 .options(joinedload(Repository.owner)) \
1097 1094 .options(joinedload(Repository.group))
1098 1095 return q.scalar()
1099 1096
1100 1097 @classmethod
1101 1098 def get_by_full_path(cls, repo_full_path):
1102 1099 base_full_path = os.path.realpath(cls.base_path())
1103 1100 repo_full_path = os.path.realpath(repo_full_path)
1104 1101 assert repo_full_path.startswith(base_full_path + os.path.sep)
1105 1102 repo_name = repo_full_path[len(base_full_path) + 1:]
1106 1103 repo_name = cls.normalize_repo_name(repo_name)
1107 1104 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1108 1105
1109 1106 @classmethod
1110 1107 def get_repo_forks(cls, repo_id):
1111 1108 return cls.query().filter(Repository.fork_id == repo_id)
1112 1109
1113 1110 @classmethod
1114 1111 def base_path(cls):
1115 1112 """
1116 1113 Returns base path where all repos are stored
1117 1114
1118 1115 :param cls:
1119 1116 """
1120 1117 q = Session().query(Ui) \
1121 1118 .filter(Ui.ui_key == cls.url_sep())
1122 1119 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1123 1120 return q.one().ui_value
1124 1121
1125 1122 @property
1126 1123 def forks(self):
1127 1124 """
1128 1125 Return forks of this repo
1129 1126 """
1130 1127 return Repository.get_repo_forks(self.repo_id)
1131 1128
1132 1129 @property
1133 1130 def parent(self):
1134 1131 """
1135 1132 Returns fork parent
1136 1133 """
1137 1134 return self.fork
1138 1135
1139 1136 @property
1140 1137 def just_name(self):
1141 1138 return self.repo_name.split(Repository.url_sep())[-1]
1142 1139
1143 1140 @property
1144 1141 def groups_with_parents(self):
1145 1142 groups = []
1146 1143 group = self.group
1147 1144 while group is not None:
1148 1145 groups.append(group)
1149 1146 group = group.parent_group
1150 1147 assert group not in groups, group # avoid recursion on bad db content
1151 1148 groups.reverse()
1152 1149 return groups
1153 1150
1154 1151 @LazyProperty
1155 1152 def repo_path(self):
1156 1153 """
1157 1154 Returns base full path for that repository means where it actually
1158 1155 exists on a filesystem
1159 1156 """
1160 1157 q = Session().query(Ui).filter(Ui.ui_key ==
1161 1158 Repository.url_sep())
1162 1159 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1163 1160 return q.one().ui_value
1164 1161
1165 1162 @property
1166 1163 def repo_full_path(self):
1167 1164 p = [self.repo_path]
1168 1165 # we need to split the name by / since this is how we store the
1169 1166 # names in the database, but that eventually needs to be converted
1170 1167 # into a valid system path
1171 1168 p += self.repo_name.split(Repository.url_sep())
1172 1169 return os.path.join(*map(safe_unicode, p))
1173 1170
1174 1171 @property
1175 1172 def cache_keys(self):
1176 1173 """
1177 1174 Returns associated cache keys for that repo
1178 1175 """
1179 1176 return CacheInvalidation.query() \
1180 1177 .filter(CacheInvalidation.cache_args == self.repo_name) \
1181 1178 .order_by(CacheInvalidation.cache_key) \
1182 1179 .all()
1183 1180
1184 1181 def get_new_name(self, repo_name):
1185 1182 """
1186 1183 returns new full repository name based on assigned group and new new
1187 1184
1188 1185 :param group_name:
1189 1186 """
1190 1187 path_prefix = self.group.full_path_splitted if self.group else []
1191 1188 return Repository.url_sep().join(path_prefix + [repo_name])
1192 1189
1193 1190 @property
1194 1191 def _ui(self):
1195 1192 """
1196 1193 Creates an db based ui object for this repository
1197 1194 """
1198 1195 from kallithea.lib.utils import make_ui
1199 1196 return make_ui(clear_session=False)
1200 1197
1201 1198 @classmethod
1202 1199 def is_valid(cls, repo_name):
1203 1200 """
1204 1201 returns True if given repo name is a valid filesystem repository
1205 1202
1206 1203 :param cls:
1207 1204 :param repo_name:
1208 1205 """
1209 1206 from kallithea.lib.utils import is_valid_repo
1210 1207
1211 1208 return is_valid_repo(repo_name, cls.base_path())
1212 1209
1213 1210 def get_api_data(self, with_revision_names=False,
1214 1211 with_pullrequests=False):
1215 1212 """
1216 1213 Common function for generating repo api data.
1217 1214 Optionally, also return tags, branches, bookmarks and PRs.
1218 1215 """
1219 1216 repo = self
1220 1217 data = dict(
1221 1218 repo_id=repo.repo_id,
1222 1219 repo_name=repo.repo_name,
1223 1220 repo_type=repo.repo_type,
1224 1221 clone_uri=repo.clone_uri,
1225 1222 private=repo.private,
1226 1223 created_on=repo.created_on,
1227 1224 description=repo.description,
1228 1225 landing_rev=repo.landing_rev,
1229 1226 owner=repo.owner.username,
1230 1227 fork_of=repo.fork.repo_name if repo.fork else None,
1231 1228 enable_statistics=repo.enable_statistics,
1232 1229 enable_downloads=repo.enable_downloads,
1233 1230 last_changeset=repo.changeset_cache,
1234 1231 )
1235 1232 if with_revision_names:
1236 1233 scm_repo = repo.scm_instance_no_cache()
1237 1234 data.update(dict(
1238 1235 tags=scm_repo.tags,
1239 1236 branches=scm_repo.branches,
1240 1237 bookmarks=scm_repo.bookmarks,
1241 1238 ))
1242 1239 if with_pullrequests:
1243 1240 data['pull_requests'] = repo.pull_requests_other
1244 1241 rc_config = Setting.get_app_settings()
1245 1242 repository_fields = str2bool(rc_config.get('repository_fields'))
1246 1243 if repository_fields:
1247 1244 for f in self.extra_fields:
1248 1245 data[f.field_key_prefixed] = f.field_value
1249 1246
1250 1247 return data
1251 1248
1252 1249 @property
1253 1250 def last_db_change(self):
1254 1251 return self.updated_on
1255 1252
1256 1253 @property
1257 1254 def clone_uri_hidden(self):
1258 1255 clone_uri = self.clone_uri
1259 1256 if clone_uri:
1260 1257 import urlobject
1261 1258 url_obj = urlobject.URLObject(self.clone_uri)
1262 1259 if url_obj.password:
1263 1260 clone_uri = url_obj.with_password('*****')
1264 1261 return clone_uri
1265 1262
1266 1263 def clone_url(self, **override):
1267 1264 import kallithea.lib.helpers as h
1268 1265 qualified_home_url = h.canonical_url('home')
1269 1266
1270 1267 uri_tmpl = None
1271 1268 if 'with_id' in override:
1272 1269 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1273 1270 del override['with_id']
1274 1271
1275 1272 if 'uri_tmpl' in override:
1276 1273 uri_tmpl = override['uri_tmpl']
1277 1274 del override['uri_tmpl']
1278 1275
1279 1276 # we didn't override our tmpl from **overrides
1280 1277 if not uri_tmpl:
1281 1278 uri_tmpl = self.DEFAULT_CLONE_URI
1282 1279 try:
1283 1280 from tg import tmpl_context as c
1284 1281 uri_tmpl = c.clone_uri_tmpl
1285 1282 except AttributeError:
1286 1283 # in any case if we call this outside of request context,
1287 1284 # ie, not having tmpl_context set up
1288 1285 pass
1289 1286
1290 1287 return get_clone_url(uri_tmpl=uri_tmpl,
1291 1288 qualified_home_url=qualified_home_url,
1292 1289 repo_name=self.repo_name,
1293 1290 repo_id=self.repo_id, **override)
1294 1291
1295 1292 def set_state(self, state):
1296 1293 self.repo_state = state
1297 1294
1298 1295 #==========================================================================
1299 1296 # SCM PROPERTIES
1300 1297 #==========================================================================
1301 1298
1302 1299 def get_changeset(self, rev=None):
1303 1300 return get_changeset_safe(self.scm_instance, rev)
1304 1301
1305 1302 def get_landing_changeset(self):
1306 1303 """
1307 1304 Returns landing changeset, or if that doesn't exist returns the tip
1308 1305 """
1309 1306 _rev_type, _rev = self.landing_rev
1310 1307 cs = self.get_changeset(_rev)
1311 1308 if isinstance(cs, EmptyChangeset):
1312 1309 return self.get_changeset()
1313 1310 return cs
1314 1311
1315 1312 def update_changeset_cache(self, cs_cache=None):
1316 1313 """
1317 1314 Update cache of last changeset for repository, keys should be::
1318 1315
1319 1316 short_id
1320 1317 raw_id
1321 1318 revision
1322 1319 message
1323 1320 date
1324 1321 author
1325 1322
1326 1323 :param cs_cache:
1327 1324 """
1328 1325 from kallithea.lib.vcs.backends.base import BaseChangeset
1329 1326 if cs_cache is None:
1330 1327 cs_cache = EmptyChangeset()
1331 1328 # use no-cache version here
1332 1329 scm_repo = self.scm_instance_no_cache()
1333 1330 if scm_repo:
1334 1331 cs_cache = scm_repo.get_changeset()
1335 1332
1336 1333 if isinstance(cs_cache, BaseChangeset):
1337 1334 cs_cache = cs_cache.__json__()
1338 1335
1339 1336 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1340 1337 _default = datetime.datetime.fromtimestamp(0)
1341 1338 last_change = cs_cache.get('date') or _default
1342 1339 log.debug('updated repo %s with new cs cache %s',
1343 1340 self.repo_name, cs_cache)
1344 1341 self.updated_on = last_change
1345 1342 self.changeset_cache = cs_cache
1346 1343 Session().commit()
1347 1344 else:
1348 1345 log.debug('changeset_cache for %s already up to date with %s',
1349 1346 self.repo_name, cs_cache['raw_id'])
1350 1347
1351 1348 @property
1352 1349 def tip(self):
1353 1350 return self.get_changeset('tip')
1354 1351
1355 1352 @property
1356 1353 def author(self):
1357 1354 return self.tip.author
1358 1355
1359 1356 @property
1360 1357 def last_change(self):
1361 1358 return self.scm_instance.last_change
1362 1359
1363 1360 def get_comments(self, revisions=None):
1364 1361 """
1365 1362 Returns comments for this repository grouped by revisions
1366 1363
1367 1364 :param revisions: filter query by revisions only
1368 1365 """
1369 1366 cmts = ChangesetComment.query() \
1370 1367 .filter(ChangesetComment.repo == self)
1371 1368 if revisions is not None:
1372 1369 if not revisions:
1373 1370 return {} # don't use sql 'in' on empty set
1374 1371 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1375 1372 grouped = collections.defaultdict(list)
1376 1373 for cmt in cmts.all():
1377 1374 grouped[cmt.revision].append(cmt)
1378 1375 return grouped
1379 1376
1380 1377 def statuses(self, revisions):
1381 1378 """
1382 1379 Returns statuses for this repository.
1383 1380 PRs without any votes do _not_ show up as unreviewed.
1384 1381
1385 1382 :param revisions: list of revisions to get statuses for
1386 1383 """
1387 1384 if not revisions:
1388 1385 return {}
1389 1386
1390 1387 statuses = ChangesetStatus.query() \
1391 1388 .filter(ChangesetStatus.repo == self) \
1392 1389 .filter(ChangesetStatus.version == 0) \
1393 1390 .filter(ChangesetStatus.revision.in_(revisions))
1394 1391
1395 1392 grouped = {}
1396 1393 for stat in statuses.all():
1397 1394 pr_id = pr_nice_id = pr_repo = None
1398 1395 if stat.pull_request:
1399 1396 pr_id = stat.pull_request.pull_request_id
1400 1397 pr_nice_id = PullRequest.make_nice_id(pr_id)
1401 1398 pr_repo = stat.pull_request.other_repo.repo_name
1402 1399 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1403 1400 pr_id, pr_repo, pr_nice_id,
1404 1401 stat.author]
1405 1402 return grouped
1406 1403
1407 1404 def _repo_size(self):
1408 1405 from kallithea.lib import helpers as h
1409 1406 log.debug('calculating repository size...')
1410 1407 return h.format_byte_size(self.scm_instance.size)
1411 1408
1412 1409 #==========================================================================
1413 1410 # SCM CACHE INSTANCE
1414 1411 #==========================================================================
1415 1412
1416 1413 def set_invalidate(self):
1417 1414 """
1418 1415 Mark caches of this repo as invalid.
1419 1416 """
1420 1417 CacheInvalidation.set_invalidate(self.repo_name)
1421 1418
1422 1419 _scm_instance = None
1423 1420
1424 1421 @property
1425 1422 def scm_instance(self):
1426 1423 if self._scm_instance is None:
1427 1424 self._scm_instance = self.scm_instance_cached()
1428 1425 return self._scm_instance
1429 1426
1430 1427 def scm_instance_cached(self, valid_cache_keys=None):
1431 1428 @cache_region('long_term', 'scm_instance_cached')
1432 1429 def _c(repo_name): # repo_name is just for the cache key
1433 1430 log.debug('Creating new %s scm_instance and populating cache', repo_name)
1434 1431 return self.scm_instance_no_cache()
1435 1432 rn = self.repo_name
1436 1433
1437 1434 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1438 1435 if not valid:
1439 1436 log.debug('Cache for %s invalidated, getting new object', rn)
1440 1437 region_invalidate(_c, None, 'scm_instance_cached', rn)
1441 1438 else:
1442 1439 log.debug('Trying to get scm_instance of %s from cache', rn)
1443 1440 return _c(rn)
1444 1441
1445 1442 def scm_instance_no_cache(self):
1446 1443 repo_full_path = safe_str(self.repo_full_path)
1447 1444 alias = get_scm(repo_full_path)[0]
1448 1445 log.debug('Creating instance of %s repository from %s',
1449 1446 alias, self.repo_full_path)
1450 1447 backend = get_backend(alias)
1451 1448
1452 1449 if alias == 'hg':
1453 1450 repo = backend(repo_full_path, create=False,
1454 1451 baseui=self._ui)
1455 1452 else:
1456 1453 repo = backend(repo_full_path, create=False)
1457 1454
1458 1455 return repo
1459 1456
1460 1457 def __json__(self):
1461 1458 return dict(
1462 1459 repo_id=self.repo_id,
1463 1460 repo_name=self.repo_name,
1464 1461 landing_rev=self.landing_rev,
1465 1462 )
1466 1463
1467 1464
1468 1465 class RepoGroup(Base, BaseDbModel):
1469 1466 __tablename__ = 'groups'
1470 1467 __table_args__ = (
1471 1468 _table_args_default_dict,
1472 1469 )
1473 __mapper_args__ = {'order_by': 'group_name'} # TODO: Deprecated as of SQLAlchemy 1.1.
1474 1470
1475 1471 SEP = ' &raquo; '
1476 1472
1477 1473 group_id = Column(Integer(), primary_key=True)
1478 1474 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1479 1475 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1480 1476 group_description = Column(Unicode(10000), nullable=False)
1481 1477 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1482 1478 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1483 1479
1484 1480 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1485 1481 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1486 1482 parent_group = relationship('RepoGroup', remote_side=group_id)
1487 1483 owner = relationship('User')
1488 1484
1489 1485 @classmethod
1490 1486 def query(cls, sorted=False):
1491 1487 """Add RepoGroup-specific helpers for common query constructs.
1492 1488
1493 1489 sorted: if True, apply the default ordering (name, case insensitive).
1494 1490 """
1495 1491 q = super(RepoGroup, cls).query()
1496 1492
1497 1493 if sorted:
1498 1494 q = q.order_by(func.lower(RepoGroup.group_name))
1499 1495
1500 1496 return q
1501 1497
1502 1498 def __init__(self, group_name='', parent_group=None):
1503 1499 self.group_name = group_name
1504 1500 self.parent_group = parent_group
1505 1501
1506 1502 def __unicode__(self):
1507 1503 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1508 1504 self.group_name)
1509 1505
1510 1506 @classmethod
1511 1507 def _generate_choice(cls, repo_group):
1512 1508 """Return tuple with group_id and name as html literal"""
1513 1509 from webhelpers.html import literal
1514 1510 if repo_group is None:
1515 1511 return (-1, u'-- %s --' % _('top level'))
1516 1512 return repo_group.group_id, literal(cls.SEP.join(repo_group.full_path_splitted))
1517 1513
1518 1514 @classmethod
1519 1515 def groups_choices(cls, groups):
1520 1516 """Return tuples with group_id and name as html literal."""
1521 1517 return sorted((cls._generate_choice(g) for g in groups),
1522 1518 key=lambda c: c[1].split(cls.SEP))
1523 1519
1524 1520 @classmethod
1525 1521 def url_sep(cls):
1526 1522 return URL_SEP
1527 1523
1528 1524 @classmethod
1529 1525 def guess_instance(cls, value):
1530 1526 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1531 1527
1532 1528 @classmethod
1533 1529 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1534 1530 group_name = group_name.rstrip('/')
1535 1531 if case_insensitive:
1536 1532 gr = cls.query() \
1537 1533 .filter(func.lower(cls.group_name) == func.lower(group_name))
1538 1534 else:
1539 1535 gr = cls.query() \
1540 1536 .filter(cls.group_name == group_name)
1541 1537 if cache:
1542 1538 gr = gr.options(FromCache(
1543 1539 "sql_cache_short",
1544 1540 "get_group_%s" % _hash_key(group_name)
1545 1541 )
1546 1542 )
1547 1543 return gr.scalar()
1548 1544
1549 1545 @property
1550 1546 def parents(self):
1551 1547 groups = []
1552 1548 group = self.parent_group
1553 1549 while group is not None:
1554 1550 groups.append(group)
1555 1551 group = group.parent_group
1556 1552 assert group not in groups, group # avoid recursion on bad db content
1557 1553 groups.reverse()
1558 1554 return groups
1559 1555
1560 1556 @property
1561 1557 def children(self):
1562 1558 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1563 1559
1564 1560 @property
1565 1561 def name(self):
1566 1562 return self.group_name.split(RepoGroup.url_sep())[-1]
1567 1563
1568 1564 @property
1569 1565 def full_path(self):
1570 1566 return self.group_name
1571 1567
1572 1568 @property
1573 1569 def full_path_splitted(self):
1574 1570 return self.group_name.split(RepoGroup.url_sep())
1575 1571
1576 1572 @property
1577 1573 def repositories(self):
1578 1574 return Repository.query(sorted=True).filter_by(group=self)
1579 1575
1580 1576 @property
1581 1577 def repositories_recursive_count(self):
1582 1578 cnt = self.repositories.count()
1583 1579
1584 1580 def children_count(group):
1585 1581 cnt = 0
1586 1582 for child in group.children:
1587 1583 cnt += child.repositories.count()
1588 1584 cnt += children_count(child)
1589 1585 return cnt
1590 1586
1591 1587 return cnt + children_count(self)
1592 1588
1593 1589 def _recursive_objects(self, include_repos=True):
1594 1590 all_ = []
1595 1591
1596 1592 def _get_members(root_gr):
1597 1593 if include_repos:
1598 1594 for r in root_gr.repositories:
1599 1595 all_.append(r)
1600 1596 childs = root_gr.children.all()
1601 1597 if childs:
1602 1598 for gr in childs:
1603 1599 all_.append(gr)
1604 1600 _get_members(gr)
1605 1601
1606 1602 _get_members(self)
1607 1603 return [self] + all_
1608 1604
1609 1605 def recursive_groups_and_repos(self):
1610 1606 """
1611 1607 Recursive return all groups, with repositories in those groups
1612 1608 """
1613 1609 return self._recursive_objects()
1614 1610
1615 1611 def recursive_groups(self):
1616 1612 """
1617 1613 Returns all children groups for this group including children of children
1618 1614 """
1619 1615 return self._recursive_objects(include_repos=False)
1620 1616
1621 1617 def get_new_name(self, group_name):
1622 1618 """
1623 1619 returns new full group name based on parent and new name
1624 1620
1625 1621 :param group_name:
1626 1622 """
1627 1623 path_prefix = (self.parent_group.full_path_splitted if
1628 1624 self.parent_group else [])
1629 1625 return RepoGroup.url_sep().join(path_prefix + [group_name])
1630 1626
1631 1627 def get_api_data(self):
1632 1628 """
1633 1629 Common function for generating api data
1634 1630
1635 1631 """
1636 1632 group = self
1637 1633 data = dict(
1638 1634 group_id=group.group_id,
1639 1635 group_name=group.group_name,
1640 1636 group_description=group.group_description,
1641 1637 parent_group=group.parent_group.group_name if group.parent_group else None,
1642 1638 repositories=[x.repo_name for x in group.repositories],
1643 1639 owner=group.owner.username
1644 1640 )
1645 1641 return data
1646 1642
1647 1643
1648 1644 class Permission(Base, BaseDbModel):
1649 1645 __tablename__ = 'permissions'
1650 1646 __table_args__ = (
1651 1647 Index('p_perm_name_idx', 'permission_name'),
1652 1648 _table_args_default_dict,
1653 1649 )
1654 1650
1655 1651 PERMS = (
1656 1652 ('hg.admin', _('Kallithea Administrator')),
1657 1653
1658 1654 ('repository.none', _('Default user has no access to new repositories')),
1659 1655 ('repository.read', _('Default user has read access to new repositories')),
1660 1656 ('repository.write', _('Default user has write access to new repositories')),
1661 1657 ('repository.admin', _('Default user has admin access to new repositories')),
1662 1658
1663 1659 ('group.none', _('Default user has no access to new repository groups')),
1664 1660 ('group.read', _('Default user has read access to new repository groups')),
1665 1661 ('group.write', _('Default user has write access to new repository groups')),
1666 1662 ('group.admin', _('Default user has admin access to new repository groups')),
1667 1663
1668 1664 ('usergroup.none', _('Default user has no access to new user groups')),
1669 1665 ('usergroup.read', _('Default user has read access to new user groups')),
1670 1666 ('usergroup.write', _('Default user has write access to new user groups')),
1671 1667 ('usergroup.admin', _('Default user has admin access to new user groups')),
1672 1668
1673 1669 ('hg.repogroup.create.false', _('Only admins can create repository groups')),
1674 1670 ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
1675 1671
1676 1672 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1677 1673 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1678 1674
1679 1675 ('hg.create.none', _('Only admins can create top level repositories')),
1680 1676 ('hg.create.repository', _('Non-admins can create top level repositories')),
1681 1677
1682 1678 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1683 1679 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1684 1680
1685 1681 ('hg.fork.none', _('Only admins can fork repositories')),
1686 1682 ('hg.fork.repository', _('Non-admins can fork repositories')),
1687 1683
1688 1684 ('hg.register.none', _('Registration disabled')),
1689 1685 ('hg.register.manual_activate', _('User registration with manual account activation')),
1690 1686 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1691 1687
1692 1688 ('hg.extern_activate.manual', _('Manual activation of external account')),
1693 1689 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1694 1690 )
1695 1691
1696 1692 # definition of system default permissions for DEFAULT user
1697 1693 DEFAULT_USER_PERMISSIONS = (
1698 1694 'repository.read',
1699 1695 'group.read',
1700 1696 'usergroup.read',
1701 1697 'hg.create.repository',
1702 1698 'hg.create.write_on_repogroup.true',
1703 1699 'hg.fork.repository',
1704 1700 'hg.register.manual_activate',
1705 1701 'hg.extern_activate.auto',
1706 1702 )
1707 1703
1708 1704 # defines which permissions are more important higher the more important
1709 1705 # Weight defines which permissions are more important.
1710 1706 # The higher number the more important.
1711 1707 PERM_WEIGHTS = {
1712 1708 'repository.none': 0,
1713 1709 'repository.read': 1,
1714 1710 'repository.write': 3,
1715 1711 'repository.admin': 4,
1716 1712
1717 1713 'group.none': 0,
1718 1714 'group.read': 1,
1719 1715 'group.write': 3,
1720 1716 'group.admin': 4,
1721 1717
1722 1718 'usergroup.none': 0,
1723 1719 'usergroup.read': 1,
1724 1720 'usergroup.write': 3,
1725 1721 'usergroup.admin': 4,
1726 1722
1727 1723 'hg.repogroup.create.false': 0,
1728 1724 'hg.repogroup.create.true': 1,
1729 1725
1730 1726 'hg.usergroup.create.false': 0,
1731 1727 'hg.usergroup.create.true': 1,
1732 1728
1733 1729 'hg.fork.none': 0,
1734 1730 'hg.fork.repository': 1,
1735 1731
1736 1732 'hg.create.none': 0,
1737 1733 'hg.create.repository': 1,
1738 1734
1739 1735 'hg.create.write_on_repogroup.false': 0,
1740 1736 'hg.create.write_on_repogroup.true': 1,
1741 1737
1742 1738 'hg.register.none': 0,
1743 1739 'hg.register.manual_activate': 1,
1744 1740 'hg.register.auto_activate': 2,
1745 1741
1746 1742 'hg.extern_activate.manual': 0,
1747 1743 'hg.extern_activate.auto': 1,
1748 1744 }
1749 1745
1750 1746 permission_id = Column(Integer(), primary_key=True)
1751 1747 permission_name = Column(String(255), nullable=False)
1752 1748
1753 1749 def __unicode__(self):
1754 1750 return u"<%s('%s:%s')>" % (
1755 1751 self.__class__.__name__, self.permission_id, self.permission_name
1756 1752 )
1757 1753
1758 1754 @classmethod
1759 1755 def guess_instance(cls, value):
1760 1756 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1761 1757
1762 1758 @classmethod
1763 1759 def get_by_key(cls, key):
1764 1760 return cls.query().filter(cls.permission_name == key).scalar()
1765 1761
1766 1762 @classmethod
1767 1763 def get_default_perms(cls, default_user_id):
1768 1764 q = Session().query(UserRepoToPerm, Repository, cls) \
1769 1765 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id)) \
1770 1766 .join((cls, UserRepoToPerm.permission_id == cls.permission_id)) \
1771 1767 .filter(UserRepoToPerm.user_id == default_user_id)
1772 1768
1773 1769 return q.all()
1774 1770
1775 1771 @classmethod
1776 1772 def get_default_group_perms(cls, default_user_id):
1777 1773 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls) \
1778 1774 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id)) \
1779 1775 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id)) \
1780 1776 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1781 1777
1782 1778 return q.all()
1783 1779
1784 1780 @classmethod
1785 1781 def get_default_user_group_perms(cls, default_user_id):
1786 1782 q = Session().query(UserUserGroupToPerm, UserGroup, cls) \
1787 1783 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id)) \
1788 1784 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id)) \
1789 1785 .filter(UserUserGroupToPerm.user_id == default_user_id)
1790 1786
1791 1787 return q.all()
1792 1788
1793 1789
1794 1790 class UserRepoToPerm(Base, BaseDbModel):
1795 1791 __tablename__ = 'repo_to_perm'
1796 1792 __table_args__ = (
1797 1793 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1798 1794 _table_args_default_dict,
1799 1795 )
1800 1796
1801 1797 repo_to_perm_id = Column(Integer(), primary_key=True)
1802 1798 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1803 1799 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1804 1800 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1805 1801
1806 1802 user = relationship('User')
1807 1803 repository = relationship('Repository')
1808 1804 permission = relationship('Permission')
1809 1805
1810 1806 @classmethod
1811 1807 def create(cls, user, repository, permission):
1812 1808 n = cls()
1813 1809 n.user = user
1814 1810 n.repository = repository
1815 1811 n.permission = permission
1816 1812 Session().add(n)
1817 1813 return n
1818 1814
1819 1815 def __unicode__(self):
1820 1816 return u'<%s => %s >' % (self.user, self.repository)
1821 1817
1822 1818
1823 1819 class UserUserGroupToPerm(Base, BaseDbModel):
1824 1820 __tablename__ = 'user_user_group_to_perm'
1825 1821 __table_args__ = (
1826 1822 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1827 1823 _table_args_default_dict,
1828 1824 )
1829 1825
1830 1826 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1831 1827 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1832 1828 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1833 1829 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1834 1830
1835 1831 user = relationship('User')
1836 1832 user_group = relationship('UserGroup')
1837 1833 permission = relationship('Permission')
1838 1834
1839 1835 @classmethod
1840 1836 def create(cls, user, user_group, permission):
1841 1837 n = cls()
1842 1838 n.user = user
1843 1839 n.user_group = user_group
1844 1840 n.permission = permission
1845 1841 Session().add(n)
1846 1842 return n
1847 1843
1848 1844 def __unicode__(self):
1849 1845 return u'<%s => %s >' % (self.user, self.user_group)
1850 1846
1851 1847
1852 1848 class UserToPerm(Base, BaseDbModel):
1853 1849 __tablename__ = 'user_to_perm'
1854 1850 __table_args__ = (
1855 1851 UniqueConstraint('user_id', 'permission_id'),
1856 1852 _table_args_default_dict,
1857 1853 )
1858 1854
1859 1855 user_to_perm_id = Column(Integer(), primary_key=True)
1860 1856 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1861 1857 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1862 1858
1863 1859 user = relationship('User')
1864 1860 permission = relationship('Permission')
1865 1861
1866 1862 def __unicode__(self):
1867 1863 return u'<%s => %s >' % (self.user, self.permission)
1868 1864
1869 1865
1870 1866 class UserGroupRepoToPerm(Base, BaseDbModel):
1871 1867 __tablename__ = 'users_group_repo_to_perm'
1872 1868 __table_args__ = (
1873 1869 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1874 1870 _table_args_default_dict,
1875 1871 )
1876 1872
1877 1873 users_group_to_perm_id = Column(Integer(), primary_key=True)
1878 1874 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1879 1875 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1880 1876 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1881 1877
1882 1878 users_group = relationship('UserGroup')
1883 1879 permission = relationship('Permission')
1884 1880 repository = relationship('Repository')
1885 1881
1886 1882 @classmethod
1887 1883 def create(cls, users_group, repository, permission):
1888 1884 n = cls()
1889 1885 n.users_group = users_group
1890 1886 n.repository = repository
1891 1887 n.permission = permission
1892 1888 Session().add(n)
1893 1889 return n
1894 1890
1895 1891 def __unicode__(self):
1896 1892 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1897 1893
1898 1894
1899 1895 class UserGroupUserGroupToPerm(Base, BaseDbModel):
1900 1896 __tablename__ = 'user_group_user_group_to_perm'
1901 1897 __table_args__ = (
1902 1898 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1903 1899 _table_args_default_dict,
1904 1900 )
1905 1901
1906 1902 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1907 1903 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1908 1904 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1909 1905 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1910 1906
1911 1907 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1912 1908 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1913 1909 permission = relationship('Permission')
1914 1910
1915 1911 @classmethod
1916 1912 def create(cls, target_user_group, user_group, permission):
1917 1913 n = cls()
1918 1914 n.target_user_group = target_user_group
1919 1915 n.user_group = user_group
1920 1916 n.permission = permission
1921 1917 Session().add(n)
1922 1918 return n
1923 1919
1924 1920 def __unicode__(self):
1925 1921 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1926 1922
1927 1923
1928 1924 class UserGroupToPerm(Base, BaseDbModel):
1929 1925 __tablename__ = 'users_group_to_perm'
1930 1926 __table_args__ = (
1931 1927 UniqueConstraint('users_group_id', 'permission_id',),
1932 1928 _table_args_default_dict,
1933 1929 )
1934 1930
1935 1931 users_group_to_perm_id = Column(Integer(), primary_key=True)
1936 1932 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1937 1933 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1938 1934
1939 1935 users_group = relationship('UserGroup')
1940 1936 permission = relationship('Permission')
1941 1937
1942 1938
1943 1939 class UserRepoGroupToPerm(Base, BaseDbModel):
1944 1940 __tablename__ = 'user_repo_group_to_perm'
1945 1941 __table_args__ = (
1946 1942 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1947 1943 _table_args_default_dict,
1948 1944 )
1949 1945
1950 1946 group_to_perm_id = Column(Integer(), primary_key=True)
1951 1947 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1952 1948 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1953 1949 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1954 1950
1955 1951 user = relationship('User')
1956 1952 group = relationship('RepoGroup')
1957 1953 permission = relationship('Permission')
1958 1954
1959 1955 @classmethod
1960 1956 def create(cls, user, repository_group, permission):
1961 1957 n = cls()
1962 1958 n.user = user
1963 1959 n.group = repository_group
1964 1960 n.permission = permission
1965 1961 Session().add(n)
1966 1962 return n
1967 1963
1968 1964
1969 1965 class UserGroupRepoGroupToPerm(Base, BaseDbModel):
1970 1966 __tablename__ = 'users_group_repo_group_to_perm'
1971 1967 __table_args__ = (
1972 1968 UniqueConstraint('users_group_id', 'group_id'),
1973 1969 _table_args_default_dict,
1974 1970 )
1975 1971
1976 1972 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1977 1973 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1978 1974 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1979 1975 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1980 1976
1981 1977 users_group = relationship('UserGroup')
1982 1978 permission = relationship('Permission')
1983 1979 group = relationship('RepoGroup')
1984 1980
1985 1981 @classmethod
1986 1982 def create(cls, user_group, repository_group, permission):
1987 1983 n = cls()
1988 1984 n.users_group = user_group
1989 1985 n.group = repository_group
1990 1986 n.permission = permission
1991 1987 Session().add(n)
1992 1988 return n
1993 1989
1994 1990
1995 1991 class Statistics(Base, BaseDbModel):
1996 1992 __tablename__ = 'statistics'
1997 1993 __table_args__ = (
1998 1994 _table_args_default_dict,
1999 1995 )
2000 1996
2001 1997 stat_id = Column(Integer(), primary_key=True)
2002 1998 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
2003 1999 stat_on_revision = Column(Integer(), nullable=False)
2004 2000 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
2005 2001 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
2006 2002 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
2007 2003
2008 2004 repository = relationship('Repository', single_parent=True)
2009 2005
2010 2006
2011 2007 class UserFollowing(Base, BaseDbModel):
2012 2008 __tablename__ = 'user_followings'
2013 2009 __table_args__ = (
2014 2010 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
2015 2011 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
2016 2012 _table_args_default_dict,
2017 2013 )
2018 2014
2019 2015 user_following_id = Column(Integer(), primary_key=True)
2020 2016 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2021 2017 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
2022 2018 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
2023 2019 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2024 2020
2025 2021 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2026 2022
2027 2023 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2028 2024 follows_repository = relationship('Repository', order_by=lambda: func.lower(Repository.repo_name))
2029 2025
2030 2026 @classmethod
2031 2027 def get_repo_followers(cls, repo_id):
2032 2028 return cls.query().filter(cls.follows_repository_id == repo_id)
2033 2029
2034 2030
2035 2031 class CacheInvalidation(Base, BaseDbModel):
2036 2032 __tablename__ = 'cache_invalidation'
2037 2033 __table_args__ = (
2038 2034 Index('key_idx', 'cache_key'),
2039 2035 _table_args_default_dict,
2040 2036 )
2041 2037
2042 2038 # cache_id, not used
2043 2039 cache_id = Column(Integer(), primary_key=True)
2044 2040 # cache_key as created by _get_cache_key
2045 2041 cache_key = Column(Unicode(255), nullable=False, unique=True)
2046 2042 # cache_args is a repo_name
2047 2043 cache_args = Column(Unicode(255), nullable=False)
2048 2044 # instance sets cache_active True when it is caching, other instances set
2049 2045 # cache_active to False to indicate that this cache is invalid
2050 2046 cache_active = Column(Boolean(), nullable=False, default=False)
2051 2047
2052 2048 def __init__(self, cache_key, repo_name=''):
2053 2049 self.cache_key = cache_key
2054 2050 self.cache_args = repo_name
2055 2051 self.cache_active = False
2056 2052
2057 2053 def __unicode__(self):
2058 2054 return u"<%s('%s:%s[%s]')>" % (
2059 2055 self.__class__.__name__,
2060 2056 self.cache_id, self.cache_key, self.cache_active)
2061 2057
2062 2058 def _cache_key_partition(self):
2063 2059 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2064 2060 return prefix, repo_name, suffix
2065 2061
2066 2062 def get_prefix(self):
2067 2063 """
2068 2064 get prefix that might have been used in _get_cache_key to
2069 2065 generate self.cache_key. Only used for informational purposes
2070 2066 in repo_edit.html.
2071 2067 """
2072 2068 # prefix, repo_name, suffix
2073 2069 return self._cache_key_partition()[0]
2074 2070
2075 2071 def get_suffix(self):
2076 2072 """
2077 2073 get suffix that might have been used in _get_cache_key to
2078 2074 generate self.cache_key. Only used for informational purposes
2079 2075 in repo_edit.html.
2080 2076 """
2081 2077 # prefix, repo_name, suffix
2082 2078 return self._cache_key_partition()[2]
2083 2079
2084 2080 @classmethod
2085 2081 def clear_cache(cls):
2086 2082 """
2087 2083 Delete all cache keys from database.
2088 2084 Should only be run when all instances are down and all entries thus stale.
2089 2085 """
2090 2086 cls.query().delete()
2091 2087 Session().commit()
2092 2088
2093 2089 @classmethod
2094 2090 def _get_cache_key(cls, key):
2095 2091 """
2096 2092 Wrapper for generating a unique cache key for this instance and "key".
2097 2093 key must / will start with a repo_name which will be stored in .cache_args .
2098 2094 """
2099 2095 import kallithea
2100 2096 prefix = kallithea.CONFIG.get('instance_id', '')
2101 2097 return "%s%s" % (prefix, key)
2102 2098
2103 2099 @classmethod
2104 2100 def set_invalidate(cls, repo_name):
2105 2101 """
2106 2102 Mark all caches of a repo as invalid in the database.
2107 2103 """
2108 2104 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2109 2105 log.debug('for repo %s got %s invalidation objects',
2110 2106 safe_str(repo_name), inv_objs)
2111 2107
2112 2108 for inv_obj in inv_objs:
2113 2109 log.debug('marking %s key for invalidation based on repo_name=%s',
2114 2110 inv_obj, safe_str(repo_name))
2115 2111 Session().delete(inv_obj)
2116 2112 Session().commit()
2117 2113
2118 2114 @classmethod
2119 2115 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2120 2116 """
2121 2117 Mark this cache key as active and currently cached.
2122 2118 Return True if the existing cache registration still was valid.
2123 2119 Return False to indicate that it had been invalidated and caches should be refreshed.
2124 2120 """
2125 2121
2126 2122 key = (repo_name + '_' + kind) if kind else repo_name
2127 2123 cache_key = cls._get_cache_key(key)
2128 2124
2129 2125 if valid_cache_keys and cache_key in valid_cache_keys:
2130 2126 return True
2131 2127
2132 2128 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2133 2129 if inv_obj is None:
2134 2130 inv_obj = cls(cache_key, repo_name)
2135 2131 Session().add(inv_obj)
2136 2132 elif inv_obj.cache_active:
2137 2133 return True
2138 2134 inv_obj.cache_active = True
2139 2135 try:
2140 2136 Session().commit()
2141 2137 except sqlalchemy.exc.IntegrityError:
2142 2138 log.error('commit of CacheInvalidation failed - retrying')
2143 2139 Session().rollback()
2144 2140 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2145 2141 if inv_obj is None:
2146 2142 log.error('failed to create CacheInvalidation entry')
2147 2143 # TODO: fail badly?
2148 2144 # else: TOCTOU - another thread added the key at the same time; no further action required
2149 2145 return False
2150 2146
2151 2147 @classmethod
2152 2148 def get_valid_cache_keys(cls):
2153 2149 """
2154 2150 Return opaque object with information of which caches still are valid
2155 2151 and can be used without checking for invalidation.
2156 2152 """
2157 2153 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2158 2154
2159 2155
2160 2156 class ChangesetComment(Base, BaseDbModel):
2161 2157 __tablename__ = 'changeset_comments'
2162 2158 __table_args__ = (
2163 2159 Index('cc_revision_idx', 'revision'),
2164 2160 Index('cc_pull_request_id_idx', 'pull_request_id'),
2165 2161 _table_args_default_dict,
2166 2162 )
2167 2163
2168 2164 comment_id = Column(Integer(), primary_key=True)
2169 2165 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2170 2166 revision = Column(String(40), nullable=True)
2171 2167 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2172 2168 line_no = Column(Unicode(10), nullable=True)
2173 2169 f_path = Column(Unicode(1000), nullable=True)
2174 2170 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2175 2171 text = Column(UnicodeText(), nullable=False)
2176 2172 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2177 2173 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2178 2174
2179 2175 author = relationship('User')
2180 2176 repo = relationship('Repository')
2181 2177 # status_change is frequently used directly in templates - make it a lazy
2182 2178 # join to avoid fetching each related ChangesetStatus on demand.
2183 2179 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2184 2180 status_change = relationship('ChangesetStatus',
2185 2181 cascade="all, delete-orphan", lazy='joined')
2186 2182 pull_request = relationship('PullRequest')
2187 2183
2188 2184 def url(self):
2189 2185 anchor = "comment-%s" % self.comment_id
2190 2186 import kallithea.lib.helpers as h
2191 2187 if self.revision:
2192 2188 return h.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
2193 2189 elif self.pull_request_id is not None:
2194 2190 return self.pull_request.url(anchor=anchor)
2195 2191
2196 2192 def __json__(self):
2197 2193 return dict(
2198 2194 comment_id=self.comment_id,
2199 2195 username=self.author.username,
2200 2196 text=self.text,
2201 2197 )
2202 2198
2203 2199 def deletable(self):
2204 2200 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
2205 2201
2206 2202
2207 2203 class ChangesetStatus(Base, BaseDbModel):
2208 2204 __tablename__ = 'changeset_statuses'
2209 2205 __table_args__ = (
2210 2206 Index('cs_revision_idx', 'revision'),
2211 2207 Index('cs_version_idx', 'version'),
2212 2208 Index('cs_pull_request_id_idx', 'pull_request_id'),
2213 2209 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2214 2210 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2215 2211 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
2216 2212 UniqueConstraint('repo_id', 'revision', 'version'),
2217 2213 _table_args_default_dict,
2218 2214 )
2219 2215
2220 2216 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2221 2217 STATUS_APPROVED = 'approved'
2222 2218 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
2223 2219 STATUS_UNDER_REVIEW = 'under_review'
2224 2220
2225 2221 STATUSES = [
2226 2222 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
2227 2223 (STATUS_UNDER_REVIEW, _("Under review")),
2228 2224 (STATUS_REJECTED, _("Not approved")),
2229 2225 (STATUS_APPROVED, _("Approved")),
2230 2226 ]
2231 2227 STATUSES_DICT = dict(STATUSES)
2232 2228
2233 2229 changeset_status_id = Column(Integer(), primary_key=True)
2234 2230 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2235 2231 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2236 2232 revision = Column(String(40), nullable=True)
2237 2233 status = Column(String(128), nullable=False, default=DEFAULT)
2238 2234 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
2239 2235 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
2240 2236 version = Column(Integer(), nullable=False, default=0)
2241 2237 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2242 2238
2243 2239 author = relationship('User')
2244 2240 repo = relationship('Repository')
2245 2241 comment = relationship('ChangesetComment')
2246 2242 pull_request = relationship('PullRequest')
2247 2243
2248 2244 def __unicode__(self):
2249 2245 return u"<%s('%s:%s')>" % (
2250 2246 self.__class__.__name__,
2251 2247 self.status, self.author
2252 2248 )
2253 2249
2254 2250 @classmethod
2255 2251 def get_status_lbl(cls, value):
2256 2252 return cls.STATUSES_DICT.get(value)
2257 2253
2258 2254 @property
2259 2255 def status_lbl(self):
2260 2256 return ChangesetStatus.get_status_lbl(self.status)
2261 2257
2262 2258 def __json__(self):
2263 2259 return dict(
2264 2260 status=self.status,
2265 2261 modified_at=self.modified_at.replace(microsecond=0),
2266 2262 reviewer=self.author.username,
2267 2263 )
2268 2264
2269 2265
2270 2266 class PullRequest(Base, BaseDbModel):
2271 2267 __tablename__ = 'pull_requests'
2272 2268 __table_args__ = (
2273 2269 Index('pr_org_repo_id_idx', 'org_repo_id'),
2274 2270 Index('pr_other_repo_id_idx', 'other_repo_id'),
2275 2271 _table_args_default_dict,
2276 2272 )
2277 2273
2278 2274 # values for .status
2279 2275 STATUS_NEW = u'new'
2280 2276 STATUS_CLOSED = u'closed'
2281 2277
2282 2278 pull_request_id = Column(Integer(), primary_key=True)
2283 2279 title = Column(Unicode(255), nullable=False)
2284 2280 description = Column(UnicodeText(), nullable=False)
2285 2281 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2286 2282 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2287 2283 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2288 2284 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2289 2285 _revisions = Column('revisions', UnicodeText(), nullable=False)
2290 2286 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2291 2287 org_ref = Column(Unicode(255), nullable=False)
2292 2288 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2293 2289 other_ref = Column(Unicode(255), nullable=False)
2294 2290
2295 2291 @hybrid_property
2296 2292 def revisions(self):
2297 2293 return self._revisions.split(':')
2298 2294
2299 2295 @revisions.setter
2300 2296 def revisions(self, val):
2301 2297 self._revisions = safe_unicode(':'.join(val))
2302 2298
2303 2299 @property
2304 2300 def org_ref_parts(self):
2305 2301 return self.org_ref.split(':')
2306 2302
2307 2303 @property
2308 2304 def other_ref_parts(self):
2309 2305 return self.other_ref.split(':')
2310 2306
2311 2307 owner = relationship('User')
2312 2308 reviewers = relationship('PullRequestReviewer',
2313 2309 cascade="all, delete-orphan")
2314 2310 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2315 2311 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2316 2312 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2317 2313 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2318 2314 cascade="all, delete-orphan")
2319 2315
2320 2316 @classmethod
2321 2317 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2322 2318 """Add PullRequest-specific helpers for common query constructs.
2323 2319
2324 2320 reviewer_id: only PRs with the specified user added as reviewer.
2325 2321
2326 2322 include_closed: if False, do not include closed PRs.
2327 2323
2328 2324 sorted: if True, apply the default ordering (newest first).
2329 2325 """
2330 2326 q = super(PullRequest, cls).query()
2331 2327
2332 2328 if reviewer_id is not None:
2333 2329 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2334 2330
2335 2331 if not include_closed:
2336 2332 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2337 2333
2338 2334 if sorted:
2339 2335 q = q.order_by(PullRequest.created_on.desc())
2340 2336
2341 2337 return q
2342 2338
2343 2339 def get_reviewer_users(self):
2344 2340 """Like .reviewers, but actually returning the users"""
2345 2341 return User.query() \
2346 2342 .join(PullRequestReviewer) \
2347 2343 .filter(PullRequestReviewer.pull_request == self) \
2348 2344 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2349 2345 .all()
2350 2346
2351 2347 def is_closed(self):
2352 2348 return self.status == self.STATUS_CLOSED
2353 2349
2354 2350 def user_review_status(self, user_id):
2355 2351 """Return the user's latest status votes on PR"""
2356 2352 # note: no filtering on repo - that would be redundant
2357 2353 status = ChangesetStatus.query() \
2358 2354 .filter(ChangesetStatus.pull_request == self) \
2359 2355 .filter(ChangesetStatus.user_id == user_id) \
2360 2356 .order_by(ChangesetStatus.version) \
2361 2357 .first()
2362 2358 return str(status.status) if status else ''
2363 2359
2364 2360 @classmethod
2365 2361 def make_nice_id(cls, pull_request_id):
2366 2362 '''Return pull request id nicely formatted for displaying'''
2367 2363 return '#%s' % pull_request_id
2368 2364
2369 2365 def nice_id(self):
2370 2366 '''Return the id of this pull request, nicely formatted for displaying'''
2371 2367 return self.make_nice_id(self.pull_request_id)
2372 2368
2373 2369 def get_api_data(self):
2374 2370 return self.__json__()
2375 2371
2376 2372 def __json__(self):
2377 2373 return dict(
2378 2374 pull_request_id=self.pull_request_id,
2379 2375 url=self.url(),
2380 2376 reviewers=self.reviewers,
2381 2377 revisions=self.revisions,
2382 2378 owner=self.owner.username,
2383 2379 title=self.title,
2384 2380 description=self.description,
2385 2381 org_repo_url=self.org_repo.clone_url(),
2386 2382 org_ref_parts=self.org_ref_parts,
2387 2383 other_ref_parts=self.other_ref_parts,
2388 2384 status=self.status,
2389 2385 comments=self.comments,
2390 2386 statuses=self.statuses,
2391 2387 )
2392 2388
2393 2389 def url(self, **kwargs):
2394 2390 canonical = kwargs.pop('canonical', None)
2395 2391 import kallithea.lib.helpers as h
2396 2392 b = self.org_ref_parts[1]
2397 2393 if b != self.other_ref_parts[1]:
2398 2394 s = '/_/' + b
2399 2395 else:
2400 2396 s = '/_/' + self.title
2401 2397 kwargs['extra'] = urlreadable(s)
2402 2398 if canonical:
2403 2399 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2404 2400 pull_request_id=self.pull_request_id, **kwargs)
2405 2401 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2406 2402 pull_request_id=self.pull_request_id, **kwargs)
2407 2403
2408 2404
2409 2405 class PullRequestReviewer(Base, BaseDbModel):
2410 2406 __tablename__ = 'pull_request_reviewers'
2411 2407 __table_args__ = (
2412 2408 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2413 2409 _table_args_default_dict,
2414 2410 )
2415 2411
2416 2412 def __init__(self, user=None, pull_request=None):
2417 2413 self.user = user
2418 2414 self.pull_request = pull_request
2419 2415
2420 2416 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2421 2417 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2422 2418 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2423 2419
2424 2420 user = relationship('User')
2425 2421 pull_request = relationship('PullRequest')
2426 2422
2427 2423 def __json__(self):
2428 2424 return dict(
2429 2425 username=self.user.username if self.user else None,
2430 2426 )
2431 2427
2432 2428
2433 2429 class Notification(object):
2434 2430 __tablename__ = 'notifications'
2435 2431
2436 2432 class UserNotification(object):
2437 2433 __tablename__ = 'user_to_notification'
2438 2434
2439 2435
2440 2436 class Gist(Base, BaseDbModel):
2441 2437 __tablename__ = 'gists'
2442 2438 __table_args__ = (
2443 2439 Index('g_gist_access_id_idx', 'gist_access_id'),
2444 2440 Index('g_created_on_idx', 'created_on'),
2445 2441 _table_args_default_dict,
2446 2442 )
2447 2443
2448 2444 GIST_PUBLIC = u'public'
2449 2445 GIST_PRIVATE = u'private'
2450 2446 DEFAULT_FILENAME = u'gistfile1.txt'
2451 2447
2452 2448 gist_id = Column(Integer(), primary_key=True)
2453 2449 gist_access_id = Column(Unicode(250), nullable=False)
2454 2450 gist_description = Column(UnicodeText(), nullable=False)
2455 2451 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2456 2452 gist_expires = Column(Float(53), nullable=False)
2457 2453 gist_type = Column(Unicode(128), nullable=False)
2458 2454 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2459 2455 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2460 2456
2461 2457 owner = relationship('User')
2462 2458
2463 2459 @hybrid_property
2464 2460 def is_expired(self):
2465 2461 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2466 2462
2467 2463 def __repr__(self):
2468 2464 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2469 2465
2470 2466 @classmethod
2471 2467 def guess_instance(cls, value):
2472 2468 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2473 2469
2474 2470 @classmethod
2475 2471 def get_or_404(cls, id_):
2476 2472 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2477 2473 if res is None:
2478 2474 raise HTTPNotFound
2479 2475 return res
2480 2476
2481 2477 @classmethod
2482 2478 def get_by_access_id(cls, gist_access_id):
2483 2479 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2484 2480
2485 2481 def gist_url(self):
2486 2482 import kallithea
2487 2483 alias_url = kallithea.CONFIG.get('gist_alias_url')
2488 2484 if alias_url:
2489 2485 return alias_url.replace('{gistid}', self.gist_access_id)
2490 2486
2491 2487 import kallithea.lib.helpers as h
2492 2488 return h.canonical_url('gist', gist_id=self.gist_access_id)
2493 2489
2494 2490 @classmethod
2495 2491 def base_path(cls):
2496 2492 """
2497 2493 Returns base path where all gists are stored
2498 2494
2499 2495 :param cls:
2500 2496 """
2501 2497 from kallithea.model.gist import GIST_STORE_LOC
2502 2498 q = Session().query(Ui) \
2503 2499 .filter(Ui.ui_key == URL_SEP)
2504 2500 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2505 2501 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2506 2502
2507 2503 def get_api_data(self):
2508 2504 """
2509 2505 Common function for generating gist related data for API
2510 2506 """
2511 2507 gist = self
2512 2508 data = dict(
2513 2509 gist_id=gist.gist_id,
2514 2510 type=gist.gist_type,
2515 2511 access_id=gist.gist_access_id,
2516 2512 description=gist.gist_description,
2517 2513 url=gist.gist_url(),
2518 2514 expires=gist.gist_expires,
2519 2515 created_on=gist.created_on,
2520 2516 )
2521 2517 return data
2522 2518
2523 2519 def __json__(self):
2524 2520 data = dict(
2525 2521 )
2526 2522 data.update(self.get_api_data())
2527 2523 return data
2528 2524 ## SCM functions
2529 2525
2530 2526 @property
2531 2527 def scm_instance(self):
2532 2528 from kallithea.lib.vcs import get_repo
2533 2529 base_path = self.base_path()
2534 2530 return get_repo(os.path.join(*map(safe_str,
2535 2531 [base_path, self.gist_access_id])))
General Comments 0
You need to be logged in to leave comments. Login now