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