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