Show More
The requested changes are too big and content was truncated. Show full diff
This diff has been collapsed as it changes many lines, (4858 lines changed) Show them Hide them | |||
@@ -0,0 +1,4858 b'' | |||
|
1 | # -*- coding: utf-8 -*- | |
|
2 | ||
|
3 | # Copyright (C) 2010-2019 RhodeCode GmbH | |
|
4 | # | |
|
5 | # This program is free software: you can redistribute it and/or modify | |
|
6 | # it under the terms of the GNU Affero General Public License, version 3 | |
|
7 | # (only), as published by the Free Software Foundation. | |
|
8 | # | |
|
9 | # This program is distributed in the hope that it will be useful, | |
|
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
|
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
|
12 | # GNU General Public License for more details. | |
|
13 | # | |
|
14 | # You should have received a copy of the GNU Affero General Public License | |
|
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
|
16 | # | |
|
17 | # This program is dual-licensed. If you wish to learn more about the | |
|
18 | # RhodeCode Enterprise Edition, including its added features, Support services, | |
|
19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ | |
|
20 | ||
|
21 | """ | |
|
22 | Database Models for RhodeCode Enterprise | |
|
23 | """ | |
|
24 | ||
|
25 | import re | |
|
26 | import os | |
|
27 | import time | |
|
28 | import hashlib | |
|
29 | import logging | |
|
30 | import datetime | |
|
31 | import warnings | |
|
32 | import ipaddress | |
|
33 | import functools | |
|
34 | import traceback | |
|
35 | import collections | |
|
36 | ||
|
37 | from sqlalchemy import ( | |
|
38 | or_, and_, not_, func, TypeDecorator, event, | |
|
39 | Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, | |
|
40 | Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, | |
|
41 | Text, Float, PickleType) | |
|
42 | from sqlalchemy.sql.expression import true, false | |
|
43 | from sqlalchemy.sql.functions import coalesce, count # pragma: no cover | |
|
44 | from sqlalchemy.orm import ( | |
|
45 | relationship, joinedload, class_mapper, validates, aliased) | |
|
46 | from sqlalchemy.ext.declarative import declared_attr | |
|
47 | from sqlalchemy.ext.hybrid import hybrid_property | |
|
48 | from sqlalchemy.exc import IntegrityError # pragma: no cover | |
|
49 | from sqlalchemy.dialects.mysql import LONGTEXT | |
|
50 | from zope.cachedescriptors.property import Lazy as LazyProperty | |
|
51 | ||
|
52 | from pyramid.threadlocal import get_current_request | |
|
53 | ||
|
54 | from rhodecode.translation import _ | |
|
55 | from rhodecode.lib.vcs import get_vcs_instance | |
|
56 | from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference | |
|
57 | from rhodecode.lib.utils2 import ( | |
|
58 | str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe, | |
|
59 | time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict, | |
|
60 | glob2re, StrictAttributeDict, cleaned_uri) | |
|
61 | from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \ | |
|
62 | JsonRaw | |
|
63 | from rhodecode.lib.ext_json import json | |
|
64 | from rhodecode.lib.caching_query import FromCache | |
|
65 | from rhodecode.lib.encrypt import AESCipher | |
|
66 | ||
|
67 | from rhodecode.model.meta import Base, Session | |
|
68 | ||
|
69 | URL_SEP = '/' | |
|
70 | log = logging.getLogger(__name__) | |
|
71 | ||
|
72 | # ============================================================================= | |
|
73 | # BASE CLASSES | |
|
74 | # ============================================================================= | |
|
75 | ||
|
76 | # this is propagated from .ini file rhodecode.encrypted_values.secret or | |
|
77 | # beaker.session.secret if first is not set. | |
|
78 | # and initialized at environment.py | |
|
79 | ENCRYPTION_KEY = None | |
|
80 | ||
|
81 | # used to sort permissions by types, '#' used here is not allowed to be in | |
|
82 | # usernames, and it's very early in sorted string.printable table. | |
|
83 | PERMISSION_TYPE_SORT = { | |
|
84 | 'admin': '####', | |
|
85 | 'write': '###', | |
|
86 | 'read': '##', | |
|
87 | 'none': '#', | |
|
88 | } | |
|
89 | ||
|
90 | ||
|
91 | def display_user_sort(obj): | |
|
92 | """ | |
|
93 | Sort function used to sort permissions in .permissions() function of | |
|
94 | Repository, RepoGroup, UserGroup. Also it put the default user in front | |
|
95 | of all other resources | |
|
96 | """ | |
|
97 | ||
|
98 | if obj.username == User.DEFAULT_USER: | |
|
99 | return '#####' | |
|
100 | prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') | |
|
101 | return prefix + obj.username | |
|
102 | ||
|
103 | ||
|
104 | def display_user_group_sort(obj): | |
|
105 | """ | |
|
106 | Sort function used to sort permissions in .permissions() function of | |
|
107 | Repository, RepoGroup, UserGroup. Also it put the default user in front | |
|
108 | of all other resources | |
|
109 | """ | |
|
110 | ||
|
111 | prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') | |
|
112 | return prefix + obj.users_group_name | |
|
113 | ||
|
114 | ||
|
115 | def _hash_key(k): | |
|
116 | return sha1_safe(k) | |
|
117 | ||
|
118 | ||
|
119 | def in_filter_generator(qry, items, limit=500): | |
|
120 | """ | |
|
121 | Splits IN() into multiple with OR | |
|
122 | e.g.:: | |
|
123 | cnt = Repository.query().filter( | |
|
124 | or_( | |
|
125 | *in_filter_generator(Repository.repo_id, range(100000)) | |
|
126 | )).count() | |
|
127 | """ | |
|
128 | if not items: | |
|
129 | # empty list will cause empty query which might cause security issues | |
|
130 | # this can lead to hidden unpleasant results | |
|
131 | items = [-1] | |
|
132 | ||
|
133 | parts = [] | |
|
134 | for chunk in xrange(0, len(items), limit): | |
|
135 | parts.append( | |
|
136 | qry.in_(items[chunk: chunk + limit]) | |
|
137 | ) | |
|
138 | ||
|
139 | return parts | |
|
140 | ||
|
141 | ||
|
142 | base_table_args = { | |
|
143 | 'extend_existing': True, | |
|
144 | 'mysql_engine': 'InnoDB', | |
|
145 | 'mysql_charset': 'utf8', | |
|
146 | 'sqlite_autoincrement': True | |
|
147 | } | |
|
148 | ||
|
149 | ||
|
150 | class EncryptedTextValue(TypeDecorator): | |
|
151 | """ | |
|
152 | Special column for encrypted long text data, use like:: | |
|
153 | ||
|
154 | value = Column("encrypted_value", EncryptedValue(), nullable=False) | |
|
155 | ||
|
156 | This column is intelligent so if value is in unencrypted form it return | |
|
157 | unencrypted form, but on save it always encrypts | |
|
158 | """ | |
|
159 | impl = Text | |
|
160 | ||
|
161 | def process_bind_param(self, value, dialect): | |
|
162 | if not value: | |
|
163 | return value | |
|
164 | if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'): | |
|
165 | # protect against double encrypting if someone manually starts | |
|
166 | # doing | |
|
167 | raise ValueError('value needs to be in unencrypted format, ie. ' | |
|
168 | 'not starting with enc$aes') | |
|
169 | return 'enc$aes_hmac$%s' % AESCipher( | |
|
170 | ENCRYPTION_KEY, hmac=True).encrypt(value) | |
|
171 | ||
|
172 | def process_result_value(self, value, dialect): | |
|
173 | import rhodecode | |
|
174 | ||
|
175 | if not value: | |
|
176 | return value | |
|
177 | ||
|
178 | parts = value.split('$', 3) | |
|
179 | if not len(parts) == 3: | |
|
180 | # probably not encrypted values | |
|
181 | return value | |
|
182 | else: | |
|
183 | if parts[0] != 'enc': | |
|
184 | # parts ok but without our header ? | |
|
185 | return value | |
|
186 | enc_strict_mode = str2bool(rhodecode.CONFIG.get( | |
|
187 | 'rhodecode.encrypted_values.strict') or True) | |
|
188 | # at that stage we know it's our encryption | |
|
189 | if parts[1] == 'aes': | |
|
190 | decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2]) | |
|
191 | elif parts[1] == 'aes_hmac': | |
|
192 | decrypted_data = AESCipher( | |
|
193 | ENCRYPTION_KEY, hmac=True, | |
|
194 | strict_verification=enc_strict_mode).decrypt(parts[2]) | |
|
195 | else: | |
|
196 | raise ValueError( | |
|
197 | 'Encryption type part is wrong, must be `aes` ' | |
|
198 | 'or `aes_hmac`, got `%s` instead' % (parts[1])) | |
|
199 | return decrypted_data | |
|
200 | ||
|
201 | ||
|
202 | class BaseModel(object): | |
|
203 | """ | |
|
204 | Base Model for all classes | |
|
205 | """ | |
|
206 | ||
|
207 | @classmethod | |
|
208 | def _get_keys(cls): | |
|
209 | """return column names for this model """ | |
|
210 | return class_mapper(cls).c.keys() | |
|
211 | ||
|
212 | def get_dict(self): | |
|
213 | """ | |
|
214 | return dict with keys and values corresponding | |
|
215 | to this model data """ | |
|
216 | ||
|
217 | d = {} | |
|
218 | for k in self._get_keys(): | |
|
219 | d[k] = getattr(self, k) | |
|
220 | ||
|
221 | # also use __json__() if present to get additional fields | |
|
222 | _json_attr = getattr(self, '__json__', None) | |
|
223 | if _json_attr: | |
|
224 | # update with attributes from __json__ | |
|
225 | if callable(_json_attr): | |
|
226 | _json_attr = _json_attr() | |
|
227 | for k, val in _json_attr.iteritems(): | |
|
228 | d[k] = val | |
|
229 | return d | |
|
230 | ||
|
231 | def get_appstruct(self): | |
|
232 | """return list with keys and values tuples corresponding | |
|
233 | to this model data """ | |
|
234 | ||
|
235 | lst = [] | |
|
236 | for k in self._get_keys(): | |
|
237 | lst.append((k, getattr(self, k),)) | |
|
238 | return lst | |
|
239 | ||
|
240 | def populate_obj(self, populate_dict): | |
|
241 | """populate model with data from given populate_dict""" | |
|
242 | ||
|
243 | for k in self._get_keys(): | |
|
244 | if k in populate_dict: | |
|
245 | setattr(self, k, populate_dict[k]) | |
|
246 | ||
|
247 | @classmethod | |
|
248 | def query(cls): | |
|
249 | return Session().query(cls) | |
|
250 | ||
|
251 | @classmethod | |
|
252 | def get(cls, id_): | |
|
253 | if id_: | |
|
254 | return cls.query().get(id_) | |
|
255 | ||
|
256 | @classmethod | |
|
257 | def get_or_404(cls, id_): | |
|
258 | from pyramid.httpexceptions import HTTPNotFound | |
|
259 | ||
|
260 | try: | |
|
261 | id_ = int(id_) | |
|
262 | except (TypeError, ValueError): | |
|
263 | raise HTTPNotFound() | |
|
264 | ||
|
265 | res = cls.query().get(id_) | |
|
266 | if not res: | |
|
267 | raise HTTPNotFound() | |
|
268 | return res | |
|
269 | ||
|
270 | @classmethod | |
|
271 | def getAll(cls): | |
|
272 | # deprecated and left for backward compatibility | |
|
273 | return cls.get_all() | |
|
274 | ||
|
275 | @classmethod | |
|
276 | def get_all(cls): | |
|
277 | return cls.query().all() | |
|
278 | ||
|
279 | @classmethod | |
|
280 | def delete(cls, id_): | |
|
281 | obj = cls.query().get(id_) | |
|
282 | Session().delete(obj) | |
|
283 | ||
|
284 | @classmethod | |
|
285 | def identity_cache(cls, session, attr_name, value): | |
|
286 | exist_in_session = [] | |
|
287 | for (item_cls, pkey), instance in session.identity_map.items(): | |
|
288 | if cls == item_cls and getattr(instance, attr_name) == value: | |
|
289 | exist_in_session.append(instance) | |
|
290 | if exist_in_session: | |
|
291 | if len(exist_in_session) == 1: | |
|
292 | return exist_in_session[0] | |
|
293 | log.exception( | |
|
294 | 'multiple objects with attr %s and ' | |
|
295 | 'value %s found with same name: %r', | |
|
296 | attr_name, value, exist_in_session) | |
|
297 | ||
|
298 | def __repr__(self): | |
|
299 | if hasattr(self, '__unicode__'): | |
|
300 | # python repr needs to return str | |
|
301 | try: | |
|
302 | return safe_str(self.__unicode__()) | |
|
303 | except UnicodeDecodeError: | |
|
304 | pass | |
|
305 | return '<DB:%s>' % (self.__class__.__name__) | |
|
306 | ||
|
307 | ||
|
308 | class RhodeCodeSetting(Base, BaseModel): | |
|
309 | __tablename__ = 'rhodecode_settings' | |
|
310 | __table_args__ = ( | |
|
311 | UniqueConstraint('app_settings_name'), | |
|
312 | base_table_args | |
|
313 | ) | |
|
314 | ||
|
315 | SETTINGS_TYPES = { | |
|
316 | 'str': safe_str, | |
|
317 | 'int': safe_int, | |
|
318 | 'unicode': safe_unicode, | |
|
319 | 'bool': str2bool, | |
|
320 | 'list': functools.partial(aslist, sep=',') | |
|
321 | } | |
|
322 | DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions' | |
|
323 | GLOBAL_CONF_KEY = 'app_settings' | |
|
324 | ||
|
325 | app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
326 | app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None) | |
|
327 | _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None) | |
|
328 | _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None) | |
|
329 | ||
|
330 | def __init__(self, key='', val='', type='unicode'): | |
|
331 | self.app_settings_name = key | |
|
332 | self.app_settings_type = type | |
|
333 | self.app_settings_value = val | |
|
334 | ||
|
335 | @validates('_app_settings_value') | |
|
336 | def validate_settings_value(self, key, val): | |
|
337 | assert type(val) == unicode | |
|
338 | return val | |
|
339 | ||
|
340 | @hybrid_property | |
|
341 | def app_settings_value(self): | |
|
342 | v = self._app_settings_value | |
|
343 | _type = self.app_settings_type | |
|
344 | if _type: | |
|
345 | _type = self.app_settings_type.split('.')[0] | |
|
346 | # decode the encrypted value | |
|
347 | if 'encrypted' in self.app_settings_type: | |
|
348 | cipher = EncryptedTextValue() | |
|
349 | v = safe_unicode(cipher.process_result_value(v, None)) | |
|
350 | ||
|
351 | converter = self.SETTINGS_TYPES.get(_type) or \ | |
|
352 | self.SETTINGS_TYPES['unicode'] | |
|
353 | return converter(v) | |
|
354 | ||
|
355 | @app_settings_value.setter | |
|
356 | def app_settings_value(self, val): | |
|
357 | """ | |
|
358 | Setter that will always make sure we use unicode in app_settings_value | |
|
359 | ||
|
360 | :param val: | |
|
361 | """ | |
|
362 | val = safe_unicode(val) | |
|
363 | # encode the encrypted value | |
|
364 | if 'encrypted' in self.app_settings_type: | |
|
365 | cipher = EncryptedTextValue() | |
|
366 | val = safe_unicode(cipher.process_bind_param(val, None)) | |
|
367 | self._app_settings_value = val | |
|
368 | ||
|
369 | @hybrid_property | |
|
370 | def app_settings_type(self): | |
|
371 | return self._app_settings_type | |
|
372 | ||
|
373 | @app_settings_type.setter | |
|
374 | def app_settings_type(self, val): | |
|
375 | if val.split('.')[0] not in self.SETTINGS_TYPES: | |
|
376 | raise Exception('type must be one of %s got %s' | |
|
377 | % (self.SETTINGS_TYPES.keys(), val)) | |
|
378 | self._app_settings_type = val | |
|
379 | ||
|
380 | @classmethod | |
|
381 | def get_by_prefix(cls, prefix): | |
|
382 | return RhodeCodeSetting.query()\ | |
|
383 | .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\ | |
|
384 | .all() | |
|
385 | ||
|
386 | def __unicode__(self): | |
|
387 | return u"<%s('%s:%s[%s]')>" % ( | |
|
388 | self.__class__.__name__, | |
|
389 | self.app_settings_name, self.app_settings_value, | |
|
390 | self.app_settings_type | |
|
391 | ) | |
|
392 | ||
|
393 | ||
|
394 | class RhodeCodeUi(Base, BaseModel): | |
|
395 | __tablename__ = 'rhodecode_ui' | |
|
396 | __table_args__ = ( | |
|
397 | UniqueConstraint('ui_key'), | |
|
398 | base_table_args | |
|
399 | ) | |
|
400 | ||
|
401 | HOOK_REPO_SIZE = 'changegroup.repo_size' | |
|
402 | # HG | |
|
403 | HOOK_PRE_PULL = 'preoutgoing.pre_pull' | |
|
404 | HOOK_PULL = 'outgoing.pull_logger' | |
|
405 | HOOK_PRE_PUSH = 'prechangegroup.pre_push' | |
|
406 | HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push' | |
|
407 | HOOK_PUSH = 'changegroup.push_logger' | |
|
408 | HOOK_PUSH_KEY = 'pushkey.key_push' | |
|
409 | ||
|
410 | # TODO: johbo: Unify way how hooks are configured for git and hg, | |
|
411 | # git part is currently hardcoded. | |
|
412 | ||
|
413 | # SVN PATTERNS | |
|
414 | SVN_BRANCH_ID = 'vcs_svn_branch' | |
|
415 | SVN_TAG_ID = 'vcs_svn_tag' | |
|
416 | ||
|
417 | ui_id = Column( | |
|
418 | "ui_id", Integer(), nullable=False, unique=True, default=None, | |
|
419 | primary_key=True) | |
|
420 | ui_section = Column( | |
|
421 | "ui_section", String(255), nullable=True, unique=None, default=None) | |
|
422 | ui_key = Column( | |
|
423 | "ui_key", String(255), nullable=True, unique=None, default=None) | |
|
424 | ui_value = Column( | |
|
425 | "ui_value", String(255), nullable=True, unique=None, default=None) | |
|
426 | ui_active = Column( | |
|
427 | "ui_active", Boolean(), nullable=True, unique=None, default=True) | |
|
428 | ||
|
429 | def __repr__(self): | |
|
430 | return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section, | |
|
431 | self.ui_key, self.ui_value) | |
|
432 | ||
|
433 | ||
|
434 | class RepoRhodeCodeSetting(Base, BaseModel): | |
|
435 | __tablename__ = 'repo_rhodecode_settings' | |
|
436 | __table_args__ = ( | |
|
437 | UniqueConstraint( | |
|
438 | 'app_settings_name', 'repository_id', | |
|
439 | name='uq_repo_rhodecode_setting_name_repo_id'), | |
|
440 | base_table_args | |
|
441 | ) | |
|
442 | ||
|
443 | repository_id = Column( | |
|
444 | "repository_id", Integer(), ForeignKey('repositories.repo_id'), | |
|
445 | nullable=False) | |
|
446 | app_settings_id = Column( | |
|
447 | "app_settings_id", Integer(), nullable=False, unique=True, | |
|
448 | default=None, primary_key=True) | |
|
449 | app_settings_name = Column( | |
|
450 | "app_settings_name", String(255), nullable=True, unique=None, | |
|
451 | default=None) | |
|
452 | _app_settings_value = Column( | |
|
453 | "app_settings_value", String(4096), nullable=True, unique=None, | |
|
454 | default=None) | |
|
455 | _app_settings_type = Column( | |
|
456 | "app_settings_type", String(255), nullable=True, unique=None, | |
|
457 | default=None) | |
|
458 | ||
|
459 | repository = relationship('Repository') | |
|
460 | ||
|
461 | def __init__(self, repository_id, key='', val='', type='unicode'): | |
|
462 | self.repository_id = repository_id | |
|
463 | self.app_settings_name = key | |
|
464 | self.app_settings_type = type | |
|
465 | self.app_settings_value = val | |
|
466 | ||
|
467 | @validates('_app_settings_value') | |
|
468 | def validate_settings_value(self, key, val): | |
|
469 | assert type(val) == unicode | |
|
470 | return val | |
|
471 | ||
|
472 | @hybrid_property | |
|
473 | def app_settings_value(self): | |
|
474 | v = self._app_settings_value | |
|
475 | type_ = self.app_settings_type | |
|
476 | SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES | |
|
477 | converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode'] | |
|
478 | return converter(v) | |
|
479 | ||
|
480 | @app_settings_value.setter | |
|
481 | def app_settings_value(self, val): | |
|
482 | """ | |
|
483 | Setter that will always make sure we use unicode in app_settings_value | |
|
484 | ||
|
485 | :param val: | |
|
486 | """ | |
|
487 | self._app_settings_value = safe_unicode(val) | |
|
488 | ||
|
489 | @hybrid_property | |
|
490 | def app_settings_type(self): | |
|
491 | return self._app_settings_type | |
|
492 | ||
|
493 | @app_settings_type.setter | |
|
494 | def app_settings_type(self, val): | |
|
495 | SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES | |
|
496 | if val not in SETTINGS_TYPES: | |
|
497 | raise Exception('type must be one of %s got %s' | |
|
498 | % (SETTINGS_TYPES.keys(), val)) | |
|
499 | self._app_settings_type = val | |
|
500 | ||
|
501 | def __unicode__(self): | |
|
502 | return u"<%s('%s:%s:%s[%s]')>" % ( | |
|
503 | self.__class__.__name__, self.repository.repo_name, | |
|
504 | self.app_settings_name, self.app_settings_value, | |
|
505 | self.app_settings_type | |
|
506 | ) | |
|
507 | ||
|
508 | ||
|
509 | class RepoRhodeCodeUi(Base, BaseModel): | |
|
510 | __tablename__ = 'repo_rhodecode_ui' | |
|
511 | __table_args__ = ( | |
|
512 | UniqueConstraint( | |
|
513 | 'repository_id', 'ui_section', 'ui_key', | |
|
514 | name='uq_repo_rhodecode_ui_repository_id_section_key'), | |
|
515 | base_table_args | |
|
516 | ) | |
|
517 | ||
|
518 | repository_id = Column( | |
|
519 | "repository_id", Integer(), ForeignKey('repositories.repo_id'), | |
|
520 | nullable=False) | |
|
521 | ui_id = Column( | |
|
522 | "ui_id", Integer(), nullable=False, unique=True, default=None, | |
|
523 | primary_key=True) | |
|
524 | ui_section = Column( | |
|
525 | "ui_section", String(255), nullable=True, unique=None, default=None) | |
|
526 | ui_key = Column( | |
|
527 | "ui_key", String(255), nullable=True, unique=None, default=None) | |
|
528 | ui_value = Column( | |
|
529 | "ui_value", String(255), nullable=True, unique=None, default=None) | |
|
530 | ui_active = Column( | |
|
531 | "ui_active", Boolean(), nullable=True, unique=None, default=True) | |
|
532 | ||
|
533 | repository = relationship('Repository') | |
|
534 | ||
|
535 | def __repr__(self): | |
|
536 | return '<%s[%s:%s]%s=>%s]>' % ( | |
|
537 | self.__class__.__name__, self.repository.repo_name, | |
|
538 | self.ui_section, self.ui_key, self.ui_value) | |
|
539 | ||
|
540 | ||
|
541 | class User(Base, BaseModel): | |
|
542 | __tablename__ = 'users' | |
|
543 | __table_args__ = ( | |
|
544 | UniqueConstraint('username'), UniqueConstraint('email'), | |
|
545 | Index('u_username_idx', 'username'), | |
|
546 | Index('u_email_idx', 'email'), | |
|
547 | base_table_args | |
|
548 | ) | |
|
549 | ||
|
550 | DEFAULT_USER = 'default' | |
|
551 | DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org' | |
|
552 | DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}' | |
|
553 | ||
|
554 | user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
555 | username = Column("username", String(255), nullable=True, unique=None, default=None) | |
|
556 | password = Column("password", String(255), nullable=True, unique=None, default=None) | |
|
557 | active = Column("active", Boolean(), nullable=True, unique=None, default=True) | |
|
558 | admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) | |
|
559 | name = Column("firstname", String(255), nullable=True, unique=None, default=None) | |
|
560 | lastname = Column("lastname", String(255), nullable=True, unique=None, default=None) | |
|
561 | _email = Column("email", String(255), nullable=True, unique=None, default=None) | |
|
562 | last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) | |
|
563 | last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None) | |
|
564 | ||
|
565 | extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None) | |
|
566 | extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None) | |
|
567 | _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None) | |
|
568 | inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) | |
|
569 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
570 | _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data | |
|
571 | ||
|
572 | user_log = relationship('UserLog') | |
|
573 | user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') | |
|
574 | ||
|
575 | repositories = relationship('Repository') | |
|
576 | repository_groups = relationship('RepoGroup') | |
|
577 | user_groups = relationship('UserGroup') | |
|
578 | ||
|
579 | user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') | |
|
580 | followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all') | |
|
581 | ||
|
582 | repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') | |
|
583 | repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') | |
|
584 | user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all') | |
|
585 | ||
|
586 | group_member = relationship('UserGroupMember', cascade='all') | |
|
587 | ||
|
588 | notifications = relationship('UserNotification', cascade='all') | |
|
589 | # notifications assigned to this user | |
|
590 | user_created_notifications = relationship('Notification', cascade='all') | |
|
591 | # comments created by this user | |
|
592 | user_comments = relationship('ChangesetComment', cascade='all') | |
|
593 | # user profile extra info | |
|
594 | user_emails = relationship('UserEmailMap', cascade='all') | |
|
595 | user_ip_map = relationship('UserIpMap', cascade='all') | |
|
596 | user_auth_tokens = relationship('UserApiKeys', cascade='all') | |
|
597 | user_ssh_keys = relationship('UserSshKeys', cascade='all') | |
|
598 | ||
|
599 | # gists | |
|
600 | user_gists = relationship('Gist', cascade='all') | |
|
601 | # user pull requests | |
|
602 | user_pull_requests = relationship('PullRequest', cascade='all') | |
|
603 | # external identities | |
|
604 | extenal_identities = relationship( | |
|
605 | 'ExternalIdentity', | |
|
606 | primaryjoin="User.user_id==ExternalIdentity.local_user_id", | |
|
607 | cascade='all') | |
|
608 | # review rules | |
|
609 | user_review_rules = relationship('RepoReviewRuleUser', cascade='all') | |
|
610 | ||
|
611 | def __unicode__(self): | |
|
612 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, | |
|
613 | self.user_id, self.username) | |
|
614 | ||
|
615 | @hybrid_property | |
|
616 | def email(self): | |
|
617 | return self._email | |
|
618 | ||
|
619 | @email.setter | |
|
620 | def email(self, val): | |
|
621 | self._email = val.lower() if val else None | |
|
622 | ||
|
623 | @hybrid_property | |
|
624 | def first_name(self): | |
|
625 | from rhodecode.lib import helpers as h | |
|
626 | if self.name: | |
|
627 | return h.escape(self.name) | |
|
628 | return self.name | |
|
629 | ||
|
630 | @hybrid_property | |
|
631 | def last_name(self): | |
|
632 | from rhodecode.lib import helpers as h | |
|
633 | if self.lastname: | |
|
634 | return h.escape(self.lastname) | |
|
635 | return self.lastname | |
|
636 | ||
|
637 | @hybrid_property | |
|
638 | def api_key(self): | |
|
639 | """ | |
|
640 | Fetch if exist an auth-token with role ALL connected to this user | |
|
641 | """ | |
|
642 | user_auth_token = UserApiKeys.query()\ | |
|
643 | .filter(UserApiKeys.user_id == self.user_id)\ | |
|
644 | .filter(or_(UserApiKeys.expires == -1, | |
|
645 | UserApiKeys.expires >= time.time()))\ | |
|
646 | .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first() | |
|
647 | if user_auth_token: | |
|
648 | user_auth_token = user_auth_token.api_key | |
|
649 | ||
|
650 | return user_auth_token | |
|
651 | ||
|
652 | @api_key.setter | |
|
653 | def api_key(self, val): | |
|
654 | # don't allow to set API key this is deprecated for now | |
|
655 | self._api_key = None | |
|
656 | ||
|
657 | @property | |
|
658 | def reviewer_pull_requests(self): | |
|
659 | return PullRequestReviewers.query() \ | |
|
660 | .options(joinedload(PullRequestReviewers.pull_request)) \ | |
|
661 | .filter(PullRequestReviewers.user_id == self.user_id) \ | |
|
662 | .all() | |
|
663 | ||
|
664 | @property | |
|
665 | def firstname(self): | |
|
666 | # alias for future | |
|
667 | return self.name | |
|
668 | ||
|
669 | @property | |
|
670 | def emails(self): | |
|
671 | other = UserEmailMap.query()\ | |
|
672 | .filter(UserEmailMap.user == self) \ | |
|
673 | .order_by(UserEmailMap.email_id.asc()) \ | |
|
674 | .all() | |
|
675 | return [self.email] + [x.email for x in other] | |
|
676 | ||
|
677 | @property | |
|
678 | def auth_tokens(self): | |
|
679 | auth_tokens = self.get_auth_tokens() | |
|
680 | return [x.api_key for x in auth_tokens] | |
|
681 | ||
|
682 | def get_auth_tokens(self): | |
|
683 | return UserApiKeys.query()\ | |
|
684 | .filter(UserApiKeys.user == self)\ | |
|
685 | .order_by(UserApiKeys.user_api_key_id.asc())\ | |
|
686 | .all() | |
|
687 | ||
|
688 | @LazyProperty | |
|
689 | def feed_token(self): | |
|
690 | return self.get_feed_token() | |
|
691 | ||
|
692 | def get_feed_token(self, cache=True): | |
|
693 | feed_tokens = UserApiKeys.query()\ | |
|
694 | .filter(UserApiKeys.user == self)\ | |
|
695 | .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED) | |
|
696 | if cache: | |
|
697 | feed_tokens = feed_tokens.options( | |
|
698 | FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id)) | |
|
699 | ||
|
700 | feed_tokens = feed_tokens.all() | |
|
701 | if feed_tokens: | |
|
702 | return feed_tokens[0].api_key | |
|
703 | return 'NO_FEED_TOKEN_AVAILABLE' | |
|
704 | ||
|
705 | @classmethod | |
|
706 | def get(cls, user_id, cache=False): | |
|
707 | if not user_id: | |
|
708 | return | |
|
709 | ||
|
710 | user = cls.query() | |
|
711 | if cache: | |
|
712 | user = user.options( | |
|
713 | FromCache("sql_cache_short", "get_users_%s" % user_id)) | |
|
714 | return user.get(user_id) | |
|
715 | ||
|
716 | @classmethod | |
|
717 | def extra_valid_auth_tokens(cls, user, role=None): | |
|
718 | tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ | |
|
719 | .filter(or_(UserApiKeys.expires == -1, | |
|
720 | UserApiKeys.expires >= time.time())) | |
|
721 | if role: | |
|
722 | tokens = tokens.filter(or_(UserApiKeys.role == role, | |
|
723 | UserApiKeys.role == UserApiKeys.ROLE_ALL)) | |
|
724 | return tokens.all() | |
|
725 | ||
|
726 | def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None): | |
|
727 | from rhodecode.lib import auth | |
|
728 | ||
|
729 | log.debug('Trying to authenticate user: %s via auth-token, ' | |
|
730 | 'and roles: %s', self, roles) | |
|
731 | ||
|
732 | if not auth_token: | |
|
733 | return False | |
|
734 | ||
|
735 | crypto_backend = auth.crypto_backend() | |
|
736 | ||
|
737 | roles = (roles or []) + [UserApiKeys.ROLE_ALL] | |
|
738 | tokens_q = UserApiKeys.query()\ | |
|
739 | .filter(UserApiKeys.user_id == self.user_id)\ | |
|
740 | .filter(or_(UserApiKeys.expires == -1, | |
|
741 | UserApiKeys.expires >= time.time())) | |
|
742 | ||
|
743 | tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles)) | |
|
744 | ||
|
745 | plain_tokens = [] | |
|
746 | hash_tokens = [] | |
|
747 | ||
|
748 | user_tokens = tokens_q.all() | |
|
749 | log.debug('Found %s user tokens to check for authentication', len(user_tokens)) | |
|
750 | for token in user_tokens: | |
|
751 | log.debug('AUTH_TOKEN: checking if user token with id `%s` matches', | |
|
752 | token.user_api_key_id) | |
|
753 | # verify scope first, since it's way faster than hash calculation of | |
|
754 | # encrypted tokens | |
|
755 | if token.repo_id: | |
|
756 | # token has a scope, we need to verify it | |
|
757 | if scope_repo_id != token.repo_id: | |
|
758 | log.debug( | |
|
759 | 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, ' | |
|
760 | 'and calling scope is:%s, skipping further checks', | |
|
761 | token.repo, scope_repo_id) | |
|
762 | # token has a scope, and it doesn't match, skip token | |
|
763 | continue | |
|
764 | ||
|
765 | if token.api_key.startswith(crypto_backend.ENC_PREF): | |
|
766 | hash_tokens.append(token.api_key) | |
|
767 | else: | |
|
768 | plain_tokens.append(token.api_key) | |
|
769 | ||
|
770 | is_plain_match = auth_token in plain_tokens | |
|
771 | if is_plain_match: | |
|
772 | return True | |
|
773 | ||
|
774 | for hashed in hash_tokens: | |
|
775 | # NOTE(marcink): this is expensive to calculate, but most secure | |
|
776 | match = crypto_backend.hash_check(auth_token, hashed) | |
|
777 | if match: | |
|
778 | return True | |
|
779 | ||
|
780 | return False | |
|
781 | ||
|
782 | @property | |
|
783 | def ip_addresses(self): | |
|
784 | ret = UserIpMap.query().filter(UserIpMap.user == self).all() | |
|
785 | return [x.ip_addr for x in ret] | |
|
786 | ||
|
787 | @property | |
|
788 | def username_and_name(self): | |
|
789 | return '%s (%s %s)' % (self.username, self.first_name, self.last_name) | |
|
790 | ||
|
791 | @property | |
|
792 | def username_or_name_or_email(self): | |
|
793 | full_name = self.full_name if self.full_name is not ' ' else None | |
|
794 | return self.username or full_name or self.email | |
|
795 | ||
|
796 | @property | |
|
797 | def full_name(self): | |
|
798 | return '%s %s' % (self.first_name, self.last_name) | |
|
799 | ||
|
800 | @property | |
|
801 | def full_name_or_username(self): | |
|
802 | return ('%s %s' % (self.first_name, self.last_name) | |
|
803 | if (self.first_name and self.last_name) else self.username) | |
|
804 | ||
|
805 | @property | |
|
806 | def full_contact(self): | |
|
807 | return '%s %s <%s>' % (self.first_name, self.last_name, self.email) | |
|
808 | ||
|
809 | @property | |
|
810 | def short_contact(self): | |
|
811 | return '%s %s' % (self.first_name, self.last_name) | |
|
812 | ||
|
813 | @property | |
|
814 | def is_admin(self): | |
|
815 | return self.admin | |
|
816 | ||
|
817 | def AuthUser(self, **kwargs): | |
|
818 | """ | |
|
819 | Returns instance of AuthUser for this user | |
|
820 | """ | |
|
821 | from rhodecode.lib.auth import AuthUser | |
|
822 | return AuthUser(user_id=self.user_id, username=self.username, **kwargs) | |
|
823 | ||
|
824 | @hybrid_property | |
|
825 | def user_data(self): | |
|
826 | if not self._user_data: | |
|
827 | return {} | |
|
828 | ||
|
829 | try: | |
|
830 | return json.loads(self._user_data) | |
|
831 | except TypeError: | |
|
832 | return {} | |
|
833 | ||
|
834 | @user_data.setter | |
|
835 | def user_data(self, val): | |
|
836 | if not isinstance(val, dict): | |
|
837 | raise Exception('user_data must be dict, got %s' % type(val)) | |
|
838 | try: | |
|
839 | self._user_data = json.dumps(val) | |
|
840 | except Exception: | |
|
841 | log.error(traceback.format_exc()) | |
|
842 | ||
|
843 | @classmethod | |
|
844 | def get_by_username(cls, username, case_insensitive=False, | |
|
845 | cache=False, identity_cache=False): | |
|
846 | session = Session() | |
|
847 | ||
|
848 | if case_insensitive: | |
|
849 | q = cls.query().filter( | |
|
850 | func.lower(cls.username) == func.lower(username)) | |
|
851 | else: | |
|
852 | q = cls.query().filter(cls.username == username) | |
|
853 | ||
|
854 | if cache: | |
|
855 | if identity_cache: | |
|
856 | val = cls.identity_cache(session, 'username', username) | |
|
857 | if val: | |
|
858 | return val | |
|
859 | else: | |
|
860 | cache_key = "get_user_by_name_%s" % _hash_key(username) | |
|
861 | q = q.options( | |
|
862 | FromCache("sql_cache_short", cache_key)) | |
|
863 | ||
|
864 | return q.scalar() | |
|
865 | ||
|
866 | @classmethod | |
|
867 | def get_by_auth_token(cls, auth_token, cache=False): | |
|
868 | q = UserApiKeys.query()\ | |
|
869 | .filter(UserApiKeys.api_key == auth_token)\ | |
|
870 | .filter(or_(UserApiKeys.expires == -1, | |
|
871 | UserApiKeys.expires >= time.time())) | |
|
872 | if cache: | |
|
873 | q = q.options( | |
|
874 | FromCache("sql_cache_short", "get_auth_token_%s" % auth_token)) | |
|
875 | ||
|
876 | match = q.first() | |
|
877 | if match: | |
|
878 | return match.user | |
|
879 | ||
|
880 | @classmethod | |
|
881 | def get_by_email(cls, email, case_insensitive=False, cache=False): | |
|
882 | ||
|
883 | if case_insensitive: | |
|
884 | q = cls.query().filter(func.lower(cls.email) == func.lower(email)) | |
|
885 | ||
|
886 | else: | |
|
887 | q = cls.query().filter(cls.email == email) | |
|
888 | ||
|
889 | email_key = _hash_key(email) | |
|
890 | if cache: | |
|
891 | q = q.options( | |
|
892 | FromCache("sql_cache_short", "get_email_key_%s" % email_key)) | |
|
893 | ||
|
894 | ret = q.scalar() | |
|
895 | if ret is None: | |
|
896 | q = UserEmailMap.query() | |
|
897 | # try fetching in alternate email map | |
|
898 | if case_insensitive: | |
|
899 | q = q.filter(func.lower(UserEmailMap.email) == func.lower(email)) | |
|
900 | else: | |
|
901 | q = q.filter(UserEmailMap.email == email) | |
|
902 | q = q.options(joinedload(UserEmailMap.user)) | |
|
903 | if cache: | |
|
904 | q = q.options( | |
|
905 | FromCache("sql_cache_short", "get_email_map_key_%s" % email_key)) | |
|
906 | ret = getattr(q.scalar(), 'user', None) | |
|
907 | ||
|
908 | return ret | |
|
909 | ||
|
910 | @classmethod | |
|
911 | def get_from_cs_author(cls, author): | |
|
912 | """ | |
|
913 | Tries to get User objects out of commit author string | |
|
914 | ||
|
915 | :param author: | |
|
916 | """ | |
|
917 | from rhodecode.lib.helpers import email, author_name | |
|
918 | # Valid email in the attribute passed, see if they're in the system | |
|
919 | _email = email(author) | |
|
920 | if _email: | |
|
921 | user = cls.get_by_email(_email, case_insensitive=True) | |
|
922 | if user: | |
|
923 | return user | |
|
924 | # Maybe we can match by username? | |
|
925 | _author = author_name(author) | |
|
926 | user = cls.get_by_username(_author, case_insensitive=True) | |
|
927 | if user: | |
|
928 | return user | |
|
929 | ||
|
930 | def update_userdata(self, **kwargs): | |
|
931 | usr = self | |
|
932 | old = usr.user_data | |
|
933 | old.update(**kwargs) | |
|
934 | usr.user_data = old | |
|
935 | Session().add(usr) | |
|
936 | log.debug('updated userdata with ', kwargs) | |
|
937 | ||
|
938 | def update_lastlogin(self): | |
|
939 | """Update user lastlogin""" | |
|
940 | self.last_login = datetime.datetime.now() | |
|
941 | Session().add(self) | |
|
942 | log.debug('updated user %s lastlogin', self.username) | |
|
943 | ||
|
944 | def update_password(self, new_password): | |
|
945 | from rhodecode.lib.auth import get_crypt_password | |
|
946 | ||
|
947 | self.password = get_crypt_password(new_password) | |
|
948 | Session().add(self) | |
|
949 | ||
|
950 | @classmethod | |
|
951 | def get_first_super_admin(cls): | |
|
952 | user = User.query()\ | |
|
953 | .filter(User.admin == true()) \ | |
|
954 | .order_by(User.user_id.asc()) \ | |
|
955 | .first() | |
|
956 | ||
|
957 | if user is None: | |
|
958 | raise Exception('FATAL: Missing administrative account!') | |
|
959 | return user | |
|
960 | ||
|
961 | @classmethod | |
|
962 | def get_all_super_admins(cls, only_active=False): | |
|
963 | """ | |
|
964 | Returns all admin accounts sorted by username | |
|
965 | """ | |
|
966 | qry = User.query().filter(User.admin == true()).order_by(User.username.asc()) | |
|
967 | if only_active: | |
|
968 | qry = qry.filter(User.active == true()) | |
|
969 | return qry.all() | |
|
970 | ||
|
971 | @classmethod | |
|
972 | def get_default_user(cls, cache=False, refresh=False): | |
|
973 | user = User.get_by_username(User.DEFAULT_USER, cache=cache) | |
|
974 | if user is None: | |
|
975 | raise Exception('FATAL: Missing default account!') | |
|
976 | if refresh: | |
|
977 | # The default user might be based on outdated state which | |
|
978 | # has been loaded from the cache. | |
|
979 | # A call to refresh() ensures that the | |
|
980 | # latest state from the database is used. | |
|
981 | Session().refresh(user) | |
|
982 | return user | |
|
983 | ||
|
984 | def _get_default_perms(self, user, suffix=''): | |
|
985 | from rhodecode.model.permission import PermissionModel | |
|
986 | return PermissionModel().get_default_perms(user.user_perms, suffix) | |
|
987 | ||
|
988 | def get_default_perms(self, suffix=''): | |
|
989 | return self._get_default_perms(self, suffix) | |
|
990 | ||
|
991 | def get_api_data(self, include_secrets=False, details='full'): | |
|
992 | """ | |
|
993 | Common function for generating user related data for API | |
|
994 | ||
|
995 | :param include_secrets: By default secrets in the API data will be replaced | |
|
996 | by a placeholder value to prevent exposing this data by accident. In case | |
|
997 | this data shall be exposed, set this flag to ``True``. | |
|
998 | ||
|
999 | :param details: details can be 'basic|full' basic gives only a subset of | |
|
1000 | the available user information that includes user_id, name and emails. | |
|
1001 | """ | |
|
1002 | user = self | |
|
1003 | user_data = self.user_data | |
|
1004 | data = { | |
|
1005 | 'user_id': user.user_id, | |
|
1006 | 'username': user.username, | |
|
1007 | 'firstname': user.name, | |
|
1008 | 'lastname': user.lastname, | |
|
1009 | 'email': user.email, | |
|
1010 | 'emails': user.emails, | |
|
1011 | } | |
|
1012 | if details == 'basic': | |
|
1013 | return data | |
|
1014 | ||
|
1015 | auth_token_length = 40 | |
|
1016 | auth_token_replacement = '*' * auth_token_length | |
|
1017 | ||
|
1018 | extras = { | |
|
1019 | 'auth_tokens': [auth_token_replacement], | |
|
1020 | 'active': user.active, | |
|
1021 | 'admin': user.admin, | |
|
1022 | 'extern_type': user.extern_type, | |
|
1023 | 'extern_name': user.extern_name, | |
|
1024 | 'last_login': user.last_login, | |
|
1025 | 'last_activity': user.last_activity, | |
|
1026 | 'ip_addresses': user.ip_addresses, | |
|
1027 | 'language': user_data.get('language') | |
|
1028 | } | |
|
1029 | data.update(extras) | |
|
1030 | ||
|
1031 | if include_secrets: | |
|
1032 | data['auth_tokens'] = user.auth_tokens | |
|
1033 | return data | |
|
1034 | ||
|
1035 | def __json__(self): | |
|
1036 | data = { | |
|
1037 | 'full_name': self.full_name, | |
|
1038 | 'full_name_or_username': self.full_name_or_username, | |
|
1039 | 'short_contact': self.short_contact, | |
|
1040 | 'full_contact': self.full_contact, | |
|
1041 | } | |
|
1042 | data.update(self.get_api_data()) | |
|
1043 | return data | |
|
1044 | ||
|
1045 | ||
|
1046 | class UserApiKeys(Base, BaseModel): | |
|
1047 | __tablename__ = 'user_api_keys' | |
|
1048 | __table_args__ = ( | |
|
1049 | Index('uak_api_key_idx', 'api_key', unique=True), | |
|
1050 | Index('uak_api_key_expires_idx', 'api_key', 'expires'), | |
|
1051 | base_table_args | |
|
1052 | ) | |
|
1053 | __mapper_args__ = {} | |
|
1054 | ||
|
1055 | # ApiKey role | |
|
1056 | ROLE_ALL = 'token_role_all' | |
|
1057 | ROLE_HTTP = 'token_role_http' | |
|
1058 | ROLE_VCS = 'token_role_vcs' | |
|
1059 | ROLE_API = 'token_role_api' | |
|
1060 | ROLE_FEED = 'token_role_feed' | |
|
1061 | ROLE_PASSWORD_RESET = 'token_password_reset' | |
|
1062 | ||
|
1063 | ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] | |
|
1064 | ||
|
1065 | user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1066 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) | |
|
1067 | api_key = Column("api_key", String(255), nullable=False, unique=True) | |
|
1068 | description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) | |
|
1069 | expires = Column('expires', Float(53), nullable=False) | |
|
1070 | role = Column('role', String(255), nullable=True) | |
|
1071 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
1072 | ||
|
1073 | # scope columns | |
|
1074 | repo_id = Column( | |
|
1075 | 'repo_id', Integer(), ForeignKey('repositories.repo_id'), | |
|
1076 | nullable=True, unique=None, default=None) | |
|
1077 | repo = relationship('Repository', lazy='joined') | |
|
1078 | ||
|
1079 | repo_group_id = Column( | |
|
1080 | 'repo_group_id', Integer(), ForeignKey('groups.group_id'), | |
|
1081 | nullable=True, unique=None, default=None) | |
|
1082 | repo_group = relationship('RepoGroup', lazy='joined') | |
|
1083 | ||
|
1084 | user = relationship('User', lazy='joined') | |
|
1085 | ||
|
1086 | def __unicode__(self): | |
|
1087 | return u"<%s('%s')>" % (self.__class__.__name__, self.role) | |
|
1088 | ||
|
1089 | def __json__(self): | |
|
1090 | data = { | |
|
1091 | 'auth_token': self.api_key, | |
|
1092 | 'role': self.role, | |
|
1093 | 'scope': self.scope_humanized, | |
|
1094 | 'expired': self.expired | |
|
1095 | } | |
|
1096 | return data | |
|
1097 | ||
|
1098 | def get_api_data(self, include_secrets=False): | |
|
1099 | data = self.__json__() | |
|
1100 | if include_secrets: | |
|
1101 | return data | |
|
1102 | else: | |
|
1103 | data['auth_token'] = self.token_obfuscated | |
|
1104 | return data | |
|
1105 | ||
|
1106 | @hybrid_property | |
|
1107 | def description_safe(self): | |
|
1108 | from rhodecode.lib import helpers as h | |
|
1109 | return h.escape(self.description) | |
|
1110 | ||
|
1111 | @property | |
|
1112 | def expired(self): | |
|
1113 | if self.expires == -1: | |
|
1114 | return False | |
|
1115 | return time.time() > self.expires | |
|
1116 | ||
|
1117 | @classmethod | |
|
1118 | def _get_role_name(cls, role): | |
|
1119 | return { | |
|
1120 | cls.ROLE_ALL: _('all'), | |
|
1121 | cls.ROLE_HTTP: _('http/web interface'), | |
|
1122 | cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'), | |
|
1123 | cls.ROLE_API: _('api calls'), | |
|
1124 | cls.ROLE_FEED: _('feed access'), | |
|
1125 | }.get(role, role) | |
|
1126 | ||
|
1127 | @property | |
|
1128 | def role_humanized(self): | |
|
1129 | return self._get_role_name(self.role) | |
|
1130 | ||
|
1131 | def _get_scope(self): | |
|
1132 | if self.repo: | |
|
1133 | return 'Repository: {}'.format(self.repo.repo_name) | |
|
1134 | if self.repo_group: | |
|
1135 | return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name) | |
|
1136 | return 'Global' | |
|
1137 | ||
|
1138 | @property | |
|
1139 | def scope_humanized(self): | |
|
1140 | return self._get_scope() | |
|
1141 | ||
|
1142 | @property | |
|
1143 | def token_obfuscated(self): | |
|
1144 | if self.api_key: | |
|
1145 | return self.api_key[:4] + "****" | |
|
1146 | ||
|
1147 | ||
|
1148 | class UserEmailMap(Base, BaseModel): | |
|
1149 | __tablename__ = 'user_email_map' | |
|
1150 | __table_args__ = ( | |
|
1151 | Index('uem_email_idx', 'email'), | |
|
1152 | UniqueConstraint('email'), | |
|
1153 | base_table_args | |
|
1154 | ) | |
|
1155 | __mapper_args__ = {} | |
|
1156 | ||
|
1157 | email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1158 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) | |
|
1159 | _email = Column("email", String(255), nullable=True, unique=False, default=None) | |
|
1160 | user = relationship('User', lazy='joined') | |
|
1161 | ||
|
1162 | @validates('_email') | |
|
1163 | def validate_email(self, key, email): | |
|
1164 | # check if this email is not main one | |
|
1165 | main_email = Session().query(User).filter(User.email == email).scalar() | |
|
1166 | if main_email is not None: | |
|
1167 | raise AttributeError('email %s is present is user table' % email) | |
|
1168 | return email | |
|
1169 | ||
|
1170 | @hybrid_property | |
|
1171 | def email(self): | |
|
1172 | return self._email | |
|
1173 | ||
|
1174 | @email.setter | |
|
1175 | def email(self, val): | |
|
1176 | self._email = val.lower() if val else None | |
|
1177 | ||
|
1178 | ||
|
1179 | class UserIpMap(Base, BaseModel): | |
|
1180 | __tablename__ = 'user_ip_map' | |
|
1181 | __table_args__ = ( | |
|
1182 | UniqueConstraint('user_id', 'ip_addr'), | |
|
1183 | base_table_args | |
|
1184 | ) | |
|
1185 | __mapper_args__ = {} | |
|
1186 | ||
|
1187 | ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1188 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) | |
|
1189 | ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None) | |
|
1190 | active = Column("active", Boolean(), nullable=True, unique=None, default=True) | |
|
1191 | description = Column("description", String(10000), nullable=True, unique=None, default=None) | |
|
1192 | user = relationship('User', lazy='joined') | |
|
1193 | ||
|
1194 | @hybrid_property | |
|
1195 | def description_safe(self): | |
|
1196 | from rhodecode.lib import helpers as h | |
|
1197 | return h.escape(self.description) | |
|
1198 | ||
|
1199 | @classmethod | |
|
1200 | def _get_ip_range(cls, ip_addr): | |
|
1201 | net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False) | |
|
1202 | return [str(net.network_address), str(net.broadcast_address)] | |
|
1203 | ||
|
1204 | def __json__(self): | |
|
1205 | return { | |
|
1206 | 'ip_addr': self.ip_addr, | |
|
1207 | 'ip_range': self._get_ip_range(self.ip_addr), | |
|
1208 | } | |
|
1209 | ||
|
1210 | def __unicode__(self): | |
|
1211 | return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__, | |
|
1212 | self.user_id, self.ip_addr) | |
|
1213 | ||
|
1214 | ||
|
1215 | class UserSshKeys(Base, BaseModel): | |
|
1216 | __tablename__ = 'user_ssh_keys' | |
|
1217 | __table_args__ = ( | |
|
1218 | Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'), | |
|
1219 | ||
|
1220 | UniqueConstraint('ssh_key_fingerprint'), | |
|
1221 | ||
|
1222 | base_table_args | |
|
1223 | ) | |
|
1224 | __mapper_args__ = {} | |
|
1225 | ||
|
1226 | ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1227 | ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None) | |
|
1228 | ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None) | |
|
1229 | ||
|
1230 | description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) | |
|
1231 | ||
|
1232 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
1233 | accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None) | |
|
1234 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) | |
|
1235 | ||
|
1236 | user = relationship('User', lazy='joined') | |
|
1237 | ||
|
1238 | def __json__(self): | |
|
1239 | data = { | |
|
1240 | 'ssh_fingerprint': self.ssh_key_fingerprint, | |
|
1241 | 'description': self.description, | |
|
1242 | 'created_on': self.created_on | |
|
1243 | } | |
|
1244 | return data | |
|
1245 | ||
|
1246 | def get_api_data(self): | |
|
1247 | data = self.__json__() | |
|
1248 | return data | |
|
1249 | ||
|
1250 | ||
|
1251 | class UserLog(Base, BaseModel): | |
|
1252 | __tablename__ = 'user_logs' | |
|
1253 | __table_args__ = ( | |
|
1254 | base_table_args, | |
|
1255 | ) | |
|
1256 | ||
|
1257 | VERSION_1 = 'v1' | |
|
1258 | VERSION_2 = 'v2' | |
|
1259 | VERSIONS = [VERSION_1, VERSION_2] | |
|
1260 | ||
|
1261 | user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1262 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None) | |
|
1263 | username = Column("username", String(255), nullable=True, unique=None, default=None) | |
|
1264 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None) | |
|
1265 | repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None) | |
|
1266 | user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None) | |
|
1267 | action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None) | |
|
1268 | action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) | |
|
1269 | ||
|
1270 | version = Column("version", String(255), nullable=True, default=VERSION_1) | |
|
1271 | user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT())))) | |
|
1272 | action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT())))) | |
|
1273 | ||
|
1274 | def __unicode__(self): | |
|
1275 | return u"<%s('id:%s:%s')>" % ( | |
|
1276 | self.__class__.__name__, self.repository_name, self.action) | |
|
1277 | ||
|
1278 | def __json__(self): | |
|
1279 | return { | |
|
1280 | 'user_id': self.user_id, | |
|
1281 | 'username': self.username, | |
|
1282 | 'repository_id': self.repository_id, | |
|
1283 | 'repository_name': self.repository_name, | |
|
1284 | 'user_ip': self.user_ip, | |
|
1285 | 'action_date': self.action_date, | |
|
1286 | 'action': self.action, | |
|
1287 | } | |
|
1288 | ||
|
1289 | @hybrid_property | |
|
1290 | def entry_id(self): | |
|
1291 | return self.user_log_id | |
|
1292 | ||
|
1293 | @property | |
|
1294 | def action_as_day(self): | |
|
1295 | return datetime.date(*self.action_date.timetuple()[:3]) | |
|
1296 | ||
|
1297 | user = relationship('User') | |
|
1298 | repository = relationship('Repository', cascade='') | |
|
1299 | ||
|
1300 | ||
|
1301 | class UserGroup(Base, BaseModel): | |
|
1302 | __tablename__ = 'users_groups' | |
|
1303 | __table_args__ = ( | |
|
1304 | base_table_args, | |
|
1305 | ) | |
|
1306 | ||
|
1307 | users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1308 | users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None) | |
|
1309 | user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None) | |
|
1310 | users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) | |
|
1311 | inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) | |
|
1312 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) | |
|
1313 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
1314 | _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data | |
|
1315 | ||
|
1316 | members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined") | |
|
1317 | users_group_to_perm = relationship('UserGroupToPerm', cascade='all') | |
|
1318 | users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all') | |
|
1319 | users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') | |
|
1320 | user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all') | |
|
1321 | user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all') | |
|
1322 | ||
|
1323 | user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all') | |
|
1324 | user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id") | |
|
1325 | ||
|
1326 | @classmethod | |
|
1327 | def _load_group_data(cls, column): | |
|
1328 | if not column: | |
|
1329 | return {} | |
|
1330 | ||
|
1331 | try: | |
|
1332 | return json.loads(column) or {} | |
|
1333 | except TypeError: | |
|
1334 | return {} | |
|
1335 | ||
|
1336 | @hybrid_property | |
|
1337 | def description_safe(self): | |
|
1338 | from rhodecode.lib import helpers as h | |
|
1339 | return h.escape(self.user_group_description) | |
|
1340 | ||
|
1341 | @hybrid_property | |
|
1342 | def group_data(self): | |
|
1343 | return self._load_group_data(self._group_data) | |
|
1344 | ||
|
1345 | @group_data.expression | |
|
1346 | def group_data(self, **kwargs): | |
|
1347 | return self._group_data | |
|
1348 | ||
|
1349 | @group_data.setter | |
|
1350 | def group_data(self, val): | |
|
1351 | try: | |
|
1352 | self._group_data = json.dumps(val) | |
|
1353 | except Exception: | |
|
1354 | log.error(traceback.format_exc()) | |
|
1355 | ||
|
1356 | @classmethod | |
|
1357 | def _load_sync(cls, group_data): | |
|
1358 | if group_data: | |
|
1359 | return group_data.get('extern_type') | |
|
1360 | ||
|
1361 | @property | |
|
1362 | def sync(self): | |
|
1363 | return self._load_sync(self.group_data) | |
|
1364 | ||
|
1365 | def __unicode__(self): | |
|
1366 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, | |
|
1367 | self.users_group_id, | |
|
1368 | self.users_group_name) | |
|
1369 | ||
|
1370 | @classmethod | |
|
1371 | def get_by_group_name(cls, group_name, cache=False, | |
|
1372 | case_insensitive=False): | |
|
1373 | if case_insensitive: | |
|
1374 | q = cls.query().filter(func.lower(cls.users_group_name) == | |
|
1375 | func.lower(group_name)) | |
|
1376 | ||
|
1377 | else: | |
|
1378 | q = cls.query().filter(cls.users_group_name == group_name) | |
|
1379 | if cache: | |
|
1380 | q = q.options( | |
|
1381 | FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name))) | |
|
1382 | return q.scalar() | |
|
1383 | ||
|
1384 | @classmethod | |
|
1385 | def get(cls, user_group_id, cache=False): | |
|
1386 | if not user_group_id: | |
|
1387 | return | |
|
1388 | ||
|
1389 | user_group = cls.query() | |
|
1390 | if cache: | |
|
1391 | user_group = user_group.options( | |
|
1392 | FromCache("sql_cache_short", "get_users_group_%s" % user_group_id)) | |
|
1393 | return user_group.get(user_group_id) | |
|
1394 | ||
|
1395 | def permissions(self, with_admins=True, with_owner=True, | |
|
1396 | expand_from_user_groups=False): | |
|
1397 | """ | |
|
1398 | Permissions for user groups | |
|
1399 | """ | |
|
1400 | _admin_perm = 'usergroup.admin' | |
|
1401 | ||
|
1402 | owner_row = [] | |
|
1403 | if with_owner: | |
|
1404 | usr = AttributeDict(self.user.get_dict()) | |
|
1405 | usr.owner_row = True | |
|
1406 | usr.permission = _admin_perm | |
|
1407 | owner_row.append(usr) | |
|
1408 | ||
|
1409 | super_admin_ids = [] | |
|
1410 | super_admin_rows = [] | |
|
1411 | if with_admins: | |
|
1412 | for usr in User.get_all_super_admins(): | |
|
1413 | super_admin_ids.append(usr.user_id) | |
|
1414 | # if this admin is also owner, don't double the record | |
|
1415 | if usr.user_id == owner_row[0].user_id: | |
|
1416 | owner_row[0].admin_row = True | |
|
1417 | else: | |
|
1418 | usr = AttributeDict(usr.get_dict()) | |
|
1419 | usr.admin_row = True | |
|
1420 | usr.permission = _admin_perm | |
|
1421 | super_admin_rows.append(usr) | |
|
1422 | ||
|
1423 | q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self) | |
|
1424 | q = q.options(joinedload(UserUserGroupToPerm.user_group), | |
|
1425 | joinedload(UserUserGroupToPerm.user), | |
|
1426 | joinedload(UserUserGroupToPerm.permission),) | |
|
1427 | ||
|
1428 | # get owners and admins and permissions. We do a trick of re-writing | |
|
1429 | # objects from sqlalchemy to named-tuples due to sqlalchemy session | |
|
1430 | # has a global reference and changing one object propagates to all | |
|
1431 | # others. This means if admin is also an owner admin_row that change | |
|
1432 | # would propagate to both objects | |
|
1433 | perm_rows = [] | |
|
1434 | for _usr in q.all(): | |
|
1435 | usr = AttributeDict(_usr.user.get_dict()) | |
|
1436 | # if this user is also owner/admin, mark as duplicate record | |
|
1437 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: | |
|
1438 | usr.duplicate_perm = True | |
|
1439 | usr.permission = _usr.permission.permission_name | |
|
1440 | perm_rows.append(usr) | |
|
1441 | ||
|
1442 | # filter the perm rows by 'default' first and then sort them by | |
|
1443 | # admin,write,read,none permissions sorted again alphabetically in | |
|
1444 | # each group | |
|
1445 | perm_rows = sorted(perm_rows, key=display_user_sort) | |
|
1446 | ||
|
1447 | user_groups_rows = [] | |
|
1448 | if expand_from_user_groups: | |
|
1449 | for ug in self.permission_user_groups(with_members=True): | |
|
1450 | for user_data in ug.members: | |
|
1451 | user_groups_rows.append(user_data) | |
|
1452 | ||
|
1453 | return super_admin_rows + owner_row + perm_rows + user_groups_rows | |
|
1454 | ||
|
1455 | def permission_user_groups(self, with_members=False): | |
|
1456 | q = UserGroupUserGroupToPerm.query()\ | |
|
1457 | .filter(UserGroupUserGroupToPerm.target_user_group == self) | |
|
1458 | q = q.options(joinedload(UserGroupUserGroupToPerm.user_group), | |
|
1459 | joinedload(UserGroupUserGroupToPerm.target_user_group), | |
|
1460 | joinedload(UserGroupUserGroupToPerm.permission),) | |
|
1461 | ||
|
1462 | perm_rows = [] | |
|
1463 | for _user_group in q.all(): | |
|
1464 | entry = AttributeDict(_user_group.user_group.get_dict()) | |
|
1465 | entry.permission = _user_group.permission.permission_name | |
|
1466 | if with_members: | |
|
1467 | entry.members = [x.user.get_dict() | |
|
1468 | for x in _user_group.users_group.members] | |
|
1469 | perm_rows.append(entry) | |
|
1470 | ||
|
1471 | perm_rows = sorted(perm_rows, key=display_user_group_sort) | |
|
1472 | return perm_rows | |
|
1473 | ||
|
1474 | def _get_default_perms(self, user_group, suffix=''): | |
|
1475 | from rhodecode.model.permission import PermissionModel | |
|
1476 | return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix) | |
|
1477 | ||
|
1478 | def get_default_perms(self, suffix=''): | |
|
1479 | return self._get_default_perms(self, suffix) | |
|
1480 | ||
|
1481 | def get_api_data(self, with_group_members=True, include_secrets=False): | |
|
1482 | """ | |
|
1483 | :param include_secrets: See :meth:`User.get_api_data`, this parameter is | |
|
1484 | basically forwarded. | |
|
1485 | ||
|
1486 | """ | |
|
1487 | user_group = self | |
|
1488 | data = { | |
|
1489 | 'users_group_id': user_group.users_group_id, | |
|
1490 | 'group_name': user_group.users_group_name, | |
|
1491 | 'group_description': user_group.user_group_description, | |
|
1492 | 'active': user_group.users_group_active, | |
|
1493 | 'owner': user_group.user.username, | |
|
1494 | 'sync': user_group.sync, | |
|
1495 | 'owner_email': user_group.user.email, | |
|
1496 | } | |
|
1497 | ||
|
1498 | if with_group_members: | |
|
1499 | users = [] | |
|
1500 | for user in user_group.members: | |
|
1501 | user = user.user | |
|
1502 | users.append(user.get_api_data(include_secrets=include_secrets)) | |
|
1503 | data['users'] = users | |
|
1504 | ||
|
1505 | return data | |
|
1506 | ||
|
1507 | ||
|
1508 | class UserGroupMember(Base, BaseModel): | |
|
1509 | __tablename__ = 'users_groups_members' | |
|
1510 | __table_args__ = ( | |
|
1511 | base_table_args, | |
|
1512 | ) | |
|
1513 | ||
|
1514 | users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1515 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
1516 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
1517 | ||
|
1518 | user = relationship('User', lazy='joined') | |
|
1519 | users_group = relationship('UserGroup') | |
|
1520 | ||
|
1521 | def __init__(self, gr_id='', u_id=''): | |
|
1522 | self.users_group_id = gr_id | |
|
1523 | self.user_id = u_id | |
|
1524 | ||
|
1525 | ||
|
1526 | class RepositoryField(Base, BaseModel): | |
|
1527 | __tablename__ = 'repositories_fields' | |
|
1528 | __table_args__ = ( | |
|
1529 | UniqueConstraint('repository_id', 'field_key'), # no-multi field | |
|
1530 | base_table_args, | |
|
1531 | ) | |
|
1532 | ||
|
1533 | PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields | |
|
1534 | ||
|
1535 | repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
1536 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) | |
|
1537 | field_key = Column("field_key", String(250)) | |
|
1538 | field_label = Column("field_label", String(1024), nullable=False) | |
|
1539 | field_value = Column("field_value", String(10000), nullable=False) | |
|
1540 | field_desc = Column("field_desc", String(1024), nullable=False) | |
|
1541 | field_type = Column("field_type", String(255), nullable=False, unique=None) | |
|
1542 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
1543 | ||
|
1544 | repository = relationship('Repository') | |
|
1545 | ||
|
1546 | @property | |
|
1547 | def field_key_prefixed(self): | |
|
1548 | return 'ex_%s' % self.field_key | |
|
1549 | ||
|
1550 | @classmethod | |
|
1551 | def un_prefix_key(cls, key): | |
|
1552 | if key.startswith(cls.PREFIX): | |
|
1553 | return key[len(cls.PREFIX):] | |
|
1554 | return key | |
|
1555 | ||
|
1556 | @classmethod | |
|
1557 | def get_by_key_name(cls, key, repo): | |
|
1558 | row = cls.query()\ | |
|
1559 | .filter(cls.repository == repo)\ | |
|
1560 | .filter(cls.field_key == key).scalar() | |
|
1561 | return row | |
|
1562 | ||
|
1563 | ||
|
1564 | class Repository(Base, BaseModel): | |
|
1565 | __tablename__ = 'repositories' | |
|
1566 | __table_args__ = ( | |
|
1567 | Index('r_repo_name_idx', 'repo_name', mysql_length=255), | |
|
1568 | base_table_args, | |
|
1569 | ) | |
|
1570 | DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}' | |
|
1571 | DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}' | |
|
1572 | DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}' | |
|
1573 | ||
|
1574 | STATE_CREATED = 'repo_state_created' | |
|
1575 | STATE_PENDING = 'repo_state_pending' | |
|
1576 | STATE_ERROR = 'repo_state_error' | |
|
1577 | ||
|
1578 | LOCK_AUTOMATIC = 'lock_auto' | |
|
1579 | LOCK_API = 'lock_api' | |
|
1580 | LOCK_WEB = 'lock_web' | |
|
1581 | LOCK_PULL = 'lock_pull' | |
|
1582 | ||
|
1583 | NAME_SEP = URL_SEP | |
|
1584 | ||
|
1585 | repo_id = Column( | |
|
1586 | "repo_id", Integer(), nullable=False, unique=True, default=None, | |
|
1587 | primary_key=True) | |
|
1588 | _repo_name = Column( | |
|
1589 | "repo_name", Text(), nullable=False, default=None) | |
|
1590 | _repo_name_hash = Column( | |
|
1591 | "repo_name_hash", String(255), nullable=False, unique=True) | |
|
1592 | repo_state = Column("repo_state", String(255), nullable=True) | |
|
1593 | ||
|
1594 | clone_uri = Column( | |
|
1595 | "clone_uri", EncryptedTextValue(), nullable=True, unique=False, | |
|
1596 | default=None) | |
|
1597 | push_uri = Column( | |
|
1598 | "push_uri", EncryptedTextValue(), nullable=True, unique=False, | |
|
1599 | default=None) | |
|
1600 | repo_type = Column( | |
|
1601 | "repo_type", String(255), nullable=False, unique=False, default=None) | |
|
1602 | user_id = Column( | |
|
1603 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, | |
|
1604 | unique=False, default=None) | |
|
1605 | private = Column( | |
|
1606 | "private", Boolean(), nullable=True, unique=None, default=None) | |
|
1607 | archived = Column( | |
|
1608 | "archived", Boolean(), nullable=True, unique=None, default=None) | |
|
1609 | enable_statistics = Column( | |
|
1610 | "statistics", Boolean(), nullable=True, unique=None, default=True) | |
|
1611 | enable_downloads = Column( | |
|
1612 | "downloads", Boolean(), nullable=True, unique=None, default=True) | |
|
1613 | description = Column( | |
|
1614 | "description", String(10000), nullable=True, unique=None, default=None) | |
|
1615 | created_on = Column( | |
|
1616 | 'created_on', DateTime(timezone=False), nullable=True, unique=None, | |
|
1617 | default=datetime.datetime.now) | |
|
1618 | updated_on = Column( | |
|
1619 | 'updated_on', DateTime(timezone=False), nullable=True, unique=None, | |
|
1620 | default=datetime.datetime.now) | |
|
1621 | _landing_revision = Column( | |
|
1622 | "landing_revision", String(255), nullable=False, unique=False, | |
|
1623 | default=None) | |
|
1624 | enable_locking = Column( | |
|
1625 | "enable_locking", Boolean(), nullable=False, unique=None, | |
|
1626 | default=False) | |
|
1627 | _locked = Column( | |
|
1628 | "locked", String(255), nullable=True, unique=False, default=None) | |
|
1629 | _changeset_cache = Column( | |
|
1630 | "changeset_cache", LargeBinary(), nullable=True) # JSON data | |
|
1631 | ||
|
1632 | fork_id = Column( | |
|
1633 | "fork_id", Integer(), ForeignKey('repositories.repo_id'), | |
|
1634 | nullable=True, unique=False, default=None) | |
|
1635 | group_id = Column( | |
|
1636 | "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, | |
|
1637 | unique=False, default=None) | |
|
1638 | ||
|
1639 | user = relationship('User', lazy='joined') | |
|
1640 | fork = relationship('Repository', remote_side=repo_id, lazy='joined') | |
|
1641 | group = relationship('RepoGroup', lazy='joined') | |
|
1642 | repo_to_perm = relationship( | |
|
1643 | 'UserRepoToPerm', cascade='all', | |
|
1644 | order_by='UserRepoToPerm.repo_to_perm_id') | |
|
1645 | users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all') | |
|
1646 | stats = relationship('Statistics', cascade='all', uselist=False) | |
|
1647 | ||
|
1648 | followers = relationship( | |
|
1649 | 'UserFollowing', | |
|
1650 | primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', | |
|
1651 | cascade='all') | |
|
1652 | extra_fields = relationship( | |
|
1653 | 'RepositoryField', cascade="all, delete, delete-orphan") | |
|
1654 | logs = relationship('UserLog') | |
|
1655 | comments = relationship( | |
|
1656 | 'ChangesetComment', cascade="all, delete, delete-orphan") | |
|
1657 | pull_requests_source = relationship( | |
|
1658 | 'PullRequest', | |
|
1659 | primaryjoin='PullRequest.source_repo_id==Repository.repo_id', | |
|
1660 | cascade="all, delete, delete-orphan") | |
|
1661 | pull_requests_target = relationship( | |
|
1662 | 'PullRequest', | |
|
1663 | primaryjoin='PullRequest.target_repo_id==Repository.repo_id', | |
|
1664 | cascade="all, delete, delete-orphan") | |
|
1665 | ui = relationship('RepoRhodeCodeUi', cascade="all") | |
|
1666 | settings = relationship('RepoRhodeCodeSetting', cascade="all") | |
|
1667 | integrations = relationship('Integration', | |
|
1668 | cascade="all, delete, delete-orphan") | |
|
1669 | ||
|
1670 | scoped_tokens = relationship('UserApiKeys', cascade="all") | |
|
1671 | ||
|
1672 | def __unicode__(self): | |
|
1673 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, | |
|
1674 | safe_unicode(self.repo_name)) | |
|
1675 | ||
|
1676 | @hybrid_property | |
|
1677 | def description_safe(self): | |
|
1678 | from rhodecode.lib import helpers as h | |
|
1679 | return h.escape(self.description) | |
|
1680 | ||
|
1681 | @hybrid_property | |
|
1682 | def landing_rev(self): | |
|
1683 | # always should return [rev_type, rev] | |
|
1684 | if self._landing_revision: | |
|
1685 | _rev_info = self._landing_revision.split(':') | |
|
1686 | if len(_rev_info) < 2: | |
|
1687 | _rev_info.insert(0, 'rev') | |
|
1688 | return [_rev_info[0], _rev_info[1]] | |
|
1689 | return [None, None] | |
|
1690 | ||
|
1691 | @landing_rev.setter | |
|
1692 | def landing_rev(self, val): | |
|
1693 | if ':' not in val: | |
|
1694 | raise ValueError('value must be delimited with `:` and consist ' | |
|
1695 | 'of <rev_type>:<rev>, got %s instead' % val) | |
|
1696 | self._landing_revision = val | |
|
1697 | ||
|
1698 | @hybrid_property | |
|
1699 | def locked(self): | |
|
1700 | if self._locked: | |
|
1701 | user_id, timelocked, reason = self._locked.split(':') | |
|
1702 | lock_values = int(user_id), timelocked, reason | |
|
1703 | else: | |
|
1704 | lock_values = [None, None, None] | |
|
1705 | return lock_values | |
|
1706 | ||
|
1707 | @locked.setter | |
|
1708 | def locked(self, val): | |
|
1709 | if val and isinstance(val, (list, tuple)): | |
|
1710 | self._locked = ':'.join(map(str, val)) | |
|
1711 | else: | |
|
1712 | self._locked = None | |
|
1713 | ||
|
1714 | @hybrid_property | |
|
1715 | def changeset_cache(self): | |
|
1716 | from rhodecode.lib.vcs.backends.base import EmptyCommit | |
|
1717 | dummy = EmptyCommit().__json__() | |
|
1718 | if not self._changeset_cache: | |
|
1719 | return dummy | |
|
1720 | try: | |
|
1721 | return json.loads(self._changeset_cache) | |
|
1722 | except TypeError: | |
|
1723 | return dummy | |
|
1724 | except Exception: | |
|
1725 | log.error(traceback.format_exc()) | |
|
1726 | return dummy | |
|
1727 | ||
|
1728 | @changeset_cache.setter | |
|
1729 | def changeset_cache(self, val): | |
|
1730 | try: | |
|
1731 | self._changeset_cache = json.dumps(val) | |
|
1732 | except Exception: | |
|
1733 | log.error(traceback.format_exc()) | |
|
1734 | ||
|
1735 | @hybrid_property | |
|
1736 | def repo_name(self): | |
|
1737 | return self._repo_name | |
|
1738 | ||
|
1739 | @repo_name.setter | |
|
1740 | def repo_name(self, value): | |
|
1741 | self._repo_name = value | |
|
1742 | self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest() | |
|
1743 | ||
|
1744 | @classmethod | |
|
1745 | def normalize_repo_name(cls, repo_name): | |
|
1746 | """ | |
|
1747 | Normalizes os specific repo_name to the format internally stored inside | |
|
1748 | database using URL_SEP | |
|
1749 | ||
|
1750 | :param cls: | |
|
1751 | :param repo_name: | |
|
1752 | """ | |
|
1753 | return cls.NAME_SEP.join(repo_name.split(os.sep)) | |
|
1754 | ||
|
1755 | @classmethod | |
|
1756 | def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False): | |
|
1757 | session = Session() | |
|
1758 | q = session.query(cls).filter(cls.repo_name == repo_name) | |
|
1759 | ||
|
1760 | if cache: | |
|
1761 | if identity_cache: | |
|
1762 | val = cls.identity_cache(session, 'repo_name', repo_name) | |
|
1763 | if val: | |
|
1764 | return val | |
|
1765 | else: | |
|
1766 | cache_key = "get_repo_by_name_%s" % _hash_key(repo_name) | |
|
1767 | q = q.options( | |
|
1768 | FromCache("sql_cache_short", cache_key)) | |
|
1769 | ||
|
1770 | return q.scalar() | |
|
1771 | ||
|
1772 | @classmethod | |
|
1773 | def get_by_id_or_repo_name(cls, repoid): | |
|
1774 | if isinstance(repoid, (int, long)): | |
|
1775 | try: | |
|
1776 | repo = cls.get(repoid) | |
|
1777 | except ValueError: | |
|
1778 | repo = None | |
|
1779 | else: | |
|
1780 | repo = cls.get_by_repo_name(repoid) | |
|
1781 | return repo | |
|
1782 | ||
|
1783 | @classmethod | |
|
1784 | def get_by_full_path(cls, repo_full_path): | |
|
1785 | repo_name = repo_full_path.split(cls.base_path(), 1)[-1] | |
|
1786 | repo_name = cls.normalize_repo_name(repo_name) | |
|
1787 | return cls.get_by_repo_name(repo_name.strip(URL_SEP)) | |
|
1788 | ||
|
1789 | @classmethod | |
|
1790 | def get_repo_forks(cls, repo_id): | |
|
1791 | return cls.query().filter(Repository.fork_id == repo_id) | |
|
1792 | ||
|
1793 | @classmethod | |
|
1794 | def base_path(cls): | |
|
1795 | """ | |
|
1796 | Returns base path when all repos are stored | |
|
1797 | ||
|
1798 | :param cls: | |
|
1799 | """ | |
|
1800 | q = Session().query(RhodeCodeUi)\ | |
|
1801 | .filter(RhodeCodeUi.ui_key == cls.NAME_SEP) | |
|
1802 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) | |
|
1803 | return q.one().ui_value | |
|
1804 | ||
|
1805 | @classmethod | |
|
1806 | def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None), | |
|
1807 | case_insensitive=True, archived=False): | |
|
1808 | q = Repository.query() | |
|
1809 | ||
|
1810 | if not archived: | |
|
1811 | q = q.filter(Repository.archived.isnot(true())) | |
|
1812 | ||
|
1813 | if not isinstance(user_id, Optional): | |
|
1814 | q = q.filter(Repository.user_id == user_id) | |
|
1815 | ||
|
1816 | if not isinstance(group_id, Optional): | |
|
1817 | q = q.filter(Repository.group_id == group_id) | |
|
1818 | ||
|
1819 | if case_insensitive: | |
|
1820 | q = q.order_by(func.lower(Repository.repo_name)) | |
|
1821 | else: | |
|
1822 | q = q.order_by(Repository.repo_name) | |
|
1823 | ||
|
1824 | return q.all() | |
|
1825 | ||
|
1826 | @property | |
|
1827 | def forks(self): | |
|
1828 | """ | |
|
1829 | Return forks of this repo | |
|
1830 | """ | |
|
1831 | return Repository.get_repo_forks(self.repo_id) | |
|
1832 | ||
|
1833 | @property | |
|
1834 | def parent(self): | |
|
1835 | """ | |
|
1836 | Returns fork parent | |
|
1837 | """ | |
|
1838 | return self.fork | |
|
1839 | ||
|
1840 | @property | |
|
1841 | def just_name(self): | |
|
1842 | return self.repo_name.split(self.NAME_SEP)[-1] | |
|
1843 | ||
|
1844 | @property | |
|
1845 | def groups_with_parents(self): | |
|
1846 | groups = [] | |
|
1847 | if self.group is None: | |
|
1848 | return groups | |
|
1849 | ||
|
1850 | cur_gr = self.group | |
|
1851 | groups.insert(0, cur_gr) | |
|
1852 | while 1: | |
|
1853 | gr = getattr(cur_gr, 'parent_group', None) | |
|
1854 | cur_gr = cur_gr.parent_group | |
|
1855 | if gr is None: | |
|
1856 | break | |
|
1857 | groups.insert(0, gr) | |
|
1858 | ||
|
1859 | return groups | |
|
1860 | ||
|
1861 | @property | |
|
1862 | def groups_and_repo(self): | |
|
1863 | return self.groups_with_parents, self | |
|
1864 | ||
|
1865 | @LazyProperty | |
|
1866 | def repo_path(self): | |
|
1867 | """ | |
|
1868 | Returns base full path for that repository means where it actually | |
|
1869 | exists on a filesystem | |
|
1870 | """ | |
|
1871 | q = Session().query(RhodeCodeUi).filter( | |
|
1872 | RhodeCodeUi.ui_key == self.NAME_SEP) | |
|
1873 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) | |
|
1874 | return q.one().ui_value | |
|
1875 | ||
|
1876 | @property | |
|
1877 | def repo_full_path(self): | |
|
1878 | p = [self.repo_path] | |
|
1879 | # we need to split the name by / since this is how we store the | |
|
1880 | # names in the database, but that eventually needs to be converted | |
|
1881 | # into a valid system path | |
|
1882 | p += self.repo_name.split(self.NAME_SEP) | |
|
1883 | return os.path.join(*map(safe_unicode, p)) | |
|
1884 | ||
|
1885 | @property | |
|
1886 | def cache_keys(self): | |
|
1887 | """ | |
|
1888 | Returns associated cache keys for that repo | |
|
1889 | """ | |
|
1890 | invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format( | |
|
1891 | repo_id=self.repo_id) | |
|
1892 | return CacheKey.query()\ | |
|
1893 | .filter(CacheKey.cache_args == invalidation_namespace)\ | |
|
1894 | .order_by(CacheKey.cache_key)\ | |
|
1895 | .all() | |
|
1896 | ||
|
1897 | @property | |
|
1898 | def cached_diffs_relative_dir(self): | |
|
1899 | """ | |
|
1900 | Return a relative to the repository store path of cached diffs | |
|
1901 | used for safe display for users, who shouldn't know the absolute store | |
|
1902 | path | |
|
1903 | """ | |
|
1904 | return os.path.join( | |
|
1905 | os.path.dirname(self.repo_name), | |
|
1906 | self.cached_diffs_dir.split(os.path.sep)[-1]) | |
|
1907 | ||
|
1908 | @property | |
|
1909 | def cached_diffs_dir(self): | |
|
1910 | path = self.repo_full_path | |
|
1911 | return os.path.join( | |
|
1912 | os.path.dirname(path), | |
|
1913 | '.__shadow_diff_cache_repo_{}'.format(self.repo_id)) | |
|
1914 | ||
|
1915 | def cached_diffs(self): | |
|
1916 | diff_cache_dir = self.cached_diffs_dir | |
|
1917 | if os.path.isdir(diff_cache_dir): | |
|
1918 | return os.listdir(diff_cache_dir) | |
|
1919 | return [] | |
|
1920 | ||
|
1921 | def shadow_repos(self): | |
|
1922 | shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id) | |
|
1923 | return [ | |
|
1924 | x for x in os.listdir(os.path.dirname(self.repo_full_path)) | |
|
1925 | if x.startswith(shadow_repos_pattern)] | |
|
1926 | ||
|
1927 | def get_new_name(self, repo_name): | |
|
1928 | """ | |
|
1929 | returns new full repository name based on assigned group and new new | |
|
1930 | ||
|
1931 | :param group_name: | |
|
1932 | """ | |
|
1933 | path_prefix = self.group.full_path_splitted if self.group else [] | |
|
1934 | return self.NAME_SEP.join(path_prefix + [repo_name]) | |
|
1935 | ||
|
1936 | @property | |
|
1937 | def _config(self): | |
|
1938 | """ | |
|
1939 | Returns db based config object. | |
|
1940 | """ | |
|
1941 | from rhodecode.lib.utils import make_db_config | |
|
1942 | return make_db_config(clear_session=False, repo=self) | |
|
1943 | ||
|
1944 | def permissions(self, with_admins=True, with_owner=True, | |
|
1945 | expand_from_user_groups=False): | |
|
1946 | """ | |
|
1947 | Permissions for repositories | |
|
1948 | """ | |
|
1949 | _admin_perm = 'repository.admin' | |
|
1950 | ||
|
1951 | owner_row = [] | |
|
1952 | if with_owner: | |
|
1953 | usr = AttributeDict(self.user.get_dict()) | |
|
1954 | usr.owner_row = True | |
|
1955 | usr.permission = _admin_perm | |
|
1956 | usr.permission_id = None | |
|
1957 | owner_row.append(usr) | |
|
1958 | ||
|
1959 | super_admin_ids = [] | |
|
1960 | super_admin_rows = [] | |
|
1961 | if with_admins: | |
|
1962 | for usr in User.get_all_super_admins(): | |
|
1963 | super_admin_ids.append(usr.user_id) | |
|
1964 | # if this admin is also owner, don't double the record | |
|
1965 | if usr.user_id == owner_row[0].user_id: | |
|
1966 | owner_row[0].admin_row = True | |
|
1967 | else: | |
|
1968 | usr = AttributeDict(usr.get_dict()) | |
|
1969 | usr.admin_row = True | |
|
1970 | usr.permission = _admin_perm | |
|
1971 | usr.permission_id = None | |
|
1972 | super_admin_rows.append(usr) | |
|
1973 | ||
|
1974 | q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self) | |
|
1975 | q = q.options(joinedload(UserRepoToPerm.repository), | |
|
1976 | joinedload(UserRepoToPerm.user), | |
|
1977 | joinedload(UserRepoToPerm.permission),) | |
|
1978 | ||
|
1979 | # get owners and admins and permissions. We do a trick of re-writing | |
|
1980 | # objects from sqlalchemy to named-tuples due to sqlalchemy session | |
|
1981 | # has a global reference and changing one object propagates to all | |
|
1982 | # others. This means if admin is also an owner admin_row that change | |
|
1983 | # would propagate to both objects | |
|
1984 | perm_rows = [] | |
|
1985 | for _usr in q.all(): | |
|
1986 | usr = AttributeDict(_usr.user.get_dict()) | |
|
1987 | # if this user is also owner/admin, mark as duplicate record | |
|
1988 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: | |
|
1989 | usr.duplicate_perm = True | |
|
1990 | # also check if this permission is maybe used by branch_permissions | |
|
1991 | if _usr.branch_perm_entry: | |
|
1992 | usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry] | |
|
1993 | ||
|
1994 | usr.permission = _usr.permission.permission_name | |
|
1995 | usr.permission_id = _usr.repo_to_perm_id | |
|
1996 | perm_rows.append(usr) | |
|
1997 | ||
|
1998 | # filter the perm rows by 'default' first and then sort them by | |
|
1999 | # admin,write,read,none permissions sorted again alphabetically in | |
|
2000 | # each group | |
|
2001 | perm_rows = sorted(perm_rows, key=display_user_sort) | |
|
2002 | ||
|
2003 | user_groups_rows = [] | |
|
2004 | if expand_from_user_groups: | |
|
2005 | for ug in self.permission_user_groups(with_members=True): | |
|
2006 | for user_data in ug.members: | |
|
2007 | user_groups_rows.append(user_data) | |
|
2008 | ||
|
2009 | return super_admin_rows + owner_row + perm_rows + user_groups_rows | |
|
2010 | ||
|
2011 | def permission_user_groups(self, with_members=True): | |
|
2012 | q = UserGroupRepoToPerm.query()\ | |
|
2013 | .filter(UserGroupRepoToPerm.repository == self) | |
|
2014 | q = q.options(joinedload(UserGroupRepoToPerm.repository), | |
|
2015 | joinedload(UserGroupRepoToPerm.users_group), | |
|
2016 | joinedload(UserGroupRepoToPerm.permission),) | |
|
2017 | ||
|
2018 | perm_rows = [] | |
|
2019 | for _user_group in q.all(): | |
|
2020 | entry = AttributeDict(_user_group.users_group.get_dict()) | |
|
2021 | entry.permission = _user_group.permission.permission_name | |
|
2022 | if with_members: | |
|
2023 | entry.members = [x.user.get_dict() | |
|
2024 | for x in _user_group.users_group.members] | |
|
2025 | perm_rows.append(entry) | |
|
2026 | ||
|
2027 | perm_rows = sorted(perm_rows, key=display_user_group_sort) | |
|
2028 | return perm_rows | |
|
2029 | ||
|
2030 | def get_api_data(self, include_secrets=False): | |
|
2031 | """ | |
|
2032 | Common function for generating repo api data | |
|
2033 | ||
|
2034 | :param include_secrets: See :meth:`User.get_api_data`. | |
|
2035 | ||
|
2036 | """ | |
|
2037 | # TODO: mikhail: Here there is an anti-pattern, we probably need to | |
|
2038 | # move this methods on models level. | |
|
2039 | from rhodecode.model.settings import SettingsModel | |
|
2040 | from rhodecode.model.repo import RepoModel | |
|
2041 | ||
|
2042 | repo = self | |
|
2043 | _user_id, _time, _reason = self.locked | |
|
2044 | ||
|
2045 | data = { | |
|
2046 | 'repo_id': repo.repo_id, | |
|
2047 | 'repo_name': repo.repo_name, | |
|
2048 | 'repo_type': repo.repo_type, | |
|
2049 | 'clone_uri': repo.clone_uri or '', | |
|
2050 | 'push_uri': repo.push_uri or '', | |
|
2051 | 'url': RepoModel().get_url(self), | |
|
2052 | 'private': repo.private, | |
|
2053 | 'created_on': repo.created_on, | |
|
2054 | 'description': repo.description_safe, | |
|
2055 | 'landing_rev': repo.landing_rev, | |
|
2056 | 'owner': repo.user.username, | |
|
2057 | 'fork_of': repo.fork.repo_name if repo.fork else None, | |
|
2058 | 'fork_of_id': repo.fork.repo_id if repo.fork else None, | |
|
2059 | 'enable_statistics': repo.enable_statistics, | |
|
2060 | 'enable_locking': repo.enable_locking, | |
|
2061 | 'enable_downloads': repo.enable_downloads, | |
|
2062 | 'last_changeset': repo.changeset_cache, | |
|
2063 | 'locked_by': User.get(_user_id).get_api_data( | |
|
2064 | include_secrets=include_secrets) if _user_id else None, | |
|
2065 | 'locked_date': time_to_datetime(_time) if _time else None, | |
|
2066 | 'lock_reason': _reason if _reason else None, | |
|
2067 | } | |
|
2068 | ||
|
2069 | # TODO: mikhail: should be per-repo settings here | |
|
2070 | rc_config = SettingsModel().get_all_settings() | |
|
2071 | repository_fields = str2bool( | |
|
2072 | rc_config.get('rhodecode_repository_fields')) | |
|
2073 | if repository_fields: | |
|
2074 | for f in self.extra_fields: | |
|
2075 | data[f.field_key_prefixed] = f.field_value | |
|
2076 | ||
|
2077 | return data | |
|
2078 | ||
|
2079 | @classmethod | |
|
2080 | def lock(cls, repo, user_id, lock_time=None, lock_reason=None): | |
|
2081 | if not lock_time: | |
|
2082 | lock_time = time.time() | |
|
2083 | if not lock_reason: | |
|
2084 | lock_reason = cls.LOCK_AUTOMATIC | |
|
2085 | repo.locked = [user_id, lock_time, lock_reason] | |
|
2086 | Session().add(repo) | |
|
2087 | Session().commit() | |
|
2088 | ||
|
2089 | @classmethod | |
|
2090 | def unlock(cls, repo): | |
|
2091 | repo.locked = None | |
|
2092 | Session().add(repo) | |
|
2093 | Session().commit() | |
|
2094 | ||
|
2095 | @classmethod | |
|
2096 | def getlock(cls, repo): | |
|
2097 | return repo.locked | |
|
2098 | ||
|
2099 | def is_user_lock(self, user_id): | |
|
2100 | if self.lock[0]: | |
|
2101 | lock_user_id = safe_int(self.lock[0]) | |
|
2102 | user_id = safe_int(user_id) | |
|
2103 | # both are ints, and they are equal | |
|
2104 | return all([lock_user_id, user_id]) and lock_user_id == user_id | |
|
2105 | ||
|
2106 | return False | |
|
2107 | ||
|
2108 | def get_locking_state(self, action, user_id, only_when_enabled=True): | |
|
2109 | """ | |
|
2110 | Checks locking on this repository, if locking is enabled and lock is | |
|
2111 | present returns a tuple of make_lock, locked, locked_by. | |
|
2112 | make_lock can have 3 states None (do nothing) True, make lock | |
|
2113 | False release lock, This value is later propagated to hooks, which | |
|
2114 | do the locking. Think about this as signals passed to hooks what to do. | |
|
2115 | ||
|
2116 | """ | |
|
2117 | # TODO: johbo: This is part of the business logic and should be moved | |
|
2118 | # into the RepositoryModel. | |
|
2119 | ||
|
2120 | if action not in ('push', 'pull'): | |
|
2121 | raise ValueError("Invalid action value: %s" % repr(action)) | |
|
2122 | ||
|
2123 | # defines if locked error should be thrown to user | |
|
2124 | currently_locked = False | |
|
2125 | # defines if new lock should be made, tri-state | |
|
2126 | make_lock = None | |
|
2127 | repo = self | |
|
2128 | user = User.get(user_id) | |
|
2129 | ||
|
2130 | lock_info = repo.locked | |
|
2131 | ||
|
2132 | if repo and (repo.enable_locking or not only_when_enabled): | |
|
2133 | if action == 'push': | |
|
2134 | # check if it's already locked !, if it is compare users | |
|
2135 | locked_by_user_id = lock_info[0] | |
|
2136 | if user.user_id == locked_by_user_id: | |
|
2137 | log.debug( | |
|
2138 | 'Got `push` action from user %s, now unlocking', user) | |
|
2139 | # unlock if we have push from user who locked | |
|
2140 | make_lock = False | |
|
2141 | else: | |
|
2142 | # we're not the same user who locked, ban with | |
|
2143 | # code defined in settings (default is 423 HTTP Locked) ! | |
|
2144 | log.debug('Repo %s is currently locked by %s', repo, user) | |
|
2145 | currently_locked = True | |
|
2146 | elif action == 'pull': | |
|
2147 | # [0] user [1] date | |
|
2148 | if lock_info[0] and lock_info[1]: | |
|
2149 | log.debug('Repo %s is currently locked by %s', repo, user) | |
|
2150 | currently_locked = True | |
|
2151 | else: | |
|
2152 | log.debug('Setting lock on repo %s by %s', repo, user) | |
|
2153 | make_lock = True | |
|
2154 | ||
|
2155 | else: | |
|
2156 | log.debug('Repository %s do not have locking enabled', repo) | |
|
2157 | ||
|
2158 | log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s', | |
|
2159 | make_lock, currently_locked, lock_info) | |
|
2160 | ||
|
2161 | from rhodecode.lib.auth import HasRepoPermissionAny | |
|
2162 | perm_check = HasRepoPermissionAny('repository.write', 'repository.admin') | |
|
2163 | if make_lock and not perm_check(repo_name=repo.repo_name, user=user): | |
|
2164 | # if we don't have at least write permission we cannot make a lock | |
|
2165 | log.debug('lock state reset back to FALSE due to lack ' | |
|
2166 | 'of at least read permission') | |
|
2167 | make_lock = False | |
|
2168 | ||
|
2169 | return make_lock, currently_locked, lock_info | |
|
2170 | ||
|
2171 | @property | |
|
2172 | def last_db_change(self): | |
|
2173 | return self.updated_on | |
|
2174 | ||
|
2175 | @property | |
|
2176 | def clone_uri_hidden(self): | |
|
2177 | clone_uri = self.clone_uri | |
|
2178 | if clone_uri: | |
|
2179 | import urlobject | |
|
2180 | url_obj = urlobject.URLObject(cleaned_uri(clone_uri)) | |
|
2181 | if url_obj.password: | |
|
2182 | clone_uri = url_obj.with_password('*****') | |
|
2183 | return clone_uri | |
|
2184 | ||
|
2185 | @property | |
|
2186 | def push_uri_hidden(self): | |
|
2187 | push_uri = self.push_uri | |
|
2188 | if push_uri: | |
|
2189 | import urlobject | |
|
2190 | url_obj = urlobject.URLObject(cleaned_uri(push_uri)) | |
|
2191 | if url_obj.password: | |
|
2192 | push_uri = url_obj.with_password('*****') | |
|
2193 | return push_uri | |
|
2194 | ||
|
2195 | def clone_url(self, **override): | |
|
2196 | from rhodecode.model.settings import SettingsModel | |
|
2197 | ||
|
2198 | uri_tmpl = None | |
|
2199 | if 'with_id' in override: | |
|
2200 | uri_tmpl = self.DEFAULT_CLONE_URI_ID | |
|
2201 | del override['with_id'] | |
|
2202 | ||
|
2203 | if 'uri_tmpl' in override: | |
|
2204 | uri_tmpl = override['uri_tmpl'] | |
|
2205 | del override['uri_tmpl'] | |
|
2206 | ||
|
2207 | ssh = False | |
|
2208 | if 'ssh' in override: | |
|
2209 | ssh = True | |
|
2210 | del override['ssh'] | |
|
2211 | ||
|
2212 | # we didn't override our tmpl from **overrides | |
|
2213 | if not uri_tmpl: | |
|
2214 | rc_config = SettingsModel().get_all_settings(cache=True) | |
|
2215 | if ssh: | |
|
2216 | uri_tmpl = rc_config.get( | |
|
2217 | 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH | |
|
2218 | else: | |
|
2219 | uri_tmpl = rc_config.get( | |
|
2220 | 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI | |
|
2221 | ||
|
2222 | request = get_current_request() | |
|
2223 | return get_clone_url(request=request, | |
|
2224 | uri_tmpl=uri_tmpl, | |
|
2225 | repo_name=self.repo_name, | |
|
2226 | repo_id=self.repo_id, **override) | |
|
2227 | ||
|
2228 | def set_state(self, state): | |
|
2229 | self.repo_state = state | |
|
2230 | Session().add(self) | |
|
2231 | #========================================================================== | |
|
2232 | # SCM PROPERTIES | |
|
2233 | #========================================================================== | |
|
2234 | ||
|
2235 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): | |
|
2236 | return get_commit_safe( | |
|
2237 | self.scm_instance(), commit_id, commit_idx, pre_load=pre_load) | |
|
2238 | ||
|
2239 | def get_changeset(self, rev=None, pre_load=None): | |
|
2240 | warnings.warn("Use get_commit", DeprecationWarning) | |
|
2241 | commit_id = None | |
|
2242 | commit_idx = None | |
|
2243 | if isinstance(rev, basestring): | |
|
2244 | commit_id = rev | |
|
2245 | else: | |
|
2246 | commit_idx = rev | |
|
2247 | return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, | |
|
2248 | pre_load=pre_load) | |
|
2249 | ||
|
2250 | def get_landing_commit(self): | |
|
2251 | """ | |
|
2252 | Returns landing commit, or if that doesn't exist returns the tip | |
|
2253 | """ | |
|
2254 | _rev_type, _rev = self.landing_rev | |
|
2255 | commit = self.get_commit(_rev) | |
|
2256 | if isinstance(commit, EmptyCommit): | |
|
2257 | return self.get_commit() | |
|
2258 | return commit | |
|
2259 | ||
|
2260 | def update_commit_cache(self, cs_cache=None, config=None): | |
|
2261 | """ | |
|
2262 | Update cache of last changeset for repository, keys should be:: | |
|
2263 | ||
|
2264 | short_id | |
|
2265 | raw_id | |
|
2266 | revision | |
|
2267 | parents | |
|
2268 | message | |
|
2269 | date | |
|
2270 | author | |
|
2271 | ||
|
2272 | :param cs_cache: | |
|
2273 | """ | |
|
2274 | from rhodecode.lib.vcs.backends.base import BaseChangeset | |
|
2275 | if cs_cache is None: | |
|
2276 | # use no-cache version here | |
|
2277 | scm_repo = self.scm_instance(cache=False, config=config) | |
|
2278 | ||
|
2279 | empty = not scm_repo or scm_repo.is_empty() | |
|
2280 | if not empty: | |
|
2281 | cs_cache = scm_repo.get_commit( | |
|
2282 | pre_load=["author", "date", "message", "parents"]) | |
|
2283 | else: | |
|
2284 | cs_cache = EmptyCommit() | |
|
2285 | ||
|
2286 | if isinstance(cs_cache, BaseChangeset): | |
|
2287 | cs_cache = cs_cache.__json__() | |
|
2288 | ||
|
2289 | def is_outdated(new_cs_cache): | |
|
2290 | if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or | |
|
2291 | new_cs_cache['revision'] != self.changeset_cache['revision']): | |
|
2292 | return True | |
|
2293 | return False | |
|
2294 | ||
|
2295 | # check if we have maybe already latest cached revision | |
|
2296 | if is_outdated(cs_cache) or not self.changeset_cache: | |
|
2297 | _default = datetime.datetime.utcnow() | |
|
2298 | last_change = cs_cache.get('date') or _default | |
|
2299 | if self.updated_on and self.updated_on > last_change: | |
|
2300 | # we check if last update is newer than the new value | |
|
2301 | # if yes, we use the current timestamp instead. Imagine you get | |
|
2302 | # old commit pushed 1y ago, we'd set last update 1y to ago. | |
|
2303 | last_change = _default | |
|
2304 | log.debug('updated repo %s with new cs cache %s', | |
|
2305 | self.repo_name, cs_cache) | |
|
2306 | self.updated_on = last_change | |
|
2307 | self.changeset_cache = cs_cache | |
|
2308 | Session().add(self) | |
|
2309 | Session().commit() | |
|
2310 | else: | |
|
2311 | log.debug('Skipping update_commit_cache for repo:`%s` ' | |
|
2312 | 'commit already with latest changes', self.repo_name) | |
|
2313 | ||
|
2314 | @property | |
|
2315 | def tip(self): | |
|
2316 | return self.get_commit('tip') | |
|
2317 | ||
|
2318 | @property | |
|
2319 | def author(self): | |
|
2320 | return self.tip.author | |
|
2321 | ||
|
2322 | @property | |
|
2323 | def last_change(self): | |
|
2324 | return self.scm_instance().last_change | |
|
2325 | ||
|
2326 | def get_comments(self, revisions=None): | |
|
2327 | """ | |
|
2328 | Returns comments for this repository grouped by revisions | |
|
2329 | ||
|
2330 | :param revisions: filter query by revisions only | |
|
2331 | """ | |
|
2332 | cmts = ChangesetComment.query()\ | |
|
2333 | .filter(ChangesetComment.repo == self) | |
|
2334 | if revisions: | |
|
2335 | cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) | |
|
2336 | grouped = collections.defaultdict(list) | |
|
2337 | for cmt in cmts.all(): | |
|
2338 | grouped[cmt.revision].append(cmt) | |
|
2339 | return grouped | |
|
2340 | ||
|
2341 | def statuses(self, revisions=None): | |
|
2342 | """ | |
|
2343 | Returns statuses for this repository | |
|
2344 | ||
|
2345 | :param revisions: list of revisions to get statuses for | |
|
2346 | """ | |
|
2347 | statuses = ChangesetStatus.query()\ | |
|
2348 | .filter(ChangesetStatus.repo == self)\ | |
|
2349 | .filter(ChangesetStatus.version == 0) | |
|
2350 | ||
|
2351 | if revisions: | |
|
2352 | # Try doing the filtering in chunks to avoid hitting limits | |
|
2353 | size = 500 | |
|
2354 | status_results = [] | |
|
2355 | for chunk in xrange(0, len(revisions), size): | |
|
2356 | status_results += statuses.filter( | |
|
2357 | ChangesetStatus.revision.in_( | |
|
2358 | revisions[chunk: chunk+size]) | |
|
2359 | ).all() | |
|
2360 | else: | |
|
2361 | status_results = statuses.all() | |
|
2362 | ||
|
2363 | grouped = {} | |
|
2364 | ||
|
2365 | # maybe we have open new pullrequest without a status? | |
|
2366 | stat = ChangesetStatus.STATUS_UNDER_REVIEW | |
|
2367 | status_lbl = ChangesetStatus.get_status_lbl(stat) | |
|
2368 | for pr in PullRequest.query().filter(PullRequest.source_repo == self).all(): | |
|
2369 | for rev in pr.revisions: | |
|
2370 | pr_id = pr.pull_request_id | |
|
2371 | pr_repo = pr.target_repo.repo_name | |
|
2372 | grouped[rev] = [stat, status_lbl, pr_id, pr_repo] | |
|
2373 | ||
|
2374 | for stat in status_results: | |
|
2375 | pr_id = pr_repo = None | |
|
2376 | if stat.pull_request: | |
|
2377 | pr_id = stat.pull_request.pull_request_id | |
|
2378 | pr_repo = stat.pull_request.target_repo.repo_name | |
|
2379 | grouped[stat.revision] = [str(stat.status), stat.status_lbl, | |
|
2380 | pr_id, pr_repo] | |
|
2381 | return grouped | |
|
2382 | ||
|
2383 | # ========================================================================== | |
|
2384 | # SCM CACHE INSTANCE | |
|
2385 | # ========================================================================== | |
|
2386 | ||
|
2387 | def scm_instance(self, **kwargs): | |
|
2388 | import rhodecode | |
|
2389 | ||
|
2390 | # Passing a config will not hit the cache currently only used | |
|
2391 | # for repo2dbmapper | |
|
2392 | config = kwargs.pop('config', None) | |
|
2393 | cache = kwargs.pop('cache', None) | |
|
2394 | full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache')) | |
|
2395 | # if cache is NOT defined use default global, else we have a full | |
|
2396 | # control over cache behaviour | |
|
2397 | if cache is None and full_cache and not config: | |
|
2398 | return self._get_instance_cached() | |
|
2399 | return self._get_instance(cache=bool(cache), config=config) | |
|
2400 | ||
|
2401 | def _get_instance_cached(self): | |
|
2402 | from rhodecode.lib import rc_cache | |
|
2403 | ||
|
2404 | cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id) | |
|
2405 | invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format( | |
|
2406 | repo_id=self.repo_id) | |
|
2407 | region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid) | |
|
2408 | ||
|
2409 | @region.conditional_cache_on_arguments(namespace=cache_namespace_uid) | |
|
2410 | def get_instance_cached(repo_id, context_id): | |
|
2411 | return self._get_instance() | |
|
2412 | ||
|
2413 | # we must use thread scoped cache here, | |
|
2414 | # because each thread of gevent needs it's own not shared connection and cache | |
|
2415 | # we also alter `args` so the cache key is individual for every green thread. | |
|
2416 | inv_context_manager = rc_cache.InvalidationContext( | |
|
2417 | uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace, | |
|
2418 | thread_scoped=True) | |
|
2419 | with inv_context_manager as invalidation_context: | |
|
2420 | args = (self.repo_id, inv_context_manager.cache_key) | |
|
2421 | # re-compute and store cache if we get invalidate signal | |
|
2422 | if invalidation_context.should_invalidate(): | |
|
2423 | instance = get_instance_cached.refresh(*args) | |
|
2424 | else: | |
|
2425 | instance = get_instance_cached(*args) | |
|
2426 | ||
|
2427 | log.debug( | |
|
2428 | 'Repo instance fetched in %.3fs', inv_context_manager.compute_time) | |
|
2429 | return instance | |
|
2430 | ||
|
2431 | def _get_instance(self, cache=True, config=None): | |
|
2432 | config = config or self._config | |
|
2433 | custom_wire = { | |
|
2434 | 'cache': cache # controls the vcs.remote cache | |
|
2435 | } | |
|
2436 | repo = get_vcs_instance( | |
|
2437 | repo_path=safe_str(self.repo_full_path), | |
|
2438 | config=config, | |
|
2439 | with_wire=custom_wire, | |
|
2440 | create=False, | |
|
2441 | _vcs_alias=self.repo_type) | |
|
2442 | ||
|
2443 | return repo | |
|
2444 | ||
|
2445 | def __json__(self): | |
|
2446 | return {'landing_rev': self.landing_rev} | |
|
2447 | ||
|
2448 | def get_dict(self): | |
|
2449 | ||
|
2450 | # Since we transformed `repo_name` to a hybrid property, we need to | |
|
2451 | # keep compatibility with the code which uses `repo_name` field. | |
|
2452 | ||
|
2453 | result = super(Repository, self).get_dict() | |
|
2454 | result['repo_name'] = result.pop('_repo_name', None) | |
|
2455 | return result | |
|
2456 | ||
|
2457 | ||
|
2458 | class RepoGroup(Base, BaseModel): | |
|
2459 | __tablename__ = 'groups' | |
|
2460 | __table_args__ = ( | |
|
2461 | UniqueConstraint('group_name', 'group_parent_id'), | |
|
2462 | CheckConstraint('group_id != group_parent_id'), | |
|
2463 | base_table_args, | |
|
2464 | ) | |
|
2465 | __mapper_args__ = {'order_by': 'group_name'} | |
|
2466 | ||
|
2467 | CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups | |
|
2468 | ||
|
2469 | group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
2470 | group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) | |
|
2471 | group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) | |
|
2472 | group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None) | |
|
2473 | enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) | |
|
2474 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) | |
|
2475 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
2476 | updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) | |
|
2477 | personal = Column('personal', Boolean(), nullable=True, unique=None, default=None) | |
|
2478 | ||
|
2479 | repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') | |
|
2480 | users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') | |
|
2481 | parent_group = relationship('RepoGroup', remote_side=group_id) | |
|
2482 | user = relationship('User') | |
|
2483 | integrations = relationship('Integration', | |
|
2484 | cascade="all, delete, delete-orphan") | |
|
2485 | ||
|
2486 | def __init__(self, group_name='', parent_group=None): | |
|
2487 | self.group_name = group_name | |
|
2488 | self.parent_group = parent_group | |
|
2489 | ||
|
2490 | def __unicode__(self): | |
|
2491 | return u"<%s('id:%s:%s')>" % ( | |
|
2492 | self.__class__.__name__, self.group_id, self.group_name) | |
|
2493 | ||
|
2494 | @hybrid_property | |
|
2495 | def description_safe(self): | |
|
2496 | from rhodecode.lib import helpers as h | |
|
2497 | return h.escape(self.group_description) | |
|
2498 | ||
|
2499 | @classmethod | |
|
2500 | def _generate_choice(cls, repo_group): | |
|
2501 | from webhelpers.html import literal as _literal | |
|
2502 | _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k)) | |
|
2503 | return repo_group.group_id, _name(repo_group.full_path_splitted) | |
|
2504 | ||
|
2505 | @classmethod | |
|
2506 | def groups_choices(cls, groups=None, show_empty_group=True): | |
|
2507 | if not groups: | |
|
2508 | groups = cls.query().all() | |
|
2509 | ||
|
2510 | repo_groups = [] | |
|
2511 | if show_empty_group: | |
|
2512 | repo_groups = [(-1, u'-- %s --' % _('No parent'))] | |
|
2513 | ||
|
2514 | repo_groups.extend([cls._generate_choice(x) for x in groups]) | |
|
2515 | ||
|
2516 | repo_groups = sorted( | |
|
2517 | repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0]) | |
|
2518 | return repo_groups | |
|
2519 | ||
|
2520 | @classmethod | |
|
2521 | def url_sep(cls): | |
|
2522 | return URL_SEP | |
|
2523 | ||
|
2524 | @classmethod | |
|
2525 | def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): | |
|
2526 | if case_insensitive: | |
|
2527 | gr = cls.query().filter(func.lower(cls.group_name) | |
|
2528 | == func.lower(group_name)) | |
|
2529 | else: | |
|
2530 | gr = cls.query().filter(cls.group_name == group_name) | |
|
2531 | if cache: | |
|
2532 | name_key = _hash_key(group_name) | |
|
2533 | gr = gr.options( | |
|
2534 | FromCache("sql_cache_short", "get_group_%s" % name_key)) | |
|
2535 | return gr.scalar() | |
|
2536 | ||
|
2537 | @classmethod | |
|
2538 | def get_user_personal_repo_group(cls, user_id): | |
|
2539 | user = User.get(user_id) | |
|
2540 | if user.username == User.DEFAULT_USER: | |
|
2541 | return None | |
|
2542 | ||
|
2543 | return cls.query()\ | |
|
2544 | .filter(cls.personal == true()) \ | |
|
2545 | .filter(cls.user == user) \ | |
|
2546 | .order_by(cls.group_id.asc()) \ | |
|
2547 | .first() | |
|
2548 | ||
|
2549 | @classmethod | |
|
2550 | def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), | |
|
2551 | case_insensitive=True): | |
|
2552 | q = RepoGroup.query() | |
|
2553 | ||
|
2554 | if not isinstance(user_id, Optional): | |
|
2555 | q = q.filter(RepoGroup.user_id == user_id) | |
|
2556 | ||
|
2557 | if not isinstance(group_id, Optional): | |
|
2558 | q = q.filter(RepoGroup.group_parent_id == group_id) | |
|
2559 | ||
|
2560 | if case_insensitive: | |
|
2561 | q = q.order_by(func.lower(RepoGroup.group_name)) | |
|
2562 | else: | |
|
2563 | q = q.order_by(RepoGroup.group_name) | |
|
2564 | return q.all() | |
|
2565 | ||
|
2566 | @property | |
|
2567 | def parents(self): | |
|
2568 | parents_recursion_limit = 10 | |
|
2569 | groups = [] | |
|
2570 | if self.parent_group is None: | |
|
2571 | return groups | |
|
2572 | cur_gr = self.parent_group | |
|
2573 | groups.insert(0, cur_gr) | |
|
2574 | cnt = 0 | |
|
2575 | while 1: | |
|
2576 | cnt += 1 | |
|
2577 | gr = getattr(cur_gr, 'parent_group', None) | |
|
2578 | cur_gr = cur_gr.parent_group | |
|
2579 | if gr is None: | |
|
2580 | break | |
|
2581 | if cnt == parents_recursion_limit: | |
|
2582 | # this will prevent accidental infinit loops | |
|
2583 | log.error('more than %s parents found for group %s, stopping ' | |
|
2584 | 'recursive parent fetching', parents_recursion_limit, self) | |
|
2585 | break | |
|
2586 | ||
|
2587 | groups.insert(0, gr) | |
|
2588 | return groups | |
|
2589 | ||
|
2590 | @property | |
|
2591 | def last_db_change(self): | |
|
2592 | return self.updated_on | |
|
2593 | ||
|
2594 | @property | |
|
2595 | def children(self): | |
|
2596 | return RepoGroup.query().filter(RepoGroup.parent_group == self) | |
|
2597 | ||
|
2598 | @property | |
|
2599 | def name(self): | |
|
2600 | return self.group_name.split(RepoGroup.url_sep())[-1] | |
|
2601 | ||
|
2602 | @property | |
|
2603 | def full_path(self): | |
|
2604 | return self.group_name | |
|
2605 | ||
|
2606 | @property | |
|
2607 | def full_path_splitted(self): | |
|
2608 | return self.group_name.split(RepoGroup.url_sep()) | |
|
2609 | ||
|
2610 | @property | |
|
2611 | def repositories(self): | |
|
2612 | return Repository.query()\ | |
|
2613 | .filter(Repository.group == self)\ | |
|
2614 | .order_by(Repository.repo_name) | |
|
2615 | ||
|
2616 | @property | |
|
2617 | def repositories_recursive_count(self): | |
|
2618 | cnt = self.repositories.count() | |
|
2619 | ||
|
2620 | def children_count(group): | |
|
2621 | cnt = 0 | |
|
2622 | for child in group.children: | |
|
2623 | cnt += child.repositories.count() | |
|
2624 | cnt += children_count(child) | |
|
2625 | return cnt | |
|
2626 | ||
|
2627 | return cnt + children_count(self) | |
|
2628 | ||
|
2629 | def _recursive_objects(self, include_repos=True): | |
|
2630 | all_ = [] | |
|
2631 | ||
|
2632 | def _get_members(root_gr): | |
|
2633 | if include_repos: | |
|
2634 | for r in root_gr.repositories: | |
|
2635 | all_.append(r) | |
|
2636 | childs = root_gr.children.all() | |
|
2637 | if childs: | |
|
2638 | for gr in childs: | |
|
2639 | all_.append(gr) | |
|
2640 | _get_members(gr) | |
|
2641 | ||
|
2642 | _get_members(self) | |
|
2643 | return [self] + all_ | |
|
2644 | ||
|
2645 | def recursive_groups_and_repos(self): | |
|
2646 | """ | |
|
2647 | Recursive return all groups, with repositories in those groups | |
|
2648 | """ | |
|
2649 | return self._recursive_objects() | |
|
2650 | ||
|
2651 | def recursive_groups(self): | |
|
2652 | """ | |
|
2653 | Returns all children groups for this group including children of children | |
|
2654 | """ | |
|
2655 | return self._recursive_objects(include_repos=False) | |
|
2656 | ||
|
2657 | def get_new_name(self, group_name): | |
|
2658 | """ | |
|
2659 | returns new full group name based on parent and new name | |
|
2660 | ||
|
2661 | :param group_name: | |
|
2662 | """ | |
|
2663 | path_prefix = (self.parent_group.full_path_splitted if | |
|
2664 | self.parent_group else []) | |
|
2665 | return RepoGroup.url_sep().join(path_prefix + [group_name]) | |
|
2666 | ||
|
2667 | def permissions(self, with_admins=True, with_owner=True, | |
|
2668 | expand_from_user_groups=False): | |
|
2669 | """ | |
|
2670 | Permissions for repository groups | |
|
2671 | """ | |
|
2672 | _admin_perm = 'group.admin' | |
|
2673 | ||
|
2674 | owner_row = [] | |
|
2675 | if with_owner: | |
|
2676 | usr = AttributeDict(self.user.get_dict()) | |
|
2677 | usr.owner_row = True | |
|
2678 | usr.permission = _admin_perm | |
|
2679 | owner_row.append(usr) | |
|
2680 | ||
|
2681 | super_admin_ids = [] | |
|
2682 | super_admin_rows = [] | |
|
2683 | if with_admins: | |
|
2684 | for usr in User.get_all_super_admins(): | |
|
2685 | super_admin_ids.append(usr.user_id) | |
|
2686 | # if this admin is also owner, don't double the record | |
|
2687 | if usr.user_id == owner_row[0].user_id: | |
|
2688 | owner_row[0].admin_row = True | |
|
2689 | else: | |
|
2690 | usr = AttributeDict(usr.get_dict()) | |
|
2691 | usr.admin_row = True | |
|
2692 | usr.permission = _admin_perm | |
|
2693 | super_admin_rows.append(usr) | |
|
2694 | ||
|
2695 | q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self) | |
|
2696 | q = q.options(joinedload(UserRepoGroupToPerm.group), | |
|
2697 | joinedload(UserRepoGroupToPerm.user), | |
|
2698 | joinedload(UserRepoGroupToPerm.permission),) | |
|
2699 | ||
|
2700 | # get owners and admins and permissions. We do a trick of re-writing | |
|
2701 | # objects from sqlalchemy to named-tuples due to sqlalchemy session | |
|
2702 | # has a global reference and changing one object propagates to all | |
|
2703 | # others. This means if admin is also an owner admin_row that change | |
|
2704 | # would propagate to both objects | |
|
2705 | perm_rows = [] | |
|
2706 | for _usr in q.all(): | |
|
2707 | usr = AttributeDict(_usr.user.get_dict()) | |
|
2708 | # if this user is also owner/admin, mark as duplicate record | |
|
2709 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: | |
|
2710 | usr.duplicate_perm = True | |
|
2711 | usr.permission = _usr.permission.permission_name | |
|
2712 | perm_rows.append(usr) | |
|
2713 | ||
|
2714 | # filter the perm rows by 'default' first and then sort them by | |
|
2715 | # admin,write,read,none permissions sorted again alphabetically in | |
|
2716 | # each group | |
|
2717 | perm_rows = sorted(perm_rows, key=display_user_sort) | |
|
2718 | ||
|
2719 | user_groups_rows = [] | |
|
2720 | if expand_from_user_groups: | |
|
2721 | for ug in self.permission_user_groups(with_members=True): | |
|
2722 | for user_data in ug.members: | |
|
2723 | user_groups_rows.append(user_data) | |
|
2724 | ||
|
2725 | return super_admin_rows + owner_row + perm_rows + user_groups_rows | |
|
2726 | ||
|
2727 | def permission_user_groups(self, with_members=False): | |
|
2728 | q = UserGroupRepoGroupToPerm.query()\ | |
|
2729 | .filter(UserGroupRepoGroupToPerm.group == self) | |
|
2730 | q = q.options(joinedload(UserGroupRepoGroupToPerm.group), | |
|
2731 | joinedload(UserGroupRepoGroupToPerm.users_group), | |
|
2732 | joinedload(UserGroupRepoGroupToPerm.permission),) | |
|
2733 | ||
|
2734 | perm_rows = [] | |
|
2735 | for _user_group in q.all(): | |
|
2736 | entry = AttributeDict(_user_group.users_group.get_dict()) | |
|
2737 | entry.permission = _user_group.permission.permission_name | |
|
2738 | if with_members: | |
|
2739 | entry.members = [x.user.get_dict() | |
|
2740 | for x in _user_group.users_group.members] | |
|
2741 | perm_rows.append(entry) | |
|
2742 | ||
|
2743 | perm_rows = sorted(perm_rows, key=display_user_group_sort) | |
|
2744 | return perm_rows | |
|
2745 | ||
|
2746 | def get_api_data(self): | |
|
2747 | """ | |
|
2748 | Common function for generating api data | |
|
2749 | ||
|
2750 | """ | |
|
2751 | group = self | |
|
2752 | data = { | |
|
2753 | 'group_id': group.group_id, | |
|
2754 | 'group_name': group.group_name, | |
|
2755 | 'group_description': group.description_safe, | |
|
2756 | 'parent_group': group.parent_group.group_name if group.parent_group else None, | |
|
2757 | 'repositories': [x.repo_name for x in group.repositories], | |
|
2758 | 'owner': group.user.username, | |
|
2759 | } | |
|
2760 | return data | |
|
2761 | ||
|
2762 | ||
|
2763 | class Permission(Base, BaseModel): | |
|
2764 | __tablename__ = 'permissions' | |
|
2765 | __table_args__ = ( | |
|
2766 | Index('p_perm_name_idx', 'permission_name'), | |
|
2767 | base_table_args, | |
|
2768 | ) | |
|
2769 | ||
|
2770 | PERMS = [ | |
|
2771 | ('hg.admin', _('RhodeCode Super Administrator')), | |
|
2772 | ||
|
2773 | ('repository.none', _('Repository no access')), | |
|
2774 | ('repository.read', _('Repository read access')), | |
|
2775 | ('repository.write', _('Repository write access')), | |
|
2776 | ('repository.admin', _('Repository admin access')), | |
|
2777 | ||
|
2778 | ('group.none', _('Repository group no access')), | |
|
2779 | ('group.read', _('Repository group read access')), | |
|
2780 | ('group.write', _('Repository group write access')), | |
|
2781 | ('group.admin', _('Repository group admin access')), | |
|
2782 | ||
|
2783 | ('usergroup.none', _('User group no access')), | |
|
2784 | ('usergroup.read', _('User group read access')), | |
|
2785 | ('usergroup.write', _('User group write access')), | |
|
2786 | ('usergroup.admin', _('User group admin access')), | |
|
2787 | ||
|
2788 | ('branch.none', _('Branch no permissions')), | |
|
2789 | ('branch.merge', _('Branch access by web merge')), | |
|
2790 | ('branch.push', _('Branch access by push')), | |
|
2791 | ('branch.push_force', _('Branch access by push with force')), | |
|
2792 | ||
|
2793 | ('hg.repogroup.create.false', _('Repository Group creation disabled')), | |
|
2794 | ('hg.repogroup.create.true', _('Repository Group creation enabled')), | |
|
2795 | ||
|
2796 | ('hg.usergroup.create.false', _('User Group creation disabled')), | |
|
2797 | ('hg.usergroup.create.true', _('User Group creation enabled')), | |
|
2798 | ||
|
2799 | ('hg.create.none', _('Repository creation disabled')), | |
|
2800 | ('hg.create.repository', _('Repository creation enabled')), | |
|
2801 | ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')), | |
|
2802 | ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')), | |
|
2803 | ||
|
2804 | ('hg.fork.none', _('Repository forking disabled')), | |
|
2805 | ('hg.fork.repository', _('Repository forking enabled')), | |
|
2806 | ||
|
2807 | ('hg.register.none', _('Registration disabled')), | |
|
2808 | ('hg.register.manual_activate', _('User Registration with manual account activation')), | |
|
2809 | ('hg.register.auto_activate', _('User Registration with automatic account activation')), | |
|
2810 | ||
|
2811 | ('hg.password_reset.enabled', _('Password reset enabled')), | |
|
2812 | ('hg.password_reset.hidden', _('Password reset hidden')), | |
|
2813 | ('hg.password_reset.disabled', _('Password reset disabled')), | |
|
2814 | ||
|
2815 | ('hg.extern_activate.manual', _('Manual activation of external account')), | |
|
2816 | ('hg.extern_activate.auto', _('Automatic activation of external account')), | |
|
2817 | ||
|
2818 | ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')), | |
|
2819 | ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')), | |
|
2820 | ] | |
|
2821 | ||
|
2822 | # definition of system default permissions for DEFAULT user, created on | |
|
2823 | # system setup | |
|
2824 | DEFAULT_USER_PERMISSIONS = [ | |
|
2825 | # object perms | |
|
2826 | 'repository.read', | |
|
2827 | 'group.read', | |
|
2828 | 'usergroup.read', | |
|
2829 | # branch, for backward compat we need same value as before so forced pushed | |
|
2830 | 'branch.push_force', | |
|
2831 | # global | |
|
2832 | 'hg.create.repository', | |
|
2833 | 'hg.repogroup.create.false', | |
|
2834 | 'hg.usergroup.create.false', | |
|
2835 | 'hg.create.write_on_repogroup.true', | |
|
2836 | 'hg.fork.repository', | |
|
2837 | 'hg.register.manual_activate', | |
|
2838 | 'hg.password_reset.enabled', | |
|
2839 | 'hg.extern_activate.auto', | |
|
2840 | 'hg.inherit_default_perms.true', | |
|
2841 | ] | |
|
2842 | ||
|
2843 | # defines which permissions are more important higher the more important | |
|
2844 | # Weight defines which permissions are more important. | |
|
2845 | # The higher number the more important. | |
|
2846 | PERM_WEIGHTS = { | |
|
2847 | 'repository.none': 0, | |
|
2848 | 'repository.read': 1, | |
|
2849 | 'repository.write': 3, | |
|
2850 | 'repository.admin': 4, | |
|
2851 | ||
|
2852 | 'group.none': 0, | |
|
2853 | 'group.read': 1, | |
|
2854 | 'group.write': 3, | |
|
2855 | 'group.admin': 4, | |
|
2856 | ||
|
2857 | 'usergroup.none': 0, | |
|
2858 | 'usergroup.read': 1, | |
|
2859 | 'usergroup.write': 3, | |
|
2860 | 'usergroup.admin': 4, | |
|
2861 | ||
|
2862 | 'branch.none': 0, | |
|
2863 | 'branch.merge': 1, | |
|
2864 | 'branch.push': 3, | |
|
2865 | 'branch.push_force': 4, | |
|
2866 | ||
|
2867 | 'hg.repogroup.create.false': 0, | |
|
2868 | 'hg.repogroup.create.true': 1, | |
|
2869 | ||
|
2870 | 'hg.usergroup.create.false': 0, | |
|
2871 | 'hg.usergroup.create.true': 1, | |
|
2872 | ||
|
2873 | 'hg.fork.none': 0, | |
|
2874 | 'hg.fork.repository': 1, | |
|
2875 | 'hg.create.none': 0, | |
|
2876 | 'hg.create.repository': 1 | |
|
2877 | } | |
|
2878 | ||
|
2879 | permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
2880 | permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None) | |
|
2881 | permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None) | |
|
2882 | ||
|
2883 | def __unicode__(self): | |
|
2884 | return u"<%s('%s:%s')>" % ( | |
|
2885 | self.__class__.__name__, self.permission_id, self.permission_name | |
|
2886 | ) | |
|
2887 | ||
|
2888 | @classmethod | |
|
2889 | def get_by_key(cls, key): | |
|
2890 | return cls.query().filter(cls.permission_name == key).scalar() | |
|
2891 | ||
|
2892 | @classmethod | |
|
2893 | def get_default_repo_perms(cls, user_id, repo_id=None): | |
|
2894 | q = Session().query(UserRepoToPerm, Repository, Permission)\ | |
|
2895 | .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ | |
|
2896 | .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ | |
|
2897 | .filter(UserRepoToPerm.user_id == user_id) | |
|
2898 | if repo_id: | |
|
2899 | q = q.filter(UserRepoToPerm.repository_id == repo_id) | |
|
2900 | return q.all() | |
|
2901 | ||
|
2902 | @classmethod | |
|
2903 | def get_default_repo_branch_perms(cls, user_id, repo_id=None): | |
|
2904 | q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \ | |
|
2905 | .join( | |
|
2906 | Permission, | |
|
2907 | UserToRepoBranchPermission.permission_id == Permission.permission_id) \ | |
|
2908 | .join( | |
|
2909 | UserRepoToPerm, | |
|
2910 | UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \ | |
|
2911 | .filter(UserRepoToPerm.user_id == user_id) | |
|
2912 | ||
|
2913 | if repo_id: | |
|
2914 | q = q.filter(UserToRepoBranchPermission.repository_id == repo_id) | |
|
2915 | return q.order_by(UserToRepoBranchPermission.rule_order).all() | |
|
2916 | ||
|
2917 | @classmethod | |
|
2918 | def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None): | |
|
2919 | q = Session().query(UserGroupRepoToPerm, Repository, Permission)\ | |
|
2920 | .join( | |
|
2921 | Permission, | |
|
2922 | UserGroupRepoToPerm.permission_id == Permission.permission_id)\ | |
|
2923 | .join( | |
|
2924 | Repository, | |
|
2925 | UserGroupRepoToPerm.repository_id == Repository.repo_id)\ | |
|
2926 | .join( | |
|
2927 | UserGroup, | |
|
2928 | UserGroupRepoToPerm.users_group_id == | |
|
2929 | UserGroup.users_group_id)\ | |
|
2930 | .join( | |
|
2931 | UserGroupMember, | |
|
2932 | UserGroupRepoToPerm.users_group_id == | |
|
2933 | UserGroupMember.users_group_id)\ | |
|
2934 | .filter( | |
|
2935 | UserGroupMember.user_id == user_id, | |
|
2936 | UserGroup.users_group_active == true()) | |
|
2937 | if repo_id: | |
|
2938 | q = q.filter(UserGroupRepoToPerm.repository_id == repo_id) | |
|
2939 | return q.all() | |
|
2940 | ||
|
2941 | @classmethod | |
|
2942 | def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None): | |
|
2943 | q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \ | |
|
2944 | .join( | |
|
2945 | Permission, | |
|
2946 | UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \ | |
|
2947 | .join( | |
|
2948 | UserGroupRepoToPerm, | |
|
2949 | UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \ | |
|
2950 | .join( | |
|
2951 | UserGroup, | |
|
2952 | UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \ | |
|
2953 | .join( | |
|
2954 | UserGroupMember, | |
|
2955 | UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \ | |
|
2956 | .filter( | |
|
2957 | UserGroupMember.user_id == user_id, | |
|
2958 | UserGroup.users_group_active == true()) | |
|
2959 | ||
|
2960 | if repo_id: | |
|
2961 | q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id) | |
|
2962 | return q.order_by(UserGroupToRepoBranchPermission.rule_order).all() | |
|
2963 | ||
|
2964 | @classmethod | |
|
2965 | def get_default_group_perms(cls, user_id, repo_group_id=None): | |
|
2966 | q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\ | |
|
2967 | .join( | |
|
2968 | Permission, | |
|
2969 | UserRepoGroupToPerm.permission_id == Permission.permission_id)\ | |
|
2970 | .join( | |
|
2971 | RepoGroup, | |
|
2972 | UserRepoGroupToPerm.group_id == RepoGroup.group_id)\ | |
|
2973 | .filter(UserRepoGroupToPerm.user_id == user_id) | |
|
2974 | if repo_group_id: | |
|
2975 | q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id) | |
|
2976 | return q.all() | |
|
2977 | ||
|
2978 | @classmethod | |
|
2979 | def get_default_group_perms_from_user_group( | |
|
2980 | cls, user_id, repo_group_id=None): | |
|
2981 | q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\ | |
|
2982 | .join( | |
|
2983 | Permission, | |
|
2984 | UserGroupRepoGroupToPerm.permission_id == | |
|
2985 | Permission.permission_id)\ | |
|
2986 | .join( | |
|
2987 | RepoGroup, | |
|
2988 | UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\ | |
|
2989 | .join( | |
|
2990 | UserGroup, | |
|
2991 | UserGroupRepoGroupToPerm.users_group_id == | |
|
2992 | UserGroup.users_group_id)\ | |
|
2993 | .join( | |
|
2994 | UserGroupMember, | |
|
2995 | UserGroupRepoGroupToPerm.users_group_id == | |
|
2996 | UserGroupMember.users_group_id)\ | |
|
2997 | .filter( | |
|
2998 | UserGroupMember.user_id == user_id, | |
|
2999 | UserGroup.users_group_active == true()) | |
|
3000 | if repo_group_id: | |
|
3001 | q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id) | |
|
3002 | return q.all() | |
|
3003 | ||
|
3004 | @classmethod | |
|
3005 | def get_default_user_group_perms(cls, user_id, user_group_id=None): | |
|
3006 | q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\ | |
|
3007 | .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\ | |
|
3008 | .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\ | |
|
3009 | .filter(UserUserGroupToPerm.user_id == user_id) | |
|
3010 | if user_group_id: | |
|
3011 | q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id) | |
|
3012 | return q.all() | |
|
3013 | ||
|
3014 | @classmethod | |
|
3015 | def get_default_user_group_perms_from_user_group( | |
|
3016 | cls, user_id, user_group_id=None): | |
|
3017 | TargetUserGroup = aliased(UserGroup, name='target_user_group') | |
|
3018 | q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\ | |
|
3019 | .join( | |
|
3020 | Permission, | |
|
3021 | UserGroupUserGroupToPerm.permission_id == | |
|
3022 | Permission.permission_id)\ | |
|
3023 | .join( | |
|
3024 | TargetUserGroup, | |
|
3025 | UserGroupUserGroupToPerm.target_user_group_id == | |
|
3026 | TargetUserGroup.users_group_id)\ | |
|
3027 | .join( | |
|
3028 | UserGroup, | |
|
3029 | UserGroupUserGroupToPerm.user_group_id == | |
|
3030 | UserGroup.users_group_id)\ | |
|
3031 | .join( | |
|
3032 | UserGroupMember, | |
|
3033 | UserGroupUserGroupToPerm.user_group_id == | |
|
3034 | UserGroupMember.users_group_id)\ | |
|
3035 | .filter( | |
|
3036 | UserGroupMember.user_id == user_id, | |
|
3037 | UserGroup.users_group_active == true()) | |
|
3038 | if user_group_id: | |
|
3039 | q = q.filter( | |
|
3040 | UserGroupUserGroupToPerm.user_group_id == user_group_id) | |
|
3041 | ||
|
3042 | return q.all() | |
|
3043 | ||
|
3044 | ||
|
3045 | class UserRepoToPerm(Base, BaseModel): | |
|
3046 | __tablename__ = 'repo_to_perm' | |
|
3047 | __table_args__ = ( | |
|
3048 | UniqueConstraint('user_id', 'repository_id', 'permission_id'), | |
|
3049 | base_table_args | |
|
3050 | ) | |
|
3051 | ||
|
3052 | repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3053 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
3054 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3055 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) | |
|
3056 | ||
|
3057 | user = relationship('User') | |
|
3058 | repository = relationship('Repository') | |
|
3059 | permission = relationship('Permission') | |
|
3060 | ||
|
3061 | branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete, delete-orphan", lazy='joined') | |
|
3062 | ||
|
3063 | @classmethod | |
|
3064 | def create(cls, user, repository, permission): | |
|
3065 | n = cls() | |
|
3066 | n.user = user | |
|
3067 | n.repository = repository | |
|
3068 | n.permission = permission | |
|
3069 | Session().add(n) | |
|
3070 | return n | |
|
3071 | ||
|
3072 | def __unicode__(self): | |
|
3073 | return u'<%s => %s >' % (self.user, self.repository) | |
|
3074 | ||
|
3075 | ||
|
3076 | class UserUserGroupToPerm(Base, BaseModel): | |
|
3077 | __tablename__ = 'user_user_group_to_perm' | |
|
3078 | __table_args__ = ( | |
|
3079 | UniqueConstraint('user_id', 'user_group_id', 'permission_id'), | |
|
3080 | base_table_args | |
|
3081 | ) | |
|
3082 | ||
|
3083 | user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3084 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
3085 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3086 | user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3087 | ||
|
3088 | user = relationship('User') | |
|
3089 | user_group = relationship('UserGroup') | |
|
3090 | permission = relationship('Permission') | |
|
3091 | ||
|
3092 | @classmethod | |
|
3093 | def create(cls, user, user_group, permission): | |
|
3094 | n = cls() | |
|
3095 | n.user = user | |
|
3096 | n.user_group = user_group | |
|
3097 | n.permission = permission | |
|
3098 | Session().add(n) | |
|
3099 | return n | |
|
3100 | ||
|
3101 | def __unicode__(self): | |
|
3102 | return u'<%s => %s >' % (self.user, self.user_group) | |
|
3103 | ||
|
3104 | ||
|
3105 | class UserToPerm(Base, BaseModel): | |
|
3106 | __tablename__ = 'user_to_perm' | |
|
3107 | __table_args__ = ( | |
|
3108 | UniqueConstraint('user_id', 'permission_id'), | |
|
3109 | base_table_args | |
|
3110 | ) | |
|
3111 | ||
|
3112 | user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3113 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
3114 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3115 | ||
|
3116 | user = relationship('User') | |
|
3117 | permission = relationship('Permission', lazy='joined') | |
|
3118 | ||
|
3119 | def __unicode__(self): | |
|
3120 | return u'<%s => %s >' % (self.user, self.permission) | |
|
3121 | ||
|
3122 | ||
|
3123 | class UserGroupRepoToPerm(Base, BaseModel): | |
|
3124 | __tablename__ = 'users_group_repo_to_perm' | |
|
3125 | __table_args__ = ( | |
|
3126 | UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), | |
|
3127 | base_table_args | |
|
3128 | ) | |
|
3129 | ||
|
3130 | users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3131 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3132 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3133 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) | |
|
3134 | ||
|
3135 | users_group = relationship('UserGroup') | |
|
3136 | permission = relationship('Permission') | |
|
3137 | repository = relationship('Repository') | |
|
3138 | user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all') | |
|
3139 | ||
|
3140 | @classmethod | |
|
3141 | def create(cls, users_group, repository, permission): | |
|
3142 | n = cls() | |
|
3143 | n.users_group = users_group | |
|
3144 | n.repository = repository | |
|
3145 | n.permission = permission | |
|
3146 | Session().add(n) | |
|
3147 | return n | |
|
3148 | ||
|
3149 | def __unicode__(self): | |
|
3150 | return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository) | |
|
3151 | ||
|
3152 | ||
|
3153 | class UserGroupUserGroupToPerm(Base, BaseModel): | |
|
3154 | __tablename__ = 'user_group_user_group_to_perm' | |
|
3155 | __table_args__ = ( | |
|
3156 | UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'), | |
|
3157 | CheckConstraint('target_user_group_id != user_group_id'), | |
|
3158 | base_table_args | |
|
3159 | ) | |
|
3160 | ||
|
3161 | 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) | |
|
3162 | target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3163 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3164 | user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3165 | ||
|
3166 | target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id') | |
|
3167 | user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id') | |
|
3168 | permission = relationship('Permission') | |
|
3169 | ||
|
3170 | @classmethod | |
|
3171 | def create(cls, target_user_group, user_group, permission): | |
|
3172 | n = cls() | |
|
3173 | n.target_user_group = target_user_group | |
|
3174 | n.user_group = user_group | |
|
3175 | n.permission = permission | |
|
3176 | Session().add(n) | |
|
3177 | return n | |
|
3178 | ||
|
3179 | def __unicode__(self): | |
|
3180 | return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group) | |
|
3181 | ||
|
3182 | ||
|
3183 | class UserGroupToPerm(Base, BaseModel): | |
|
3184 | __tablename__ = 'users_group_to_perm' | |
|
3185 | __table_args__ = ( | |
|
3186 | UniqueConstraint('users_group_id', 'permission_id',), | |
|
3187 | base_table_args | |
|
3188 | ) | |
|
3189 | ||
|
3190 | users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3191 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3192 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3193 | ||
|
3194 | users_group = relationship('UserGroup') | |
|
3195 | permission = relationship('Permission') | |
|
3196 | ||
|
3197 | ||
|
3198 | class UserRepoGroupToPerm(Base, BaseModel): | |
|
3199 | __tablename__ = 'user_repo_group_to_perm' | |
|
3200 | __table_args__ = ( | |
|
3201 | UniqueConstraint('user_id', 'group_id', 'permission_id'), | |
|
3202 | base_table_args | |
|
3203 | ) | |
|
3204 | ||
|
3205 | group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3206 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
3207 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) | |
|
3208 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3209 | ||
|
3210 | user = relationship('User') | |
|
3211 | group = relationship('RepoGroup') | |
|
3212 | permission = relationship('Permission') | |
|
3213 | ||
|
3214 | @classmethod | |
|
3215 | def create(cls, user, repository_group, permission): | |
|
3216 | n = cls() | |
|
3217 | n.user = user | |
|
3218 | n.group = repository_group | |
|
3219 | n.permission = permission | |
|
3220 | Session().add(n) | |
|
3221 | return n | |
|
3222 | ||
|
3223 | ||
|
3224 | class UserGroupRepoGroupToPerm(Base, BaseModel): | |
|
3225 | __tablename__ = 'users_group_repo_group_to_perm' | |
|
3226 | __table_args__ = ( | |
|
3227 | UniqueConstraint('users_group_id', 'group_id'), | |
|
3228 | base_table_args | |
|
3229 | ) | |
|
3230 | ||
|
3231 | 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) | |
|
3232 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) | |
|
3233 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) | |
|
3234 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
3235 | ||
|
3236 | users_group = relationship('UserGroup') | |
|
3237 | permission = relationship('Permission') | |
|
3238 | group = relationship('RepoGroup') | |
|
3239 | ||
|
3240 | @classmethod | |
|
3241 | def create(cls, user_group, repository_group, permission): | |
|
3242 | n = cls() | |
|
3243 | n.users_group = user_group | |
|
3244 | n.group = repository_group | |
|
3245 | n.permission = permission | |
|
3246 | Session().add(n) | |
|
3247 | return n | |
|
3248 | ||
|
3249 | def __unicode__(self): | |
|
3250 | return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group) | |
|
3251 | ||
|
3252 | ||
|
3253 | class Statistics(Base, BaseModel): | |
|
3254 | __tablename__ = 'statistics' | |
|
3255 | __table_args__ = ( | |
|
3256 | base_table_args | |
|
3257 | ) | |
|
3258 | ||
|
3259 | stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3260 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) | |
|
3261 | stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) | |
|
3262 | commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data | |
|
3263 | commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data | |
|
3264 | languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data | |
|
3265 | ||
|
3266 | repository = relationship('Repository', single_parent=True) | |
|
3267 | ||
|
3268 | ||
|
3269 | class UserFollowing(Base, BaseModel): | |
|
3270 | __tablename__ = 'user_followings' | |
|
3271 | __table_args__ = ( | |
|
3272 | UniqueConstraint('user_id', 'follows_repository_id'), | |
|
3273 | UniqueConstraint('user_id', 'follows_user_id'), | |
|
3274 | base_table_args | |
|
3275 | ) | |
|
3276 | ||
|
3277 | user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3278 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
3279 | follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) | |
|
3280 | follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) | |
|
3281 | follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) | |
|
3282 | ||
|
3283 | user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') | |
|
3284 | ||
|
3285 | follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') | |
|
3286 | follows_repository = relationship('Repository', order_by='Repository.repo_name') | |
|
3287 | ||
|
3288 | @classmethod | |
|
3289 | def get_repo_followers(cls, repo_id): | |
|
3290 | return cls.query().filter(cls.follows_repo_id == repo_id) | |
|
3291 | ||
|
3292 | ||
|
3293 | class CacheKey(Base, BaseModel): | |
|
3294 | __tablename__ = 'cache_invalidation' | |
|
3295 | __table_args__ = ( | |
|
3296 | UniqueConstraint('cache_key'), | |
|
3297 | Index('key_idx', 'cache_key'), | |
|
3298 | base_table_args, | |
|
3299 | ) | |
|
3300 | ||
|
3301 | CACHE_TYPE_FEED = 'FEED' | |
|
3302 | CACHE_TYPE_README = 'README' | |
|
3303 | # namespaces used to register process/thread aware caches | |
|
3304 | REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}' | |
|
3305 | SETTINGS_INVALIDATION_NAMESPACE = 'system_settings' | |
|
3306 | ||
|
3307 | cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
3308 | cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None) | |
|
3309 | cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None) | |
|
3310 | cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) | |
|
3311 | ||
|
3312 | def __init__(self, cache_key, cache_args=''): | |
|
3313 | self.cache_key = cache_key | |
|
3314 | self.cache_args = cache_args | |
|
3315 | self.cache_active = False | |
|
3316 | ||
|
3317 | def __unicode__(self): | |
|
3318 | return u"<%s('%s:%s[%s]')>" % ( | |
|
3319 | self.__class__.__name__, | |
|
3320 | self.cache_id, self.cache_key, self.cache_active) | |
|
3321 | ||
|
3322 | def _cache_key_partition(self): | |
|
3323 | prefix, repo_name, suffix = self.cache_key.partition(self.cache_args) | |
|
3324 | return prefix, repo_name, suffix | |
|
3325 | ||
|
3326 | def get_prefix(self): | |
|
3327 | """ | |
|
3328 | Try to extract prefix from existing cache key. The key could consist | |
|
3329 | of prefix, repo_name, suffix | |
|
3330 | """ | |
|
3331 | # this returns prefix, repo_name, suffix | |
|
3332 | return self._cache_key_partition()[0] | |
|
3333 | ||
|
3334 | def get_suffix(self): | |
|
3335 | """ | |
|
3336 | get suffix that might have been used in _get_cache_key to | |
|
3337 | generate self.cache_key. Only used for informational purposes | |
|
3338 | in repo_edit.mako. | |
|
3339 | """ | |
|
3340 | # prefix, repo_name, suffix | |
|
3341 | return self._cache_key_partition()[2] | |
|
3342 | ||
|
3343 | @classmethod | |
|
3344 | def delete_all_cache(cls): | |
|
3345 | """ | |
|
3346 | Delete all cache keys from database. | |
|
3347 | Should only be run when all instances are down and all entries | |
|
3348 | thus stale. | |
|
3349 | """ | |
|
3350 | cls.query().delete() | |
|
3351 | Session().commit() | |
|
3352 | ||
|
3353 | @classmethod | |
|
3354 | def set_invalidate(cls, cache_uid, delete=False): | |
|
3355 | """ | |
|
3356 | Mark all caches of a repo as invalid in the database. | |
|
3357 | """ | |
|
3358 | ||
|
3359 | try: | |
|
3360 | qry = Session().query(cls).filter(cls.cache_args == cache_uid) | |
|
3361 | if delete: | |
|
3362 | qry.delete() | |
|
3363 | log.debug('cache objects deleted for cache args %s', | |
|
3364 | safe_str(cache_uid)) | |
|
3365 | else: | |
|
3366 | qry.update({"cache_active": False}) | |
|
3367 | log.debug('cache objects marked as invalid for cache args %s', | |
|
3368 | safe_str(cache_uid)) | |
|
3369 | ||
|
3370 | Session().commit() | |
|
3371 | except Exception: | |
|
3372 | log.exception( | |
|
3373 | 'Cache key invalidation failed for cache args %s', | |
|
3374 | safe_str(cache_uid)) | |
|
3375 | Session().rollback() | |
|
3376 | ||
|
3377 | @classmethod | |
|
3378 | def get_active_cache(cls, cache_key): | |
|
3379 | inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar() | |
|
3380 | if inv_obj: | |
|
3381 | return inv_obj | |
|
3382 | return None | |
|
3383 | ||
|
3384 | ||
|
3385 | class ChangesetComment(Base, BaseModel): | |
|
3386 | __tablename__ = 'changeset_comments' | |
|
3387 | __table_args__ = ( | |
|
3388 | Index('cc_revision_idx', 'revision'), | |
|
3389 | base_table_args, | |
|
3390 | ) | |
|
3391 | ||
|
3392 | COMMENT_OUTDATED = u'comment_outdated' | |
|
3393 | COMMENT_TYPE_NOTE = u'note' | |
|
3394 | COMMENT_TYPE_TODO = u'todo' | |
|
3395 | COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO] | |
|
3396 | ||
|
3397 | comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) | |
|
3398 | repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) | |
|
3399 | revision = Column('revision', String(40), nullable=True) | |
|
3400 | pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) | |
|
3401 | pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True) | |
|
3402 | line_no = Column('line_no', Unicode(10), nullable=True) | |
|
3403 | hl_lines = Column('hl_lines', Unicode(512), nullable=True) | |
|
3404 | f_path = Column('f_path', Unicode(1000), nullable=True) | |
|
3405 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) | |
|
3406 | text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False) | |
|
3407 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
3408 | modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
3409 | renderer = Column('renderer', Unicode(64), nullable=True) | |
|
3410 | display_state = Column('display_state', Unicode(128), nullable=True) | |
|
3411 | ||
|
3412 | comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE) | |
|
3413 | resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True) | |
|
3414 | ||
|
3415 | resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by') | |
|
3416 | resolved_by = relationship('ChangesetComment', back_populates='resolved_comment') | |
|
3417 | ||
|
3418 | author = relationship('User', lazy='joined') | |
|
3419 | repo = relationship('Repository') | |
|
3420 | status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined') | |
|
3421 | pull_request = relationship('PullRequest', lazy='joined') | |
|
3422 | pull_request_version = relationship('PullRequestVersion') | |
|
3423 | ||
|
3424 | @classmethod | |
|
3425 | def get_users(cls, revision=None, pull_request_id=None): | |
|
3426 | """ | |
|
3427 | Returns user associated with this ChangesetComment. ie those | |
|
3428 | who actually commented | |
|
3429 | ||
|
3430 | :param cls: | |
|
3431 | :param revision: | |
|
3432 | """ | |
|
3433 | q = Session().query(User)\ | |
|
3434 | .join(ChangesetComment.author) | |
|
3435 | if revision: | |
|
3436 | q = q.filter(cls.revision == revision) | |
|
3437 | elif pull_request_id: | |
|
3438 | q = q.filter(cls.pull_request_id == pull_request_id) | |
|
3439 | return q.all() | |
|
3440 | ||
|
3441 | @classmethod | |
|
3442 | def get_index_from_version(cls, pr_version, versions): | |
|
3443 | num_versions = [x.pull_request_version_id for x in versions] | |
|
3444 | try: | |
|
3445 | return num_versions.index(pr_version) +1 | |
|
3446 | except (IndexError, ValueError): | |
|
3447 | return | |
|
3448 | ||
|
3449 | @property | |
|
3450 | def outdated(self): | |
|
3451 | return self.display_state == self.COMMENT_OUTDATED | |
|
3452 | ||
|
3453 | def outdated_at_version(self, version): | |
|
3454 | """ | |
|
3455 | Checks if comment is outdated for given pull request version | |
|
3456 | """ | |
|
3457 | return self.outdated and self.pull_request_version_id != version | |
|
3458 | ||
|
3459 | def older_than_version(self, version): | |
|
3460 | """ | |
|
3461 | Checks if comment is made from previous version than given | |
|
3462 | """ | |
|
3463 | if version is None: | |
|
3464 | return self.pull_request_version_id is not None | |
|
3465 | ||
|
3466 | return self.pull_request_version_id < version | |
|
3467 | ||
|
3468 | @property | |
|
3469 | def resolved(self): | |
|
3470 | return self.resolved_by[0] if self.resolved_by else None | |
|
3471 | ||
|
3472 | @property | |
|
3473 | def is_todo(self): | |
|
3474 | return self.comment_type == self.COMMENT_TYPE_TODO | |
|
3475 | ||
|
3476 | @property | |
|
3477 | def is_inline(self): | |
|
3478 | return self.line_no and self.f_path | |
|
3479 | ||
|
3480 | def get_index_version(self, versions): | |
|
3481 | return self.get_index_from_version( | |
|
3482 | self.pull_request_version_id, versions) | |
|
3483 | ||
|
3484 | def __repr__(self): | |
|
3485 | if self.comment_id: | |
|
3486 | return '<DB:Comment #%s>' % self.comment_id | |
|
3487 | else: | |
|
3488 | return '<DB:Comment at %#x>' % id(self) | |
|
3489 | ||
|
3490 | def get_api_data(self): | |
|
3491 | comment = self | |
|
3492 | data = { | |
|
3493 | 'comment_id': comment.comment_id, | |
|
3494 | 'comment_type': comment.comment_type, | |
|
3495 | 'comment_text': comment.text, | |
|
3496 | 'comment_status': comment.status_change, | |
|
3497 | 'comment_f_path': comment.f_path, | |
|
3498 | 'comment_lineno': comment.line_no, | |
|
3499 | 'comment_author': comment.author, | |
|
3500 | 'comment_created_on': comment.created_on | |
|
3501 | } | |
|
3502 | return data | |
|
3503 | ||
|
3504 | def __json__(self): | |
|
3505 | data = dict() | |
|
3506 | data.update(self.get_api_data()) | |
|
3507 | return data | |
|
3508 | ||
|
3509 | ||
|
3510 | class ChangesetStatus(Base, BaseModel): | |
|
3511 | __tablename__ = 'changeset_statuses' | |
|
3512 | __table_args__ = ( | |
|
3513 | Index('cs_revision_idx', 'revision'), | |
|
3514 | Index('cs_version_idx', 'version'), | |
|
3515 | UniqueConstraint('repo_id', 'revision', 'version'), | |
|
3516 | base_table_args | |
|
3517 | ) | |
|
3518 | ||
|
3519 | STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed' | |
|
3520 | STATUS_APPROVED = 'approved' | |
|
3521 | STATUS_REJECTED = 'rejected' | |
|
3522 | STATUS_UNDER_REVIEW = 'under_review' | |
|
3523 | ||
|
3524 | STATUSES = [ | |
|
3525 | (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default | |
|
3526 | (STATUS_APPROVED, _("Approved")), | |
|
3527 | (STATUS_REJECTED, _("Rejected")), | |
|
3528 | (STATUS_UNDER_REVIEW, _("Under Review")), | |
|
3529 | ] | |
|
3530 | ||
|
3531 | changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True) | |
|
3532 | repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) | |
|
3533 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) | |
|
3534 | revision = Column('revision', String(40), nullable=False) | |
|
3535 | status = Column('status', String(128), nullable=False, default=DEFAULT) | |
|
3536 | changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id')) | |
|
3537 | modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) | |
|
3538 | version = Column('version', Integer(), nullable=False, default=0) | |
|
3539 | pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) | |
|
3540 | ||
|
3541 | author = relationship('User', lazy='joined') | |
|
3542 | repo = relationship('Repository') | |
|
3543 | comment = relationship('ChangesetComment', lazy='joined') | |
|
3544 | pull_request = relationship('PullRequest', lazy='joined') | |
|
3545 | ||
|
3546 | def __unicode__(self): | |
|
3547 | return u"<%s('%s[v%s]:%s')>" % ( | |
|
3548 | self.__class__.__name__, | |
|
3549 | self.status, self.version, self.author | |
|
3550 | ) | |
|
3551 | ||
|
3552 | @classmethod | |
|
3553 | def get_status_lbl(cls, value): | |
|
3554 | return dict(cls.STATUSES).get(value) | |
|
3555 | ||
|
3556 | @property | |
|
3557 | def status_lbl(self): | |
|
3558 | return ChangesetStatus.get_status_lbl(self.status) | |
|
3559 | ||
|
3560 | def get_api_data(self): | |
|
3561 | status = self | |
|
3562 | data = { | |
|
3563 | 'status_id': status.changeset_status_id, | |
|
3564 | 'status': status.status, | |
|
3565 | } | |
|
3566 | return data | |
|
3567 | ||
|
3568 | def __json__(self): | |
|
3569 | data = dict() | |
|
3570 | data.update(self.get_api_data()) | |
|
3571 | return data | |
|
3572 | ||
|
3573 | ||
|
3574 | class _SetState(object): | |
|
3575 | """ | |
|
3576 | Context processor allowing changing state for sensitive operation such as | |
|
3577 | pull request update or merge | |
|
3578 | """ | |
|
3579 | ||
|
3580 | def __init__(self, pull_request, pr_state, back_state=None): | |
|
3581 | self._pr = pull_request | |
|
3582 | self._org_state = back_state or pull_request.pull_request_state | |
|
3583 | self._pr_state = pr_state | |
|
3584 | ||
|
3585 | def __enter__(self): | |
|
3586 | log.debug('StateLock: entering set state context, setting state to: `%s`', | |
|
3587 | self._pr_state) | |
|
3588 | self._pr.pull_request_state = self._pr_state | |
|
3589 | Session().add(self._pr) | |
|
3590 | Session().commit() | |
|
3591 | ||
|
3592 | def __exit__(self, exc_type, exc_val, exc_tb): | |
|
3593 | log.debug('StateLock: exiting set state context, setting state to: `%s`', | |
|
3594 | self._org_state) | |
|
3595 | self._pr.pull_request_state = self._org_state | |
|
3596 | Session().add(self._pr) | |
|
3597 | Session().commit() | |
|
3598 | ||
|
3599 | ||
|
3600 | class _PullRequestBase(BaseModel): | |
|
3601 | """ | |
|
3602 | Common attributes of pull request and version entries. | |
|
3603 | """ | |
|
3604 | ||
|
3605 | # .status values | |
|
3606 | STATUS_NEW = u'new' | |
|
3607 | STATUS_OPEN = u'open' | |
|
3608 | STATUS_CLOSED = u'closed' | |
|
3609 | ||
|
3610 | # available states | |
|
3611 | STATE_CREATING = u'creating' | |
|
3612 | STATE_UPDATING = u'updating' | |
|
3613 | STATE_MERGING = u'merging' | |
|
3614 | STATE_CREATED = u'created' | |
|
3615 | ||
|
3616 | title = Column('title', Unicode(255), nullable=True) | |
|
3617 | description = Column( | |
|
3618 | 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), | |
|
3619 | nullable=True) | |
|
3620 | description_renderer = Column('description_renderer', Unicode(64), nullable=True) | |
|
3621 | ||
|
3622 | # new/open/closed status of pull request (not approve/reject/etc) | |
|
3623 | status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW) | |
|
3624 | created_on = Column( | |
|
3625 | 'created_on', DateTime(timezone=False), nullable=False, | |
|
3626 | default=datetime.datetime.now) | |
|
3627 | updated_on = Column( | |
|
3628 | 'updated_on', DateTime(timezone=False), nullable=False, | |
|
3629 | default=datetime.datetime.now) | |
|
3630 | ||
|
3631 | pull_request_state = Column("pull_request_state", String(255), nullable=True) | |
|
3632 | ||
|
3633 | @declared_attr | |
|
3634 | def user_id(cls): | |
|
3635 | return Column( | |
|
3636 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, | |
|
3637 | unique=None) | |
|
3638 | ||
|
3639 | # 500 revisions max | |
|
3640 | _revisions = Column( | |
|
3641 | 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) | |
|
3642 | ||
|
3643 | @declared_attr | |
|
3644 | def source_repo_id(cls): | |
|
3645 | # TODO: dan: rename column to source_repo_id | |
|
3646 | return Column( | |
|
3647 | 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'), | |
|
3648 | nullable=False) | |
|
3649 | ||
|
3650 | _source_ref = Column('org_ref', Unicode(255), nullable=False) | |
|
3651 | ||
|
3652 | @hybrid_property | |
|
3653 | def source_ref(self): | |
|
3654 | return self._source_ref | |
|
3655 | ||
|
3656 | @source_ref.setter | |
|
3657 | def source_ref(self, val): | |
|
3658 | parts = (val or '').split(':') | |
|
3659 | if len(parts) != 3: | |
|
3660 | raise ValueError( | |
|
3661 | 'Invalid reference format given: {}, expected X:Y:Z'.format(val)) | |
|
3662 | self._source_ref = safe_unicode(val) | |
|
3663 | ||
|
3664 | _target_ref = Column('other_ref', Unicode(255), nullable=False) | |
|
3665 | ||
|
3666 | @hybrid_property | |
|
3667 | def target_ref(self): | |
|
3668 | return self._target_ref | |
|
3669 | ||
|
3670 | @target_ref.setter | |
|
3671 | def target_ref(self, val): | |
|
3672 | parts = (val or '').split(':') | |
|
3673 | if len(parts) != 3: | |
|
3674 | raise ValueError( | |
|
3675 | 'Invalid reference format given: {}, expected X:Y:Z'.format(val)) | |
|
3676 | self._target_ref = safe_unicode(val) | |
|
3677 | ||
|
3678 | @declared_attr | |
|
3679 | def target_repo_id(cls): | |
|
3680 | # TODO: dan: rename column to target_repo_id | |
|
3681 | return Column( | |
|
3682 | 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'), | |
|
3683 | nullable=False) | |
|
3684 | ||
|
3685 | _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True) | |
|
3686 | ||
|
3687 | # TODO: dan: rename column to last_merge_source_rev | |
|
3688 | _last_merge_source_rev = Column( | |
|
3689 | 'last_merge_org_rev', String(40), nullable=True) | |
|
3690 | # TODO: dan: rename column to last_merge_target_rev | |
|
3691 | _last_merge_target_rev = Column( | |
|
3692 | 'last_merge_other_rev', String(40), nullable=True) | |
|
3693 | _last_merge_status = Column('merge_status', Integer(), nullable=True) | |
|
3694 | merge_rev = Column('merge_rev', String(40), nullable=True) | |
|
3695 | ||
|
3696 | reviewer_data = Column( | |
|
3697 | 'reviewer_data_json', MutationObj.as_mutable( | |
|
3698 | JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) | |
|
3699 | ||
|
3700 | @property | |
|
3701 | def reviewer_data_json(self): | |
|
3702 | return json.dumps(self.reviewer_data) | |
|
3703 | ||
|
3704 | @hybrid_property | |
|
3705 | def description_safe(self): | |
|
3706 | from rhodecode.lib import helpers as h | |
|
3707 | return h.escape(self.description) | |
|
3708 | ||
|
3709 | @hybrid_property | |
|
3710 | def revisions(self): | |
|
3711 | return self._revisions.split(':') if self._revisions else [] | |
|
3712 | ||
|
3713 | @revisions.setter | |
|
3714 | def revisions(self, val): | |
|
3715 | self._revisions = ':'.join(val) | |
|
3716 | ||
|
3717 | @hybrid_property | |
|
3718 | def last_merge_status(self): | |
|
3719 | return safe_int(self._last_merge_status) | |
|
3720 | ||
|
3721 | @last_merge_status.setter | |
|
3722 | def last_merge_status(self, val): | |
|
3723 | self._last_merge_status = val | |
|
3724 | ||
|
3725 | @declared_attr | |
|
3726 | def author(cls): | |
|
3727 | return relationship('User', lazy='joined') | |
|
3728 | ||
|
3729 | @declared_attr | |
|
3730 | def source_repo(cls): | |
|
3731 | return relationship( | |
|
3732 | 'Repository', | |
|
3733 | primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__) | |
|
3734 | ||
|
3735 | @property | |
|
3736 | def source_ref_parts(self): | |
|
3737 | return self.unicode_to_reference(self.source_ref) | |
|
3738 | ||
|
3739 | @declared_attr | |
|
3740 | def target_repo(cls): | |
|
3741 | return relationship( | |
|
3742 | 'Repository', | |
|
3743 | primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__) | |
|
3744 | ||
|
3745 | @property | |
|
3746 | def target_ref_parts(self): | |
|
3747 | return self.unicode_to_reference(self.target_ref) | |
|
3748 | ||
|
3749 | @property | |
|
3750 | def shadow_merge_ref(self): | |
|
3751 | return self.unicode_to_reference(self._shadow_merge_ref) | |
|
3752 | ||
|
3753 | @shadow_merge_ref.setter | |
|
3754 | def shadow_merge_ref(self, ref): | |
|
3755 | self._shadow_merge_ref = self.reference_to_unicode(ref) | |
|
3756 | ||
|
3757 | @staticmethod | |
|
3758 | def unicode_to_reference(raw): | |
|
3759 | """ | |
|
3760 | Convert a unicode (or string) to a reference object. | |
|
3761 | If unicode evaluates to False it returns None. | |
|
3762 | """ | |
|
3763 | if raw: | |
|
3764 | refs = raw.split(':') | |
|
3765 | return Reference(*refs) | |
|
3766 | else: | |
|
3767 | return None | |
|
3768 | ||
|
3769 | @staticmethod | |
|
3770 | def reference_to_unicode(ref): | |
|
3771 | """ | |
|
3772 | Convert a reference object to unicode. | |
|
3773 | If reference is None it returns None. | |
|
3774 | """ | |
|
3775 | if ref: | |
|
3776 | return u':'.join(ref) | |
|
3777 | else: | |
|
3778 | return None | |
|
3779 | ||
|
3780 | def get_api_data(self, with_merge_state=True): | |
|
3781 | from rhodecode.model.pull_request import PullRequestModel | |
|
3782 | ||
|
3783 | pull_request = self | |
|
3784 | if with_merge_state: | |
|
3785 | merge_status = PullRequestModel().merge_status(pull_request) | |
|
3786 | merge_state = { | |
|
3787 | 'status': merge_status[0], | |
|
3788 | 'message': safe_unicode(merge_status[1]), | |
|
3789 | } | |
|
3790 | else: | |
|
3791 | merge_state = {'status': 'not_available', | |
|
3792 | 'message': 'not_available'} | |
|
3793 | ||
|
3794 | merge_data = { | |
|
3795 | 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request), | |
|
3796 | 'reference': ( | |
|
3797 | pull_request.shadow_merge_ref._asdict() | |
|
3798 | if pull_request.shadow_merge_ref else None), | |
|
3799 | } | |
|
3800 | ||
|
3801 | data = { | |
|
3802 | 'pull_request_id': pull_request.pull_request_id, | |
|
3803 | 'url': PullRequestModel().get_url(pull_request), | |
|
3804 | 'title': pull_request.title, | |
|
3805 | 'description': pull_request.description, | |
|
3806 | 'status': pull_request.status, | |
|
3807 | 'state': pull_request.pull_request_state, | |
|
3808 | 'created_on': pull_request.created_on, | |
|
3809 | 'updated_on': pull_request.updated_on, | |
|
3810 | 'commit_ids': pull_request.revisions, | |
|
3811 | 'review_status': pull_request.calculated_review_status(), | |
|
3812 | 'mergeable': merge_state, | |
|
3813 | 'source': { | |
|
3814 | 'clone_url': pull_request.source_repo.clone_url(), | |
|
3815 | 'repository': pull_request.source_repo.repo_name, | |
|
3816 | 'reference': { | |
|
3817 | 'name': pull_request.source_ref_parts.name, | |
|
3818 | 'type': pull_request.source_ref_parts.type, | |
|
3819 | 'commit_id': pull_request.source_ref_parts.commit_id, | |
|
3820 | }, | |
|
3821 | }, | |
|
3822 | 'target': { | |
|
3823 | 'clone_url': pull_request.target_repo.clone_url(), | |
|
3824 | 'repository': pull_request.target_repo.repo_name, | |
|
3825 | 'reference': { | |
|
3826 | 'name': pull_request.target_ref_parts.name, | |
|
3827 | 'type': pull_request.target_ref_parts.type, | |
|
3828 | 'commit_id': pull_request.target_ref_parts.commit_id, | |
|
3829 | }, | |
|
3830 | }, | |
|
3831 | 'merge': merge_data, | |
|
3832 | 'author': pull_request.author.get_api_data(include_secrets=False, | |
|
3833 | details='basic'), | |
|
3834 | 'reviewers': [ | |
|
3835 | { | |
|
3836 | 'user': reviewer.get_api_data(include_secrets=False, | |
|
3837 | details='basic'), | |
|
3838 | 'reasons': reasons, | |
|
3839 | 'review_status': st[0][1].status if st else 'not_reviewed', | |
|
3840 | } | |
|
3841 | for obj, reviewer, reasons, mandatory, st in | |
|
3842 | pull_request.reviewers_statuses() | |
|
3843 | ] | |
|
3844 | } | |
|
3845 | ||
|
3846 | return data | |
|
3847 | ||
|
3848 | def set_state(self, pull_request_state, final_state=None): | |
|
3849 | """ | |
|
3850 | # goes from initial state to updating to initial state. | |
|
3851 | # initial state can be changed by specifying back_state= | |
|
3852 | with pull_request_obj.set_state(PullRequest.STATE_UPDATING): | |
|
3853 | pull_request.merge() | |
|
3854 | ||
|
3855 | :param pull_request_state: | |
|
3856 | :param final_state: | |
|
3857 | ||
|
3858 | """ | |
|
3859 | ||
|
3860 | return _SetState(self, pull_request_state, back_state=final_state) | |
|
3861 | ||
|
3862 | ||
|
3863 | class PullRequest(Base, _PullRequestBase): | |
|
3864 | __tablename__ = 'pull_requests' | |
|
3865 | __table_args__ = ( | |
|
3866 | base_table_args, | |
|
3867 | ) | |
|
3868 | ||
|
3869 | pull_request_id = Column( | |
|
3870 | 'pull_request_id', Integer(), nullable=False, primary_key=True) | |
|
3871 | ||
|
3872 | def __repr__(self): | |
|
3873 | if self.pull_request_id: | |
|
3874 | return '<DB:PullRequest #%s>' % self.pull_request_id | |
|
3875 | else: | |
|
3876 | return '<DB:PullRequest at %#x>' % id(self) | |
|
3877 | ||
|
3878 | reviewers = relationship('PullRequestReviewers', | |
|
3879 | cascade="all, delete, delete-orphan") | |
|
3880 | statuses = relationship('ChangesetStatus', | |
|
3881 | cascade="all, delete, delete-orphan") | |
|
3882 | comments = relationship('ChangesetComment', | |
|
3883 | cascade="all, delete, delete-orphan") | |
|
3884 | versions = relationship('PullRequestVersion', | |
|
3885 | cascade="all, delete, delete-orphan", | |
|
3886 | lazy='dynamic') | |
|
3887 | ||
|
3888 | @classmethod | |
|
3889 | def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj, | |
|
3890 | internal_methods=None): | |
|
3891 | ||
|
3892 | class PullRequestDisplay(object): | |
|
3893 | """ | |
|
3894 | Special object wrapper for showing PullRequest data via Versions | |
|
3895 | It mimics PR object as close as possible. This is read only object | |
|
3896 | just for display | |
|
3897 | """ | |
|
3898 | ||
|
3899 | def __init__(self, attrs, internal=None): | |
|
3900 | self.attrs = attrs | |
|
3901 | # internal have priority over the given ones via attrs | |
|
3902 | self.internal = internal or ['versions'] | |
|
3903 | ||
|
3904 | def __getattr__(self, item): | |
|
3905 | if item in self.internal: | |
|
3906 | return getattr(self, item) | |
|
3907 | try: | |
|
3908 | return self.attrs[item] | |
|
3909 | except KeyError: | |
|
3910 | raise AttributeError( | |
|
3911 | '%s object has no attribute %s' % (self, item)) | |
|
3912 | ||
|
3913 | def __repr__(self): | |
|
3914 | return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id') | |
|
3915 | ||
|
3916 | def versions(self): | |
|
3917 | return pull_request_obj.versions.order_by( | |
|
3918 | PullRequestVersion.pull_request_version_id).all() | |
|
3919 | ||
|
3920 | def is_closed(self): | |
|
3921 | return pull_request_obj.is_closed() | |
|
3922 | ||
|
3923 | @property | |
|
3924 | def pull_request_version_id(self): | |
|
3925 | return getattr(pull_request_obj, 'pull_request_version_id', None) | |
|
3926 | ||
|
3927 | attrs = StrictAttributeDict(pull_request_obj.get_api_data()) | |
|
3928 | ||
|
3929 | attrs.author = StrictAttributeDict( | |
|
3930 | pull_request_obj.author.get_api_data()) | |
|
3931 | if pull_request_obj.target_repo: | |
|
3932 | attrs.target_repo = StrictAttributeDict( | |
|
3933 | pull_request_obj.target_repo.get_api_data()) | |
|
3934 | attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url | |
|
3935 | ||
|
3936 | if pull_request_obj.source_repo: | |
|
3937 | attrs.source_repo = StrictAttributeDict( | |
|
3938 | pull_request_obj.source_repo.get_api_data()) | |
|
3939 | attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url | |
|
3940 | ||
|
3941 | attrs.source_ref_parts = pull_request_obj.source_ref_parts | |
|
3942 | attrs.target_ref_parts = pull_request_obj.target_ref_parts | |
|
3943 | attrs.revisions = pull_request_obj.revisions | |
|
3944 | ||
|
3945 | attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref | |
|
3946 | attrs.reviewer_data = org_pull_request_obj.reviewer_data | |
|
3947 | attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json | |
|
3948 | ||
|
3949 | return PullRequestDisplay(attrs, internal=internal_methods) | |
|
3950 | ||
|
3951 | def is_closed(self): | |
|
3952 | return self.status == self.STATUS_CLOSED | |
|
3953 | ||
|
3954 | def __json__(self): | |
|
3955 | return { | |
|
3956 | 'revisions': self.revisions, | |
|
3957 | } | |
|
3958 | ||
|
3959 | def calculated_review_status(self): | |
|
3960 | from rhodecode.model.changeset_status import ChangesetStatusModel | |
|
3961 | return ChangesetStatusModel().calculated_review_status(self) | |
|
3962 | ||
|
3963 | def reviewers_statuses(self): | |
|
3964 | from rhodecode.model.changeset_status import ChangesetStatusModel | |
|
3965 | return ChangesetStatusModel().reviewers_statuses(self) | |
|
3966 | ||
|
3967 | @property | |
|
3968 | def workspace_id(self): | |
|
3969 | from rhodecode.model.pull_request import PullRequestModel | |
|
3970 | return PullRequestModel()._workspace_id(self) | |
|
3971 | ||
|
3972 | def get_shadow_repo(self): | |
|
3973 | workspace_id = self.workspace_id | |
|
3974 | vcs_obj = self.target_repo.scm_instance() | |
|
3975 | shadow_repository_path = vcs_obj._get_shadow_repository_path( | |
|
3976 | self.target_repo.repo_id, workspace_id) | |
|
3977 | if os.path.isdir(shadow_repository_path): | |
|
3978 | return vcs_obj._get_shadow_instance(shadow_repository_path) | |
|
3979 | ||
|
3980 | ||
|
3981 | class PullRequestVersion(Base, _PullRequestBase): | |
|
3982 | __tablename__ = 'pull_request_versions' | |
|
3983 | __table_args__ = ( | |
|
3984 | base_table_args, | |
|
3985 | ) | |
|
3986 | ||
|
3987 | pull_request_version_id = Column( | |
|
3988 | 'pull_request_version_id', Integer(), nullable=False, primary_key=True) | |
|
3989 | pull_request_id = Column( | |
|
3990 | 'pull_request_id', Integer(), | |
|
3991 | ForeignKey('pull_requests.pull_request_id'), nullable=False) | |
|
3992 | pull_request = relationship('PullRequest') | |
|
3993 | ||
|
3994 | def __repr__(self): | |
|
3995 | if self.pull_request_version_id: | |
|
3996 | return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id | |
|
3997 | else: | |
|
3998 | return '<DB:PullRequestVersion at %#x>' % id(self) | |
|
3999 | ||
|
4000 | @property | |
|
4001 | def reviewers(self): | |
|
4002 | return self.pull_request.reviewers | |
|
4003 | ||
|
4004 | @property | |
|
4005 | def versions(self): | |
|
4006 | return self.pull_request.versions | |
|
4007 | ||
|
4008 | def is_closed(self): | |
|
4009 | # calculate from original | |
|
4010 | return self.pull_request.status == self.STATUS_CLOSED | |
|
4011 | ||
|
4012 | def calculated_review_status(self): | |
|
4013 | return self.pull_request.calculated_review_status() | |
|
4014 | ||
|
4015 | def reviewers_statuses(self): | |
|
4016 | return self.pull_request.reviewers_statuses() | |
|
4017 | ||
|
4018 | ||
|
4019 | class PullRequestReviewers(Base, BaseModel): | |
|
4020 | __tablename__ = 'pull_request_reviewers' | |
|
4021 | __table_args__ = ( | |
|
4022 | base_table_args, | |
|
4023 | ) | |
|
4024 | ||
|
4025 | @hybrid_property | |
|
4026 | def reasons(self): | |
|
4027 | if not self._reasons: | |
|
4028 | return [] | |
|
4029 | return self._reasons | |
|
4030 | ||
|
4031 | @reasons.setter | |
|
4032 | def reasons(self, val): | |
|
4033 | val = val or [] | |
|
4034 | if any(not isinstance(x, basestring) for x in val): | |
|
4035 | raise Exception('invalid reasons type, must be list of strings') | |
|
4036 | self._reasons = val | |
|
4037 | ||
|
4038 | pull_requests_reviewers_id = Column( | |
|
4039 | 'pull_requests_reviewers_id', Integer(), nullable=False, | |
|
4040 | primary_key=True) | |
|
4041 | pull_request_id = Column( | |
|
4042 | "pull_request_id", Integer(), | |
|
4043 | ForeignKey('pull_requests.pull_request_id'), nullable=False) | |
|
4044 | user_id = Column( | |
|
4045 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=True) | |
|
4046 | _reasons = Column( | |
|
4047 | 'reason', MutationList.as_mutable( | |
|
4048 | JsonType('list', dialect_map=dict(mysql=UnicodeText(16384))))) | |
|
4049 | ||
|
4050 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | |
|
4051 | user = relationship('User') | |
|
4052 | pull_request = relationship('PullRequest') | |
|
4053 | ||
|
4054 | rule_data = Column( | |
|
4055 | 'rule_data_json', | |
|
4056 | JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) | |
|
4057 | ||
|
4058 | def rule_user_group_data(self): | |
|
4059 | """ | |
|
4060 | Returns the voting user group rule data for this reviewer | |
|
4061 | """ | |
|
4062 | ||
|
4063 | if self.rule_data and 'vote_rule' in self.rule_data: | |
|
4064 | user_group_data = {} | |
|
4065 | if 'rule_user_group_entry_id' in self.rule_data: | |
|
4066 | # means a group with voting rules ! | |
|
4067 | user_group_data['id'] = self.rule_data['rule_user_group_entry_id'] | |
|
4068 | user_group_data['name'] = self.rule_data['rule_name'] | |
|
4069 | user_group_data['vote_rule'] = self.rule_data['vote_rule'] | |
|
4070 | ||
|
4071 | return user_group_data | |
|
4072 | ||
|
4073 | def __unicode__(self): | |
|
4074 | return u"<%s('id:%s')>" % (self.__class__.__name__, | |
|
4075 | self.pull_requests_reviewers_id) | |
|
4076 | ||
|
4077 | ||
|
4078 | class Notification(Base, BaseModel): | |
|
4079 | __tablename__ = 'notifications' | |
|
4080 | __table_args__ = ( | |
|
4081 | Index('notification_type_idx', 'type'), | |
|
4082 | base_table_args, | |
|
4083 | ) | |
|
4084 | ||
|
4085 | TYPE_CHANGESET_COMMENT = u'cs_comment' | |
|
4086 | TYPE_MESSAGE = u'message' | |
|
4087 | TYPE_MENTION = u'mention' | |
|
4088 | TYPE_REGISTRATION = u'registration' | |
|
4089 | TYPE_PULL_REQUEST = u'pull_request' | |
|
4090 | TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' | |
|
4091 | ||
|
4092 | notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) | |
|
4093 | subject = Column('subject', Unicode(512), nullable=True) | |
|
4094 | body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True) | |
|
4095 | created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) | |
|
4096 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4097 | type_ = Column('type', Unicode(255)) | |
|
4098 | ||
|
4099 | created_by_user = relationship('User') | |
|
4100 | notifications_to_users = relationship('UserNotification', lazy='joined', | |
|
4101 | cascade="all, delete, delete-orphan") | |
|
4102 | ||
|
4103 | @property | |
|
4104 | def recipients(self): | |
|
4105 | return [x.user for x in UserNotification.query()\ | |
|
4106 | .filter(UserNotification.notification == self)\ | |
|
4107 | .order_by(UserNotification.user_id.asc()).all()] | |
|
4108 | ||
|
4109 | @classmethod | |
|
4110 | def create(cls, created_by, subject, body, recipients, type_=None): | |
|
4111 | if type_ is None: | |
|
4112 | type_ = Notification.TYPE_MESSAGE | |
|
4113 | ||
|
4114 | notification = cls() | |
|
4115 | notification.created_by_user = created_by | |
|
4116 | notification.subject = subject | |
|
4117 | notification.body = body | |
|
4118 | notification.type_ = type_ | |
|
4119 | notification.created_on = datetime.datetime.now() | |
|
4120 | ||
|
4121 | # For each recipient link the created notification to his account | |
|
4122 | for u in recipients: | |
|
4123 | assoc = UserNotification() | |
|
4124 | assoc.user_id = u.user_id | |
|
4125 | assoc.notification = notification | |
|
4126 | ||
|
4127 | # if created_by is inside recipients mark his notification | |
|
4128 | # as read | |
|
4129 | if u.user_id == created_by.user_id: | |
|
4130 | assoc.read = True | |
|
4131 | Session().add(assoc) | |
|
4132 | ||
|
4133 | Session().add(notification) | |
|
4134 | ||
|
4135 | return notification | |
|
4136 | ||
|
4137 | ||
|
4138 | class UserNotification(Base, BaseModel): | |
|
4139 | __tablename__ = 'user_to_notification' | |
|
4140 | __table_args__ = ( | |
|
4141 | UniqueConstraint('user_id', 'notification_id'), | |
|
4142 | base_table_args | |
|
4143 | ) | |
|
4144 | ||
|
4145 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) | |
|
4146 | notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) | |
|
4147 | read = Column('read', Boolean, default=False) | |
|
4148 | sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) | |
|
4149 | ||
|
4150 | user = relationship('User', lazy="joined") | |
|
4151 | notification = relationship('Notification', lazy="joined", | |
|
4152 | order_by=lambda: Notification.created_on.desc(),) | |
|
4153 | ||
|
4154 | def mark_as_read(self): | |
|
4155 | self.read = True | |
|
4156 | Session().add(self) | |
|
4157 | ||
|
4158 | ||
|
4159 | class Gist(Base, BaseModel): | |
|
4160 | __tablename__ = 'gists' | |
|
4161 | __table_args__ = ( | |
|
4162 | Index('g_gist_access_id_idx', 'gist_access_id'), | |
|
4163 | Index('g_created_on_idx', 'created_on'), | |
|
4164 | base_table_args | |
|
4165 | ) | |
|
4166 | ||
|
4167 | GIST_PUBLIC = u'public' | |
|
4168 | GIST_PRIVATE = u'private' | |
|
4169 | DEFAULT_FILENAME = u'gistfile1.txt' | |
|
4170 | ||
|
4171 | ACL_LEVEL_PUBLIC = u'acl_public' | |
|
4172 | ACL_LEVEL_PRIVATE = u'acl_private' | |
|
4173 | ||
|
4174 | gist_id = Column('gist_id', Integer(), primary_key=True) | |
|
4175 | gist_access_id = Column('gist_access_id', Unicode(250)) | |
|
4176 | gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) | |
|
4177 | gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True) | |
|
4178 | gist_expires = Column('gist_expires', Float(53), nullable=False) | |
|
4179 | gist_type = Column('gist_type', Unicode(128), nullable=False) | |
|
4180 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4181 | modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4182 | acl_level = Column('acl_level', Unicode(128), nullable=True) | |
|
4183 | ||
|
4184 | owner = relationship('User') | |
|
4185 | ||
|
4186 | def __repr__(self): | |
|
4187 | return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id) | |
|
4188 | ||
|
4189 | @hybrid_property | |
|
4190 | def description_safe(self): | |
|
4191 | from rhodecode.lib import helpers as h | |
|
4192 | return h.escape(self.gist_description) | |
|
4193 | ||
|
4194 | @classmethod | |
|
4195 | def get_or_404(cls, id_): | |
|
4196 | from pyramid.httpexceptions import HTTPNotFound | |
|
4197 | ||
|
4198 | res = cls.query().filter(cls.gist_access_id == id_).scalar() | |
|
4199 | if not res: | |
|
4200 | raise HTTPNotFound() | |
|
4201 | return res | |
|
4202 | ||
|
4203 | @classmethod | |
|
4204 | def get_by_access_id(cls, gist_access_id): | |
|
4205 | return cls.query().filter(cls.gist_access_id == gist_access_id).scalar() | |
|
4206 | ||
|
4207 | def gist_url(self): | |
|
4208 | from rhodecode.model.gist import GistModel | |
|
4209 | return GistModel().get_url(self) | |
|
4210 | ||
|
4211 | @classmethod | |
|
4212 | def base_path(cls): | |
|
4213 | """ | |
|
4214 | Returns base path when all gists are stored | |
|
4215 | ||
|
4216 | :param cls: | |
|
4217 | """ | |
|
4218 | from rhodecode.model.gist import GIST_STORE_LOC | |
|
4219 | q = Session().query(RhodeCodeUi)\ | |
|
4220 | .filter(RhodeCodeUi.ui_key == URL_SEP) | |
|
4221 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) | |
|
4222 | return os.path.join(q.one().ui_value, GIST_STORE_LOC) | |
|
4223 | ||
|
4224 | def get_api_data(self): | |
|
4225 | """ | |
|
4226 | Common function for generating gist related data for API | |
|
4227 | """ | |
|
4228 | gist = self | |
|
4229 | data = { | |
|
4230 | 'gist_id': gist.gist_id, | |
|
4231 | 'type': gist.gist_type, | |
|
4232 | 'access_id': gist.gist_access_id, | |
|
4233 | 'description': gist.gist_description, | |
|
4234 | 'url': gist.gist_url(), | |
|
4235 | 'expires': gist.gist_expires, | |
|
4236 | 'created_on': gist.created_on, | |
|
4237 | 'modified_at': gist.modified_at, | |
|
4238 | 'content': None, | |
|
4239 | 'acl_level': gist.acl_level, | |
|
4240 | } | |
|
4241 | return data | |
|
4242 | ||
|
4243 | def __json__(self): | |
|
4244 | data = dict( | |
|
4245 | ) | |
|
4246 | data.update(self.get_api_data()) | |
|
4247 | return data | |
|
4248 | # SCM functions | |
|
4249 | ||
|
4250 | def scm_instance(self, **kwargs): | |
|
4251 | full_repo_path = os.path.join(self.base_path(), self.gist_access_id) | |
|
4252 | return get_vcs_instance( | |
|
4253 | repo_path=safe_str(full_repo_path), create=False) | |
|
4254 | ||
|
4255 | ||
|
4256 | class ExternalIdentity(Base, BaseModel): | |
|
4257 | __tablename__ = 'external_identities' | |
|
4258 | __table_args__ = ( | |
|
4259 | Index('local_user_id_idx', 'local_user_id'), | |
|
4260 | Index('external_id_idx', 'external_id'), | |
|
4261 | base_table_args | |
|
4262 | ) | |
|
4263 | ||
|
4264 | external_id = Column('external_id', Unicode(255), default=u'', primary_key=True) | |
|
4265 | external_username = Column('external_username', Unicode(1024), default=u'') | |
|
4266 | local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) | |
|
4267 | provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True) | |
|
4268 | access_token = Column('access_token', String(1024), default=u'') | |
|
4269 | alt_token = Column('alt_token', String(1024), default=u'') | |
|
4270 | token_secret = Column('token_secret', String(1024), default=u'') | |
|
4271 | ||
|
4272 | @classmethod | |
|
4273 | def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None): | |
|
4274 | """ | |
|
4275 | Returns ExternalIdentity instance based on search params | |
|
4276 | ||
|
4277 | :param external_id: | |
|
4278 | :param provider_name: | |
|
4279 | :return: ExternalIdentity | |
|
4280 | """ | |
|
4281 | query = cls.query() | |
|
4282 | query = query.filter(cls.external_id == external_id) | |
|
4283 | query = query.filter(cls.provider_name == provider_name) | |
|
4284 | if local_user_id: | |
|
4285 | query = query.filter(cls.local_user_id == local_user_id) | |
|
4286 | return query.first() | |
|
4287 | ||
|
4288 | @classmethod | |
|
4289 | def user_by_external_id_and_provider(cls, external_id, provider_name): | |
|
4290 | """ | |
|
4291 | Returns User instance based on search params | |
|
4292 | ||
|
4293 | :param external_id: | |
|
4294 | :param provider_name: | |
|
4295 | :return: User | |
|
4296 | """ | |
|
4297 | query = User.query() | |
|
4298 | query = query.filter(cls.external_id == external_id) | |
|
4299 | query = query.filter(cls.provider_name == provider_name) | |
|
4300 | query = query.filter(User.user_id == cls.local_user_id) | |
|
4301 | return query.first() | |
|
4302 | ||
|
4303 | @classmethod | |
|
4304 | def by_local_user_id(cls, local_user_id): | |
|
4305 | """ | |
|
4306 | Returns all tokens for user | |
|
4307 | ||
|
4308 | :param local_user_id: | |
|
4309 | :return: ExternalIdentity | |
|
4310 | """ | |
|
4311 | query = cls.query() | |
|
4312 | query = query.filter(cls.local_user_id == local_user_id) | |
|
4313 | return query | |
|
4314 | ||
|
4315 | @classmethod | |
|
4316 | def load_provider_plugin(cls, plugin_id): | |
|
4317 | from rhodecode.authentication.base import loadplugin | |
|
4318 | _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id) | |
|
4319 | auth_plugin = loadplugin(_plugin_id) | |
|
4320 | return auth_plugin | |
|
4321 | ||
|
4322 | ||
|
4323 | class Integration(Base, BaseModel): | |
|
4324 | __tablename__ = 'integrations' | |
|
4325 | __table_args__ = ( | |
|
4326 | base_table_args | |
|
4327 | ) | |
|
4328 | ||
|
4329 | integration_id = Column('integration_id', Integer(), primary_key=True) | |
|
4330 | integration_type = Column('integration_type', String(255)) | |
|
4331 | enabled = Column('enabled', Boolean(), nullable=False) | |
|
4332 | name = Column('name', String(255), nullable=False) | |
|
4333 | child_repos_only = Column('child_repos_only', Boolean(), nullable=False, | |
|
4334 | default=False) | |
|
4335 | ||
|
4336 | settings = Column( | |
|
4337 | 'settings_json', MutationObj.as_mutable( | |
|
4338 | JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) | |
|
4339 | repo_id = Column( | |
|
4340 | 'repo_id', Integer(), ForeignKey('repositories.repo_id'), | |
|
4341 | nullable=True, unique=None, default=None) | |
|
4342 | repo = relationship('Repository', lazy='joined') | |
|
4343 | ||
|
4344 | repo_group_id = Column( | |
|
4345 | 'repo_group_id', Integer(), ForeignKey('groups.group_id'), | |
|
4346 | nullable=True, unique=None, default=None) | |
|
4347 | repo_group = relationship('RepoGroup', lazy='joined') | |
|
4348 | ||
|
4349 | @property | |
|
4350 | def scope(self): | |
|
4351 | if self.repo: | |
|
4352 | return repr(self.repo) | |
|
4353 | if self.repo_group: | |
|
4354 | if self.child_repos_only: | |
|
4355 | return repr(self.repo_group) + ' (child repos only)' | |
|
4356 | else: | |
|
4357 | return repr(self.repo_group) + ' (recursive)' | |
|
4358 | if self.child_repos_only: | |
|
4359 | return 'root_repos' | |
|
4360 | return 'global' | |
|
4361 | ||
|
4362 | def __repr__(self): | |
|
4363 | return '<Integration(%r, %r)>' % (self.integration_type, self.scope) | |
|
4364 | ||
|
4365 | ||
|
4366 | class RepoReviewRuleUser(Base, BaseModel): | |
|
4367 | __tablename__ = 'repo_review_rules_users' | |
|
4368 | __table_args__ = ( | |
|
4369 | base_table_args | |
|
4370 | ) | |
|
4371 | ||
|
4372 | repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True) | |
|
4373 | repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) | |
|
4374 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False) | |
|
4375 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | |
|
4376 | user = relationship('User') | |
|
4377 | ||
|
4378 | def rule_data(self): | |
|
4379 | return { | |
|
4380 | 'mandatory': self.mandatory | |
|
4381 | } | |
|
4382 | ||
|
4383 | ||
|
4384 | class RepoReviewRuleUserGroup(Base, BaseModel): | |
|
4385 | __tablename__ = 'repo_review_rules_users_groups' | |
|
4386 | __table_args__ = ( | |
|
4387 | base_table_args | |
|
4388 | ) | |
|
4389 | ||
|
4390 | VOTE_RULE_ALL = -1 | |
|
4391 | ||
|
4392 | repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True) | |
|
4393 | repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) | |
|
4394 | users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False) | |
|
4395 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) | |
|
4396 | vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL) | |
|
4397 | users_group = relationship('UserGroup') | |
|
4398 | ||
|
4399 | def rule_data(self): | |
|
4400 | return { | |
|
4401 | 'mandatory': self.mandatory, | |
|
4402 | 'vote_rule': self.vote_rule | |
|
4403 | } | |
|
4404 | ||
|
4405 | @property | |
|
4406 | def vote_rule_label(self): | |
|
4407 | if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL: | |
|
4408 | return 'all must vote' | |
|
4409 | else: | |
|
4410 | return 'min. vote {}'.format(self.vote_rule) | |
|
4411 | ||
|
4412 | ||
|
4413 | class RepoReviewRule(Base, BaseModel): | |
|
4414 | __tablename__ = 'repo_review_rules' | |
|
4415 | __table_args__ = ( | |
|
4416 | base_table_args | |
|
4417 | ) | |
|
4418 | ||
|
4419 | repo_review_rule_id = Column( | |
|
4420 | 'repo_review_rule_id', Integer(), primary_key=True) | |
|
4421 | repo_id = Column( | |
|
4422 | "repo_id", Integer(), ForeignKey('repositories.repo_id')) | |
|
4423 | repo = relationship('Repository', backref='review_rules') | |
|
4424 | ||
|
4425 | review_rule_name = Column('review_rule_name', String(255)) | |
|
4426 | _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob | |
|
4427 | _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob | |
|
4428 | _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob | |
|
4429 | ||
|
4430 | use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False) | |
|
4431 | forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False) | |
|
4432 | forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False) | |
|
4433 | forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False) | |
|
4434 | ||
|
4435 | rule_users = relationship('RepoReviewRuleUser') | |
|
4436 | rule_user_groups = relationship('RepoReviewRuleUserGroup') | |
|
4437 | ||
|
4438 | def _validate_pattern(self, value): | |
|
4439 | re.compile('^' + glob2re(value) + '$') | |
|
4440 | ||
|
4441 | @hybrid_property | |
|
4442 | def source_branch_pattern(self): | |
|
4443 | return self._branch_pattern or '*' | |
|
4444 | ||
|
4445 | @source_branch_pattern.setter | |
|
4446 | def source_branch_pattern(self, value): | |
|
4447 | self._validate_pattern(value) | |
|
4448 | self._branch_pattern = value or '*' | |
|
4449 | ||
|
4450 | @hybrid_property | |
|
4451 | def target_branch_pattern(self): | |
|
4452 | return self._target_branch_pattern or '*' | |
|
4453 | ||
|
4454 | @target_branch_pattern.setter | |
|
4455 | def target_branch_pattern(self, value): | |
|
4456 | self._validate_pattern(value) | |
|
4457 | self._target_branch_pattern = value or '*' | |
|
4458 | ||
|
4459 | @hybrid_property | |
|
4460 | def file_pattern(self): | |
|
4461 | return self._file_pattern or '*' | |
|
4462 | ||
|
4463 | @file_pattern.setter | |
|
4464 | def file_pattern(self, value): | |
|
4465 | self._validate_pattern(value) | |
|
4466 | self._file_pattern = value or '*' | |
|
4467 | ||
|
4468 | def matches(self, source_branch, target_branch, files_changed): | |
|
4469 | """ | |
|
4470 | Check if this review rule matches a branch/files in a pull request | |
|
4471 | ||
|
4472 | :param source_branch: source branch name for the commit | |
|
4473 | :param target_branch: target branch name for the commit | |
|
4474 | :param files_changed: list of file paths changed in the pull request | |
|
4475 | """ | |
|
4476 | ||
|
4477 | source_branch = source_branch or '' | |
|
4478 | target_branch = target_branch or '' | |
|
4479 | files_changed = files_changed or [] | |
|
4480 | ||
|
4481 | branch_matches = True | |
|
4482 | if source_branch or target_branch: | |
|
4483 | if self.source_branch_pattern == '*': | |
|
4484 | source_branch_match = True | |
|
4485 | else: | |
|
4486 | if self.source_branch_pattern.startswith('re:'): | |
|
4487 | source_pattern = self.source_branch_pattern[3:] | |
|
4488 | else: | |
|
4489 | source_pattern = '^' + glob2re(self.source_branch_pattern) + '$' | |
|
4490 | source_branch_regex = re.compile(source_pattern) | |
|
4491 | source_branch_match = bool(source_branch_regex.search(source_branch)) | |
|
4492 | if self.target_branch_pattern == '*': | |
|
4493 | target_branch_match = True | |
|
4494 | else: | |
|
4495 | if self.target_branch_pattern.startswith('re:'): | |
|
4496 | target_pattern = self.target_branch_pattern[3:] | |
|
4497 | else: | |
|
4498 | target_pattern = '^' + glob2re(self.target_branch_pattern) + '$' | |
|
4499 | target_branch_regex = re.compile(target_pattern) | |
|
4500 | target_branch_match = bool(target_branch_regex.search(target_branch)) | |
|
4501 | ||
|
4502 | branch_matches = source_branch_match and target_branch_match | |
|
4503 | ||
|
4504 | files_matches = True | |
|
4505 | if self.file_pattern != '*': | |
|
4506 | files_matches = False | |
|
4507 | if self.file_pattern.startswith('re:'): | |
|
4508 | file_pattern = self.file_pattern[3:] | |
|
4509 | else: | |
|
4510 | file_pattern = glob2re(self.file_pattern) | |
|
4511 | file_regex = re.compile(file_pattern) | |
|
4512 | for filename in files_changed: | |
|
4513 | if file_regex.search(filename): | |
|
4514 | files_matches = True | |
|
4515 | break | |
|
4516 | ||
|
4517 | return branch_matches and files_matches | |
|
4518 | ||
|
4519 | @property | |
|
4520 | def review_users(self): | |
|
4521 | """ Returns the users which this rule applies to """ | |
|
4522 | ||
|
4523 | users = collections.OrderedDict() | |
|
4524 | ||
|
4525 | for rule_user in self.rule_users: | |
|
4526 | if rule_user.user.active: | |
|
4527 | if rule_user.user not in users: | |
|
4528 | users[rule_user.user.username] = { | |
|
4529 | 'user': rule_user.user, | |
|
4530 | 'source': 'user', | |
|
4531 | 'source_data': {}, | |
|
4532 | 'data': rule_user.rule_data() | |
|
4533 | } | |
|
4534 | ||
|
4535 | for rule_user_group in self.rule_user_groups: | |
|
4536 | source_data = { | |
|
4537 | 'user_group_id': rule_user_group.users_group.users_group_id, | |
|
4538 | 'name': rule_user_group.users_group.users_group_name, | |
|
4539 | 'members': len(rule_user_group.users_group.members) | |
|
4540 | } | |
|
4541 | for member in rule_user_group.users_group.members: | |
|
4542 | if member.user.active: | |
|
4543 | key = member.user.username | |
|
4544 | if key in users: | |
|
4545 | # skip this member as we have him already | |
|
4546 | # this prevents from override the "first" matched | |
|
4547 | # users with duplicates in multiple groups | |
|
4548 | continue | |
|
4549 | ||
|
4550 | users[key] = { | |
|
4551 | 'user': member.user, | |
|
4552 | 'source': 'user_group', | |
|
4553 | 'source_data': source_data, | |
|
4554 | 'data': rule_user_group.rule_data() | |
|
4555 | } | |
|
4556 | ||
|
4557 | return users | |
|
4558 | ||
|
4559 | def user_group_vote_rule(self, user_id): | |
|
4560 | ||
|
4561 | rules = [] | |
|
4562 | if not self.rule_user_groups: | |
|
4563 | return rules | |
|
4564 | ||
|
4565 | for user_group in self.rule_user_groups: | |
|
4566 | user_group_members = [x.user_id for x in user_group.users_group.members] | |
|
4567 | if user_id in user_group_members: | |
|
4568 | rules.append(user_group) | |
|
4569 | return rules | |
|
4570 | ||
|
4571 | def __repr__(self): | |
|
4572 | return '<RepoReviewerRule(id=%r, repo=%r)>' % ( | |
|
4573 | self.repo_review_rule_id, self.repo) | |
|
4574 | ||
|
4575 | ||
|
4576 | class ScheduleEntry(Base, BaseModel): | |
|
4577 | __tablename__ = 'schedule_entries' | |
|
4578 | __table_args__ = ( | |
|
4579 | UniqueConstraint('schedule_name', name='s_schedule_name_idx'), | |
|
4580 | UniqueConstraint('task_uid', name='s_task_uid_idx'), | |
|
4581 | base_table_args, | |
|
4582 | ) | |
|
4583 | ||
|
4584 | schedule_types = ['crontab', 'timedelta', 'integer'] | |
|
4585 | schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True) | |
|
4586 | ||
|
4587 | schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None) | |
|
4588 | schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None) | |
|
4589 | schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True) | |
|
4590 | ||
|
4591 | _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None) | |
|
4592 | schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT())))) | |
|
4593 | ||
|
4594 | schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None) | |
|
4595 | schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0) | |
|
4596 | ||
|
4597 | # task | |
|
4598 | task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None) | |
|
4599 | task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None) | |
|
4600 | task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT())))) | |
|
4601 | task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT())))) | |
|
4602 | ||
|
4603 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4604 | updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None) | |
|
4605 | ||
|
4606 | @hybrid_property | |
|
4607 | def schedule_type(self): | |
|
4608 | return self._schedule_type | |
|
4609 | ||
|
4610 | @schedule_type.setter | |
|
4611 | def schedule_type(self, val): | |
|
4612 | if val not in self.schedule_types: | |
|
4613 | raise ValueError('Value must be on of `{}` and got `{}`'.format( | |
|
4614 | val, self.schedule_type)) | |
|
4615 | ||
|
4616 | self._schedule_type = val | |
|
4617 | ||
|
4618 | @classmethod | |
|
4619 | def get_uid(cls, obj): | |
|
4620 | args = obj.task_args | |
|
4621 | kwargs = obj.task_kwargs | |
|
4622 | if isinstance(args, JsonRaw): | |
|
4623 | try: | |
|
4624 | args = json.loads(args) | |
|
4625 | except ValueError: | |
|
4626 | args = tuple() | |
|
4627 | ||
|
4628 | if isinstance(kwargs, JsonRaw): | |
|
4629 | try: | |
|
4630 | kwargs = json.loads(kwargs) | |
|
4631 | except ValueError: | |
|
4632 | kwargs = dict() | |
|
4633 | ||
|
4634 | dot_notation = obj.task_dot_notation | |
|
4635 | val = '.'.join(map(safe_str, [ | |
|
4636 | sorted(dot_notation), args, sorted(kwargs.items())])) | |
|
4637 | return hashlib.sha1(val).hexdigest() | |
|
4638 | ||
|
4639 | @classmethod | |
|
4640 | def get_by_schedule_name(cls, schedule_name): | |
|
4641 | return cls.query().filter(cls.schedule_name == schedule_name).scalar() | |
|
4642 | ||
|
4643 | @classmethod | |
|
4644 | def get_by_schedule_id(cls, schedule_id): | |
|
4645 | return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar() | |
|
4646 | ||
|
4647 | @property | |
|
4648 | def task(self): | |
|
4649 | return self.task_dot_notation | |
|
4650 | ||
|
4651 | @property | |
|
4652 | def schedule(self): | |
|
4653 | from rhodecode.lib.celerylib.utils import raw_2_schedule | |
|
4654 | schedule = raw_2_schedule(self.schedule_definition, self.schedule_type) | |
|
4655 | return schedule | |
|
4656 | ||
|
4657 | @property | |
|
4658 | def args(self): | |
|
4659 | try: | |
|
4660 | return list(self.task_args or []) | |
|
4661 | except ValueError: | |
|
4662 | return list() | |
|
4663 | ||
|
4664 | @property | |
|
4665 | def kwargs(self): | |
|
4666 | try: | |
|
4667 | return dict(self.task_kwargs or {}) | |
|
4668 | except ValueError: | |
|
4669 | return dict() | |
|
4670 | ||
|
4671 | def _as_raw(self, val): | |
|
4672 | if hasattr(val, 'de_coerce'): | |
|
4673 | val = val.de_coerce() | |
|
4674 | if val: | |
|
4675 | val = json.dumps(val) | |
|
4676 | ||
|
4677 | return val | |
|
4678 | ||
|
4679 | @property | |
|
4680 | def schedule_definition_raw(self): | |
|
4681 | return self._as_raw(self.schedule_definition) | |
|
4682 | ||
|
4683 | @property | |
|
4684 | def args_raw(self): | |
|
4685 | return self._as_raw(self.task_args) | |
|
4686 | ||
|
4687 | @property | |
|
4688 | def kwargs_raw(self): | |
|
4689 | return self._as_raw(self.task_kwargs) | |
|
4690 | ||
|
4691 | def __repr__(self): | |
|
4692 | return '<DB:ScheduleEntry({}:{})>'.format( | |
|
4693 | self.schedule_entry_id, self.schedule_name) | |
|
4694 | ||
|
4695 | ||
|
4696 | @event.listens_for(ScheduleEntry, 'before_update') | |
|
4697 | def update_task_uid(mapper, connection, target): | |
|
4698 | target.task_uid = ScheduleEntry.get_uid(target) | |
|
4699 | ||
|
4700 | ||
|
4701 | @event.listens_for(ScheduleEntry, 'before_insert') | |
|
4702 | def set_task_uid(mapper, connection, target): | |
|
4703 | target.task_uid = ScheduleEntry.get_uid(target) | |
|
4704 | ||
|
4705 | ||
|
4706 | class _BaseBranchPerms(BaseModel): | |
|
4707 | @classmethod | |
|
4708 | def compute_hash(cls, value): | |
|
4709 | return sha1_safe(value) | |
|
4710 | ||
|
4711 | @hybrid_property | |
|
4712 | def branch_pattern(self): | |
|
4713 | return self._branch_pattern or '*' | |
|
4714 | ||
|
4715 | @hybrid_property | |
|
4716 | def branch_hash(self): | |
|
4717 | return self._branch_hash | |
|
4718 | ||
|
4719 | def _validate_glob(self, value): | |
|
4720 | re.compile('^' + glob2re(value) + '$') | |
|
4721 | ||
|
4722 | @branch_pattern.setter | |
|
4723 | def branch_pattern(self, value): | |
|
4724 | self._validate_glob(value) | |
|
4725 | self._branch_pattern = value or '*' | |
|
4726 | # set the Hash when setting the branch pattern | |
|
4727 | self._branch_hash = self.compute_hash(self._branch_pattern) | |
|
4728 | ||
|
4729 | def matches(self, branch): | |
|
4730 | """ | |
|
4731 | Check if this the branch matches entry | |
|
4732 | ||
|
4733 | :param branch: branch name for the commit | |
|
4734 | """ | |
|
4735 | ||
|
4736 | branch = branch or '' | |
|
4737 | ||
|
4738 | branch_matches = True | |
|
4739 | if branch: | |
|
4740 | branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$') | |
|
4741 | branch_matches = bool(branch_regex.search(branch)) | |
|
4742 | ||
|
4743 | return branch_matches | |
|
4744 | ||
|
4745 | ||
|
4746 | class UserToRepoBranchPermission(Base, _BaseBranchPerms): | |
|
4747 | __tablename__ = 'user_to_repo_branch_permissions' | |
|
4748 | __table_args__ = ( | |
|
4749 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
|
4750 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} | |
|
4751 | ) | |
|
4752 | ||
|
4753 | branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True) | |
|
4754 | ||
|
4755 | repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) | |
|
4756 | repo = relationship('Repository', backref='user_branch_perms') | |
|
4757 | ||
|
4758 | permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
4759 | permission = relationship('Permission') | |
|
4760 | ||
|
4761 | rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None) | |
|
4762 | user_repo_to_perm = relationship('UserRepoToPerm') | |
|
4763 | ||
|
4764 | rule_order = Column('rule_order', Integer(), nullable=False) | |
|
4765 | _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob | |
|
4766 | _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql')) | |
|
4767 | ||
|
4768 | def __unicode__(self): | |
|
4769 | return u'<UserBranchPermission(%s => %r)>' % ( | |
|
4770 | self.user_repo_to_perm, self.branch_pattern) | |
|
4771 | ||
|
4772 | ||
|
4773 | class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms): | |
|
4774 | __tablename__ = 'user_group_to_repo_branch_permissions' | |
|
4775 | __table_args__ = ( | |
|
4776 | {'extend_existing': True, 'mysql_engine': 'InnoDB', | |
|
4777 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} | |
|
4778 | ) | |
|
4779 | ||
|
4780 | branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True) | |
|
4781 | ||
|
4782 | repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) | |
|
4783 | repo = relationship('Repository', backref='user_group_branch_perms') | |
|
4784 | ||
|
4785 | permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) | |
|
4786 | permission = relationship('Permission') | |
|
4787 | ||
|
4788 | rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None) | |
|
4789 | user_group_repo_to_perm = relationship('UserGroupRepoToPerm') | |
|
4790 | ||
|
4791 | rule_order = Column('rule_order', Integer(), nullable=False) | |
|
4792 | _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob | |
|
4793 | _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql')) | |
|
4794 | ||
|
4795 | def __unicode__(self): | |
|
4796 | return u'<UserBranchPermission(%s => %r)>' % ( | |
|
4797 | self.user_group_repo_to_perm, self.branch_pattern) | |
|
4798 | ||
|
4799 | ||
|
4800 | class UserBookmark(Base, BaseModel): | |
|
4801 | __tablename__ = 'user_bookmarks' | |
|
4802 | __table_args__ = ( | |
|
4803 | UniqueConstraint('user_id', 'bookmark_repo_id'), | |
|
4804 | UniqueConstraint('user_id', 'bookmark_repo_group_id'), | |
|
4805 | UniqueConstraint('user_id', 'bookmark_position'), | |
|
4806 | base_table_args | |
|
4807 | ) | |
|
4808 | ||
|
4809 | user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
4810 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
4811 | position = Column("bookmark_position", Integer(), nullable=False) | |
|
4812 | title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None) | |
|
4813 | redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None) | |
|
4814 | created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4815 | ||
|
4816 | bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None) | |
|
4817 | bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None) | |
|
4818 | ||
|
4819 | user = relationship("User") | |
|
4820 | ||
|
4821 | repository = relationship("Repository") | |
|
4822 | repository_group = relationship("RepoGroup") | |
|
4823 | ||
|
4824 | ||
|
4825 | class DbMigrateVersion(Base, BaseModel): | |
|
4826 | __tablename__ = 'db_migrate_version' | |
|
4827 | __table_args__ = ( | |
|
4828 | base_table_args, | |
|
4829 | ) | |
|
4830 | ||
|
4831 | repository_id = Column('repository_id', String(250), primary_key=True) | |
|
4832 | repository_path = Column('repository_path', Text) | |
|
4833 | version = Column('version', Integer) | |
|
4834 | ||
|
4835 | @classmethod | |
|
4836 | def set_version(cls, version): | |
|
4837 | """ | |
|
4838 | Helper for forcing a different version, usually for debugging purposes via ishell. | |
|
4839 | """ | |
|
4840 | ver = DbMigrateVersion.query().first() | |
|
4841 | ver.version = version | |
|
4842 | Session().commit() | |
|
4843 | ||
|
4844 | ||
|
4845 | class DbSession(Base, BaseModel): | |
|
4846 | __tablename__ = 'db_session' | |
|
4847 | __table_args__ = ( | |
|
4848 | base_table_args, | |
|
4849 | ) | |
|
4850 | ||
|
4851 | def __repr__(self): | |
|
4852 | return '<DB:DbSession({})>'.format(self.id) | |
|
4853 | ||
|
4854 | id = Column('id', Integer()) | |
|
4855 | namespace = Column('namespace', String(255), primary_key=True) | |
|
4856 | accessed = Column('accessed', DateTime, nullable=False) | |
|
4857 | created = Column('created', DateTime, nullable=False) | |
|
4858 | data = Column('data', PickleType, nullable=False) |
@@ -0,0 +1,30 b'' | |||
|
1 | import logging | |
|
2 | ||
|
3 | from sqlalchemy import * | |
|
4 | ||
|
5 | from rhodecode.model import meta | |
|
6 | from rhodecode.lib.dbmigrate.versions import _reset_base, notify | |
|
7 | ||
|
8 | log = logging.getLogger(__name__) | |
|
9 | ||
|
10 | ||
|
11 | def upgrade(migrate_engine): | |
|
12 | """ | |
|
13 | Upgrade operations go here. | |
|
14 | Don't create your own engine; bind migrate_engine to your metadata | |
|
15 | """ | |
|
16 | _reset_base(migrate_engine) | |
|
17 | from rhodecode.lib.dbmigrate.schema import db_4_16_0_1 as db | |
|
18 | ||
|
19 | db.UserBookmark.__table__.create() | |
|
20 | ||
|
21 | fixups(db, meta.Session) | |
|
22 | ||
|
23 | ||
|
24 | def downgrade(migrate_engine): | |
|
25 | meta = MetaData() | |
|
26 | meta.bind = migrate_engine | |
|
27 | ||
|
28 | ||
|
29 | def fixups(models, _SESSION): | |
|
30 | pass |
|
1 | NO CONTENT: new file 100644 | |
The requested commit or file is too big and content was truncated. Show full diff |
@@ -1,57 +1,57 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import os |
|
22 | 22 | import sys |
|
23 | 23 | import platform |
|
24 | 24 | |
|
25 | 25 | VERSION = tuple(open(os.path.join( |
|
26 | 26 | os.path.dirname(__file__), 'VERSION')).read().split('.')) |
|
27 | 27 | |
|
28 | 28 | BACKENDS = { |
|
29 | 29 | 'hg': 'Mercurial repository', |
|
30 | 30 | 'git': 'Git repository', |
|
31 | 31 | 'svn': 'Subversion repository', |
|
32 | 32 | } |
|
33 | 33 | |
|
34 | 34 | CELERY_ENABLED = False |
|
35 | 35 | CELERY_EAGER = False |
|
36 | 36 | |
|
37 | 37 | # link to config for pyramid |
|
38 | 38 | CONFIG = {} |
|
39 | 39 | |
|
40 | 40 | # Populated with the settings dictionary from application init in |
|
41 | 41 | # rhodecode.conf.environment.load_pyramid_environment |
|
42 | 42 | PYRAMID_SETTINGS = {} |
|
43 | 43 | |
|
44 | 44 | # Linked module for extensions |
|
45 | 45 | EXTENSIONS = {} |
|
46 | 46 | |
|
47 | 47 | __version__ = ('.'.join((str(each) for each in VERSION[:3]))) |
|
48 |
__dbversion__ = 9 |
|
|
48 | __dbversion__ = 94 # defines current db version for migrations | |
|
49 | 49 | __platform__ = platform.system() |
|
50 | 50 | __license__ = 'AGPLv3, and Commercial License' |
|
51 | 51 | __author__ = 'RhodeCode GmbH' |
|
52 | 52 | __url__ = 'https://code.rhodecode.com' |
|
53 | 53 | |
|
54 | 54 | is_windows = __platform__ in ['Windows'] |
|
55 | 55 | is_unix = not is_windows |
|
56 | 56 | is_test = False |
|
57 | 57 | disable_error_handler = False |
@@ -1,77 +1,81 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | from rhodecode.config import routing_links |
|
21 | 21 | |
|
22 | 22 | |
|
23 | 23 | class VCSCallPredicate(object): |
|
24 | 24 | def __init__(self, val, config): |
|
25 | 25 | self.val = val |
|
26 | 26 | |
|
27 | 27 | def text(self): |
|
28 | 28 | return 'vcs_call route = %s' % self.val |
|
29 | 29 | |
|
30 | 30 | phash = text |
|
31 | 31 | |
|
32 | 32 | def __call__(self, info, request): |
|
33 | 33 | if hasattr(request, 'vcs_call'): |
|
34 | 34 | # skip vcs calls |
|
35 | 35 | return False |
|
36 | 36 | |
|
37 | 37 | return True |
|
38 | 38 | |
|
39 | 39 | |
|
40 | 40 | def includeme(config): |
|
41 | 41 | |
|
42 | 42 | config.add_route( |
|
43 | 43 | name='home', |
|
44 | 44 | pattern='/') |
|
45 | 45 | |
|
46 | 46 | config.add_route( |
|
47 | 47 | name='user_autocomplete_data', |
|
48 | 48 | pattern='/_users') |
|
49 | 49 | |
|
50 | 50 | config.add_route( |
|
51 | 51 | name='user_group_autocomplete_data', |
|
52 | 52 | pattern='/_user_groups') |
|
53 | 53 | |
|
54 | 54 | config.add_route( |
|
55 | 55 | name='repo_list_data', |
|
56 | 56 | pattern='/_repos') |
|
57 | 57 | |
|
58 | 58 | config.add_route( |
|
59 | name='repo_group_list_data', | |
|
60 | pattern='/_repo_groups') | |
|
61 | ||
|
62 | config.add_route( | |
|
59 | 63 | name='goto_switcher_data', |
|
60 | 64 | pattern='/_goto_data') |
|
61 | 65 | |
|
62 | 66 | config.add_route( |
|
63 | 67 | name='markup_preview', |
|
64 | 68 | pattern='/_markup_preview') |
|
65 | 69 | |
|
66 | 70 | config.add_route( |
|
67 | 71 | name='store_user_session_value', |
|
68 | 72 | pattern='/_store_session_attr') |
|
69 | 73 | |
|
70 | 74 | # register our static links via redirection mechanism |
|
71 | 75 | routing_links.connect_redirection_links(config) |
|
72 | 76 | |
|
73 | 77 | # Scan module for configuration decorators. |
|
74 | 78 | config.scan('.views', ignore='.tests') |
|
75 | 79 | |
|
76 | 80 | config.add_route_predicate( |
|
77 | 81 | 'skip_vcs_call', VCSCallPredicate) |
@@ -1,534 +1,563 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import re |
|
22 | 22 | import logging |
|
23 | 23 | import collections |
|
24 | 24 | |
|
25 | 25 | from pyramid.view import view_config |
|
26 | 26 | |
|
27 | 27 | from rhodecode.apps._base import BaseAppView |
|
28 | 28 | from rhodecode.lib import helpers as h |
|
29 | 29 | from rhodecode.lib.auth import ( |
|
30 | 30 | LoginRequired, NotAnonymous, HasRepoGroupPermissionAnyDecorator, |
|
31 | 31 | CSRFRequired) |
|
32 | 32 | from rhodecode.lib.index import searcher_from_config |
|
33 | 33 | from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int |
|
34 | 34 | from rhodecode.lib.ext_json import json |
|
35 | 35 | from rhodecode.model.db import ( |
|
36 | 36 | func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup) |
|
37 | 37 | from rhodecode.model.repo import RepoModel |
|
38 | 38 | from rhodecode.model.repo_group import RepoGroupModel |
|
39 | 39 | from rhodecode.model.scm import RepoGroupList, RepoList |
|
40 | 40 | from rhodecode.model.user import UserModel |
|
41 | 41 | from rhodecode.model.user_group import UserGroupModel |
|
42 | 42 | |
|
43 | 43 | log = logging.getLogger(__name__) |
|
44 | 44 | |
|
45 | 45 | |
|
46 | 46 | class HomeView(BaseAppView): |
|
47 | 47 | |
|
48 | 48 | def load_default_context(self): |
|
49 | 49 | c = self._get_local_tmpl_context() |
|
50 | 50 | c.user = c.auth_user.get_instance() |
|
51 | 51 | |
|
52 | 52 | return c |
|
53 | 53 | |
|
54 | 54 | @LoginRequired() |
|
55 | 55 | @view_config( |
|
56 | 56 | route_name='user_autocomplete_data', request_method='GET', |
|
57 | 57 | renderer='json_ext', xhr=True) |
|
58 | 58 | def user_autocomplete_data(self): |
|
59 | 59 | self.load_default_context() |
|
60 | 60 | query = self.request.GET.get('query') |
|
61 | 61 | active = str2bool(self.request.GET.get('active') or True) |
|
62 | 62 | include_groups = str2bool(self.request.GET.get('user_groups')) |
|
63 | 63 | expand_groups = str2bool(self.request.GET.get('user_groups_expand')) |
|
64 | 64 | skip_default_user = str2bool(self.request.GET.get('skip_default_user')) |
|
65 | 65 | |
|
66 | 66 | log.debug('generating user list, query:%s, active:%s, with_groups:%s', |
|
67 | 67 | query, active, include_groups) |
|
68 | 68 | |
|
69 | 69 | _users = UserModel().get_users( |
|
70 | 70 | name_contains=query, only_active=active) |
|
71 | 71 | |
|
72 | 72 | def maybe_skip_default_user(usr): |
|
73 | 73 | if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER: |
|
74 | 74 | return False |
|
75 | 75 | return True |
|
76 | 76 | _users = filter(maybe_skip_default_user, _users) |
|
77 | 77 | |
|
78 | 78 | if include_groups: |
|
79 | 79 | # extend with user groups |
|
80 | 80 | _user_groups = UserGroupModel().get_user_groups( |
|
81 | 81 | name_contains=query, only_active=active, |
|
82 | 82 | expand_groups=expand_groups) |
|
83 | 83 | _users = _users + _user_groups |
|
84 | 84 | |
|
85 | 85 | return {'suggestions': _users} |
|
86 | 86 | |
|
87 | 87 | @LoginRequired() |
|
88 | 88 | @NotAnonymous() |
|
89 | 89 | @view_config( |
|
90 | 90 | route_name='user_group_autocomplete_data', request_method='GET', |
|
91 | 91 | renderer='json_ext', xhr=True) |
|
92 | 92 | def user_group_autocomplete_data(self): |
|
93 | 93 | self.load_default_context() |
|
94 | 94 | query = self.request.GET.get('query') |
|
95 | 95 | active = str2bool(self.request.GET.get('active') or True) |
|
96 | 96 | expand_groups = str2bool(self.request.GET.get('user_groups_expand')) |
|
97 | 97 | |
|
98 | 98 | log.debug('generating user group list, query:%s, active:%s', |
|
99 | 99 | query, active) |
|
100 | 100 | |
|
101 | 101 | _user_groups = UserGroupModel().get_user_groups( |
|
102 | 102 | name_contains=query, only_active=active, |
|
103 | 103 | expand_groups=expand_groups) |
|
104 | 104 | _user_groups = _user_groups |
|
105 | 105 | |
|
106 | 106 | return {'suggestions': _user_groups} |
|
107 | 107 | |
|
108 | 108 | def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): |
|
109 | 109 | org_query = name_contains |
|
110 | 110 | allowed_ids = self._rhodecode_user.repo_acl_ids( |
|
111 | 111 | ['repository.read', 'repository.write', 'repository.admin'], |
|
112 | 112 | cache=False, name_filter=name_contains) or [-1] |
|
113 | 113 | |
|
114 | 114 | query = Repository.query()\ |
|
115 | 115 | .order_by(func.length(Repository.repo_name))\ |
|
116 | 116 | .order_by(Repository.repo_name)\ |
|
117 | 117 | .filter(Repository.archived.isnot(true()))\ |
|
118 | 118 | .filter(or_( |
|
119 | 119 | # generate multiple IN to fix limitation problems |
|
120 | 120 | *in_filter_generator(Repository.repo_id, allowed_ids) |
|
121 | 121 | )) |
|
122 | 122 | |
|
123 | 123 | if repo_type: |
|
124 | 124 | query = query.filter(Repository.repo_type == repo_type) |
|
125 | 125 | |
|
126 | 126 | if name_contains: |
|
127 | 127 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
128 | 128 | query = query.filter( |
|
129 | 129 | Repository.repo_name.ilike(ilike_expression)) |
|
130 | 130 | query = query.limit(limit) |
|
131 | 131 | |
|
132 | 132 | acl_iter = query |
|
133 | 133 | |
|
134 | 134 | return [ |
|
135 | 135 | { |
|
136 | 136 | 'id': obj.repo_name, |
|
137 | 137 | 'value': org_query, |
|
138 | 138 | 'value_display': obj.repo_name, |
|
139 | 139 | 'text': obj.repo_name, |
|
140 | 140 | 'type': 'repo', |
|
141 | 141 | 'repo_id': obj.repo_id, |
|
142 | 142 | 'repo_type': obj.repo_type, |
|
143 | 143 | 'private': obj.private, |
|
144 | 144 | 'url': h.route_path('repo_summary', repo_name=obj.repo_name) |
|
145 | 145 | } |
|
146 | 146 | for obj in acl_iter] |
|
147 | 147 | |
|
148 | 148 | def _get_repo_group_list(self, name_contains=None, limit=20): |
|
149 | 149 | org_query = name_contains |
|
150 | 150 | allowed_ids = self._rhodecode_user.repo_group_acl_ids( |
|
151 | 151 | ['group.read', 'group.write', 'group.admin'], |
|
152 | 152 | cache=False, name_filter=name_contains) or [-1] |
|
153 | 153 | |
|
154 | 154 | query = RepoGroup.query()\ |
|
155 | 155 | .order_by(func.length(RepoGroup.group_name))\ |
|
156 | 156 | .order_by(RepoGroup.group_name) \ |
|
157 | 157 | .filter(or_( |
|
158 | 158 | # generate multiple IN to fix limitation problems |
|
159 | 159 | *in_filter_generator(RepoGroup.group_id, allowed_ids) |
|
160 | 160 | )) |
|
161 | 161 | |
|
162 | 162 | if name_contains: |
|
163 | 163 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
164 | 164 | query = query.filter( |
|
165 | 165 | RepoGroup.group_name.ilike(ilike_expression)) |
|
166 | 166 | query = query.limit(limit) |
|
167 | 167 | |
|
168 | 168 | acl_iter = query |
|
169 | 169 | |
|
170 | 170 | return [ |
|
171 | 171 | { |
|
172 | 172 | 'id': obj.group_name, |
|
173 | 173 | 'value': org_query, |
|
174 | 174 | 'value_display': obj.group_name, |
|
175 | 'text': obj.group_name, | |
|
175 | 176 | 'type': 'repo_group', |
|
177 | 'repo_group_id': obj.group_id, | |
|
176 | 178 | 'url': h.route_path( |
|
177 | 179 | 'repo_group_home', repo_group_name=obj.group_name) |
|
178 | 180 | } |
|
179 | 181 | for obj in acl_iter] |
|
180 | 182 | |
|
181 | 183 | def _get_user_list(self, name_contains=None, limit=20): |
|
182 | 184 | org_query = name_contains |
|
183 | 185 | if not name_contains: |
|
184 | 186 | return [] |
|
185 | 187 | |
|
186 | 188 | name_contains = re.compile('(?:user:)(.+)').findall(name_contains) |
|
187 | 189 | if len(name_contains) != 1: |
|
188 | 190 | return [] |
|
189 | 191 | name_contains = name_contains[0] |
|
190 | 192 | |
|
191 | 193 | query = User.query()\ |
|
192 | 194 | .order_by(func.length(User.username))\ |
|
193 | 195 | .order_by(User.username) \ |
|
194 | 196 | .filter(User.username != User.DEFAULT_USER) |
|
195 | 197 | |
|
196 | 198 | if name_contains: |
|
197 | 199 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
198 | 200 | query = query.filter( |
|
199 | 201 | User.username.ilike(ilike_expression)) |
|
200 | 202 | query = query.limit(limit) |
|
201 | 203 | |
|
202 | 204 | acl_iter = query |
|
203 | 205 | |
|
204 | 206 | return [ |
|
205 | 207 | { |
|
206 | 208 | 'id': obj.user_id, |
|
207 | 209 | 'value': org_query, |
|
208 | 210 | 'value_display': obj.username, |
|
209 | 211 | 'type': 'user', |
|
210 | 212 | 'icon_link': h.gravatar_url(obj.email, 30), |
|
211 | 213 | 'url': h.route_path( |
|
212 | 214 | 'user_profile', username=obj.username) |
|
213 | 215 | } |
|
214 | 216 | for obj in acl_iter] |
|
215 | 217 | |
|
216 | 218 | def _get_user_groups_list(self, name_contains=None, limit=20): |
|
217 | 219 | org_query = name_contains |
|
218 | 220 | if not name_contains: |
|
219 | 221 | return [] |
|
220 | 222 | |
|
221 | 223 | name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains) |
|
222 | 224 | if len(name_contains) != 1: |
|
223 | 225 | return [] |
|
224 | 226 | name_contains = name_contains[0] |
|
225 | 227 | |
|
226 | 228 | query = UserGroup.query()\ |
|
227 | 229 | .order_by(func.length(UserGroup.users_group_name))\ |
|
228 | 230 | .order_by(UserGroup.users_group_name) |
|
229 | 231 | |
|
230 | 232 | if name_contains: |
|
231 | 233 | ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) |
|
232 | 234 | query = query.filter( |
|
233 | 235 | UserGroup.users_group_name.ilike(ilike_expression)) |
|
234 | 236 | query = query.limit(limit) |
|
235 | 237 | |
|
236 | 238 | acl_iter = query |
|
237 | 239 | |
|
238 | 240 | return [ |
|
239 | 241 | { |
|
240 | 242 | 'id': obj.users_group_id, |
|
241 | 243 | 'value': org_query, |
|
242 | 244 | 'value_display': obj.users_group_name, |
|
243 | 245 | 'type': 'user_group', |
|
244 | 246 | 'url': h.route_path( |
|
245 | 247 | 'user_group_profile', user_group_name=obj.users_group_name) |
|
246 | 248 | } |
|
247 | 249 | for obj in acl_iter] |
|
248 | 250 | |
|
249 | 251 | def _get_hash_commit_list(self, auth_user, searcher, query): |
|
250 | 252 | org_query = query |
|
251 | 253 | if not query or len(query) < 3 or not searcher: |
|
252 | 254 | return [] |
|
253 | 255 | |
|
254 | 256 | commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query) |
|
255 | 257 | |
|
256 | 258 | if len(commit_hashes) != 1: |
|
257 | 259 | return [] |
|
258 | 260 | commit_hash = commit_hashes[0] |
|
259 | 261 | |
|
260 | 262 | result = searcher.search( |
|
261 | 263 | 'commit_id:{}*'.format(commit_hash), 'commit', auth_user, |
|
262 | 264 | raise_on_exc=False) |
|
263 | 265 | |
|
264 | 266 | return [ |
|
265 | 267 | { |
|
266 | 268 | 'id': entry['commit_id'], |
|
267 | 269 | 'value': org_query, |
|
268 | 270 | 'value_display': 'repo `{}` commit: {}'.format( |
|
269 | 271 | entry['repository'], entry['commit_id']), |
|
270 | 272 | 'type': 'commit', |
|
271 | 273 | 'repo': entry['repository'], |
|
272 | 274 | 'url': h.route_path( |
|
273 | 275 | 'repo_commit', |
|
274 | 276 | repo_name=entry['repository'], commit_id=entry['commit_id']) |
|
275 | 277 | } |
|
276 | 278 | for entry in result['results']] |
|
277 | 279 | |
|
278 | 280 | @LoginRequired() |
|
279 | 281 | @view_config( |
|
280 | 282 | route_name='repo_list_data', request_method='GET', |
|
281 | 283 | renderer='json_ext', xhr=True) |
|
282 | 284 | def repo_list_data(self): |
|
283 | 285 | _ = self.request.translate |
|
284 | 286 | self.load_default_context() |
|
285 | 287 | |
|
286 | 288 | query = self.request.GET.get('query') |
|
287 | 289 | repo_type = self.request.GET.get('repo_type') |
|
288 | 290 | log.debug('generating repo list, query:%s, repo_type:%s', |
|
289 | 291 | query, repo_type) |
|
290 | 292 | |
|
291 | 293 | res = [] |
|
292 | 294 | repos = self._get_repo_list(query, repo_type=repo_type) |
|
293 | 295 | if repos: |
|
294 | 296 | res.append({ |
|
295 | 297 | 'text': _('Repositories'), |
|
296 | 298 | 'children': repos |
|
297 | 299 | }) |
|
298 | 300 | |
|
299 | 301 | data = { |
|
300 | 302 | 'more': False, |
|
301 | 303 | 'results': res |
|
302 | 304 | } |
|
303 | 305 | return data |
|
304 | 306 | |
|
307 | @LoginRequired() | |
|
308 | @view_config( | |
|
309 | route_name='repo_group_list_data', request_method='GET', | |
|
310 | renderer='json_ext', xhr=True) | |
|
311 | def repo_group_list_data(self): | |
|
312 | _ = self.request.translate | |
|
313 | self.load_default_context() | |
|
314 | ||
|
315 | query = self.request.GET.get('query') | |
|
316 | ||
|
317 | log.debug('generating repo group list, query:%s', | |
|
318 | query) | |
|
319 | ||
|
320 | res = [] | |
|
321 | repo_groups = self._get_repo_group_list(query) | |
|
322 | if repo_groups: | |
|
323 | res.append({ | |
|
324 | 'text': _('Repository Groups'), | |
|
325 | 'children': repo_groups | |
|
326 | }) | |
|
327 | ||
|
328 | data = { | |
|
329 | 'more': False, | |
|
330 | 'results': res | |
|
331 | } | |
|
332 | return data | |
|
333 | ||
|
305 | 334 | def _get_default_search_queries(self, search_context, searcher, query): |
|
306 | 335 | if not searcher: |
|
307 | 336 | return [] |
|
308 | 337 | is_es_6 = searcher.is_es_6 |
|
309 | 338 | |
|
310 | 339 | queries = [] |
|
311 | 340 | repo_group_name, repo_name, repo_context = None, None, None |
|
312 | 341 | |
|
313 | 342 | # repo group context |
|
314 | 343 | if search_context.get('search_context[repo_group_name]'): |
|
315 | 344 | repo_group_name = search_context.get('search_context[repo_group_name]') |
|
316 | 345 | if search_context.get('search_context[repo_name]'): |
|
317 | 346 | repo_name = search_context.get('search_context[repo_name]') |
|
318 | 347 | repo_context = search_context.get('search_context[repo_view_type]') |
|
319 | 348 | |
|
320 | 349 | if is_es_6 and repo_name: |
|
321 | 350 | def query_modifier(): |
|
322 | 351 | qry = '{} repo_name.raw:{} '.format( |
|
323 | 352 | query, searcher.escape_specials(repo_name)) |
|
324 | 353 | return {'q': qry, 'type': 'content'} |
|
325 | 354 | label = u'Search for `{}` through files in this repository.'.format(query) |
|
326 | 355 | queries.append( |
|
327 | 356 | { |
|
328 | 357 | 'id': -10, |
|
329 | 358 | 'value': query, |
|
330 | 359 | 'value_display': label, |
|
331 | 360 | 'type': 'search', |
|
332 | 361 | 'url': h.route_path( |
|
333 | 362 | 'search_repo', repo_name=repo_name, _query=query_modifier()) |
|
334 | 363 | } |
|
335 | 364 | ) |
|
336 | 365 | |
|
337 | 366 | def query_modifier(): |
|
338 | 367 | qry = '{} repo_name.raw:{} '.format( |
|
339 | 368 | query, searcher.escape_specials(repo_name)) |
|
340 | 369 | return {'q': qry, 'type': 'commit'} |
|
341 | 370 | label = u'Search for `{}` through commits in this repository.'.format(query) |
|
342 | 371 | queries.append( |
|
343 | 372 | { |
|
344 | 373 | 'id': -10, |
|
345 | 374 | 'value': query, |
|
346 | 375 | 'value_display': label, |
|
347 | 376 | 'type': 'search', |
|
348 | 377 | 'url': h.route_path( |
|
349 | 378 | 'search_repo', repo_name=repo_name, _query=query_modifier()) |
|
350 | 379 | } |
|
351 | 380 | ) |
|
352 | 381 | |
|
353 | 382 | elif is_es_6 and repo_group_name: |
|
354 | 383 | def query_modifier(): |
|
355 | 384 | qry = '{} repo_name.raw:{} '.format( |
|
356 | 385 | query, searcher.escape_specials(repo_group_name + '/*')) |
|
357 | 386 | return {'q': qry, 'type': 'content'} |
|
358 | 387 | label = u'Search for `{}` through files in this repository group'.format(query) |
|
359 | 388 | queries.append( |
|
360 | 389 | { |
|
361 | 390 | 'id': -20, |
|
362 | 391 | 'value': query, |
|
363 | 392 | 'value_display': label, |
|
364 | 393 | 'type': 'search', |
|
365 | 394 | 'url': h.route_path('search', _query=query_modifier()) |
|
366 | 395 | } |
|
367 | 396 | ) |
|
368 | 397 | |
|
369 | 398 | if not queries: |
|
370 | 399 | queries.append( |
|
371 | 400 | { |
|
372 | 401 | 'id': -1, |
|
373 | 402 | 'value': query, |
|
374 | 403 | 'value_display': u'Search for: `{}`'.format(query), |
|
375 | 404 | 'type': 'search', |
|
376 | 405 | 'url': h.route_path('search', |
|
377 | 406 | _query={'q': query, 'type': 'content'}) |
|
378 | 407 | } |
|
379 | 408 | ) |
|
380 | 409 | |
|
381 | 410 | return queries |
|
382 | 411 | |
|
383 | 412 | @LoginRequired() |
|
384 | 413 | @view_config( |
|
385 | 414 | route_name='goto_switcher_data', request_method='GET', |
|
386 | 415 | renderer='json_ext', xhr=True) |
|
387 | 416 | def goto_switcher_data(self): |
|
388 | 417 | c = self.load_default_context() |
|
389 | 418 | |
|
390 | 419 | _ = self.request.translate |
|
391 | 420 | |
|
392 | 421 | query = self.request.GET.get('query') |
|
393 | 422 | log.debug('generating main filter data, query %s', query) |
|
394 | 423 | |
|
395 | 424 | res = [] |
|
396 | 425 | if not query: |
|
397 | 426 | return {'suggestions': res} |
|
398 | 427 | |
|
399 | 428 | searcher = searcher_from_config(self.request.registry.settings) |
|
400 | 429 | for _q in self._get_default_search_queries(self.request.GET, searcher, query): |
|
401 | 430 | res.append(_q) |
|
402 | 431 | |
|
403 | 432 | repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]')) |
|
404 | 433 | if repo_group_id: |
|
405 | 434 | repo_group = RepoGroup.get(repo_group_id) |
|
406 | 435 | composed_hint = '{}/{}'.format(repo_group.group_name, query) |
|
407 | 436 | show_hint = not query.startswith(repo_group.group_name) |
|
408 | 437 | if repo_group and show_hint: |
|
409 | 438 | hint = u'Repository search inside: `{}`'.format(composed_hint) |
|
410 | 439 | res.append({ |
|
411 | 440 | 'id': -1, |
|
412 | 441 | 'value': composed_hint, |
|
413 | 442 | 'value_display': hint, |
|
414 | 443 | 'type': 'hint', |
|
415 | 444 | 'url': "" |
|
416 | 445 | }) |
|
417 | 446 | |
|
418 | 447 | repo_groups = self._get_repo_group_list(query) |
|
419 | 448 | for serialized_repo_group in repo_groups: |
|
420 | 449 | res.append(serialized_repo_group) |
|
421 | 450 | |
|
422 | 451 | repos = self._get_repo_list(query) |
|
423 | 452 | for serialized_repo in repos: |
|
424 | 453 | res.append(serialized_repo) |
|
425 | 454 | |
|
426 | 455 | # TODO(marcink): should all logged in users be allowed to search others? |
|
427 | 456 | allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER |
|
428 | 457 | if allowed_user_search: |
|
429 | 458 | users = self._get_user_list(query) |
|
430 | 459 | for serialized_user in users: |
|
431 | 460 | res.append(serialized_user) |
|
432 | 461 | |
|
433 | 462 | user_groups = self._get_user_groups_list(query) |
|
434 | 463 | for serialized_user_group in user_groups: |
|
435 | 464 | res.append(serialized_user_group) |
|
436 | 465 | |
|
437 | 466 | commits = self._get_hash_commit_list(c.auth_user, searcher, query) |
|
438 | 467 | if commits: |
|
439 | 468 | unique_repos = collections.OrderedDict() |
|
440 | 469 | for commit in commits: |
|
441 | 470 | repo_name = commit['repo'] |
|
442 | 471 | unique_repos.setdefault(repo_name, []).append(commit) |
|
443 | 472 | |
|
444 | 473 | for repo, commits in unique_repos.items(): |
|
445 | 474 | for commit in commits: |
|
446 | 475 | res.append(commit) |
|
447 | 476 | |
|
448 | 477 | return {'suggestions': res} |
|
449 | 478 | |
|
450 | 479 | def _get_groups_and_repos(self, repo_group_id=None): |
|
451 | 480 | # repo groups groups |
|
452 | 481 | repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) |
|
453 | 482 | _perms = ['group.read', 'group.write', 'group.admin'] |
|
454 | 483 | repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) |
|
455 | 484 | repo_group_data = RepoGroupModel().get_repo_groups_as_dict( |
|
456 | 485 | repo_group_list=repo_group_list_acl, admin=False) |
|
457 | 486 | |
|
458 | 487 | # repositories |
|
459 | 488 | repo_list = Repository.get_all_repos(group_id=repo_group_id) |
|
460 | 489 | _perms = ['repository.read', 'repository.write', 'repository.admin'] |
|
461 | 490 | repo_list_acl = RepoList(repo_list, perm_set=_perms) |
|
462 | 491 | repo_data = RepoModel().get_repos_as_dict( |
|
463 | 492 | repo_list=repo_list_acl, admin=False) |
|
464 | 493 | |
|
465 | 494 | return repo_data, repo_group_data |
|
466 | 495 | |
|
467 | 496 | @LoginRequired() |
|
468 | 497 | @view_config( |
|
469 | 498 | route_name='home', request_method='GET', |
|
470 | 499 | renderer='rhodecode:templates/index.mako') |
|
471 | 500 | def main_page(self): |
|
472 | 501 | c = self.load_default_context() |
|
473 | 502 | c.repo_group = None |
|
474 | 503 | |
|
475 | 504 | repo_data, repo_group_data = self._get_groups_and_repos() |
|
476 | 505 | # json used to render the grids |
|
477 | 506 | c.repos_data = json.dumps(repo_data) |
|
478 | 507 | c.repo_groups_data = json.dumps(repo_group_data) |
|
479 | 508 | |
|
480 | 509 | return self._get_template_context(c) |
|
481 | 510 | |
|
482 | 511 | @LoginRequired() |
|
483 | 512 | @HasRepoGroupPermissionAnyDecorator( |
|
484 | 513 | 'group.read', 'group.write', 'group.admin') |
|
485 | 514 | @view_config( |
|
486 | 515 | route_name='repo_group_home', request_method='GET', |
|
487 | 516 | renderer='rhodecode:templates/index_repo_group.mako') |
|
488 | 517 | @view_config( |
|
489 | 518 | route_name='repo_group_home_slash', request_method='GET', |
|
490 | 519 | renderer='rhodecode:templates/index_repo_group.mako') |
|
491 | 520 | def repo_group_main_page(self): |
|
492 | 521 | c = self.load_default_context() |
|
493 | 522 | c.repo_group = self.request.db_repo_group |
|
494 | 523 | repo_data, repo_group_data = self._get_groups_and_repos( |
|
495 | 524 | c.repo_group.group_id) |
|
496 | 525 | |
|
497 | 526 | # json used to render the grids |
|
498 | 527 | c.repos_data = json.dumps(repo_data) |
|
499 | 528 | c.repo_groups_data = json.dumps(repo_group_data) |
|
500 | 529 | |
|
501 | 530 | return self._get_template_context(c) |
|
502 | 531 | |
|
503 | 532 | @LoginRequired() |
|
504 | 533 | @CSRFRequired() |
|
505 | 534 | @view_config( |
|
506 | 535 | route_name='markup_preview', request_method='POST', |
|
507 | 536 | renderer='string', xhr=True) |
|
508 | 537 | def markup_preview(self): |
|
509 | 538 | # Technically a CSRF token is not needed as no state changes with this |
|
510 | 539 | # call. However, as this is a POST is better to have it, so automated |
|
511 | 540 | # tools don't flag it as potential CSRF. |
|
512 | 541 | # Post is required because the payload could be bigger than the maximum |
|
513 | 542 | # allowed by GET. |
|
514 | 543 | |
|
515 | 544 | text = self.request.POST.get('text') |
|
516 | 545 | renderer = self.request.POST.get('renderer') or 'rst' |
|
517 | 546 | if text: |
|
518 | 547 | return h.render(text, renderer=renderer, mentions=True) |
|
519 | 548 | return '' |
|
520 | 549 | |
|
521 | 550 | @LoginRequired() |
|
522 | 551 | @CSRFRequired() |
|
523 | 552 | @view_config( |
|
524 | 553 | route_name='store_user_session_value', request_method='POST', |
|
525 | 554 | renderer='string', xhr=True) |
|
526 | 555 | def store_user_session_attr(self): |
|
527 | 556 | key = self.request.POST.get('key') |
|
528 | 557 | val = self.request.POST.get('val') |
|
529 | 558 | |
|
530 | 559 | existing_value = self.request.session.get(key) |
|
531 | 560 | if existing_value != val: |
|
532 | 561 | self.request.session[key] = val |
|
533 | 562 | |
|
534 | 563 | return 'stored:{}'.format(key) |
@@ -1,145 +1,157 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | |
|
22 | 22 | from rhodecode.apps._base import ADMIN_PREFIX |
|
23 | 23 | |
|
24 | 24 | |
|
25 | 25 | def includeme(config): |
|
26 | 26 | |
|
27 | 27 | config.add_route( |
|
28 | 28 | name='my_account_profile', |
|
29 | 29 | pattern=ADMIN_PREFIX + '/my_account/profile') |
|
30 | 30 | |
|
31 | 31 | # my account edit details |
|
32 | 32 | config.add_route( |
|
33 | 33 | name='my_account_edit', |
|
34 | 34 | pattern=ADMIN_PREFIX + '/my_account/edit') |
|
35 | 35 | config.add_route( |
|
36 | 36 | name='my_account_update', |
|
37 | 37 | pattern=ADMIN_PREFIX + '/my_account/update') |
|
38 | 38 | |
|
39 | 39 | # my account password |
|
40 | 40 | config.add_route( |
|
41 | 41 | name='my_account_password', |
|
42 | 42 | pattern=ADMIN_PREFIX + '/my_account/password') |
|
43 | 43 | |
|
44 | 44 | config.add_route( |
|
45 | 45 | name='my_account_password_update', |
|
46 | 46 | pattern=ADMIN_PREFIX + '/my_account/password/update') |
|
47 | 47 | |
|
48 | 48 | # my account tokens |
|
49 | 49 | config.add_route( |
|
50 | 50 | name='my_account_auth_tokens', |
|
51 | 51 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens') |
|
52 | 52 | config.add_route( |
|
53 | 53 | name='my_account_auth_tokens_add', |
|
54 | 54 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new') |
|
55 | 55 | config.add_route( |
|
56 | 56 | name='my_account_auth_tokens_delete', |
|
57 | 57 | pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete') |
|
58 | 58 | |
|
59 | 59 | # my account ssh keys |
|
60 | 60 | config.add_route( |
|
61 | 61 | name='my_account_ssh_keys', |
|
62 | 62 | pattern=ADMIN_PREFIX + '/my_account/ssh_keys') |
|
63 | 63 | config.add_route( |
|
64 | 64 | name='my_account_ssh_keys_generate', |
|
65 | 65 | pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate') |
|
66 | 66 | config.add_route( |
|
67 | 67 | name='my_account_ssh_keys_add', |
|
68 | 68 | pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new') |
|
69 | 69 | config.add_route( |
|
70 | 70 | name='my_account_ssh_keys_delete', |
|
71 | 71 | pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete') |
|
72 | 72 | |
|
73 | 73 | # my account user group membership |
|
74 | 74 | config.add_route( |
|
75 | 75 | name='my_account_user_group_membership', |
|
76 | 76 | pattern=ADMIN_PREFIX + '/my_account/user_group_membership') |
|
77 | 77 | |
|
78 | 78 | # my account emails |
|
79 | 79 | config.add_route( |
|
80 | 80 | name='my_account_emails', |
|
81 | 81 | pattern=ADMIN_PREFIX + '/my_account/emails') |
|
82 | 82 | config.add_route( |
|
83 | 83 | name='my_account_emails_add', |
|
84 | 84 | pattern=ADMIN_PREFIX + '/my_account/emails/new') |
|
85 | 85 | config.add_route( |
|
86 | 86 | name='my_account_emails_delete', |
|
87 | 87 | pattern=ADMIN_PREFIX + '/my_account/emails/delete') |
|
88 | 88 | |
|
89 | 89 | config.add_route( |
|
90 | 90 | name='my_account_repos', |
|
91 | 91 | pattern=ADMIN_PREFIX + '/my_account/repos') |
|
92 | 92 | |
|
93 | 93 | config.add_route( |
|
94 | 94 | name='my_account_watched', |
|
95 | 95 | pattern=ADMIN_PREFIX + '/my_account/watched') |
|
96 | 96 | |
|
97 | 97 | config.add_route( |
|
98 | name='my_account_bookmarks', | |
|
99 | pattern=ADMIN_PREFIX + '/my_account/bookmarks') | |
|
100 | ||
|
101 | config.add_route( | |
|
102 | name='my_account_bookmarks_update', | |
|
103 | pattern=ADMIN_PREFIX + '/my_account/bookmarks/update') | |
|
104 | ||
|
105 | config.add_route( | |
|
106 | name='my_account_goto_bookmark', | |
|
107 | pattern=ADMIN_PREFIX + '/my_account/bookmark/{bookmark_id}') | |
|
108 | ||
|
109 | config.add_route( | |
|
98 | 110 | name='my_account_perms', |
|
99 | 111 | pattern=ADMIN_PREFIX + '/my_account/perms') |
|
100 | 112 | |
|
101 | 113 | config.add_route( |
|
102 | 114 | name='my_account_notifications', |
|
103 | 115 | pattern=ADMIN_PREFIX + '/my_account/notifications') |
|
104 | 116 | |
|
105 | 117 | config.add_route( |
|
106 | 118 | name='my_account_notifications_toggle_visibility', |
|
107 | 119 | pattern=ADMIN_PREFIX + '/my_account/toggle_visibility') |
|
108 | 120 | |
|
109 | 121 | # my account pull requests |
|
110 | 122 | config.add_route( |
|
111 | 123 | name='my_account_pullrequests', |
|
112 | 124 | pattern=ADMIN_PREFIX + '/my_account/pull_requests') |
|
113 | 125 | config.add_route( |
|
114 | 126 | name='my_account_pullrequests_data', |
|
115 | 127 | pattern=ADMIN_PREFIX + '/my_account/pull_requests/data') |
|
116 | 128 | |
|
117 | 129 | # notifications |
|
118 | 130 | config.add_route( |
|
119 | 131 | name='notifications_show_all', |
|
120 | 132 | pattern=ADMIN_PREFIX + '/notifications') |
|
121 | 133 | |
|
122 | 134 | # notifications |
|
123 | 135 | config.add_route( |
|
124 | 136 | name='notifications_mark_all_read', |
|
125 | 137 | pattern=ADMIN_PREFIX + '/notifications/mark_all_read') |
|
126 | 138 | |
|
127 | 139 | config.add_route( |
|
128 | 140 | name='notifications_show', |
|
129 | 141 | pattern=ADMIN_PREFIX + '/notifications/{notification_id}') |
|
130 | 142 | |
|
131 | 143 | config.add_route( |
|
132 | 144 | name='notifications_update', |
|
133 | 145 | pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update') |
|
134 | 146 | |
|
135 | 147 | config.add_route( |
|
136 | 148 | name='notifications_delete', |
|
137 | 149 | pattern=ADMIN_PREFIX + '/notifications/{notification_id}/delete') |
|
138 | 150 | |
|
139 | 151 | # channelstream test |
|
140 | 152 | config.add_route( |
|
141 | 153 | name='my_account_notifications_test_channelstream', |
|
142 | 154 | pattern=ADMIN_PREFIX + '/my_account/test_channelstream') |
|
143 | 155 | |
|
144 | 156 | # Scan module for configuration decorators. |
|
145 | 157 | config.scan('.views', ignore='.tests') |
@@ -1,605 +1,740 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2016-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | import logging |
|
22 | 22 | import datetime |
|
23 | import string | |
|
23 | 24 | |
|
24 | 25 | import formencode |
|
25 | 26 | import formencode.htmlfill |
|
27 | import peppercorn | |
|
26 | 28 | from pyramid.httpexceptions import HTTPFound |
|
27 | 29 | from pyramid.view import view_config |
|
28 | from pyramid.renderers import render | |
|
29 | from pyramid.response import Response | |
|
30 | 30 | |
|
31 | 31 | from rhodecode.apps._base import BaseAppView, DataGridAppView |
|
32 | 32 | from rhodecode import forms |
|
33 | 33 | from rhodecode.lib import helpers as h |
|
34 | 34 | from rhodecode.lib import audit_logger |
|
35 | 35 | from rhodecode.lib.ext_json import json |
|
36 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired | |
|
36 | from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired, \ | |
|
37 | HasRepoPermissionAny, HasRepoGroupPermissionAny | |
|
37 | 38 | from rhodecode.lib.channelstream import ( |
|
38 | 39 | channelstream_request, ChannelstreamException) |
|
39 | 40 | from rhodecode.lib.utils2 import safe_int, md5, str2bool |
|
40 | 41 | from rhodecode.model.auth_token import AuthTokenModel |
|
41 | 42 | from rhodecode.model.comment import CommentsModel |
|
42 | 43 | from rhodecode.model.db import ( |
|
43 | Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload, | |
|
44 | PullRequest) | |
|
45 | from rhodecode.model.forms import UserForm, UserExtraEmailForm | |
|
44 | IntegrityError, joinedload, | |
|
45 | Repository, UserEmailMap, UserApiKeys, UserFollowing, | |
|
46 | PullRequest, UserBookmark, RepoGroup) | |
|
46 | 47 | from rhodecode.model.meta import Session |
|
47 | 48 | from rhodecode.model.pull_request import PullRequestModel |
|
48 | 49 | from rhodecode.model.scm import RepoList |
|
49 | 50 | from rhodecode.model.user import UserModel |
|
50 | 51 | from rhodecode.model.repo import RepoModel |
|
51 | 52 | from rhodecode.model.user_group import UserGroupModel |
|
52 | 53 | from rhodecode.model.validation_schema.schemas import user_schema |
|
53 | 54 | |
|
54 | 55 | log = logging.getLogger(__name__) |
|
55 | 56 | |
|
56 | 57 | |
|
57 | 58 | class MyAccountView(BaseAppView, DataGridAppView): |
|
58 | 59 | ALLOW_SCOPED_TOKENS = False |
|
59 | 60 | """ |
|
60 | 61 | This view has alternative version inside EE, if modified please take a look |
|
61 | 62 | in there as well. |
|
62 | 63 | """ |
|
63 | 64 | |
|
64 | 65 | def load_default_context(self): |
|
65 | 66 | c = self._get_local_tmpl_context() |
|
66 | 67 | c.user = c.auth_user.get_instance() |
|
67 | 68 | c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS |
|
68 | 69 | |
|
69 | 70 | return c |
|
70 | 71 | |
|
71 | 72 | @LoginRequired() |
|
72 | 73 | @NotAnonymous() |
|
73 | 74 | @view_config( |
|
74 | 75 | route_name='my_account_profile', request_method='GET', |
|
75 | 76 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
76 | 77 | def my_account_profile(self): |
|
77 | 78 | c = self.load_default_context() |
|
78 | 79 | c.active = 'profile' |
|
79 | 80 | return self._get_template_context(c) |
|
80 | 81 | |
|
81 | 82 | @LoginRequired() |
|
82 | 83 | @NotAnonymous() |
|
83 | 84 | @view_config( |
|
84 | 85 | route_name='my_account_password', request_method='GET', |
|
85 | 86 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
86 | 87 | def my_account_password(self): |
|
87 | 88 | c = self.load_default_context() |
|
88 | 89 | c.active = 'password' |
|
89 | 90 | c.extern_type = c.user.extern_type |
|
90 | 91 | |
|
91 | 92 | schema = user_schema.ChangePasswordSchema().bind( |
|
92 | 93 | username=c.user.username) |
|
93 | 94 | |
|
94 | 95 | form = forms.Form( |
|
95 | 96 | schema, |
|
96 | 97 | action=h.route_path('my_account_password_update'), |
|
97 | 98 | buttons=(forms.buttons.save, forms.buttons.reset)) |
|
98 | 99 | |
|
99 | 100 | c.form = form |
|
100 | 101 | return self._get_template_context(c) |
|
101 | 102 | |
|
102 | 103 | @LoginRequired() |
|
103 | 104 | @NotAnonymous() |
|
104 | 105 | @CSRFRequired() |
|
105 | 106 | @view_config( |
|
106 | 107 | route_name='my_account_password_update', request_method='POST', |
|
107 | 108 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
108 | 109 | def my_account_password_update(self): |
|
109 | 110 | _ = self.request.translate |
|
110 | 111 | c = self.load_default_context() |
|
111 | 112 | c.active = 'password' |
|
112 | 113 | c.extern_type = c.user.extern_type |
|
113 | 114 | |
|
114 | 115 | schema = user_schema.ChangePasswordSchema().bind( |
|
115 | 116 | username=c.user.username) |
|
116 | 117 | |
|
117 | 118 | form = forms.Form( |
|
118 | 119 | schema, buttons=(forms.buttons.save, forms.buttons.reset)) |
|
119 | 120 | |
|
120 | 121 | if c.extern_type != 'rhodecode': |
|
121 | 122 | raise HTTPFound(self.request.route_path('my_account_password')) |
|
122 | 123 | |
|
123 | 124 | controls = self.request.POST.items() |
|
124 | 125 | try: |
|
125 | 126 | valid_data = form.validate(controls) |
|
126 | 127 | UserModel().update_user(c.user.user_id, **valid_data) |
|
127 | 128 | c.user.update_userdata(force_password_change=False) |
|
128 | 129 | Session().commit() |
|
129 | 130 | except forms.ValidationFailure as e: |
|
130 | 131 | c.form = e |
|
131 | 132 | return self._get_template_context(c) |
|
132 | 133 | |
|
133 | 134 | except Exception: |
|
134 | 135 | log.exception("Exception updating password") |
|
135 | 136 | h.flash(_('Error occurred during update of user password'), |
|
136 | 137 | category='error') |
|
137 | 138 | else: |
|
138 | 139 | instance = c.auth_user.get_instance() |
|
139 | 140 | self.session.setdefault('rhodecode_user', {}).update( |
|
140 | 141 | {'password': md5(instance.password)}) |
|
141 | 142 | self.session.save() |
|
142 | 143 | h.flash(_("Successfully updated password"), category='success') |
|
143 | 144 | |
|
144 | 145 | raise HTTPFound(self.request.route_path('my_account_password')) |
|
145 | 146 | |
|
146 | 147 | @LoginRequired() |
|
147 | 148 | @NotAnonymous() |
|
148 | 149 | @view_config( |
|
149 | 150 | route_name='my_account_auth_tokens', request_method='GET', |
|
150 | 151 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
151 | 152 | def my_account_auth_tokens(self): |
|
152 | 153 | _ = self.request.translate |
|
153 | 154 | |
|
154 | 155 | c = self.load_default_context() |
|
155 | 156 | c.active = 'auth_tokens' |
|
156 | 157 | c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_) |
|
157 | 158 | c.role_values = [ |
|
158 | 159 | (x, AuthTokenModel.cls._get_role_name(x)) |
|
159 | 160 | for x in AuthTokenModel.cls.ROLES] |
|
160 | 161 | c.role_options = [(c.role_values, _("Role"))] |
|
161 | 162 | c.user_auth_tokens = AuthTokenModel().get_auth_tokens( |
|
162 | 163 | c.user.user_id, show_expired=True) |
|
163 | 164 | c.role_vcs = AuthTokenModel.cls.ROLE_VCS |
|
164 | 165 | return self._get_template_context(c) |
|
165 | 166 | |
|
166 | 167 | def maybe_attach_token_scope(self, token): |
|
167 | 168 | # implemented in EE edition |
|
168 | 169 | pass |
|
169 | 170 | |
|
170 | 171 | @LoginRequired() |
|
171 | 172 | @NotAnonymous() |
|
172 | 173 | @CSRFRequired() |
|
173 | 174 | @view_config( |
|
174 | 175 | route_name='my_account_auth_tokens_add', request_method='POST',) |
|
175 | 176 | def my_account_auth_tokens_add(self): |
|
176 | 177 | _ = self.request.translate |
|
177 | 178 | c = self.load_default_context() |
|
178 | 179 | |
|
179 | 180 | lifetime = safe_int(self.request.POST.get('lifetime'), -1) |
|
180 | 181 | description = self.request.POST.get('description') |
|
181 | 182 | role = self.request.POST.get('role') |
|
182 | 183 | |
|
183 | 184 | token = UserModel().add_auth_token( |
|
184 | 185 | user=c.user.user_id, |
|
185 | 186 | lifetime_minutes=lifetime, role=role, description=description, |
|
186 | 187 | scope_callback=self.maybe_attach_token_scope) |
|
187 | 188 | token_data = token.get_api_data() |
|
188 | 189 | |
|
189 | 190 | audit_logger.store_web( |
|
190 | 191 | 'user.edit.token.add', action_data={ |
|
191 | 192 | 'data': {'token': token_data, 'user': 'self'}}, |
|
192 | 193 | user=self._rhodecode_user, ) |
|
193 | 194 | Session().commit() |
|
194 | 195 | |
|
195 | 196 | h.flash(_("Auth token successfully created"), category='success') |
|
196 | 197 | return HTTPFound(h.route_path('my_account_auth_tokens')) |
|
197 | 198 | |
|
198 | 199 | @LoginRequired() |
|
199 | 200 | @NotAnonymous() |
|
200 | 201 | @CSRFRequired() |
|
201 | 202 | @view_config( |
|
202 | 203 | route_name='my_account_auth_tokens_delete', request_method='POST') |
|
203 | 204 | def my_account_auth_tokens_delete(self): |
|
204 | 205 | _ = self.request.translate |
|
205 | 206 | c = self.load_default_context() |
|
206 | 207 | |
|
207 | 208 | del_auth_token = self.request.POST.get('del_auth_token') |
|
208 | 209 | |
|
209 | 210 | if del_auth_token: |
|
210 | 211 | token = UserApiKeys.get_or_404(del_auth_token) |
|
211 | 212 | token_data = token.get_api_data() |
|
212 | 213 | |
|
213 | 214 | AuthTokenModel().delete(del_auth_token, c.user.user_id) |
|
214 | 215 | audit_logger.store_web( |
|
215 | 216 | 'user.edit.token.delete', action_data={ |
|
216 | 217 | 'data': {'token': token_data, 'user': 'self'}}, |
|
217 | 218 | user=self._rhodecode_user,) |
|
218 | 219 | Session().commit() |
|
219 | 220 | h.flash(_("Auth token successfully deleted"), category='success') |
|
220 | 221 | |
|
221 | 222 | return HTTPFound(h.route_path('my_account_auth_tokens')) |
|
222 | 223 | |
|
223 | 224 | @LoginRequired() |
|
224 | 225 | @NotAnonymous() |
|
225 | 226 | @view_config( |
|
226 | 227 | route_name='my_account_emails', request_method='GET', |
|
227 | 228 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
228 | 229 | def my_account_emails(self): |
|
229 | 230 | _ = self.request.translate |
|
230 | 231 | |
|
231 | 232 | c = self.load_default_context() |
|
232 | 233 | c.active = 'emails' |
|
233 | 234 | |
|
234 | 235 | c.user_email_map = UserEmailMap.query()\ |
|
235 | 236 | .filter(UserEmailMap.user == c.user).all() |
|
236 | 237 | |
|
237 | 238 | schema = user_schema.AddEmailSchema().bind( |
|
238 | 239 | username=c.user.username, user_emails=c.user.emails) |
|
239 | 240 | |
|
240 | 241 | form = forms.RcForm(schema, |
|
241 | 242 | action=h.route_path('my_account_emails_add'), |
|
242 | 243 | buttons=(forms.buttons.save, forms.buttons.reset)) |
|
243 | 244 | |
|
244 | 245 | c.form = form |
|
245 | 246 | return self._get_template_context(c) |
|
246 | 247 | |
|
247 | 248 | @LoginRequired() |
|
248 | 249 | @NotAnonymous() |
|
249 | 250 | @CSRFRequired() |
|
250 | 251 | @view_config( |
|
251 | 252 | route_name='my_account_emails_add', request_method='POST', |
|
252 | 253 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
253 | 254 | def my_account_emails_add(self): |
|
254 | 255 | _ = self.request.translate |
|
255 | 256 | c = self.load_default_context() |
|
256 | 257 | c.active = 'emails' |
|
257 | 258 | |
|
258 | 259 | schema = user_schema.AddEmailSchema().bind( |
|
259 | 260 | username=c.user.username, user_emails=c.user.emails) |
|
260 | 261 | |
|
261 | 262 | form = forms.RcForm( |
|
262 | 263 | schema, action=h.route_path('my_account_emails_add'), |
|
263 | 264 | buttons=(forms.buttons.save, forms.buttons.reset)) |
|
264 | 265 | |
|
265 | 266 | controls = self.request.POST.items() |
|
266 | 267 | try: |
|
267 | 268 | valid_data = form.validate(controls) |
|
268 | 269 | UserModel().add_extra_email(c.user.user_id, valid_data['email']) |
|
269 | 270 | audit_logger.store_web( |
|
270 | 271 | 'user.edit.email.add', action_data={ |
|
271 | 272 | 'data': {'email': valid_data['email'], 'user': 'self'}}, |
|
272 | 273 | user=self._rhodecode_user,) |
|
273 | 274 | Session().commit() |
|
274 | 275 | except formencode.Invalid as error: |
|
275 | 276 | h.flash(h.escape(error.error_dict['email']), category='error') |
|
276 | 277 | except forms.ValidationFailure as e: |
|
277 | 278 | c.user_email_map = UserEmailMap.query() \ |
|
278 | 279 | .filter(UserEmailMap.user == c.user).all() |
|
279 | 280 | c.form = e |
|
280 | 281 | return self._get_template_context(c) |
|
281 | 282 | except Exception: |
|
282 | 283 | log.exception("Exception adding email") |
|
283 | 284 | h.flash(_('Error occurred during adding email'), |
|
284 | 285 | category='error') |
|
285 | 286 | else: |
|
286 | 287 | h.flash(_("Successfully added email"), category='success') |
|
287 | 288 | |
|
288 | 289 | raise HTTPFound(self.request.route_path('my_account_emails')) |
|
289 | 290 | |
|
290 | 291 | @LoginRequired() |
|
291 | 292 | @NotAnonymous() |
|
292 | 293 | @CSRFRequired() |
|
293 | 294 | @view_config( |
|
294 | 295 | route_name='my_account_emails_delete', request_method='POST') |
|
295 | 296 | def my_account_emails_delete(self): |
|
296 | 297 | _ = self.request.translate |
|
297 | 298 | c = self.load_default_context() |
|
298 | 299 | |
|
299 | 300 | del_email_id = self.request.POST.get('del_email_id') |
|
300 | 301 | if del_email_id: |
|
301 | 302 | email = UserEmailMap.get_or_404(del_email_id).email |
|
302 | 303 | UserModel().delete_extra_email(c.user.user_id, del_email_id) |
|
303 | 304 | audit_logger.store_web( |
|
304 | 305 | 'user.edit.email.delete', action_data={ |
|
305 | 306 | 'data': {'email': email, 'user': 'self'}}, |
|
306 | 307 | user=self._rhodecode_user,) |
|
307 | 308 | Session().commit() |
|
308 | 309 | h.flash(_("Email successfully deleted"), |
|
309 | 310 | category='success') |
|
310 | 311 | return HTTPFound(h.route_path('my_account_emails')) |
|
311 | 312 | |
|
312 | 313 | @LoginRequired() |
|
313 | 314 | @NotAnonymous() |
|
314 | 315 | @CSRFRequired() |
|
315 | 316 | @view_config( |
|
316 | 317 | route_name='my_account_notifications_test_channelstream', |
|
317 | 318 | request_method='POST', renderer='json_ext') |
|
318 | 319 | def my_account_notifications_test_channelstream(self): |
|
319 | 320 | message = 'Test message sent via Channelstream by user: {}, on {}'.format( |
|
320 | 321 | self._rhodecode_user.username, datetime.datetime.now()) |
|
321 | 322 | payload = { |
|
322 | 323 | # 'channel': 'broadcast', |
|
323 | 324 | 'type': 'message', |
|
324 | 325 | 'timestamp': datetime.datetime.utcnow(), |
|
325 | 326 | 'user': 'system', |
|
326 | 327 | 'pm_users': [self._rhodecode_user.username], |
|
327 | 328 | 'message': { |
|
328 | 329 | 'message': message, |
|
329 | 330 | 'level': 'info', |
|
330 | 331 | 'topic': '/notifications' |
|
331 | 332 | } |
|
332 | 333 | } |
|
333 | 334 | |
|
334 | 335 | registry = self.request.registry |
|
335 | 336 | rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) |
|
336 | 337 | channelstream_config = rhodecode_plugins.get('channelstream', {}) |
|
337 | 338 | |
|
338 | 339 | try: |
|
339 | 340 | channelstream_request(channelstream_config, [payload], '/message') |
|
340 | 341 | except ChannelstreamException as e: |
|
341 | 342 | log.exception('Failed to send channelstream data') |
|
342 | 343 | return {"response": 'ERROR: {}'.format(e.__class__.__name__)} |
|
343 | 344 | return {"response": 'Channelstream data sent. ' |
|
344 | 345 | 'You should see a new live message now.'} |
|
345 | 346 | |
|
346 | 347 | def _load_my_repos_data(self, watched=False): |
|
347 | 348 | if watched: |
|
348 | 349 | admin = False |
|
349 | 350 | follows_repos = Session().query(UserFollowing)\ |
|
350 | 351 | .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ |
|
351 | 352 | .options(joinedload(UserFollowing.follows_repository))\ |
|
352 | 353 | .all() |
|
353 | 354 | repo_list = [x.follows_repository for x in follows_repos] |
|
354 | 355 | else: |
|
355 | 356 | admin = True |
|
356 | 357 | repo_list = Repository.get_all_repos( |
|
357 | 358 | user_id=self._rhodecode_user.user_id) |
|
358 | 359 | repo_list = RepoList(repo_list, perm_set=[ |
|
359 | 360 | 'repository.read', 'repository.write', 'repository.admin']) |
|
360 | 361 | |
|
361 | 362 | repos_data = RepoModel().get_repos_as_dict( |
|
362 | 363 | repo_list=repo_list, admin=admin) |
|
363 | 364 | # json used to render the grid |
|
364 | 365 | return json.dumps(repos_data) |
|
365 | 366 | |
|
366 | 367 | @LoginRequired() |
|
367 | 368 | @NotAnonymous() |
|
368 | 369 | @view_config( |
|
369 | 370 | route_name='my_account_repos', request_method='GET', |
|
370 | 371 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
371 | 372 | def my_account_repos(self): |
|
372 | 373 | c = self.load_default_context() |
|
373 | 374 | c.active = 'repos' |
|
374 | 375 | |
|
375 | 376 | # json used to render the grid |
|
376 | 377 | c.data = self._load_my_repos_data() |
|
377 | 378 | return self._get_template_context(c) |
|
378 | 379 | |
|
379 | 380 | @LoginRequired() |
|
380 | 381 | @NotAnonymous() |
|
381 | 382 | @view_config( |
|
382 | 383 | route_name='my_account_watched', request_method='GET', |
|
383 | 384 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
384 | 385 | def my_account_watched(self): |
|
385 | 386 | c = self.load_default_context() |
|
386 | 387 | c.active = 'watched' |
|
387 | 388 | |
|
388 | 389 | # json used to render the grid |
|
389 | 390 | c.data = self._load_my_repos_data(watched=True) |
|
390 | 391 | return self._get_template_context(c) |
|
391 | 392 | |
|
392 | 393 | @LoginRequired() |
|
393 | 394 | @NotAnonymous() |
|
394 | 395 | @view_config( |
|
396 | route_name='my_account_bookmarks', request_method='GET', | |
|
397 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |
|
398 | def my_account_bookmarks(self): | |
|
399 | c = self.load_default_context() | |
|
400 | c.active = 'bookmarks' | |
|
401 | return self._get_template_context(c) | |
|
402 | ||
|
403 | def _process_entry(self, entry, user_id): | |
|
404 | position = safe_int(entry.get('position')) | |
|
405 | if position is None: | |
|
406 | return | |
|
407 | ||
|
408 | # check if this is an existing entry | |
|
409 | is_new = False | |
|
410 | db_entry = UserBookmark().get_by_position_for_user(position, user_id) | |
|
411 | ||
|
412 | if db_entry and str2bool(entry.get('remove')): | |
|
413 | log.debug('Marked bookmark %s for deletion', db_entry) | |
|
414 | Session().delete(db_entry) | |
|
415 | return | |
|
416 | ||
|
417 | if not db_entry: | |
|
418 | # new | |
|
419 | db_entry = UserBookmark() | |
|
420 | is_new = True | |
|
421 | ||
|
422 | should_save = False | |
|
423 | default_redirect_url = '' | |
|
424 | ||
|
425 | # save repo | |
|
426 | if entry.get('bookmark_repo'): | |
|
427 | repo = Repository.get(entry['bookmark_repo']) | |
|
428 | perm_check = HasRepoPermissionAny( | |
|
429 | 'repository.read', 'repository.write', 'repository.admin') | |
|
430 | if repo and perm_check(repo_name=repo.repo_name): | |
|
431 | db_entry.repository = repo | |
|
432 | should_save = True | |
|
433 | default_redirect_url = '${repo_url}' | |
|
434 | # save repo group | |
|
435 | elif entry.get('bookmark_repo_group'): | |
|
436 | repo_group = RepoGroup.get(entry['bookmark_repo_group']) | |
|
437 | perm_check = HasRepoGroupPermissionAny( | |
|
438 | 'group.read', 'group.write', 'group.admin') | |
|
439 | ||
|
440 | if repo_group and perm_check(group_name=repo_group.group_name): | |
|
441 | db_entry.repository_group = repo_group | |
|
442 | should_save = True | |
|
443 | default_redirect_url = '${repo_group_url}' | |
|
444 | # save generic info | |
|
445 | elif entry.get('title') and entry.get('redirect_url'): | |
|
446 | should_save = True | |
|
447 | ||
|
448 | if should_save: | |
|
449 | log.debug('Saving bookmark %s, new:%s', db_entry, is_new) | |
|
450 | # mark user and position | |
|
451 | db_entry.user_id = user_id | |
|
452 | db_entry.position = position | |
|
453 | db_entry.title = entry.get('title') | |
|
454 | db_entry.redirect_url = entry.get('redirect_url') or default_redirect_url | |
|
455 | ||
|
456 | Session().add(db_entry) | |
|
457 | ||
|
458 | @LoginRequired() | |
|
459 | @NotAnonymous() | |
|
460 | @CSRFRequired() | |
|
461 | @view_config( | |
|
462 | route_name='my_account_bookmarks_update', request_method='POST') | |
|
463 | def my_account_bookmarks_update(self): | |
|
464 | _ = self.request.translate | |
|
465 | c = self.load_default_context() | |
|
466 | c.active = 'bookmarks' | |
|
467 | ||
|
468 | controls = peppercorn.parse(self.request.POST.items()) | |
|
469 | user_id = c.user.user_id | |
|
470 | ||
|
471 | try: | |
|
472 | for entry in controls.get('bookmarks', []): | |
|
473 | self._process_entry(entry, user_id) | |
|
474 | ||
|
475 | Session().commit() | |
|
476 | h.flash(_("Update Bookmarks"), category='success') | |
|
477 | except IntegrityError: | |
|
478 | h.flash(_("Failed to update bookmarks. " | |
|
479 | "Make sure an unique position is used"), category='error') | |
|
480 | ||
|
481 | return HTTPFound(h.route_path('my_account_bookmarks')) | |
|
482 | ||
|
483 | @LoginRequired() | |
|
484 | @NotAnonymous() | |
|
485 | @view_config( | |
|
486 | route_name='my_account_goto_bookmark', request_method='GET', | |
|
487 | renderer='rhodecode:templates/admin/my_account/my_account.mako') | |
|
488 | def my_account_goto_bookmark(self): | |
|
489 | ||
|
490 | bookmark_id = self.request.matchdict['bookmark_id'] | |
|
491 | user_bookmark = UserBookmark().query()\ | |
|
492 | .filter(UserBookmark.user_id == self.request.user.user_id) \ | |
|
493 | .filter(UserBookmark.position == bookmark_id).scalar() | |
|
494 | ||
|
495 | redirect_url = h.route_path('my_account_bookmarks') | |
|
496 | if not user_bookmark: | |
|
497 | raise HTTPFound(redirect_url) | |
|
498 | ||
|
499 | if user_bookmark.repository: | |
|
500 | repo_name = user_bookmark.repository.repo_name | |
|
501 | base_redirect_url = h.route_path( | |
|
502 | 'repo_summary', repo_name=repo_name) | |
|
503 | if user_bookmark.redirect_url and \ | |
|
504 | '${repo_url}' in user_bookmark.redirect_url: | |
|
505 | redirect_url = string.Template(user_bookmark.redirect_url)\ | |
|
506 | .safe_substitute({'repo_url': base_redirect_url}) | |
|
507 | else: | |
|
508 | redirect_url = base_redirect_url | |
|
509 | ||
|
510 | elif user_bookmark.repository_group: | |
|
511 | repo_group_name = user_bookmark.repository_group.group_name | |
|
512 | base_redirect_url = h.route_path( | |
|
513 | 'repo_group_home', repo_group_name=repo_group_name) | |
|
514 | if user_bookmark.redirect_url and \ | |
|
515 | '${repo_group_url}' in user_bookmark.redirect_url: | |
|
516 | redirect_url = string.Template(user_bookmark.redirect_url)\ | |
|
517 | .safe_substitute({'repo_group_url': base_redirect_url}) | |
|
518 | else: | |
|
519 | redirect_url = base_redirect_url | |
|
520 | ||
|
521 | elif user_bookmark.redirect_url: | |
|
522 | redirect_url = user_bookmark.redirect_url | |
|
523 | ||
|
524 | log.debug('Redirecting bookmark %s to %s', user_bookmark, redirect_url) | |
|
525 | raise HTTPFound(redirect_url) | |
|
526 | ||
|
527 | @LoginRequired() | |
|
528 | @NotAnonymous() | |
|
529 | @view_config( | |
|
395 | 530 | route_name='my_account_perms', request_method='GET', |
|
396 | 531 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
397 | 532 | def my_account_perms(self): |
|
398 | 533 | c = self.load_default_context() |
|
399 | 534 | c.active = 'perms' |
|
400 | 535 | |
|
401 | 536 | c.perm_user = c.auth_user |
|
402 | 537 | return self._get_template_context(c) |
|
403 | 538 | |
|
404 | 539 | @LoginRequired() |
|
405 | 540 | @NotAnonymous() |
|
406 | 541 | @view_config( |
|
407 | 542 | route_name='my_account_notifications', request_method='GET', |
|
408 | 543 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
409 | 544 | def my_notifications(self): |
|
410 | 545 | c = self.load_default_context() |
|
411 | 546 | c.active = 'notifications' |
|
412 | 547 | |
|
413 | 548 | return self._get_template_context(c) |
|
414 | 549 | |
|
415 | 550 | @LoginRequired() |
|
416 | 551 | @NotAnonymous() |
|
417 | 552 | @CSRFRequired() |
|
418 | 553 | @view_config( |
|
419 | 554 | route_name='my_account_notifications_toggle_visibility', |
|
420 | 555 | request_method='POST', renderer='json_ext') |
|
421 | 556 | def my_notifications_toggle_visibility(self): |
|
422 | 557 | user = self._rhodecode_db_user |
|
423 | 558 | new_status = not user.user_data.get('notification_status', True) |
|
424 | 559 | user.update_userdata(notification_status=new_status) |
|
425 | 560 | Session().commit() |
|
426 | 561 | return user.user_data['notification_status'] |
|
427 | 562 | |
|
428 | 563 | @LoginRequired() |
|
429 | 564 | @NotAnonymous() |
|
430 | 565 | @view_config( |
|
431 | 566 | route_name='my_account_edit', |
|
432 | 567 | request_method='GET', |
|
433 | 568 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
434 | 569 | def my_account_edit(self): |
|
435 | 570 | c = self.load_default_context() |
|
436 | 571 | c.active = 'profile_edit' |
|
437 | 572 | c.extern_type = c.user.extern_type |
|
438 | 573 | c.extern_name = c.user.extern_name |
|
439 | 574 | |
|
440 | 575 | schema = user_schema.UserProfileSchema().bind( |
|
441 | 576 | username=c.user.username, user_emails=c.user.emails) |
|
442 | 577 | appstruct = { |
|
443 | 578 | 'username': c.user.username, |
|
444 | 579 | 'email': c.user.email, |
|
445 | 580 | 'firstname': c.user.firstname, |
|
446 | 581 | 'lastname': c.user.lastname, |
|
447 | 582 | } |
|
448 | 583 | c.form = forms.RcForm( |
|
449 | 584 | schema, appstruct=appstruct, |
|
450 | 585 | action=h.route_path('my_account_update'), |
|
451 | 586 | buttons=(forms.buttons.save, forms.buttons.reset)) |
|
452 | 587 | |
|
453 | 588 | return self._get_template_context(c) |
|
454 | 589 | |
|
455 | 590 | @LoginRequired() |
|
456 | 591 | @NotAnonymous() |
|
457 | 592 | @CSRFRequired() |
|
458 | 593 | @view_config( |
|
459 | 594 | route_name='my_account_update', |
|
460 | 595 | request_method='POST', |
|
461 | 596 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
462 | 597 | def my_account_update(self): |
|
463 | 598 | _ = self.request.translate |
|
464 | 599 | c = self.load_default_context() |
|
465 | 600 | c.active = 'profile_edit' |
|
466 | 601 | c.perm_user = c.auth_user |
|
467 | 602 | c.extern_type = c.user.extern_type |
|
468 | 603 | c.extern_name = c.user.extern_name |
|
469 | 604 | |
|
470 | 605 | schema = user_schema.UserProfileSchema().bind( |
|
471 | 606 | username=c.user.username, user_emails=c.user.emails) |
|
472 | 607 | form = forms.RcForm( |
|
473 | 608 | schema, buttons=(forms.buttons.save, forms.buttons.reset)) |
|
474 | 609 | |
|
475 | 610 | controls = self.request.POST.items() |
|
476 | 611 | try: |
|
477 | 612 | valid_data = form.validate(controls) |
|
478 | 613 | skip_attrs = ['admin', 'active', 'extern_type', 'extern_name', |
|
479 | 614 | 'new_password', 'password_confirmation'] |
|
480 | 615 | if c.extern_type != "rhodecode": |
|
481 | 616 | # forbid updating username for external accounts |
|
482 | 617 | skip_attrs.append('username') |
|
483 | 618 | old_email = c.user.email |
|
484 | 619 | UserModel().update_user( |
|
485 | 620 | self._rhodecode_user.user_id, skip_attrs=skip_attrs, |
|
486 | 621 | **valid_data) |
|
487 | 622 | if old_email != valid_data['email']: |
|
488 | 623 | old = UserEmailMap.query() \ |
|
489 | 624 | .filter(UserEmailMap.user == c.user).filter(UserEmailMap.email == valid_data['email']).first() |
|
490 | 625 | old.email = old_email |
|
491 | 626 | h.flash(_('Your account was updated successfully'), category='success') |
|
492 | 627 | Session().commit() |
|
493 | 628 | except forms.ValidationFailure as e: |
|
494 | 629 | c.form = e |
|
495 | 630 | return self._get_template_context(c) |
|
496 | 631 | except Exception: |
|
497 | 632 | log.exception("Exception updating user") |
|
498 | 633 | h.flash(_('Error occurred during update of user'), |
|
499 | 634 | category='error') |
|
500 | 635 | raise HTTPFound(h.route_path('my_account_profile')) |
|
501 | 636 | |
|
502 | 637 | def _get_pull_requests_list(self, statuses): |
|
503 | 638 | draw, start, limit = self._extract_chunk(self.request) |
|
504 | 639 | search_q, order_by, order_dir = self._extract_ordering(self.request) |
|
505 | 640 | _render = self.request.get_partial_renderer( |
|
506 | 641 | 'rhodecode:templates/data_table/_dt_elements.mako') |
|
507 | 642 | |
|
508 | 643 | pull_requests = PullRequestModel().get_im_participating_in( |
|
509 | 644 | user_id=self._rhodecode_user.user_id, |
|
510 | 645 | statuses=statuses, |
|
511 | 646 | offset=start, length=limit, order_by=order_by, |
|
512 | 647 | order_dir=order_dir) |
|
513 | 648 | |
|
514 | 649 | pull_requests_total_count = PullRequestModel().count_im_participating_in( |
|
515 | 650 | user_id=self._rhodecode_user.user_id, statuses=statuses) |
|
516 | 651 | |
|
517 | 652 | data = [] |
|
518 | 653 | comments_model = CommentsModel() |
|
519 | 654 | for pr in pull_requests: |
|
520 | 655 | repo_id = pr.target_repo_id |
|
521 | 656 | comments = comments_model.get_all_comments( |
|
522 | 657 | repo_id, pull_request=pr) |
|
523 | 658 | owned = pr.user_id == self._rhodecode_user.user_id |
|
524 | 659 | |
|
525 | 660 | data.append({ |
|
526 | 661 | 'target_repo': _render('pullrequest_target_repo', |
|
527 | 662 | pr.target_repo.repo_name), |
|
528 | 663 | 'name': _render('pullrequest_name', |
|
529 | 664 | pr.pull_request_id, pr.target_repo.repo_name, |
|
530 | 665 | short=True), |
|
531 | 666 | 'name_raw': pr.pull_request_id, |
|
532 | 667 | 'status': _render('pullrequest_status', |
|
533 | 668 | pr.calculated_review_status()), |
|
534 | 669 | 'title': _render( |
|
535 | 670 | 'pullrequest_title', pr.title, pr.description), |
|
536 | 671 | 'description': h.escape(pr.description), |
|
537 | 672 | 'updated_on': _render('pullrequest_updated_on', |
|
538 | 673 | h.datetime_to_time(pr.updated_on)), |
|
539 | 674 | 'updated_on_raw': h.datetime_to_time(pr.updated_on), |
|
540 | 675 | 'created_on': _render('pullrequest_updated_on', |
|
541 | 676 | h.datetime_to_time(pr.created_on)), |
|
542 | 677 | 'created_on_raw': h.datetime_to_time(pr.created_on), |
|
543 | 678 | 'author': _render('pullrequest_author', |
|
544 | 679 | pr.author.full_contact, ), |
|
545 | 680 | 'author_raw': pr.author.full_name, |
|
546 | 681 | 'comments': _render('pullrequest_comments', len(comments)), |
|
547 | 682 | 'comments_raw': len(comments), |
|
548 | 683 | 'closed': pr.is_closed(), |
|
549 | 684 | 'owned': owned |
|
550 | 685 | }) |
|
551 | 686 | |
|
552 | 687 | # json used to render the grid |
|
553 | 688 | data = ({ |
|
554 | 689 | 'draw': draw, |
|
555 | 690 | 'data': data, |
|
556 | 691 | 'recordsTotal': pull_requests_total_count, |
|
557 | 692 | 'recordsFiltered': pull_requests_total_count, |
|
558 | 693 | }) |
|
559 | 694 | return data |
|
560 | 695 | |
|
561 | 696 | @LoginRequired() |
|
562 | 697 | @NotAnonymous() |
|
563 | 698 | @view_config( |
|
564 | 699 | route_name='my_account_pullrequests', |
|
565 | 700 | request_method='GET', |
|
566 | 701 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
567 | 702 | def my_account_pullrequests(self): |
|
568 | 703 | c = self.load_default_context() |
|
569 | 704 | c.active = 'pullrequests' |
|
570 | 705 | req_get = self.request.GET |
|
571 | 706 | |
|
572 | 707 | c.closed = str2bool(req_get.get('pr_show_closed')) |
|
573 | 708 | |
|
574 | 709 | return self._get_template_context(c) |
|
575 | 710 | |
|
576 | 711 | @LoginRequired() |
|
577 | 712 | @NotAnonymous() |
|
578 | 713 | @view_config( |
|
579 | 714 | route_name='my_account_pullrequests_data', |
|
580 | 715 | request_method='GET', renderer='json_ext') |
|
581 | 716 | def my_account_pullrequests_data(self): |
|
582 | 717 | self.load_default_context() |
|
583 | 718 | req_get = self.request.GET |
|
584 | 719 | closed = str2bool(req_get.get('closed')) |
|
585 | 720 | |
|
586 | 721 | statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN] |
|
587 | 722 | if closed: |
|
588 | 723 | statuses += [PullRequest.STATUS_CLOSED] |
|
589 | 724 | |
|
590 | 725 | data = self._get_pull_requests_list(statuses=statuses) |
|
591 | 726 | return data |
|
592 | 727 | |
|
593 | 728 | @LoginRequired() |
|
594 | 729 | @NotAnonymous() |
|
595 | 730 | @view_config( |
|
596 | 731 | route_name='my_account_user_group_membership', |
|
597 | 732 | request_method='GET', |
|
598 | 733 | renderer='rhodecode:templates/admin/my_account/my_account.mako') |
|
599 | 734 | def my_account_user_group_membership(self): |
|
600 | 735 | c = self.load_default_context() |
|
601 | 736 | c.active = 'user_group_membership' |
|
602 | 737 | groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) |
|
603 | 738 | for group in self._rhodecode_db_user.group_member] |
|
604 | 739 | c.user_groups = json.dumps(groups) |
|
605 | 740 | return self._get_template_context(c) |
@@ -1,576 +1,582 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | The base Controller API |
|
23 | 23 | Provides the BaseController class for subclassing. And usage in different |
|
24 | 24 | controllers |
|
25 | 25 | """ |
|
26 | 26 | |
|
27 | 27 | import logging |
|
28 | 28 | import socket |
|
29 | 29 | |
|
30 | 30 | import markupsafe |
|
31 | 31 | import ipaddress |
|
32 | 32 | |
|
33 | 33 | from paste.auth.basic import AuthBasicAuthenticator |
|
34 | 34 | from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception |
|
35 | 35 | from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION |
|
36 | 36 | |
|
37 | 37 | import rhodecode |
|
38 | 38 | from rhodecode.apps._base import TemplateArgs |
|
39 | 39 | from rhodecode.authentication.base import VCS_TYPE |
|
40 | 40 | from rhodecode.lib import auth, utils2 |
|
41 | 41 | from rhodecode.lib import helpers as h |
|
42 | 42 | from rhodecode.lib.auth import AuthUser, CookieStoreWrapper |
|
43 | 43 | from rhodecode.lib.exceptions import UserCreationError |
|
44 | 44 | from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes) |
|
45 | 45 | from rhodecode.lib.utils2 import ( |
|
46 | 46 | str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str) |
|
47 | from rhodecode.model.db import Repository, User, ChangesetComment | |
|
47 | from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark | |
|
48 | 48 | from rhodecode.model.notification import NotificationModel |
|
49 | 49 | from rhodecode.model.settings import VcsSettingsModel, SettingsModel |
|
50 | 50 | |
|
51 | 51 | log = logging.getLogger(__name__) |
|
52 | 52 | |
|
53 | 53 | |
|
54 | 54 | def _filter_proxy(ip): |
|
55 | 55 | """ |
|
56 | 56 | Passed in IP addresses in HEADERS can be in a special format of multiple |
|
57 | 57 | ips. Those comma separated IPs are passed from various proxies in the |
|
58 | 58 | chain of request processing. The left-most being the original client. |
|
59 | 59 | We only care about the first IP which came from the org. client. |
|
60 | 60 | |
|
61 | 61 | :param ip: ip string from headers |
|
62 | 62 | """ |
|
63 | 63 | if ',' in ip: |
|
64 | 64 | _ips = ip.split(',') |
|
65 | 65 | _first_ip = _ips[0].strip() |
|
66 | 66 | log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip) |
|
67 | 67 | return _first_ip |
|
68 | 68 | return ip |
|
69 | 69 | |
|
70 | 70 | |
|
71 | 71 | def _filter_port(ip): |
|
72 | 72 | """ |
|
73 | 73 | Removes a port from ip, there are 4 main cases to handle here. |
|
74 | 74 | - ipv4 eg. 127.0.0.1 |
|
75 | 75 | - ipv6 eg. ::1 |
|
76 | 76 | - ipv4+port eg. 127.0.0.1:8080 |
|
77 | 77 | - ipv6+port eg. [::1]:8080 |
|
78 | 78 | |
|
79 | 79 | :param ip: |
|
80 | 80 | """ |
|
81 | 81 | def is_ipv6(ip_addr): |
|
82 | 82 | if hasattr(socket, 'inet_pton'): |
|
83 | 83 | try: |
|
84 | 84 | socket.inet_pton(socket.AF_INET6, ip_addr) |
|
85 | 85 | except socket.error: |
|
86 | 86 | return False |
|
87 | 87 | else: |
|
88 | 88 | # fallback to ipaddress |
|
89 | 89 | try: |
|
90 | 90 | ipaddress.IPv6Address(safe_unicode(ip_addr)) |
|
91 | 91 | except Exception: |
|
92 | 92 | return False |
|
93 | 93 | return True |
|
94 | 94 | |
|
95 | 95 | if ':' not in ip: # must be ipv4 pure ip |
|
96 | 96 | return ip |
|
97 | 97 | |
|
98 | 98 | if '[' in ip and ']' in ip: # ipv6 with port |
|
99 | 99 | return ip.split(']')[0][1:].lower() |
|
100 | 100 | |
|
101 | 101 | # must be ipv6 or ipv4 with port |
|
102 | 102 | if is_ipv6(ip): |
|
103 | 103 | return ip |
|
104 | 104 | else: |
|
105 | 105 | ip, _port = ip.split(':')[:2] # means ipv4+port |
|
106 | 106 | return ip |
|
107 | 107 | |
|
108 | 108 | |
|
109 | 109 | def get_ip_addr(environ): |
|
110 | 110 | proxy_key = 'HTTP_X_REAL_IP' |
|
111 | 111 | proxy_key2 = 'HTTP_X_FORWARDED_FOR' |
|
112 | 112 | def_key = 'REMOTE_ADDR' |
|
113 | 113 | _filters = lambda x: _filter_port(_filter_proxy(x)) |
|
114 | 114 | |
|
115 | 115 | ip = environ.get(proxy_key) |
|
116 | 116 | if ip: |
|
117 | 117 | return _filters(ip) |
|
118 | 118 | |
|
119 | 119 | ip = environ.get(proxy_key2) |
|
120 | 120 | if ip: |
|
121 | 121 | return _filters(ip) |
|
122 | 122 | |
|
123 | 123 | ip = environ.get(def_key, '0.0.0.0') |
|
124 | 124 | return _filters(ip) |
|
125 | 125 | |
|
126 | 126 | |
|
127 | 127 | def get_server_ip_addr(environ, log_errors=True): |
|
128 | 128 | hostname = environ.get('SERVER_NAME') |
|
129 | 129 | try: |
|
130 | 130 | return socket.gethostbyname(hostname) |
|
131 | 131 | except Exception as e: |
|
132 | 132 | if log_errors: |
|
133 | 133 | # in some cases this lookup is not possible, and we don't want to |
|
134 | 134 | # make it an exception in logs |
|
135 | 135 | log.exception('Could not retrieve server ip address: %s', e) |
|
136 | 136 | return hostname |
|
137 | 137 | |
|
138 | 138 | |
|
139 | 139 | def get_server_port(environ): |
|
140 | 140 | return environ.get('SERVER_PORT') |
|
141 | 141 | |
|
142 | 142 | |
|
143 | 143 | def get_access_path(environ): |
|
144 | 144 | path = environ.get('PATH_INFO') |
|
145 | 145 | org_req = environ.get('pylons.original_request') |
|
146 | 146 | if org_req: |
|
147 | 147 | path = org_req.environ.get('PATH_INFO') |
|
148 | 148 | return path |
|
149 | 149 | |
|
150 | 150 | |
|
151 | 151 | def get_user_agent(environ): |
|
152 | 152 | return environ.get('HTTP_USER_AGENT') |
|
153 | 153 | |
|
154 | 154 | |
|
155 | 155 | def vcs_operation_context( |
|
156 | 156 | environ, repo_name, username, action, scm, check_locking=True, |
|
157 | 157 | is_shadow_repo=False, check_branch_perms=False, detect_force_push=False): |
|
158 | 158 | """ |
|
159 | 159 | Generate the context for a vcs operation, e.g. push or pull. |
|
160 | 160 | |
|
161 | 161 | This context is passed over the layers so that hooks triggered by the |
|
162 | 162 | vcs operation know details like the user, the user's IP address etc. |
|
163 | 163 | |
|
164 | 164 | :param check_locking: Allows to switch of the computation of the locking |
|
165 | 165 | data. This serves mainly the need of the simplevcs middleware to be |
|
166 | 166 | able to disable this for certain operations. |
|
167 | 167 | |
|
168 | 168 | """ |
|
169 | 169 | # Tri-state value: False: unlock, None: nothing, True: lock |
|
170 | 170 | make_lock = None |
|
171 | 171 | locked_by = [None, None, None] |
|
172 | 172 | is_anonymous = username == User.DEFAULT_USER |
|
173 | 173 | user = User.get_by_username(username) |
|
174 | 174 | if not is_anonymous and check_locking: |
|
175 | 175 | log.debug('Checking locking on repository "%s"', repo_name) |
|
176 | 176 | repo = Repository.get_by_repo_name(repo_name) |
|
177 | 177 | make_lock, __, locked_by = repo.get_locking_state( |
|
178 | 178 | action, user.user_id) |
|
179 | 179 | user_id = user.user_id |
|
180 | 180 | settings_model = VcsSettingsModel(repo=repo_name) |
|
181 | 181 | ui_settings = settings_model.get_ui_settings() |
|
182 | 182 | |
|
183 | 183 | # NOTE(marcink): This should be also in sync with |
|
184 | 184 | # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data |
|
185 | 185 | store = [x for x in ui_settings if x.key == '/'] |
|
186 | 186 | repo_store = '' |
|
187 | 187 | if store: |
|
188 | 188 | repo_store = store[0].value |
|
189 | 189 | |
|
190 | 190 | scm_data = { |
|
191 | 191 | 'ip': get_ip_addr(environ), |
|
192 | 192 | 'username': username, |
|
193 | 193 | 'user_id': user_id, |
|
194 | 194 | 'action': action, |
|
195 | 195 | 'repository': repo_name, |
|
196 | 196 | 'scm': scm, |
|
197 | 197 | 'config': rhodecode.CONFIG['__file__'], |
|
198 | 198 | 'repo_store': repo_store, |
|
199 | 199 | 'make_lock': make_lock, |
|
200 | 200 | 'locked_by': locked_by, |
|
201 | 201 | 'server_url': utils2.get_server_url(environ), |
|
202 | 202 | 'user_agent': get_user_agent(environ), |
|
203 | 203 | 'hooks': get_enabled_hook_classes(ui_settings), |
|
204 | 204 | 'is_shadow_repo': is_shadow_repo, |
|
205 | 205 | 'detect_force_push': detect_force_push, |
|
206 | 206 | 'check_branch_perms': check_branch_perms, |
|
207 | 207 | } |
|
208 | 208 | return scm_data |
|
209 | 209 | |
|
210 | 210 | |
|
211 | 211 | class BasicAuth(AuthBasicAuthenticator): |
|
212 | 212 | |
|
213 | 213 | def __init__(self, realm, authfunc, registry, auth_http_code=None, |
|
214 | 214 | initial_call_detection=False, acl_repo_name=None): |
|
215 | 215 | self.realm = realm |
|
216 | 216 | self.initial_call = initial_call_detection |
|
217 | 217 | self.authfunc = authfunc |
|
218 | 218 | self.registry = registry |
|
219 | 219 | self.acl_repo_name = acl_repo_name |
|
220 | 220 | self._rc_auth_http_code = auth_http_code |
|
221 | 221 | |
|
222 | 222 | def _get_response_from_code(self, http_code): |
|
223 | 223 | try: |
|
224 | 224 | return get_exception(safe_int(http_code)) |
|
225 | 225 | except Exception: |
|
226 | 226 | log.exception('Failed to fetch response for code %s', http_code) |
|
227 | 227 | return HTTPForbidden |
|
228 | 228 | |
|
229 | 229 | def get_rc_realm(self): |
|
230 | 230 | return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm')) |
|
231 | 231 | |
|
232 | 232 | def build_authentication(self): |
|
233 | 233 | head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) |
|
234 | 234 | if self._rc_auth_http_code and not self.initial_call: |
|
235 | 235 | # return alternative HTTP code if alternative http return code |
|
236 | 236 | # is specified in RhodeCode config, but ONLY if it's not the |
|
237 | 237 | # FIRST call |
|
238 | 238 | custom_response_klass = self._get_response_from_code( |
|
239 | 239 | self._rc_auth_http_code) |
|
240 | 240 | return custom_response_klass(headers=head) |
|
241 | 241 | return HTTPUnauthorized(headers=head) |
|
242 | 242 | |
|
243 | 243 | def authenticate(self, environ): |
|
244 | 244 | authorization = AUTHORIZATION(environ) |
|
245 | 245 | if not authorization: |
|
246 | 246 | return self.build_authentication() |
|
247 | 247 | (authmeth, auth) = authorization.split(' ', 1) |
|
248 | 248 | if 'basic' != authmeth.lower(): |
|
249 | 249 | return self.build_authentication() |
|
250 | 250 | auth = auth.strip().decode('base64') |
|
251 | 251 | _parts = auth.split(':', 1) |
|
252 | 252 | if len(_parts) == 2: |
|
253 | 253 | username, password = _parts |
|
254 | 254 | auth_data = self.authfunc( |
|
255 | 255 | username, password, environ, VCS_TYPE, |
|
256 | 256 | registry=self.registry, acl_repo_name=self.acl_repo_name) |
|
257 | 257 | if auth_data: |
|
258 | 258 | return {'username': username, 'auth_data': auth_data} |
|
259 | 259 | if username and password: |
|
260 | 260 | # we mark that we actually executed authentication once, at |
|
261 | 261 | # that point we can use the alternative auth code |
|
262 | 262 | self.initial_call = False |
|
263 | 263 | |
|
264 | 264 | return self.build_authentication() |
|
265 | 265 | |
|
266 | 266 | __call__ = authenticate |
|
267 | 267 | |
|
268 | 268 | |
|
269 | 269 | def calculate_version_hash(config): |
|
270 | 270 | return sha1( |
|
271 | 271 | config.get('beaker.session.secret', '') + |
|
272 | 272 | rhodecode.__version__)[:8] |
|
273 | 273 | |
|
274 | 274 | |
|
275 | 275 | def get_current_lang(request): |
|
276 | 276 | # NOTE(marcink): remove after pyramid move |
|
277 | 277 | try: |
|
278 | 278 | return translation.get_lang()[0] |
|
279 | 279 | except: |
|
280 | 280 | pass |
|
281 | 281 | |
|
282 | 282 | return getattr(request, '_LOCALE_', request.locale_name) |
|
283 | 283 | |
|
284 | 284 | |
|
285 | def attach_context_attributes(context, request, user_id): | |
|
285 | def attach_context_attributes(context, request, user_id=None): | |
|
286 | 286 | """ |
|
287 | 287 | Attach variables into template context called `c`. |
|
288 | 288 | """ |
|
289 | 289 | config = request.registry.settings |
|
290 | 290 | |
|
291 | 291 | |
|
292 | 292 | rc_config = SettingsModel().get_all_settings(cache=True) |
|
293 | 293 | |
|
294 | 294 | context.rhodecode_version = rhodecode.__version__ |
|
295 | 295 | context.rhodecode_edition = config.get('rhodecode.edition') |
|
296 | 296 | # unique secret + version does not leak the version but keep consistency |
|
297 | 297 | context.rhodecode_version_hash = calculate_version_hash(config) |
|
298 | 298 | |
|
299 | 299 | # Default language set for the incoming request |
|
300 | 300 | context.language = get_current_lang(request) |
|
301 | 301 | |
|
302 | 302 | # Visual options |
|
303 | 303 | context.visual = AttributeDict({}) |
|
304 | 304 | |
|
305 | 305 | # DB stored Visual Items |
|
306 | 306 | context.visual.show_public_icon = str2bool( |
|
307 | 307 | rc_config.get('rhodecode_show_public_icon')) |
|
308 | 308 | context.visual.show_private_icon = str2bool( |
|
309 | 309 | rc_config.get('rhodecode_show_private_icon')) |
|
310 | 310 | context.visual.stylify_metatags = str2bool( |
|
311 | 311 | rc_config.get('rhodecode_stylify_metatags')) |
|
312 | 312 | context.visual.dashboard_items = safe_int( |
|
313 | 313 | rc_config.get('rhodecode_dashboard_items', 100)) |
|
314 | 314 | context.visual.admin_grid_items = safe_int( |
|
315 | 315 | rc_config.get('rhodecode_admin_grid_items', 100)) |
|
316 | 316 | context.visual.show_revision_number = str2bool( |
|
317 | 317 | rc_config.get('rhodecode_show_revision_number', True)) |
|
318 | 318 | context.visual.show_sha_length = safe_int( |
|
319 | 319 | rc_config.get('rhodecode_show_sha_length', 100)) |
|
320 | 320 | context.visual.repository_fields = str2bool( |
|
321 | 321 | rc_config.get('rhodecode_repository_fields')) |
|
322 | 322 | context.visual.show_version = str2bool( |
|
323 | 323 | rc_config.get('rhodecode_show_version')) |
|
324 | 324 | context.visual.use_gravatar = str2bool( |
|
325 | 325 | rc_config.get('rhodecode_use_gravatar')) |
|
326 | 326 | context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url') |
|
327 | 327 | context.visual.default_renderer = rc_config.get( |
|
328 | 328 | 'rhodecode_markup_renderer', 'rst') |
|
329 | 329 | context.visual.comment_types = ChangesetComment.COMMENT_TYPES |
|
330 | 330 | context.visual.rhodecode_support_url = \ |
|
331 | 331 | rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support') |
|
332 | 332 | |
|
333 | 333 | context.visual.affected_files_cut_off = 60 |
|
334 | 334 | |
|
335 | 335 | context.pre_code = rc_config.get('rhodecode_pre_code') |
|
336 | 336 | context.post_code = rc_config.get('rhodecode_post_code') |
|
337 | 337 | context.rhodecode_name = rc_config.get('rhodecode_title') |
|
338 | 338 | context.default_encodings = aslist(config.get('default_encoding'), sep=',') |
|
339 | 339 | # if we have specified default_encoding in the request, it has more |
|
340 | 340 | # priority |
|
341 | 341 | if request.GET.get('default_encoding'): |
|
342 | 342 | context.default_encodings.insert(0, request.GET.get('default_encoding')) |
|
343 | 343 | context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl') |
|
344 | 344 | context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl') |
|
345 | 345 | |
|
346 | 346 | # INI stored |
|
347 | 347 | context.labs_active = str2bool( |
|
348 | 348 | config.get('labs_settings_active', 'false')) |
|
349 | 349 | context.ssh_enabled = str2bool( |
|
350 | 350 | config.get('ssh.generate_authorized_keyfile', 'false')) |
|
351 | 351 | |
|
352 | 352 | context.visual.allow_repo_location_change = str2bool( |
|
353 | 353 | config.get('allow_repo_location_change', True)) |
|
354 | 354 | context.visual.allow_custom_hooks_settings = str2bool( |
|
355 | 355 | config.get('allow_custom_hooks_settings', True)) |
|
356 | 356 | context.debug_style = str2bool(config.get('debug_style', False)) |
|
357 | 357 | |
|
358 | 358 | context.rhodecode_instanceid = config.get('instance_id') |
|
359 | 359 | |
|
360 | 360 | context.visual.cut_off_limit_diff = safe_int( |
|
361 | 361 | config.get('cut_off_limit_diff')) |
|
362 | 362 | context.visual.cut_off_limit_file = safe_int( |
|
363 | 363 | config.get('cut_off_limit_file')) |
|
364 | 364 | |
|
365 | 365 | # AppEnlight |
|
366 | 366 | context.appenlight_enabled = str2bool(config.get('appenlight', 'false')) |
|
367 | 367 | context.appenlight_api_public_key = config.get( |
|
368 | 368 | 'appenlight.api_public_key', '') |
|
369 | 369 | context.appenlight_server_url = config.get('appenlight.server_url', '') |
|
370 | 370 | |
|
371 | 371 | diffmode = { |
|
372 | 372 | "unified": "unified", |
|
373 | 373 | "sideside": "sideside" |
|
374 | 374 | }.get(request.GET.get('diffmode')) |
|
375 | 375 | |
|
376 | 376 | if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'): |
|
377 | 377 | request.session['rc_user_session_attr.diffmode'] = diffmode |
|
378 | 378 | |
|
379 | 379 | # session settings per user |
|
380 | 380 | session_attrs = { |
|
381 | 381 | # defaults |
|
382 | 382 | "clone_url_format": "http", |
|
383 | 383 | "diffmode": "sideside" |
|
384 | 384 | } |
|
385 | 385 | for k, v in request.session.items(): |
|
386 | 386 | pref = 'rc_user_session_attr.' |
|
387 | 387 | if k and k.startswith(pref): |
|
388 | 388 | k = k[len(pref):] |
|
389 | 389 | session_attrs[k] = v |
|
390 | 390 | |
|
391 | 391 | context.user_session_attrs = session_attrs |
|
392 | 392 | |
|
393 | 393 | # JS template context |
|
394 | 394 | context.template_context = { |
|
395 | 395 | 'repo_name': None, |
|
396 | 396 | 'repo_type': None, |
|
397 | 397 | 'repo_landing_commit': None, |
|
398 | 398 | 'rhodecode_user': { |
|
399 | 399 | 'username': None, |
|
400 | 400 | 'email': None, |
|
401 | 401 | 'notification_status': False |
|
402 | 402 | }, |
|
403 | 403 | 'session_attrs': session_attrs, |
|
404 | 404 | 'visual': { |
|
405 | 405 | 'default_renderer': None |
|
406 | 406 | }, |
|
407 | 407 | 'commit_data': { |
|
408 | 408 | 'commit_id': None |
|
409 | 409 | }, |
|
410 | 410 | 'pull_request_data': {'pull_request_id': None}, |
|
411 | 411 | 'timeago': { |
|
412 | 412 | 'refresh_time': 120 * 1000, |
|
413 | 413 | 'cutoff_limit': 1000 * 60 * 60 * 24 * 7 |
|
414 | 414 | }, |
|
415 | 415 | 'pyramid_dispatch': { |
|
416 | 416 | |
|
417 | 417 | }, |
|
418 | 418 | 'extra': {'plugins': {}} |
|
419 | 419 | } |
|
420 | 420 | # END CONFIG VARS |
|
421 | 421 | |
|
422 | 422 | context.csrf_token = auth.get_csrf_token(session=request.session) |
|
423 | 423 | context.backends = rhodecode.BACKENDS.keys() |
|
424 | 424 | context.backends.sort() |
|
425 | context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id) | |
|
425 | unread_count = 0 | |
|
426 | user_bookmark_list = [] | |
|
427 | if user_id: | |
|
428 | unread_count = NotificationModel().get_unread_cnt_for_user(user_id) | |
|
429 | user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id) | |
|
430 | context.unread_notifications = unread_count | |
|
431 | context.bookmark_items = user_bookmark_list | |
|
426 | 432 | |
|
427 | 433 | # web case |
|
428 | 434 | if hasattr(request, 'user'): |
|
429 | 435 | context.auth_user = request.user |
|
430 | 436 | context.rhodecode_user = request.user |
|
431 | 437 | |
|
432 | 438 | # api case |
|
433 | 439 | if hasattr(request, 'rpc_user'): |
|
434 | 440 | context.auth_user = request.rpc_user |
|
435 | 441 | context.rhodecode_user = request.rpc_user |
|
436 | 442 | |
|
437 | 443 | # attach the whole call context to the request |
|
438 | 444 | request.call_context = context |
|
439 | 445 | |
|
440 | 446 | |
|
441 | 447 | def get_auth_user(request): |
|
442 | 448 | environ = request.environ |
|
443 | 449 | session = request.session |
|
444 | 450 | |
|
445 | 451 | ip_addr = get_ip_addr(environ) |
|
446 | 452 | # make sure that we update permissions each time we call controller |
|
447 | 453 | _auth_token = (request.GET.get('auth_token', '') or |
|
448 | 454 | request.GET.get('api_key', '')) |
|
449 | 455 | |
|
450 | 456 | if _auth_token: |
|
451 | 457 | # when using API_KEY we assume user exists, and |
|
452 | 458 | # doesn't need auth based on cookies. |
|
453 | 459 | auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr) |
|
454 | 460 | authenticated = False |
|
455 | 461 | else: |
|
456 | 462 | cookie_store = CookieStoreWrapper(session.get('rhodecode_user')) |
|
457 | 463 | try: |
|
458 | 464 | auth_user = AuthUser(user_id=cookie_store.get('user_id', None), |
|
459 | 465 | ip_addr=ip_addr) |
|
460 | 466 | except UserCreationError as e: |
|
461 | 467 | h.flash(e, 'error') |
|
462 | 468 | # container auth or other auth functions that create users |
|
463 | 469 | # on the fly can throw this exception signaling that there's |
|
464 | 470 | # issue with user creation, explanation should be provided |
|
465 | 471 | # in Exception itself. We then create a simple blank |
|
466 | 472 | # AuthUser |
|
467 | 473 | auth_user = AuthUser(ip_addr=ip_addr) |
|
468 | 474 | |
|
469 | 475 | # in case someone changes a password for user it triggers session |
|
470 | 476 | # flush and forces a re-login |
|
471 | 477 | if password_changed(auth_user, session): |
|
472 | 478 | session.invalidate() |
|
473 | 479 | cookie_store = CookieStoreWrapper(session.get('rhodecode_user')) |
|
474 | 480 | auth_user = AuthUser(ip_addr=ip_addr) |
|
475 | 481 | |
|
476 | 482 | authenticated = cookie_store.get('is_authenticated') |
|
477 | 483 | |
|
478 | 484 | if not auth_user.is_authenticated and auth_user.is_user_object: |
|
479 | 485 | # user is not authenticated and not empty |
|
480 | 486 | auth_user.set_authenticated(authenticated) |
|
481 | 487 | |
|
482 | 488 | return auth_user |
|
483 | 489 | |
|
484 | 490 | |
|
485 | 491 | def h_filter(s): |
|
486 | 492 | """ |
|
487 | 493 | Custom filter for Mako templates. Mako by standard uses `markupsafe.escape` |
|
488 | 494 | we wrap this with additional functionality that converts None to empty |
|
489 | 495 | strings |
|
490 | 496 | """ |
|
491 | 497 | if s is None: |
|
492 | 498 | return markupsafe.Markup() |
|
493 | 499 | return markupsafe.escape(s) |
|
494 | 500 | |
|
495 | 501 | |
|
496 | 502 | def add_events_routes(config): |
|
497 | 503 | """ |
|
498 | 504 | Adds routing that can be used in events. Because some events are triggered |
|
499 | 505 | outside of pyramid context, we need to bootstrap request with some |
|
500 | 506 | routing registered |
|
501 | 507 | """ |
|
502 | 508 | |
|
503 | 509 | from rhodecode.apps._base import ADMIN_PREFIX |
|
504 | 510 | |
|
505 | 511 | config.add_route(name='home', pattern='/') |
|
506 | 512 | |
|
507 | 513 | config.add_route(name='login', pattern=ADMIN_PREFIX + '/login') |
|
508 | 514 | config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout') |
|
509 | 515 | config.add_route(name='repo_summary', pattern='/{repo_name}') |
|
510 | 516 | config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary') |
|
511 | 517 | config.add_route(name='repo_group_home', pattern='/{repo_group_name}') |
|
512 | 518 | |
|
513 | 519 | config.add_route(name='pullrequest_show', |
|
514 | 520 | pattern='/{repo_name}/pull-request/{pull_request_id}') |
|
515 | 521 | config.add_route(name='pull_requests_global', |
|
516 | 522 | pattern='/pull-request/{pull_request_id}') |
|
517 | 523 | config.add_route(name='repo_commit', |
|
518 | 524 | pattern='/{repo_name}/changeset/{commit_id}') |
|
519 | 525 | |
|
520 | 526 | config.add_route(name='repo_files', |
|
521 | 527 | pattern='/{repo_name}/files/{commit_id}/{f_path}') |
|
522 | 528 | |
|
523 | 529 | |
|
524 | 530 | def bootstrap_config(request): |
|
525 | 531 | import pyramid.testing |
|
526 | 532 | registry = pyramid.testing.Registry('RcTestRegistry') |
|
527 | 533 | |
|
528 | 534 | config = pyramid.testing.setUp(registry=registry, request=request) |
|
529 | 535 | |
|
530 | 536 | # allow pyramid lookup in testing |
|
531 | 537 | config.include('pyramid_mako') |
|
532 | 538 | config.include('pyramid_beaker') |
|
533 | 539 | config.include('rhodecode.lib.rc_cache') |
|
534 | 540 | |
|
535 | 541 | add_events_routes(config) |
|
536 | 542 | |
|
537 | 543 | return config |
|
538 | 544 | |
|
539 | 545 | |
|
540 | 546 | def bootstrap_request(**kwargs): |
|
541 | 547 | import pyramid.testing |
|
542 | 548 | |
|
543 | 549 | class TestRequest(pyramid.testing.DummyRequest): |
|
544 | 550 | application_url = kwargs.pop('application_url', 'http://example.com') |
|
545 | 551 | host = kwargs.pop('host', 'example.com:80') |
|
546 | 552 | domain = kwargs.pop('domain', 'example.com') |
|
547 | 553 | |
|
548 | 554 | def translate(self, msg): |
|
549 | 555 | return msg |
|
550 | 556 | |
|
551 | 557 | def plularize(self, singular, plural, n): |
|
552 | 558 | return singular |
|
553 | 559 | |
|
554 | 560 | def get_partial_renderer(self, tmpl_name): |
|
555 | 561 | |
|
556 | 562 | from rhodecode.lib.partial_renderer import get_partial_renderer |
|
557 | 563 | return get_partial_renderer(request=self, tmpl_name=tmpl_name) |
|
558 | 564 | |
|
559 | 565 | _call_context = TemplateArgs() |
|
560 | 566 | _call_context.visual = TemplateArgs() |
|
561 | 567 | _call_context.visual.show_sha_length = 12 |
|
562 | 568 | _call_context.visual.show_revision_number = True |
|
563 | 569 | |
|
564 | 570 | @property |
|
565 | 571 | def call_context(self): |
|
566 | 572 | return self._call_context |
|
567 | 573 | |
|
568 | 574 | class TestDummySession(pyramid.testing.DummySession): |
|
569 | 575 | def save(*arg, **kw): |
|
570 | 576 | pass |
|
571 | 577 | |
|
572 | 578 | request = TestRequest(**kwargs) |
|
573 | 579 | request.session = TestDummySession() |
|
574 | 580 | |
|
575 | 581 | return request |
|
576 | 582 |
@@ -1,4833 +1,4876 b'' | |||
|
1 | 1 | # -*- coding: utf-8 -*- |
|
2 | 2 | |
|
3 | 3 | # Copyright (C) 2010-2019 RhodeCode GmbH |
|
4 | 4 | # |
|
5 | 5 | # This program is free software: you can redistribute it and/or modify |
|
6 | 6 | # it under the terms of the GNU Affero General Public License, version 3 |
|
7 | 7 | # (only), as published by the Free Software Foundation. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU Affero General Public License |
|
15 | 15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | 16 | # |
|
17 | 17 | # This program is dual-licensed. If you wish to learn more about the |
|
18 | 18 | # RhodeCode Enterprise Edition, including its added features, Support services, |
|
19 | 19 | # and proprietary license terms, please see https://rhodecode.com/licenses/ |
|
20 | 20 | |
|
21 | 21 | """ |
|
22 | 22 | Database Models for RhodeCode Enterprise |
|
23 | 23 | """ |
|
24 | 24 | |
|
25 | 25 | import re |
|
26 | 26 | import os |
|
27 | 27 | import time |
|
28 | 28 | import hashlib |
|
29 | 29 | import logging |
|
30 | 30 | import datetime |
|
31 | 31 | import warnings |
|
32 | 32 | import ipaddress |
|
33 | 33 | import functools |
|
34 | 34 | import traceback |
|
35 | 35 | import collections |
|
36 | 36 | |
|
37 | 37 | from sqlalchemy import ( |
|
38 | 38 | or_, and_, not_, func, TypeDecorator, event, |
|
39 | 39 | Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, |
|
40 | 40 | Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, |
|
41 | 41 | Text, Float, PickleType) |
|
42 | 42 | from sqlalchemy.sql.expression import true, false |
|
43 | 43 | from sqlalchemy.sql.functions import coalesce, count # pragma: no cover |
|
44 | 44 | from sqlalchemy.orm import ( |
|
45 | 45 | relationship, joinedload, class_mapper, validates, aliased) |
|
46 | 46 | from sqlalchemy.ext.declarative import declared_attr |
|
47 | 47 | from sqlalchemy.ext.hybrid import hybrid_property |
|
48 | 48 | from sqlalchemy.exc import IntegrityError # pragma: no cover |
|
49 | 49 | from sqlalchemy.dialects.mysql import LONGTEXT |
|
50 | 50 | from zope.cachedescriptors.property import Lazy as LazyProperty |
|
51 | 51 | |
|
52 | 52 | from pyramid.threadlocal import get_current_request |
|
53 | 53 | |
|
54 | 54 | from rhodecode.translation import _ |
|
55 | 55 | from rhodecode.lib.vcs import get_vcs_instance |
|
56 | 56 | from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference |
|
57 | 57 | from rhodecode.lib.utils2 import ( |
|
58 | 58 | str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe, |
|
59 | 59 | time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict, |
|
60 | 60 | glob2re, StrictAttributeDict, cleaned_uri) |
|
61 | 61 | from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \ |
|
62 | 62 | JsonRaw |
|
63 | 63 | from rhodecode.lib.ext_json import json |
|
64 | 64 | from rhodecode.lib.caching_query import FromCache |
|
65 | 65 | from rhodecode.lib.encrypt import AESCipher |
|
66 | 66 | |
|
67 | 67 | from rhodecode.model.meta import Base, Session |
|
68 | 68 | |
|
69 | 69 | URL_SEP = '/' |
|
70 | 70 | log = logging.getLogger(__name__) |
|
71 | 71 | |
|
72 | 72 | # ============================================================================= |
|
73 | 73 | # BASE CLASSES |
|
74 | 74 | # ============================================================================= |
|
75 | 75 | |
|
76 | 76 | # this is propagated from .ini file rhodecode.encrypted_values.secret or |
|
77 | 77 | # beaker.session.secret if first is not set. |
|
78 | 78 | # and initialized at environment.py |
|
79 | 79 | ENCRYPTION_KEY = None |
|
80 | 80 | |
|
81 | 81 | # used to sort permissions by types, '#' used here is not allowed to be in |
|
82 | 82 | # usernames, and it's very early in sorted string.printable table. |
|
83 | 83 | PERMISSION_TYPE_SORT = { |
|
84 | 84 | 'admin': '####', |
|
85 | 85 | 'write': '###', |
|
86 | 86 | 'read': '##', |
|
87 | 87 | 'none': '#', |
|
88 | 88 | } |
|
89 | 89 | |
|
90 | 90 | |
|
91 | 91 | def display_user_sort(obj): |
|
92 | 92 | """ |
|
93 | 93 | Sort function used to sort permissions in .permissions() function of |
|
94 | 94 | Repository, RepoGroup, UserGroup. Also it put the default user in front |
|
95 | 95 | of all other resources |
|
96 | 96 | """ |
|
97 | 97 | |
|
98 | 98 | if obj.username == User.DEFAULT_USER: |
|
99 | 99 | return '#####' |
|
100 | 100 | prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') |
|
101 | 101 | return prefix + obj.username |
|
102 | 102 | |
|
103 | 103 | |
|
104 | 104 | def display_user_group_sort(obj): |
|
105 | 105 | """ |
|
106 | 106 | Sort function used to sort permissions in .permissions() function of |
|
107 | 107 | Repository, RepoGroup, UserGroup. Also it put the default user in front |
|
108 | 108 | of all other resources |
|
109 | 109 | """ |
|
110 | 110 | |
|
111 | 111 | prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '') |
|
112 | 112 | return prefix + obj.users_group_name |
|
113 | 113 | |
|
114 | 114 | |
|
115 | 115 | def _hash_key(k): |
|
116 | 116 | return sha1_safe(k) |
|
117 | 117 | |
|
118 | 118 | |
|
119 | 119 | def in_filter_generator(qry, items, limit=500): |
|
120 | 120 | """ |
|
121 | 121 | Splits IN() into multiple with OR |
|
122 | 122 | e.g.:: |
|
123 | 123 | cnt = Repository.query().filter( |
|
124 | 124 | or_( |
|
125 | 125 | *in_filter_generator(Repository.repo_id, range(100000)) |
|
126 | 126 | )).count() |
|
127 | 127 | """ |
|
128 | 128 | if not items: |
|
129 | 129 | # empty list will cause empty query which might cause security issues |
|
130 | 130 | # this can lead to hidden unpleasant results |
|
131 | 131 | items = [-1] |
|
132 | 132 | |
|
133 | 133 | parts = [] |
|
134 | 134 | for chunk in xrange(0, len(items), limit): |
|
135 | 135 | parts.append( |
|
136 | 136 | qry.in_(items[chunk: chunk + limit]) |
|
137 | 137 | ) |
|
138 | 138 | |
|
139 | 139 | return parts |
|
140 | 140 | |
|
141 | 141 | |
|
142 | 142 | base_table_args = { |
|
143 | 143 | 'extend_existing': True, |
|
144 | 144 | 'mysql_engine': 'InnoDB', |
|
145 | 145 | 'mysql_charset': 'utf8', |
|
146 | 146 | 'sqlite_autoincrement': True |
|
147 | 147 | } |
|
148 | 148 | |
|
149 | 149 | |
|
150 | 150 | class EncryptedTextValue(TypeDecorator): |
|
151 | 151 | """ |
|
152 | 152 | Special column for encrypted long text data, use like:: |
|
153 | 153 | |
|
154 | 154 | value = Column("encrypted_value", EncryptedValue(), nullable=False) |
|
155 | 155 | |
|
156 | 156 | This column is intelligent so if value is in unencrypted form it return |
|
157 | 157 | unencrypted form, but on save it always encrypts |
|
158 | 158 | """ |
|
159 | 159 | impl = Text |
|
160 | 160 | |
|
161 | 161 | def process_bind_param(self, value, dialect): |
|
162 | 162 | if not value: |
|
163 | 163 | return value |
|
164 | 164 | if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'): |
|
165 | 165 | # protect against double encrypting if someone manually starts |
|
166 | 166 | # doing |
|
167 | 167 | raise ValueError('value needs to be in unencrypted format, ie. ' |
|
168 | 168 | 'not starting with enc$aes') |
|
169 | 169 | return 'enc$aes_hmac$%s' % AESCipher( |
|
170 | 170 | ENCRYPTION_KEY, hmac=True).encrypt(value) |
|
171 | 171 | |
|
172 | 172 | def process_result_value(self, value, dialect): |
|
173 | 173 | import rhodecode |
|
174 | 174 | |
|
175 | 175 | if not value: |
|
176 | 176 | return value |
|
177 | 177 | |
|
178 | 178 | parts = value.split('$', 3) |
|
179 | 179 | if not len(parts) == 3: |
|
180 | 180 | # probably not encrypted values |
|
181 | 181 | return value |
|
182 | 182 | else: |
|
183 | 183 | if parts[0] != 'enc': |
|
184 | 184 | # parts ok but without our header ? |
|
185 | 185 | return value |
|
186 | 186 | enc_strict_mode = str2bool(rhodecode.CONFIG.get( |
|
187 | 187 | 'rhodecode.encrypted_values.strict') or True) |
|
188 | 188 | # at that stage we know it's our encryption |
|
189 | 189 | if parts[1] == 'aes': |
|
190 | 190 | decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2]) |
|
191 | 191 | elif parts[1] == 'aes_hmac': |
|
192 | 192 | decrypted_data = AESCipher( |
|
193 | 193 | ENCRYPTION_KEY, hmac=True, |
|
194 | 194 | strict_verification=enc_strict_mode).decrypt(parts[2]) |
|
195 | 195 | else: |
|
196 | 196 | raise ValueError( |
|
197 | 197 | 'Encryption type part is wrong, must be `aes` ' |
|
198 | 198 | 'or `aes_hmac`, got `%s` instead' % (parts[1])) |
|
199 | 199 | return decrypted_data |
|
200 | 200 | |
|
201 | 201 | |
|
202 | 202 | class BaseModel(object): |
|
203 | 203 | """ |
|
204 | 204 | Base Model for all classes |
|
205 | 205 | """ |
|
206 | 206 | |
|
207 | 207 | @classmethod |
|
208 | 208 | def _get_keys(cls): |
|
209 | 209 | """return column names for this model """ |
|
210 | 210 | return class_mapper(cls).c.keys() |
|
211 | 211 | |
|
212 | 212 | def get_dict(self): |
|
213 | 213 | """ |
|
214 | 214 | return dict with keys and values corresponding |
|
215 | 215 | to this model data """ |
|
216 | 216 | |
|
217 | 217 | d = {} |
|
218 | 218 | for k in self._get_keys(): |
|
219 | 219 | d[k] = getattr(self, k) |
|
220 | 220 | |
|
221 | 221 | # also use __json__() if present to get additional fields |
|
222 | 222 | _json_attr = getattr(self, '__json__', None) |
|
223 | 223 | if _json_attr: |
|
224 | 224 | # update with attributes from __json__ |
|
225 | 225 | if callable(_json_attr): |
|
226 | 226 | _json_attr = _json_attr() |
|
227 | 227 | for k, val in _json_attr.iteritems(): |
|
228 | 228 | d[k] = val |
|
229 | 229 | return d |
|
230 | 230 | |
|
231 | 231 | def get_appstruct(self): |
|
232 | 232 | """return list with keys and values tuples corresponding |
|
233 | 233 | to this model data """ |
|
234 | 234 | |
|
235 | 235 | lst = [] |
|
236 | 236 | for k in self._get_keys(): |
|
237 | 237 | lst.append((k, getattr(self, k),)) |
|
238 | 238 | return lst |
|
239 | 239 | |
|
240 | 240 | def populate_obj(self, populate_dict): |
|
241 | 241 | """populate model with data from given populate_dict""" |
|
242 | 242 | |
|
243 | 243 | for k in self._get_keys(): |
|
244 | 244 | if k in populate_dict: |
|
245 | 245 | setattr(self, k, populate_dict[k]) |
|
246 | 246 | |
|
247 | 247 | @classmethod |
|
248 | 248 | def query(cls): |
|
249 | 249 | return Session().query(cls) |
|
250 | 250 | |
|
251 | 251 | @classmethod |
|
252 | 252 | def get(cls, id_): |
|
253 | 253 | if id_: |
|
254 | 254 | return cls.query().get(id_) |
|
255 | 255 | |
|
256 | 256 | @classmethod |
|
257 | 257 | def get_or_404(cls, id_): |
|
258 | 258 | from pyramid.httpexceptions import HTTPNotFound |
|
259 | 259 | |
|
260 | 260 | try: |
|
261 | 261 | id_ = int(id_) |
|
262 | 262 | except (TypeError, ValueError): |
|
263 | 263 | raise HTTPNotFound() |
|
264 | 264 | |
|
265 | 265 | res = cls.query().get(id_) |
|
266 | 266 | if not res: |
|
267 | 267 | raise HTTPNotFound() |
|
268 | 268 | return res |
|
269 | 269 | |
|
270 | 270 | @classmethod |
|
271 | 271 | def getAll(cls): |
|
272 | 272 | # deprecated and left for backward compatibility |
|
273 | 273 | return cls.get_all() |
|
274 | 274 | |
|
275 | 275 | @classmethod |
|
276 | 276 | def get_all(cls): |
|
277 | 277 | return cls.query().all() |
|
278 | 278 | |
|
279 | 279 | @classmethod |
|
280 | 280 | def delete(cls, id_): |
|
281 | 281 | obj = cls.query().get(id_) |
|
282 | 282 | Session().delete(obj) |
|
283 | 283 | |
|
284 | 284 | @classmethod |
|
285 | 285 | def identity_cache(cls, session, attr_name, value): |
|
286 | 286 | exist_in_session = [] |
|
287 | 287 | for (item_cls, pkey), instance in session.identity_map.items(): |
|
288 | 288 | if cls == item_cls and getattr(instance, attr_name) == value: |
|
289 | 289 | exist_in_session.append(instance) |
|
290 | 290 | if exist_in_session: |
|
291 | 291 | if len(exist_in_session) == 1: |
|
292 | 292 | return exist_in_session[0] |
|
293 | 293 | log.exception( |
|
294 | 294 | 'multiple objects with attr %s and ' |
|
295 | 295 | 'value %s found with same name: %r', |
|
296 | 296 | attr_name, value, exist_in_session) |
|
297 | 297 | |
|
298 | 298 | def __repr__(self): |
|
299 | 299 | if hasattr(self, '__unicode__'): |
|
300 | 300 | # python repr needs to return str |
|
301 | 301 | try: |
|
302 | 302 | return safe_str(self.__unicode__()) |
|
303 | 303 | except UnicodeDecodeError: |
|
304 | 304 | pass |
|
305 | 305 | return '<DB:%s>' % (self.__class__.__name__) |
|
306 | 306 | |
|
307 | 307 | |
|
308 | 308 | class RhodeCodeSetting(Base, BaseModel): |
|
309 | 309 | __tablename__ = 'rhodecode_settings' |
|
310 | 310 | __table_args__ = ( |
|
311 | 311 | UniqueConstraint('app_settings_name'), |
|
312 | 312 | base_table_args |
|
313 | 313 | ) |
|
314 | 314 | |
|
315 | 315 | SETTINGS_TYPES = { |
|
316 | 316 | 'str': safe_str, |
|
317 | 317 | 'int': safe_int, |
|
318 | 318 | 'unicode': safe_unicode, |
|
319 | 319 | 'bool': str2bool, |
|
320 | 320 | 'list': functools.partial(aslist, sep=',') |
|
321 | 321 | } |
|
322 | 322 | DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions' |
|
323 | 323 | GLOBAL_CONF_KEY = 'app_settings' |
|
324 | 324 | |
|
325 | 325 | app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
326 | 326 | app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None) |
|
327 | 327 | _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None) |
|
328 | 328 | _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None) |
|
329 | 329 | |
|
330 | 330 | def __init__(self, key='', val='', type='unicode'): |
|
331 | 331 | self.app_settings_name = key |
|
332 | 332 | self.app_settings_type = type |
|
333 | 333 | self.app_settings_value = val |
|
334 | 334 | |
|
335 | 335 | @validates('_app_settings_value') |
|
336 | 336 | def validate_settings_value(self, key, val): |
|
337 | 337 | assert type(val) == unicode |
|
338 | 338 | return val |
|
339 | 339 | |
|
340 | 340 | @hybrid_property |
|
341 | 341 | def app_settings_value(self): |
|
342 | 342 | v = self._app_settings_value |
|
343 | 343 | _type = self.app_settings_type |
|
344 | 344 | if _type: |
|
345 | 345 | _type = self.app_settings_type.split('.')[0] |
|
346 | 346 | # decode the encrypted value |
|
347 | 347 | if 'encrypted' in self.app_settings_type: |
|
348 | 348 | cipher = EncryptedTextValue() |
|
349 | 349 | v = safe_unicode(cipher.process_result_value(v, None)) |
|
350 | 350 | |
|
351 | 351 | converter = self.SETTINGS_TYPES.get(_type) or \ |
|
352 | 352 | self.SETTINGS_TYPES['unicode'] |
|
353 | 353 | return converter(v) |
|
354 | 354 | |
|
355 | 355 | @app_settings_value.setter |
|
356 | 356 | def app_settings_value(self, val): |
|
357 | 357 | """ |
|
358 | 358 | Setter that will always make sure we use unicode in app_settings_value |
|
359 | 359 | |
|
360 | 360 | :param val: |
|
361 | 361 | """ |
|
362 | 362 | val = safe_unicode(val) |
|
363 | 363 | # encode the encrypted value |
|
364 | 364 | if 'encrypted' in self.app_settings_type: |
|
365 | 365 | cipher = EncryptedTextValue() |
|
366 | 366 | val = safe_unicode(cipher.process_bind_param(val, None)) |
|
367 | 367 | self._app_settings_value = val |
|
368 | 368 | |
|
369 | 369 | @hybrid_property |
|
370 | 370 | def app_settings_type(self): |
|
371 | 371 | return self._app_settings_type |
|
372 | 372 | |
|
373 | 373 | @app_settings_type.setter |
|
374 | 374 | def app_settings_type(self, val): |
|
375 | 375 | if val.split('.')[0] not in self.SETTINGS_TYPES: |
|
376 | 376 | raise Exception('type must be one of %s got %s' |
|
377 | 377 | % (self.SETTINGS_TYPES.keys(), val)) |
|
378 | 378 | self._app_settings_type = val |
|
379 | 379 | |
|
380 | 380 | @classmethod |
|
381 | 381 | def get_by_prefix(cls, prefix): |
|
382 | 382 | return RhodeCodeSetting.query()\ |
|
383 | 383 | .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\ |
|
384 | 384 | .all() |
|
385 | 385 | |
|
386 | 386 | def __unicode__(self): |
|
387 | 387 | return u"<%s('%s:%s[%s]')>" % ( |
|
388 | 388 | self.__class__.__name__, |
|
389 | 389 | self.app_settings_name, self.app_settings_value, |
|
390 | 390 | self.app_settings_type |
|
391 | 391 | ) |
|
392 | 392 | |
|
393 | 393 | |
|
394 | 394 | class RhodeCodeUi(Base, BaseModel): |
|
395 | 395 | __tablename__ = 'rhodecode_ui' |
|
396 | 396 | __table_args__ = ( |
|
397 | 397 | UniqueConstraint('ui_key'), |
|
398 | 398 | base_table_args |
|
399 | 399 | ) |
|
400 | 400 | |
|
401 | 401 | HOOK_REPO_SIZE = 'changegroup.repo_size' |
|
402 | 402 | # HG |
|
403 | 403 | HOOK_PRE_PULL = 'preoutgoing.pre_pull' |
|
404 | 404 | HOOK_PULL = 'outgoing.pull_logger' |
|
405 | 405 | HOOK_PRE_PUSH = 'prechangegroup.pre_push' |
|
406 | 406 | HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push' |
|
407 | 407 | HOOK_PUSH = 'changegroup.push_logger' |
|
408 | 408 | HOOK_PUSH_KEY = 'pushkey.key_push' |
|
409 | 409 | |
|
410 | 410 | # TODO: johbo: Unify way how hooks are configured for git and hg, |
|
411 | 411 | # git part is currently hardcoded. |
|
412 | 412 | |
|
413 | 413 | # SVN PATTERNS |
|
414 | 414 | SVN_BRANCH_ID = 'vcs_svn_branch' |
|
415 | 415 | SVN_TAG_ID = 'vcs_svn_tag' |
|
416 | 416 | |
|
417 | 417 | ui_id = Column( |
|
418 | 418 | "ui_id", Integer(), nullable=False, unique=True, default=None, |
|
419 | 419 | primary_key=True) |
|
420 | 420 | ui_section = Column( |
|
421 | 421 | "ui_section", String(255), nullable=True, unique=None, default=None) |
|
422 | 422 | ui_key = Column( |
|
423 | 423 | "ui_key", String(255), nullable=True, unique=None, default=None) |
|
424 | 424 | ui_value = Column( |
|
425 | 425 | "ui_value", String(255), nullable=True, unique=None, default=None) |
|
426 | 426 | ui_active = Column( |
|
427 | 427 | "ui_active", Boolean(), nullable=True, unique=None, default=True) |
|
428 | 428 | |
|
429 | 429 | def __repr__(self): |
|
430 | 430 | return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section, |
|
431 | 431 | self.ui_key, self.ui_value) |
|
432 | 432 | |
|
433 | 433 | |
|
434 | 434 | class RepoRhodeCodeSetting(Base, BaseModel): |
|
435 | 435 | __tablename__ = 'repo_rhodecode_settings' |
|
436 | 436 | __table_args__ = ( |
|
437 | 437 | UniqueConstraint( |
|
438 | 438 | 'app_settings_name', 'repository_id', |
|
439 | 439 | name='uq_repo_rhodecode_setting_name_repo_id'), |
|
440 | 440 | base_table_args |
|
441 | 441 | ) |
|
442 | 442 | |
|
443 | 443 | repository_id = Column( |
|
444 | 444 | "repository_id", Integer(), ForeignKey('repositories.repo_id'), |
|
445 | 445 | nullable=False) |
|
446 | 446 | app_settings_id = Column( |
|
447 | 447 | "app_settings_id", Integer(), nullable=False, unique=True, |
|
448 | 448 | default=None, primary_key=True) |
|
449 | 449 | app_settings_name = Column( |
|
450 | 450 | "app_settings_name", String(255), nullable=True, unique=None, |
|
451 | 451 | default=None) |
|
452 | 452 | _app_settings_value = Column( |
|
453 | 453 | "app_settings_value", String(4096), nullable=True, unique=None, |
|
454 | 454 | default=None) |
|
455 | 455 | _app_settings_type = Column( |
|
456 | 456 | "app_settings_type", String(255), nullable=True, unique=None, |
|
457 | 457 | default=None) |
|
458 | 458 | |
|
459 | 459 | repository = relationship('Repository') |
|
460 | 460 | |
|
461 | 461 | def __init__(self, repository_id, key='', val='', type='unicode'): |
|
462 | 462 | self.repository_id = repository_id |
|
463 | 463 | self.app_settings_name = key |
|
464 | 464 | self.app_settings_type = type |
|
465 | 465 | self.app_settings_value = val |
|
466 | 466 | |
|
467 | 467 | @validates('_app_settings_value') |
|
468 | 468 | def validate_settings_value(self, key, val): |
|
469 | 469 | assert type(val) == unicode |
|
470 | 470 | return val |
|
471 | 471 | |
|
472 | 472 | @hybrid_property |
|
473 | 473 | def app_settings_value(self): |
|
474 | 474 | v = self._app_settings_value |
|
475 | 475 | type_ = self.app_settings_type |
|
476 | 476 | SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES |
|
477 | 477 | converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode'] |
|
478 | 478 | return converter(v) |
|
479 | 479 | |
|
480 | 480 | @app_settings_value.setter |
|
481 | 481 | def app_settings_value(self, val): |
|
482 | 482 | """ |
|
483 | 483 | Setter that will always make sure we use unicode in app_settings_value |
|
484 | 484 | |
|
485 | 485 | :param val: |
|
486 | 486 | """ |
|
487 | 487 | self._app_settings_value = safe_unicode(val) |
|
488 | 488 | |
|
489 | 489 | @hybrid_property |
|
490 | 490 | def app_settings_type(self): |
|
491 | 491 | return self._app_settings_type |
|
492 | 492 | |
|
493 | 493 | @app_settings_type.setter |
|
494 | 494 | def app_settings_type(self, val): |
|
495 | 495 | SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES |
|
496 | 496 | if val not in SETTINGS_TYPES: |
|
497 | 497 | raise Exception('type must be one of %s got %s' |
|
498 | 498 | % (SETTINGS_TYPES.keys(), val)) |
|
499 | 499 | self._app_settings_type = val |
|
500 | 500 | |
|
501 | 501 | def __unicode__(self): |
|
502 | 502 | return u"<%s('%s:%s:%s[%s]')>" % ( |
|
503 | 503 | self.__class__.__name__, self.repository.repo_name, |
|
504 | 504 | self.app_settings_name, self.app_settings_value, |
|
505 | 505 | self.app_settings_type |
|
506 | 506 | ) |
|
507 | 507 | |
|
508 | 508 | |
|
509 | 509 | class RepoRhodeCodeUi(Base, BaseModel): |
|
510 | 510 | __tablename__ = 'repo_rhodecode_ui' |
|
511 | 511 | __table_args__ = ( |
|
512 | 512 | UniqueConstraint( |
|
513 | 513 | 'repository_id', 'ui_section', 'ui_key', |
|
514 | 514 | name='uq_repo_rhodecode_ui_repository_id_section_key'), |
|
515 | 515 | base_table_args |
|
516 | 516 | ) |
|
517 | 517 | |
|
518 | 518 | repository_id = Column( |
|
519 | 519 | "repository_id", Integer(), ForeignKey('repositories.repo_id'), |
|
520 | 520 | nullable=False) |
|
521 | 521 | ui_id = Column( |
|
522 | 522 | "ui_id", Integer(), nullable=False, unique=True, default=None, |
|
523 | 523 | primary_key=True) |
|
524 | 524 | ui_section = Column( |
|
525 | 525 | "ui_section", String(255), nullable=True, unique=None, default=None) |
|
526 | 526 | ui_key = Column( |
|
527 | 527 | "ui_key", String(255), nullable=True, unique=None, default=None) |
|
528 | 528 | ui_value = Column( |
|
529 | 529 | "ui_value", String(255), nullable=True, unique=None, default=None) |
|
530 | 530 | ui_active = Column( |
|
531 | 531 | "ui_active", Boolean(), nullable=True, unique=None, default=True) |
|
532 | 532 | |
|
533 | 533 | repository = relationship('Repository') |
|
534 | 534 | |
|
535 | 535 | def __repr__(self): |
|
536 | 536 | return '<%s[%s:%s]%s=>%s]>' % ( |
|
537 | 537 | self.__class__.__name__, self.repository.repo_name, |
|
538 | 538 | self.ui_section, self.ui_key, self.ui_value) |
|
539 | 539 | |
|
540 | 540 | |
|
541 | 541 | class User(Base, BaseModel): |
|
542 | 542 | __tablename__ = 'users' |
|
543 | 543 | __table_args__ = ( |
|
544 | 544 | UniqueConstraint('username'), UniqueConstraint('email'), |
|
545 | 545 | Index('u_username_idx', 'username'), |
|
546 | 546 | Index('u_email_idx', 'email'), |
|
547 | 547 | base_table_args |
|
548 | 548 | ) |
|
549 | 549 | |
|
550 | 550 | DEFAULT_USER = 'default' |
|
551 | 551 | DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org' |
|
552 | 552 | DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}' |
|
553 | 553 | |
|
554 | 554 | user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
555 | 555 | username = Column("username", String(255), nullable=True, unique=None, default=None) |
|
556 | 556 | password = Column("password", String(255), nullable=True, unique=None, default=None) |
|
557 | 557 | active = Column("active", Boolean(), nullable=True, unique=None, default=True) |
|
558 | 558 | admin = Column("admin", Boolean(), nullable=True, unique=None, default=False) |
|
559 | 559 | name = Column("firstname", String(255), nullable=True, unique=None, default=None) |
|
560 | 560 | lastname = Column("lastname", String(255), nullable=True, unique=None, default=None) |
|
561 | 561 | _email = Column("email", String(255), nullable=True, unique=None, default=None) |
|
562 | 562 | last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None) |
|
563 | 563 | last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None) |
|
564 | 564 | |
|
565 | 565 | extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None) |
|
566 | 566 | extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None) |
|
567 | 567 | _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None) |
|
568 | 568 | inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) |
|
569 | 569 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
570 | 570 | _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data |
|
571 | 571 | |
|
572 | 572 | user_log = relationship('UserLog') |
|
573 | 573 | user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all') |
|
574 | 574 | |
|
575 | 575 | repositories = relationship('Repository') |
|
576 | 576 | repository_groups = relationship('RepoGroup') |
|
577 | 577 | user_groups = relationship('UserGroup') |
|
578 | 578 | |
|
579 | 579 | user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') |
|
580 | 580 | followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all') |
|
581 | 581 | |
|
582 | 582 | repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') |
|
583 | 583 | repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') |
|
584 | 584 | user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all') |
|
585 | 585 | |
|
586 | 586 | group_member = relationship('UserGroupMember', cascade='all') |
|
587 | 587 | |
|
588 | 588 | notifications = relationship('UserNotification', cascade='all') |
|
589 | 589 | # notifications assigned to this user |
|
590 | 590 | user_created_notifications = relationship('Notification', cascade='all') |
|
591 | 591 | # comments created by this user |
|
592 | 592 | user_comments = relationship('ChangesetComment', cascade='all') |
|
593 | 593 | # user profile extra info |
|
594 | 594 | user_emails = relationship('UserEmailMap', cascade='all') |
|
595 | 595 | user_ip_map = relationship('UserIpMap', cascade='all') |
|
596 | 596 | user_auth_tokens = relationship('UserApiKeys', cascade='all') |
|
597 | 597 | user_ssh_keys = relationship('UserSshKeys', cascade='all') |
|
598 | 598 | |
|
599 | 599 | # gists |
|
600 | 600 | user_gists = relationship('Gist', cascade='all') |
|
601 | 601 | # user pull requests |
|
602 | 602 | user_pull_requests = relationship('PullRequest', cascade='all') |
|
603 | 603 | # external identities |
|
604 | 604 | extenal_identities = relationship( |
|
605 | 605 | 'ExternalIdentity', |
|
606 | 606 | primaryjoin="User.user_id==ExternalIdentity.local_user_id", |
|
607 | 607 | cascade='all') |
|
608 | 608 | # review rules |
|
609 | 609 | user_review_rules = relationship('RepoReviewRuleUser', cascade='all') |
|
610 | 610 | |
|
611 | 611 | def __unicode__(self): |
|
612 | 612 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, |
|
613 | 613 | self.user_id, self.username) |
|
614 | 614 | |
|
615 | 615 | @hybrid_property |
|
616 | 616 | def email(self): |
|
617 | 617 | return self._email |
|
618 | 618 | |
|
619 | 619 | @email.setter |
|
620 | 620 | def email(self, val): |
|
621 | 621 | self._email = val.lower() if val else None |
|
622 | 622 | |
|
623 | 623 | @hybrid_property |
|
624 | 624 | def first_name(self): |
|
625 | 625 | from rhodecode.lib import helpers as h |
|
626 | 626 | if self.name: |
|
627 | 627 | return h.escape(self.name) |
|
628 | 628 | return self.name |
|
629 | 629 | |
|
630 | 630 | @hybrid_property |
|
631 | 631 | def last_name(self): |
|
632 | 632 | from rhodecode.lib import helpers as h |
|
633 | 633 | if self.lastname: |
|
634 | 634 | return h.escape(self.lastname) |
|
635 | 635 | return self.lastname |
|
636 | 636 | |
|
637 | 637 | @hybrid_property |
|
638 | 638 | def api_key(self): |
|
639 | 639 | """ |
|
640 | 640 | Fetch if exist an auth-token with role ALL connected to this user |
|
641 | 641 | """ |
|
642 | 642 | user_auth_token = UserApiKeys.query()\ |
|
643 | 643 | .filter(UserApiKeys.user_id == self.user_id)\ |
|
644 | 644 | .filter(or_(UserApiKeys.expires == -1, |
|
645 | 645 | UserApiKeys.expires >= time.time()))\ |
|
646 | 646 | .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first() |
|
647 | 647 | if user_auth_token: |
|
648 | 648 | user_auth_token = user_auth_token.api_key |
|
649 | 649 | |
|
650 | 650 | return user_auth_token |
|
651 | 651 | |
|
652 | 652 | @api_key.setter |
|
653 | 653 | def api_key(self, val): |
|
654 | 654 | # don't allow to set API key this is deprecated for now |
|
655 | 655 | self._api_key = None |
|
656 | 656 | |
|
657 | 657 | @property |
|
658 | 658 | def reviewer_pull_requests(self): |
|
659 | 659 | return PullRequestReviewers.query() \ |
|
660 | 660 | .options(joinedload(PullRequestReviewers.pull_request)) \ |
|
661 | 661 | .filter(PullRequestReviewers.user_id == self.user_id) \ |
|
662 | 662 | .all() |
|
663 | 663 | |
|
664 | 664 | @property |
|
665 | 665 | def firstname(self): |
|
666 | 666 | # alias for future |
|
667 | 667 | return self.name |
|
668 | 668 | |
|
669 | 669 | @property |
|
670 | 670 | def emails(self): |
|
671 | 671 | other = UserEmailMap.query()\ |
|
672 | 672 | .filter(UserEmailMap.user == self) \ |
|
673 | 673 | .order_by(UserEmailMap.email_id.asc()) \ |
|
674 | 674 | .all() |
|
675 | 675 | return [self.email] + [x.email for x in other] |
|
676 | 676 | |
|
677 | 677 | @property |
|
678 | 678 | def auth_tokens(self): |
|
679 | 679 | auth_tokens = self.get_auth_tokens() |
|
680 | 680 | return [x.api_key for x in auth_tokens] |
|
681 | 681 | |
|
682 | 682 | def get_auth_tokens(self): |
|
683 | 683 | return UserApiKeys.query()\ |
|
684 | 684 | .filter(UserApiKeys.user == self)\ |
|
685 | 685 | .order_by(UserApiKeys.user_api_key_id.asc())\ |
|
686 | 686 | .all() |
|
687 | 687 | |
|
688 | 688 | @LazyProperty |
|
689 | 689 | def feed_token(self): |
|
690 | 690 | return self.get_feed_token() |
|
691 | 691 | |
|
692 | 692 | def get_feed_token(self, cache=True): |
|
693 | 693 | feed_tokens = UserApiKeys.query()\ |
|
694 | 694 | .filter(UserApiKeys.user == self)\ |
|
695 | 695 | .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED) |
|
696 | 696 | if cache: |
|
697 | 697 | feed_tokens = feed_tokens.options( |
|
698 | 698 | FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id)) |
|
699 | 699 | |
|
700 | 700 | feed_tokens = feed_tokens.all() |
|
701 | 701 | if feed_tokens: |
|
702 | 702 | return feed_tokens[0].api_key |
|
703 | 703 | return 'NO_FEED_TOKEN_AVAILABLE' |
|
704 | 704 | |
|
705 | 705 | @classmethod |
|
706 | 706 | def get(cls, user_id, cache=False): |
|
707 | 707 | if not user_id: |
|
708 | 708 | return |
|
709 | 709 | |
|
710 | 710 | user = cls.query() |
|
711 | 711 | if cache: |
|
712 | 712 | user = user.options( |
|
713 | 713 | FromCache("sql_cache_short", "get_users_%s" % user_id)) |
|
714 | 714 | return user.get(user_id) |
|
715 | 715 | |
|
716 | 716 | @classmethod |
|
717 | 717 | def extra_valid_auth_tokens(cls, user, role=None): |
|
718 | 718 | tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\ |
|
719 | 719 | .filter(or_(UserApiKeys.expires == -1, |
|
720 | 720 | UserApiKeys.expires >= time.time())) |
|
721 | 721 | if role: |
|
722 | 722 | tokens = tokens.filter(or_(UserApiKeys.role == role, |
|
723 | 723 | UserApiKeys.role == UserApiKeys.ROLE_ALL)) |
|
724 | 724 | return tokens.all() |
|
725 | 725 | |
|
726 | 726 | def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None): |
|
727 | 727 | from rhodecode.lib import auth |
|
728 | 728 | |
|
729 | 729 | log.debug('Trying to authenticate user: %s via auth-token, ' |
|
730 | 730 | 'and roles: %s', self, roles) |
|
731 | 731 | |
|
732 | 732 | if not auth_token: |
|
733 | 733 | return False |
|
734 | 734 | |
|
735 | 735 | crypto_backend = auth.crypto_backend() |
|
736 | 736 | |
|
737 | 737 | roles = (roles or []) + [UserApiKeys.ROLE_ALL] |
|
738 | 738 | tokens_q = UserApiKeys.query()\ |
|
739 | 739 | .filter(UserApiKeys.user_id == self.user_id)\ |
|
740 | 740 | .filter(or_(UserApiKeys.expires == -1, |
|
741 | 741 | UserApiKeys.expires >= time.time())) |
|
742 | 742 | |
|
743 | 743 | tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles)) |
|
744 | 744 | |
|
745 | 745 | plain_tokens = [] |
|
746 | 746 | hash_tokens = [] |
|
747 | 747 | |
|
748 | 748 | user_tokens = tokens_q.all() |
|
749 | 749 | log.debug('Found %s user tokens to check for authentication', len(user_tokens)) |
|
750 | 750 | for token in user_tokens: |
|
751 | 751 | log.debug('AUTH_TOKEN: checking if user token with id `%s` matches', |
|
752 | 752 | token.user_api_key_id) |
|
753 | 753 | # verify scope first, since it's way faster than hash calculation of |
|
754 | 754 | # encrypted tokens |
|
755 | 755 | if token.repo_id: |
|
756 | 756 | # token has a scope, we need to verify it |
|
757 | 757 | if scope_repo_id != token.repo_id: |
|
758 | 758 | log.debug( |
|
759 | 759 | 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, ' |
|
760 | 760 | 'and calling scope is:%s, skipping further checks', |
|
761 | 761 | token.repo, scope_repo_id) |
|
762 | 762 | # token has a scope, and it doesn't match, skip token |
|
763 | 763 | continue |
|
764 | 764 | |
|
765 | 765 | if token.api_key.startswith(crypto_backend.ENC_PREF): |
|
766 | 766 | hash_tokens.append(token.api_key) |
|
767 | 767 | else: |
|
768 | 768 | plain_tokens.append(token.api_key) |
|
769 | 769 | |
|
770 | 770 | is_plain_match = auth_token in plain_tokens |
|
771 | 771 | if is_plain_match: |
|
772 | 772 | return True |
|
773 | 773 | |
|
774 | 774 | for hashed in hash_tokens: |
|
775 | 775 | # NOTE(marcink): this is expensive to calculate, but most secure |
|
776 | 776 | match = crypto_backend.hash_check(auth_token, hashed) |
|
777 | 777 | if match: |
|
778 | 778 | return True |
|
779 | 779 | |
|
780 | 780 | return False |
|
781 | 781 | |
|
782 | 782 | @property |
|
783 | 783 | def ip_addresses(self): |
|
784 | 784 | ret = UserIpMap.query().filter(UserIpMap.user == self).all() |
|
785 | 785 | return [x.ip_addr for x in ret] |
|
786 | 786 | |
|
787 | 787 | @property |
|
788 | 788 | def username_and_name(self): |
|
789 | 789 | return '%s (%s %s)' % (self.username, self.first_name, self.last_name) |
|
790 | 790 | |
|
791 | 791 | @property |
|
792 | 792 | def username_or_name_or_email(self): |
|
793 | 793 | full_name = self.full_name if self.full_name is not ' ' else None |
|
794 | 794 | return self.username or full_name or self.email |
|
795 | 795 | |
|
796 | 796 | @property |
|
797 | 797 | def full_name(self): |
|
798 | 798 | return '%s %s' % (self.first_name, self.last_name) |
|
799 | 799 | |
|
800 | 800 | @property |
|
801 | 801 | def full_name_or_username(self): |
|
802 | 802 | return ('%s %s' % (self.first_name, self.last_name) |
|
803 | 803 | if (self.first_name and self.last_name) else self.username) |
|
804 | 804 | |
|
805 | 805 | @property |
|
806 | 806 | def full_contact(self): |
|
807 | 807 | return '%s %s <%s>' % (self.first_name, self.last_name, self.email) |
|
808 | 808 | |
|
809 | 809 | @property |
|
810 | 810 | def short_contact(self): |
|
811 | 811 | return '%s %s' % (self.first_name, self.last_name) |
|
812 | 812 | |
|
813 | 813 | @property |
|
814 | 814 | def is_admin(self): |
|
815 | 815 | return self.admin |
|
816 | 816 | |
|
817 | 817 | def AuthUser(self, **kwargs): |
|
818 | 818 | """ |
|
819 | 819 | Returns instance of AuthUser for this user |
|
820 | 820 | """ |
|
821 | 821 | from rhodecode.lib.auth import AuthUser |
|
822 | 822 | return AuthUser(user_id=self.user_id, username=self.username, **kwargs) |
|
823 | 823 | |
|
824 | 824 | @hybrid_property |
|
825 | 825 | def user_data(self): |
|
826 | 826 | if not self._user_data: |
|
827 | 827 | return {} |
|
828 | 828 | |
|
829 | 829 | try: |
|
830 | 830 | return json.loads(self._user_data) |
|
831 | 831 | except TypeError: |
|
832 | 832 | return {} |
|
833 | 833 | |
|
834 | 834 | @user_data.setter |
|
835 | 835 | def user_data(self, val): |
|
836 | 836 | if not isinstance(val, dict): |
|
837 | 837 | raise Exception('user_data must be dict, got %s' % type(val)) |
|
838 | 838 | try: |
|
839 | 839 | self._user_data = json.dumps(val) |
|
840 | 840 | except Exception: |
|
841 | 841 | log.error(traceback.format_exc()) |
|
842 | 842 | |
|
843 | 843 | @classmethod |
|
844 | 844 | def get_by_username(cls, username, case_insensitive=False, |
|
845 | 845 | cache=False, identity_cache=False): |
|
846 | 846 | session = Session() |
|
847 | 847 | |
|
848 | 848 | if case_insensitive: |
|
849 | 849 | q = cls.query().filter( |
|
850 | 850 | func.lower(cls.username) == func.lower(username)) |
|
851 | 851 | else: |
|
852 | 852 | q = cls.query().filter(cls.username == username) |
|
853 | 853 | |
|
854 | 854 | if cache: |
|
855 | 855 | if identity_cache: |
|
856 | 856 | val = cls.identity_cache(session, 'username', username) |
|
857 | 857 | if val: |
|
858 | 858 | return val |
|
859 | 859 | else: |
|
860 | 860 | cache_key = "get_user_by_name_%s" % _hash_key(username) |
|
861 | 861 | q = q.options( |
|
862 | 862 | FromCache("sql_cache_short", cache_key)) |
|
863 | 863 | |
|
864 | 864 | return q.scalar() |
|
865 | 865 | |
|
866 | 866 | @classmethod |
|
867 | 867 | def get_by_auth_token(cls, auth_token, cache=False): |
|
868 | 868 | q = UserApiKeys.query()\ |
|
869 | 869 | .filter(UserApiKeys.api_key == auth_token)\ |
|
870 | 870 | .filter(or_(UserApiKeys.expires == -1, |
|
871 | 871 | UserApiKeys.expires >= time.time())) |
|
872 | 872 | if cache: |
|
873 | 873 | q = q.options( |
|
874 | 874 | FromCache("sql_cache_short", "get_auth_token_%s" % auth_token)) |
|
875 | 875 | |
|
876 | 876 | match = q.first() |
|
877 | 877 | if match: |
|
878 | 878 | return match.user |
|
879 | 879 | |
|
880 | 880 | @classmethod |
|
881 | 881 | def get_by_email(cls, email, case_insensitive=False, cache=False): |
|
882 | 882 | |
|
883 | 883 | if case_insensitive: |
|
884 | 884 | q = cls.query().filter(func.lower(cls.email) == func.lower(email)) |
|
885 | 885 | |
|
886 | 886 | else: |
|
887 | 887 | q = cls.query().filter(cls.email == email) |
|
888 | 888 | |
|
889 | 889 | email_key = _hash_key(email) |
|
890 | 890 | if cache: |
|
891 | 891 | q = q.options( |
|
892 | 892 | FromCache("sql_cache_short", "get_email_key_%s" % email_key)) |
|
893 | 893 | |
|
894 | 894 | ret = q.scalar() |
|
895 | 895 | if ret is None: |
|
896 | 896 | q = UserEmailMap.query() |
|
897 | 897 | # try fetching in alternate email map |
|
898 | 898 | if case_insensitive: |
|
899 | 899 | q = q.filter(func.lower(UserEmailMap.email) == func.lower(email)) |
|
900 | 900 | else: |
|
901 | 901 | q = q.filter(UserEmailMap.email == email) |
|
902 | 902 | q = q.options(joinedload(UserEmailMap.user)) |
|
903 | 903 | if cache: |
|
904 | 904 | q = q.options( |
|
905 | 905 | FromCache("sql_cache_short", "get_email_map_key_%s" % email_key)) |
|
906 | 906 | ret = getattr(q.scalar(), 'user', None) |
|
907 | 907 | |
|
908 | 908 | return ret |
|
909 | 909 | |
|
910 | 910 | @classmethod |
|
911 | 911 | def get_from_cs_author(cls, author): |
|
912 | 912 | """ |
|
913 | 913 | Tries to get User objects out of commit author string |
|
914 | 914 | |
|
915 | 915 | :param author: |
|
916 | 916 | """ |
|
917 | 917 | from rhodecode.lib.helpers import email, author_name |
|
918 | 918 | # Valid email in the attribute passed, see if they're in the system |
|
919 | 919 | _email = email(author) |
|
920 | 920 | if _email: |
|
921 | 921 | user = cls.get_by_email(_email, case_insensitive=True) |
|
922 | 922 | if user: |
|
923 | 923 | return user |
|
924 | 924 | # Maybe we can match by username? |
|
925 | 925 | _author = author_name(author) |
|
926 | 926 | user = cls.get_by_username(_author, case_insensitive=True) |
|
927 | 927 | if user: |
|
928 | 928 | return user |
|
929 | 929 | |
|
930 | 930 | def update_userdata(self, **kwargs): |
|
931 | 931 | usr = self |
|
932 | 932 | old = usr.user_data |
|
933 | 933 | old.update(**kwargs) |
|
934 | 934 | usr.user_data = old |
|
935 | 935 | Session().add(usr) |
|
936 | 936 | log.debug('updated userdata with ', kwargs) |
|
937 | 937 | |
|
938 | 938 | def update_lastlogin(self): |
|
939 | 939 | """Update user lastlogin""" |
|
940 | 940 | self.last_login = datetime.datetime.now() |
|
941 | 941 | Session().add(self) |
|
942 | 942 | log.debug('updated user %s lastlogin', self.username) |
|
943 | 943 | |
|
944 | 944 | def update_password(self, new_password): |
|
945 | 945 | from rhodecode.lib.auth import get_crypt_password |
|
946 | 946 | |
|
947 | 947 | self.password = get_crypt_password(new_password) |
|
948 | 948 | Session().add(self) |
|
949 | 949 | |
|
950 | 950 | @classmethod |
|
951 | 951 | def get_first_super_admin(cls): |
|
952 | 952 | user = User.query()\ |
|
953 | 953 | .filter(User.admin == true()) \ |
|
954 | 954 | .order_by(User.user_id.asc()) \ |
|
955 | 955 | .first() |
|
956 | 956 | |
|
957 | 957 | if user is None: |
|
958 | 958 | raise Exception('FATAL: Missing administrative account!') |
|
959 | 959 | return user |
|
960 | 960 | |
|
961 | 961 | @classmethod |
|
962 | 962 | def get_all_super_admins(cls, only_active=False): |
|
963 | 963 | """ |
|
964 | 964 | Returns all admin accounts sorted by username |
|
965 | 965 | """ |
|
966 | 966 | qry = User.query().filter(User.admin == true()).order_by(User.username.asc()) |
|
967 | 967 | if only_active: |
|
968 | 968 | qry = qry.filter(User.active == true()) |
|
969 | 969 | return qry.all() |
|
970 | 970 | |
|
971 | 971 | @classmethod |
|
972 | 972 | def get_default_user(cls, cache=False, refresh=False): |
|
973 | 973 | user = User.get_by_username(User.DEFAULT_USER, cache=cache) |
|
974 | 974 | if user is None: |
|
975 | 975 | raise Exception('FATAL: Missing default account!') |
|
976 | 976 | if refresh: |
|
977 | 977 | # The default user might be based on outdated state which |
|
978 | 978 | # has been loaded from the cache. |
|
979 | 979 | # A call to refresh() ensures that the |
|
980 | 980 | # latest state from the database is used. |
|
981 | 981 | Session().refresh(user) |
|
982 | 982 | return user |
|
983 | 983 | |
|
984 | 984 | def _get_default_perms(self, user, suffix=''): |
|
985 | 985 | from rhodecode.model.permission import PermissionModel |
|
986 | 986 | return PermissionModel().get_default_perms(user.user_perms, suffix) |
|
987 | 987 | |
|
988 | 988 | def get_default_perms(self, suffix=''): |
|
989 | 989 | return self._get_default_perms(self, suffix) |
|
990 | 990 | |
|
991 | 991 | def get_api_data(self, include_secrets=False, details='full'): |
|
992 | 992 | """ |
|
993 | 993 | Common function for generating user related data for API |
|
994 | 994 | |
|
995 | 995 | :param include_secrets: By default secrets in the API data will be replaced |
|
996 | 996 | by a placeholder value to prevent exposing this data by accident. In case |
|
997 | 997 | this data shall be exposed, set this flag to ``True``. |
|
998 | 998 | |
|
999 | 999 | :param details: details can be 'basic|full' basic gives only a subset of |
|
1000 | 1000 | the available user information that includes user_id, name and emails. |
|
1001 | 1001 | """ |
|
1002 | 1002 | user = self |
|
1003 | 1003 | user_data = self.user_data |
|
1004 | 1004 | data = { |
|
1005 | 1005 | 'user_id': user.user_id, |
|
1006 | 1006 | 'username': user.username, |
|
1007 | 1007 | 'firstname': user.name, |
|
1008 | 1008 | 'lastname': user.lastname, |
|
1009 | 1009 | 'email': user.email, |
|
1010 | 1010 | 'emails': user.emails, |
|
1011 | 1011 | } |
|
1012 | 1012 | if details == 'basic': |
|
1013 | 1013 | return data |
|
1014 | 1014 | |
|
1015 | 1015 | auth_token_length = 40 |
|
1016 | 1016 | auth_token_replacement = '*' * auth_token_length |
|
1017 | 1017 | |
|
1018 | 1018 | extras = { |
|
1019 | 1019 | 'auth_tokens': [auth_token_replacement], |
|
1020 | 1020 | 'active': user.active, |
|
1021 | 1021 | 'admin': user.admin, |
|
1022 | 1022 | 'extern_type': user.extern_type, |
|
1023 | 1023 | 'extern_name': user.extern_name, |
|
1024 | 1024 | 'last_login': user.last_login, |
|
1025 | 1025 | 'last_activity': user.last_activity, |
|
1026 | 1026 | 'ip_addresses': user.ip_addresses, |
|
1027 | 1027 | 'language': user_data.get('language') |
|
1028 | 1028 | } |
|
1029 | 1029 | data.update(extras) |
|
1030 | 1030 | |
|
1031 | 1031 | if include_secrets: |
|
1032 | 1032 | data['auth_tokens'] = user.auth_tokens |
|
1033 | 1033 | return data |
|
1034 | 1034 | |
|
1035 | 1035 | def __json__(self): |
|
1036 | 1036 | data = { |
|
1037 | 1037 | 'full_name': self.full_name, |
|
1038 | 1038 | 'full_name_or_username': self.full_name_or_username, |
|
1039 | 1039 | 'short_contact': self.short_contact, |
|
1040 | 1040 | 'full_contact': self.full_contact, |
|
1041 | 1041 | } |
|
1042 | 1042 | data.update(self.get_api_data()) |
|
1043 | 1043 | return data |
|
1044 | 1044 | |
|
1045 | 1045 | |
|
1046 | 1046 | class UserApiKeys(Base, BaseModel): |
|
1047 | 1047 | __tablename__ = 'user_api_keys' |
|
1048 | 1048 | __table_args__ = ( |
|
1049 | 1049 | Index('uak_api_key_idx', 'api_key', unique=True), |
|
1050 | 1050 | Index('uak_api_key_expires_idx', 'api_key', 'expires'), |
|
1051 | 1051 | base_table_args |
|
1052 | 1052 | ) |
|
1053 | 1053 | __mapper_args__ = {} |
|
1054 | 1054 | |
|
1055 | 1055 | # ApiKey role |
|
1056 | 1056 | ROLE_ALL = 'token_role_all' |
|
1057 | 1057 | ROLE_HTTP = 'token_role_http' |
|
1058 | 1058 | ROLE_VCS = 'token_role_vcs' |
|
1059 | 1059 | ROLE_API = 'token_role_api' |
|
1060 | 1060 | ROLE_FEED = 'token_role_feed' |
|
1061 | 1061 | ROLE_PASSWORD_RESET = 'token_password_reset' |
|
1062 | 1062 | |
|
1063 | 1063 | ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED] |
|
1064 | 1064 | |
|
1065 | 1065 | user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1066 | 1066 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) |
|
1067 | 1067 | api_key = Column("api_key", String(255), nullable=False, unique=True) |
|
1068 | 1068 | description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) |
|
1069 | 1069 | expires = Column('expires', Float(53), nullable=False) |
|
1070 | 1070 | role = Column('role', String(255), nullable=True) |
|
1071 | 1071 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
1072 | 1072 | |
|
1073 | 1073 | # scope columns |
|
1074 | 1074 | repo_id = Column( |
|
1075 | 1075 | 'repo_id', Integer(), ForeignKey('repositories.repo_id'), |
|
1076 | 1076 | nullable=True, unique=None, default=None) |
|
1077 | 1077 | repo = relationship('Repository', lazy='joined') |
|
1078 | 1078 | |
|
1079 | 1079 | repo_group_id = Column( |
|
1080 | 1080 | 'repo_group_id', Integer(), ForeignKey('groups.group_id'), |
|
1081 | 1081 | nullable=True, unique=None, default=None) |
|
1082 | 1082 | repo_group = relationship('RepoGroup', lazy='joined') |
|
1083 | 1083 | |
|
1084 | 1084 | user = relationship('User', lazy='joined') |
|
1085 | 1085 | |
|
1086 | 1086 | def __unicode__(self): |
|
1087 | 1087 | return u"<%s('%s')>" % (self.__class__.__name__, self.role) |
|
1088 | 1088 | |
|
1089 | 1089 | def __json__(self): |
|
1090 | 1090 | data = { |
|
1091 | 1091 | 'auth_token': self.api_key, |
|
1092 | 1092 | 'role': self.role, |
|
1093 | 1093 | 'scope': self.scope_humanized, |
|
1094 | 1094 | 'expired': self.expired |
|
1095 | 1095 | } |
|
1096 | 1096 | return data |
|
1097 | 1097 | |
|
1098 | 1098 | def get_api_data(self, include_secrets=False): |
|
1099 | 1099 | data = self.__json__() |
|
1100 | 1100 | if include_secrets: |
|
1101 | 1101 | return data |
|
1102 | 1102 | else: |
|
1103 | 1103 | data['auth_token'] = self.token_obfuscated |
|
1104 | 1104 | return data |
|
1105 | 1105 | |
|
1106 | 1106 | @hybrid_property |
|
1107 | 1107 | def description_safe(self): |
|
1108 | 1108 | from rhodecode.lib import helpers as h |
|
1109 | 1109 | return h.escape(self.description) |
|
1110 | 1110 | |
|
1111 | 1111 | @property |
|
1112 | 1112 | def expired(self): |
|
1113 | 1113 | if self.expires == -1: |
|
1114 | 1114 | return False |
|
1115 | 1115 | return time.time() > self.expires |
|
1116 | 1116 | |
|
1117 | 1117 | @classmethod |
|
1118 | 1118 | def _get_role_name(cls, role): |
|
1119 | 1119 | return { |
|
1120 | 1120 | cls.ROLE_ALL: _('all'), |
|
1121 | 1121 | cls.ROLE_HTTP: _('http/web interface'), |
|
1122 | 1122 | cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'), |
|
1123 | 1123 | cls.ROLE_API: _('api calls'), |
|
1124 | 1124 | cls.ROLE_FEED: _('feed access'), |
|
1125 | 1125 | }.get(role, role) |
|
1126 | 1126 | |
|
1127 | 1127 | @property |
|
1128 | 1128 | def role_humanized(self): |
|
1129 | 1129 | return self._get_role_name(self.role) |
|
1130 | 1130 | |
|
1131 | 1131 | def _get_scope(self): |
|
1132 | 1132 | if self.repo: |
|
1133 | 1133 | return 'Repository: {}'.format(self.repo.repo_name) |
|
1134 | 1134 | if self.repo_group: |
|
1135 | 1135 | return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name) |
|
1136 | 1136 | return 'Global' |
|
1137 | 1137 | |
|
1138 | 1138 | @property |
|
1139 | 1139 | def scope_humanized(self): |
|
1140 | 1140 | return self._get_scope() |
|
1141 | 1141 | |
|
1142 | 1142 | @property |
|
1143 | 1143 | def token_obfuscated(self): |
|
1144 | 1144 | if self.api_key: |
|
1145 | 1145 | return self.api_key[:4] + "****" |
|
1146 | 1146 | |
|
1147 | 1147 | |
|
1148 | 1148 | class UserEmailMap(Base, BaseModel): |
|
1149 | 1149 | __tablename__ = 'user_email_map' |
|
1150 | 1150 | __table_args__ = ( |
|
1151 | 1151 | Index('uem_email_idx', 'email'), |
|
1152 | 1152 | UniqueConstraint('email'), |
|
1153 | 1153 | base_table_args |
|
1154 | 1154 | ) |
|
1155 | 1155 | __mapper_args__ = {} |
|
1156 | 1156 | |
|
1157 | 1157 | email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1158 | 1158 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) |
|
1159 | 1159 | _email = Column("email", String(255), nullable=True, unique=False, default=None) |
|
1160 | 1160 | user = relationship('User', lazy='joined') |
|
1161 | 1161 | |
|
1162 | 1162 | @validates('_email') |
|
1163 | 1163 | def validate_email(self, key, email): |
|
1164 | 1164 | # check if this email is not main one |
|
1165 | 1165 | main_email = Session().query(User).filter(User.email == email).scalar() |
|
1166 | 1166 | if main_email is not None: |
|
1167 | 1167 | raise AttributeError('email %s is present is user table' % email) |
|
1168 | 1168 | return email |
|
1169 | 1169 | |
|
1170 | 1170 | @hybrid_property |
|
1171 | 1171 | def email(self): |
|
1172 | 1172 | return self._email |
|
1173 | 1173 | |
|
1174 | 1174 | @email.setter |
|
1175 | 1175 | def email(self, val): |
|
1176 | 1176 | self._email = val.lower() if val else None |
|
1177 | 1177 | |
|
1178 | 1178 | |
|
1179 | 1179 | class UserIpMap(Base, BaseModel): |
|
1180 | 1180 | __tablename__ = 'user_ip_map' |
|
1181 | 1181 | __table_args__ = ( |
|
1182 | 1182 | UniqueConstraint('user_id', 'ip_addr'), |
|
1183 | 1183 | base_table_args |
|
1184 | 1184 | ) |
|
1185 | 1185 | __mapper_args__ = {} |
|
1186 | 1186 | |
|
1187 | 1187 | ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1188 | 1188 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) |
|
1189 | 1189 | ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None) |
|
1190 | 1190 | active = Column("active", Boolean(), nullable=True, unique=None, default=True) |
|
1191 | 1191 | description = Column("description", String(10000), nullable=True, unique=None, default=None) |
|
1192 | 1192 | user = relationship('User', lazy='joined') |
|
1193 | 1193 | |
|
1194 | 1194 | @hybrid_property |
|
1195 | 1195 | def description_safe(self): |
|
1196 | 1196 | from rhodecode.lib import helpers as h |
|
1197 | 1197 | return h.escape(self.description) |
|
1198 | 1198 | |
|
1199 | 1199 | @classmethod |
|
1200 | 1200 | def _get_ip_range(cls, ip_addr): |
|
1201 | 1201 | net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False) |
|
1202 | 1202 | return [str(net.network_address), str(net.broadcast_address)] |
|
1203 | 1203 | |
|
1204 | 1204 | def __json__(self): |
|
1205 | 1205 | return { |
|
1206 | 1206 | 'ip_addr': self.ip_addr, |
|
1207 | 1207 | 'ip_range': self._get_ip_range(self.ip_addr), |
|
1208 | 1208 | } |
|
1209 | 1209 | |
|
1210 | 1210 | def __unicode__(self): |
|
1211 | 1211 | return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__, |
|
1212 | 1212 | self.user_id, self.ip_addr) |
|
1213 | 1213 | |
|
1214 | 1214 | |
|
1215 | 1215 | class UserSshKeys(Base, BaseModel): |
|
1216 | 1216 | __tablename__ = 'user_ssh_keys' |
|
1217 | 1217 | __table_args__ = ( |
|
1218 | 1218 | Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'), |
|
1219 | 1219 | |
|
1220 | 1220 | UniqueConstraint('ssh_key_fingerprint'), |
|
1221 | 1221 | |
|
1222 | 1222 | base_table_args |
|
1223 | 1223 | ) |
|
1224 | 1224 | __mapper_args__ = {} |
|
1225 | 1225 | |
|
1226 | 1226 | ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1227 | 1227 | ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None) |
|
1228 | 1228 | ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None) |
|
1229 | 1229 | |
|
1230 | 1230 | description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) |
|
1231 | 1231 | |
|
1232 | 1232 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
1233 | 1233 | accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None) |
|
1234 | 1234 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) |
|
1235 | 1235 | |
|
1236 | 1236 | user = relationship('User', lazy='joined') |
|
1237 | 1237 | |
|
1238 | 1238 | def __json__(self): |
|
1239 | 1239 | data = { |
|
1240 | 1240 | 'ssh_fingerprint': self.ssh_key_fingerprint, |
|
1241 | 1241 | 'description': self.description, |
|
1242 | 1242 | 'created_on': self.created_on |
|
1243 | 1243 | } |
|
1244 | 1244 | return data |
|
1245 | 1245 | |
|
1246 | 1246 | def get_api_data(self): |
|
1247 | 1247 | data = self.__json__() |
|
1248 | 1248 | return data |
|
1249 | 1249 | |
|
1250 | 1250 | |
|
1251 | 1251 | class UserLog(Base, BaseModel): |
|
1252 | 1252 | __tablename__ = 'user_logs' |
|
1253 | 1253 | __table_args__ = ( |
|
1254 | 1254 | base_table_args, |
|
1255 | 1255 | ) |
|
1256 | 1256 | |
|
1257 | 1257 | VERSION_1 = 'v1' |
|
1258 | 1258 | VERSION_2 = 'v2' |
|
1259 | 1259 | VERSIONS = [VERSION_1, VERSION_2] |
|
1260 | 1260 | |
|
1261 | 1261 | user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1262 | 1262 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None) |
|
1263 | 1263 | username = Column("username", String(255), nullable=True, unique=None, default=None) |
|
1264 | 1264 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None) |
|
1265 | 1265 | repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None) |
|
1266 | 1266 | user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None) |
|
1267 | 1267 | action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None) |
|
1268 | 1268 | action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None) |
|
1269 | 1269 | |
|
1270 | 1270 | version = Column("version", String(255), nullable=True, default=VERSION_1) |
|
1271 | 1271 | user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT())))) |
|
1272 | 1272 | action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT())))) |
|
1273 | 1273 | |
|
1274 | 1274 | def __unicode__(self): |
|
1275 | 1275 | return u"<%s('id:%s:%s')>" % ( |
|
1276 | 1276 | self.__class__.__name__, self.repository_name, self.action) |
|
1277 | 1277 | |
|
1278 | 1278 | def __json__(self): |
|
1279 | 1279 | return { |
|
1280 | 1280 | 'user_id': self.user_id, |
|
1281 | 1281 | 'username': self.username, |
|
1282 | 1282 | 'repository_id': self.repository_id, |
|
1283 | 1283 | 'repository_name': self.repository_name, |
|
1284 | 1284 | 'user_ip': self.user_ip, |
|
1285 | 1285 | 'action_date': self.action_date, |
|
1286 | 1286 | 'action': self.action, |
|
1287 | 1287 | } |
|
1288 | 1288 | |
|
1289 | 1289 | @hybrid_property |
|
1290 | 1290 | def entry_id(self): |
|
1291 | 1291 | return self.user_log_id |
|
1292 | 1292 | |
|
1293 | 1293 | @property |
|
1294 | 1294 | def action_as_day(self): |
|
1295 | 1295 | return datetime.date(*self.action_date.timetuple()[:3]) |
|
1296 | 1296 | |
|
1297 | 1297 | user = relationship('User') |
|
1298 | 1298 | repository = relationship('Repository', cascade='') |
|
1299 | 1299 | |
|
1300 | 1300 | |
|
1301 | 1301 | class UserGroup(Base, BaseModel): |
|
1302 | 1302 | __tablename__ = 'users_groups' |
|
1303 | 1303 | __table_args__ = ( |
|
1304 | 1304 | base_table_args, |
|
1305 | 1305 | ) |
|
1306 | 1306 | |
|
1307 | 1307 | users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1308 | 1308 | users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None) |
|
1309 | 1309 | user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None) |
|
1310 | 1310 | users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None) |
|
1311 | 1311 | inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True) |
|
1312 | 1312 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) |
|
1313 | 1313 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
1314 | 1314 | _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data |
|
1315 | 1315 | |
|
1316 | 1316 | members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined") |
|
1317 | 1317 | users_group_to_perm = relationship('UserGroupToPerm', cascade='all') |
|
1318 | 1318 | users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all') |
|
1319 | 1319 | users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') |
|
1320 | 1320 | user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all') |
|
1321 | 1321 | user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all') |
|
1322 | 1322 | |
|
1323 | 1323 | user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all') |
|
1324 | 1324 | user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id") |
|
1325 | 1325 | |
|
1326 | 1326 | @classmethod |
|
1327 | 1327 | def _load_group_data(cls, column): |
|
1328 | 1328 | if not column: |
|
1329 | 1329 | return {} |
|
1330 | 1330 | |
|
1331 | 1331 | try: |
|
1332 | 1332 | return json.loads(column) or {} |
|
1333 | 1333 | except TypeError: |
|
1334 | 1334 | return {} |
|
1335 | 1335 | |
|
1336 | 1336 | @hybrid_property |
|
1337 | 1337 | def description_safe(self): |
|
1338 | 1338 | from rhodecode.lib import helpers as h |
|
1339 | 1339 | return h.escape(self.user_group_description) |
|
1340 | 1340 | |
|
1341 | 1341 | @hybrid_property |
|
1342 | 1342 | def group_data(self): |
|
1343 | 1343 | return self._load_group_data(self._group_data) |
|
1344 | 1344 | |
|
1345 | 1345 | @group_data.expression |
|
1346 | 1346 | def group_data(self, **kwargs): |
|
1347 | 1347 | return self._group_data |
|
1348 | 1348 | |
|
1349 | 1349 | @group_data.setter |
|
1350 | 1350 | def group_data(self, val): |
|
1351 | 1351 | try: |
|
1352 | 1352 | self._group_data = json.dumps(val) |
|
1353 | 1353 | except Exception: |
|
1354 | 1354 | log.error(traceback.format_exc()) |
|
1355 | 1355 | |
|
1356 | 1356 | @classmethod |
|
1357 | 1357 | def _load_sync(cls, group_data): |
|
1358 | 1358 | if group_data: |
|
1359 | 1359 | return group_data.get('extern_type') |
|
1360 | 1360 | |
|
1361 | 1361 | @property |
|
1362 | 1362 | def sync(self): |
|
1363 | 1363 | return self._load_sync(self.group_data) |
|
1364 | 1364 | |
|
1365 | 1365 | def __unicode__(self): |
|
1366 | 1366 | return u"<%s('id:%s:%s')>" % (self.__class__.__name__, |
|
1367 | 1367 | self.users_group_id, |
|
1368 | 1368 | self.users_group_name) |
|
1369 | 1369 | |
|
1370 | 1370 | @classmethod |
|
1371 | 1371 | def get_by_group_name(cls, group_name, cache=False, |
|
1372 | 1372 | case_insensitive=False): |
|
1373 | 1373 | if case_insensitive: |
|
1374 | 1374 | q = cls.query().filter(func.lower(cls.users_group_name) == |
|
1375 | 1375 | func.lower(group_name)) |
|
1376 | 1376 | |
|
1377 | 1377 | else: |
|
1378 | 1378 | q = cls.query().filter(cls.users_group_name == group_name) |
|
1379 | 1379 | if cache: |
|
1380 | 1380 | q = q.options( |
|
1381 | 1381 | FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name))) |
|
1382 | 1382 | return q.scalar() |
|
1383 | 1383 | |
|
1384 | 1384 | @classmethod |
|
1385 | 1385 | def get(cls, user_group_id, cache=False): |
|
1386 | 1386 | if not user_group_id: |
|
1387 | 1387 | return |
|
1388 | 1388 | |
|
1389 | 1389 | user_group = cls.query() |
|
1390 | 1390 | if cache: |
|
1391 | 1391 | user_group = user_group.options( |
|
1392 | 1392 | FromCache("sql_cache_short", "get_users_group_%s" % user_group_id)) |
|
1393 | 1393 | return user_group.get(user_group_id) |
|
1394 | 1394 | |
|
1395 | 1395 | def permissions(self, with_admins=True, with_owner=True, |
|
1396 | 1396 | expand_from_user_groups=False): |
|
1397 | 1397 | """ |
|
1398 | 1398 | Permissions for user groups |
|
1399 | 1399 | """ |
|
1400 | 1400 | _admin_perm = 'usergroup.admin' |
|
1401 | 1401 | |
|
1402 | 1402 | owner_row = [] |
|
1403 | 1403 | if with_owner: |
|
1404 | 1404 | usr = AttributeDict(self.user.get_dict()) |
|
1405 | 1405 | usr.owner_row = True |
|
1406 | 1406 | usr.permission = _admin_perm |
|
1407 | 1407 | owner_row.append(usr) |
|
1408 | 1408 | |
|
1409 | 1409 | super_admin_ids = [] |
|
1410 | 1410 | super_admin_rows = [] |
|
1411 | 1411 | if with_admins: |
|
1412 | 1412 | for usr in User.get_all_super_admins(): |
|
1413 | 1413 | super_admin_ids.append(usr.user_id) |
|
1414 | 1414 | # if this admin is also owner, don't double the record |
|
1415 | 1415 | if usr.user_id == owner_row[0].user_id: |
|
1416 | 1416 | owner_row[0].admin_row = True |
|
1417 | 1417 | else: |
|
1418 | 1418 | usr = AttributeDict(usr.get_dict()) |
|
1419 | 1419 | usr.admin_row = True |
|
1420 | 1420 | usr.permission = _admin_perm |
|
1421 | 1421 | super_admin_rows.append(usr) |
|
1422 | 1422 | |
|
1423 | 1423 | q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self) |
|
1424 | 1424 | q = q.options(joinedload(UserUserGroupToPerm.user_group), |
|
1425 | 1425 | joinedload(UserUserGroupToPerm.user), |
|
1426 | 1426 | joinedload(UserUserGroupToPerm.permission),) |
|
1427 | 1427 | |
|
1428 | 1428 | # get owners and admins and permissions. We do a trick of re-writing |
|
1429 | 1429 | # objects from sqlalchemy to named-tuples due to sqlalchemy session |
|
1430 | 1430 | # has a global reference and changing one object propagates to all |
|
1431 | 1431 | # others. This means if admin is also an owner admin_row that change |
|
1432 | 1432 | # would propagate to both objects |
|
1433 | 1433 | perm_rows = [] |
|
1434 | 1434 | for _usr in q.all(): |
|
1435 | 1435 | usr = AttributeDict(_usr.user.get_dict()) |
|
1436 | 1436 | # if this user is also owner/admin, mark as duplicate record |
|
1437 | 1437 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: |
|
1438 | 1438 | usr.duplicate_perm = True |
|
1439 | 1439 | usr.permission = _usr.permission.permission_name |
|
1440 | 1440 | perm_rows.append(usr) |
|
1441 | 1441 | |
|
1442 | 1442 | # filter the perm rows by 'default' first and then sort them by |
|
1443 | 1443 | # admin,write,read,none permissions sorted again alphabetically in |
|
1444 | 1444 | # each group |
|
1445 | 1445 | perm_rows = sorted(perm_rows, key=display_user_sort) |
|
1446 | 1446 | |
|
1447 | 1447 | user_groups_rows = [] |
|
1448 | 1448 | if expand_from_user_groups: |
|
1449 | 1449 | for ug in self.permission_user_groups(with_members=True): |
|
1450 | 1450 | for user_data in ug.members: |
|
1451 | 1451 | user_groups_rows.append(user_data) |
|
1452 | 1452 | |
|
1453 | 1453 | return super_admin_rows + owner_row + perm_rows + user_groups_rows |
|
1454 | 1454 | |
|
1455 | 1455 | def permission_user_groups(self, with_members=False): |
|
1456 | 1456 | q = UserGroupUserGroupToPerm.query()\ |
|
1457 | 1457 | .filter(UserGroupUserGroupToPerm.target_user_group == self) |
|
1458 | 1458 | q = q.options(joinedload(UserGroupUserGroupToPerm.user_group), |
|
1459 | 1459 | joinedload(UserGroupUserGroupToPerm.target_user_group), |
|
1460 | 1460 | joinedload(UserGroupUserGroupToPerm.permission),) |
|
1461 | 1461 | |
|
1462 | 1462 | perm_rows = [] |
|
1463 | 1463 | for _user_group in q.all(): |
|
1464 | 1464 | entry = AttributeDict(_user_group.user_group.get_dict()) |
|
1465 | 1465 | entry.permission = _user_group.permission.permission_name |
|
1466 | 1466 | if with_members: |
|
1467 | 1467 | entry.members = [x.user.get_dict() |
|
1468 | 1468 | for x in _user_group.users_group.members] |
|
1469 | 1469 | perm_rows.append(entry) |
|
1470 | 1470 | |
|
1471 | 1471 | perm_rows = sorted(perm_rows, key=display_user_group_sort) |
|
1472 | 1472 | return perm_rows |
|
1473 | 1473 | |
|
1474 | 1474 | def _get_default_perms(self, user_group, suffix=''): |
|
1475 | 1475 | from rhodecode.model.permission import PermissionModel |
|
1476 | 1476 | return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix) |
|
1477 | 1477 | |
|
1478 | 1478 | def get_default_perms(self, suffix=''): |
|
1479 | 1479 | return self._get_default_perms(self, suffix) |
|
1480 | 1480 | |
|
1481 | 1481 | def get_api_data(self, with_group_members=True, include_secrets=False): |
|
1482 | 1482 | """ |
|
1483 | 1483 | :param include_secrets: See :meth:`User.get_api_data`, this parameter is |
|
1484 | 1484 | basically forwarded. |
|
1485 | 1485 | |
|
1486 | 1486 | """ |
|
1487 | 1487 | user_group = self |
|
1488 | 1488 | data = { |
|
1489 | 1489 | 'users_group_id': user_group.users_group_id, |
|
1490 | 1490 | 'group_name': user_group.users_group_name, |
|
1491 | 1491 | 'group_description': user_group.user_group_description, |
|
1492 | 1492 | 'active': user_group.users_group_active, |
|
1493 | 1493 | 'owner': user_group.user.username, |
|
1494 | 1494 | 'sync': user_group.sync, |
|
1495 | 1495 | 'owner_email': user_group.user.email, |
|
1496 | 1496 | } |
|
1497 | 1497 | |
|
1498 | 1498 | if with_group_members: |
|
1499 | 1499 | users = [] |
|
1500 | 1500 | for user in user_group.members: |
|
1501 | 1501 | user = user.user |
|
1502 | 1502 | users.append(user.get_api_data(include_secrets=include_secrets)) |
|
1503 | 1503 | data['users'] = users |
|
1504 | 1504 | |
|
1505 | 1505 | return data |
|
1506 | 1506 | |
|
1507 | 1507 | |
|
1508 | 1508 | class UserGroupMember(Base, BaseModel): |
|
1509 | 1509 | __tablename__ = 'users_groups_members' |
|
1510 | 1510 | __table_args__ = ( |
|
1511 | 1511 | base_table_args, |
|
1512 | 1512 | ) |
|
1513 | 1513 | |
|
1514 | 1514 | users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1515 | 1515 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
1516 | 1516 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
1517 | 1517 | |
|
1518 | 1518 | user = relationship('User', lazy='joined') |
|
1519 | 1519 | users_group = relationship('UserGroup') |
|
1520 | 1520 | |
|
1521 | 1521 | def __init__(self, gr_id='', u_id=''): |
|
1522 | 1522 | self.users_group_id = gr_id |
|
1523 | 1523 | self.user_id = u_id |
|
1524 | 1524 | |
|
1525 | 1525 | |
|
1526 | 1526 | class RepositoryField(Base, BaseModel): |
|
1527 | 1527 | __tablename__ = 'repositories_fields' |
|
1528 | 1528 | __table_args__ = ( |
|
1529 | 1529 | UniqueConstraint('repository_id', 'field_key'), # no-multi field |
|
1530 | 1530 | base_table_args, |
|
1531 | 1531 | ) |
|
1532 | 1532 | |
|
1533 | 1533 | PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields |
|
1534 | 1534 | |
|
1535 | 1535 | repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
1536 | 1536 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) |
|
1537 | 1537 | field_key = Column("field_key", String(250)) |
|
1538 | 1538 | field_label = Column("field_label", String(1024), nullable=False) |
|
1539 | 1539 | field_value = Column("field_value", String(10000), nullable=False) |
|
1540 | 1540 | field_desc = Column("field_desc", String(1024), nullable=False) |
|
1541 | 1541 | field_type = Column("field_type", String(255), nullable=False, unique=None) |
|
1542 | 1542 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
1543 | 1543 | |
|
1544 | 1544 | repository = relationship('Repository') |
|
1545 | 1545 | |
|
1546 | 1546 | @property |
|
1547 | 1547 | def field_key_prefixed(self): |
|
1548 | 1548 | return 'ex_%s' % self.field_key |
|
1549 | 1549 | |
|
1550 | 1550 | @classmethod |
|
1551 | 1551 | def un_prefix_key(cls, key): |
|
1552 | 1552 | if key.startswith(cls.PREFIX): |
|
1553 | 1553 | return key[len(cls.PREFIX):] |
|
1554 | 1554 | return key |
|
1555 | 1555 | |
|
1556 | 1556 | @classmethod |
|
1557 | 1557 | def get_by_key_name(cls, key, repo): |
|
1558 | 1558 | row = cls.query()\ |
|
1559 | 1559 | .filter(cls.repository == repo)\ |
|
1560 | 1560 | .filter(cls.field_key == key).scalar() |
|
1561 | 1561 | return row |
|
1562 | 1562 | |
|
1563 | 1563 | |
|
1564 | 1564 | class Repository(Base, BaseModel): |
|
1565 | 1565 | __tablename__ = 'repositories' |
|
1566 | 1566 | __table_args__ = ( |
|
1567 | 1567 | Index('r_repo_name_idx', 'repo_name', mysql_length=255), |
|
1568 | 1568 | base_table_args, |
|
1569 | 1569 | ) |
|
1570 | 1570 | DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}' |
|
1571 | 1571 | DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}' |
|
1572 | 1572 | DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}' |
|
1573 | 1573 | |
|
1574 | 1574 | STATE_CREATED = 'repo_state_created' |
|
1575 | 1575 | STATE_PENDING = 'repo_state_pending' |
|
1576 | 1576 | STATE_ERROR = 'repo_state_error' |
|
1577 | 1577 | |
|
1578 | 1578 | LOCK_AUTOMATIC = 'lock_auto' |
|
1579 | 1579 | LOCK_API = 'lock_api' |
|
1580 | 1580 | LOCK_WEB = 'lock_web' |
|
1581 | 1581 | LOCK_PULL = 'lock_pull' |
|
1582 | 1582 | |
|
1583 | 1583 | NAME_SEP = URL_SEP |
|
1584 | 1584 | |
|
1585 | 1585 | repo_id = Column( |
|
1586 | 1586 | "repo_id", Integer(), nullable=False, unique=True, default=None, |
|
1587 | 1587 | primary_key=True) |
|
1588 | 1588 | _repo_name = Column( |
|
1589 | 1589 | "repo_name", Text(), nullable=False, default=None) |
|
1590 | 1590 | _repo_name_hash = Column( |
|
1591 | 1591 | "repo_name_hash", String(255), nullable=False, unique=True) |
|
1592 | 1592 | repo_state = Column("repo_state", String(255), nullable=True) |
|
1593 | 1593 | |
|
1594 | 1594 | clone_uri = Column( |
|
1595 | 1595 | "clone_uri", EncryptedTextValue(), nullable=True, unique=False, |
|
1596 | 1596 | default=None) |
|
1597 | 1597 | push_uri = Column( |
|
1598 | 1598 | "push_uri", EncryptedTextValue(), nullable=True, unique=False, |
|
1599 | 1599 | default=None) |
|
1600 | 1600 | repo_type = Column( |
|
1601 | 1601 | "repo_type", String(255), nullable=False, unique=False, default=None) |
|
1602 | 1602 | user_id = Column( |
|
1603 | 1603 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, |
|
1604 | 1604 | unique=False, default=None) |
|
1605 | 1605 | private = Column( |
|
1606 | 1606 | "private", Boolean(), nullable=True, unique=None, default=None) |
|
1607 | 1607 | archived = Column( |
|
1608 | 1608 | "archived", Boolean(), nullable=True, unique=None, default=None) |
|
1609 | 1609 | enable_statistics = Column( |
|
1610 | 1610 | "statistics", Boolean(), nullable=True, unique=None, default=True) |
|
1611 | 1611 | enable_downloads = Column( |
|
1612 | 1612 | "downloads", Boolean(), nullable=True, unique=None, default=True) |
|
1613 | 1613 | description = Column( |
|
1614 | 1614 | "description", String(10000), nullable=True, unique=None, default=None) |
|
1615 | 1615 | created_on = Column( |
|
1616 | 1616 | 'created_on', DateTime(timezone=False), nullable=True, unique=None, |
|
1617 | 1617 | default=datetime.datetime.now) |
|
1618 | 1618 | updated_on = Column( |
|
1619 | 1619 | 'updated_on', DateTime(timezone=False), nullable=True, unique=None, |
|
1620 | 1620 | default=datetime.datetime.now) |
|
1621 | 1621 | _landing_revision = Column( |
|
1622 | 1622 | "landing_revision", String(255), nullable=False, unique=False, |
|
1623 | 1623 | default=None) |
|
1624 | 1624 | enable_locking = Column( |
|
1625 | 1625 | "enable_locking", Boolean(), nullable=False, unique=None, |
|
1626 | 1626 | default=False) |
|
1627 | 1627 | _locked = Column( |
|
1628 | 1628 | "locked", String(255), nullable=True, unique=False, default=None) |
|
1629 | 1629 | _changeset_cache = Column( |
|
1630 | 1630 | "changeset_cache", LargeBinary(), nullable=True) # JSON data |
|
1631 | 1631 | |
|
1632 | 1632 | fork_id = Column( |
|
1633 | 1633 | "fork_id", Integer(), ForeignKey('repositories.repo_id'), |
|
1634 | 1634 | nullable=True, unique=False, default=None) |
|
1635 | 1635 | group_id = Column( |
|
1636 | 1636 | "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, |
|
1637 | 1637 | unique=False, default=None) |
|
1638 | 1638 | |
|
1639 | 1639 | user = relationship('User', lazy='joined') |
|
1640 | 1640 | fork = relationship('Repository', remote_side=repo_id, lazy='joined') |
|
1641 | 1641 | group = relationship('RepoGroup', lazy='joined') |
|
1642 | 1642 | repo_to_perm = relationship( |
|
1643 | 1643 | 'UserRepoToPerm', cascade='all', |
|
1644 | 1644 | order_by='UserRepoToPerm.repo_to_perm_id') |
|
1645 | 1645 | users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all') |
|
1646 | 1646 | stats = relationship('Statistics', cascade='all', uselist=False) |
|
1647 | 1647 | |
|
1648 | 1648 | followers = relationship( |
|
1649 | 1649 | 'UserFollowing', |
|
1650 | 1650 | primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', |
|
1651 | 1651 | cascade='all') |
|
1652 | 1652 | extra_fields = relationship( |
|
1653 | 1653 | 'RepositoryField', cascade="all, delete, delete-orphan") |
|
1654 | 1654 | logs = relationship('UserLog') |
|
1655 | 1655 | comments = relationship( |
|
1656 | 1656 | 'ChangesetComment', cascade="all, delete, delete-orphan") |
|
1657 | 1657 | pull_requests_source = relationship( |
|
1658 | 1658 | 'PullRequest', |
|
1659 | 1659 | primaryjoin='PullRequest.source_repo_id==Repository.repo_id', |
|
1660 | 1660 | cascade="all, delete, delete-orphan") |
|
1661 | 1661 | pull_requests_target = relationship( |
|
1662 | 1662 | 'PullRequest', |
|
1663 | 1663 | primaryjoin='PullRequest.target_repo_id==Repository.repo_id', |
|
1664 | 1664 | cascade="all, delete, delete-orphan") |
|
1665 | 1665 | ui = relationship('RepoRhodeCodeUi', cascade="all") |
|
1666 | 1666 | settings = relationship('RepoRhodeCodeSetting', cascade="all") |
|
1667 | 1667 | integrations = relationship('Integration', |
|
1668 | 1668 | cascade="all, delete, delete-orphan") |
|
1669 | 1669 | |
|
1670 | 1670 | scoped_tokens = relationship('UserApiKeys', cascade="all") |
|
1671 | 1671 | |
|
1672 | 1672 | def __unicode__(self): |
|
1673 | 1673 | return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, |
|
1674 | 1674 | safe_unicode(self.repo_name)) |
|
1675 | 1675 | |
|
1676 | 1676 | @hybrid_property |
|
1677 | 1677 | def description_safe(self): |
|
1678 | 1678 | from rhodecode.lib import helpers as h |
|
1679 | 1679 | return h.escape(self.description) |
|
1680 | 1680 | |
|
1681 | 1681 | @hybrid_property |
|
1682 | 1682 | def landing_rev(self): |
|
1683 | 1683 | # always should return [rev_type, rev] |
|
1684 | 1684 | if self._landing_revision: |
|
1685 | 1685 | _rev_info = self._landing_revision.split(':') |
|
1686 | 1686 | if len(_rev_info) < 2: |
|
1687 | 1687 | _rev_info.insert(0, 'rev') |
|
1688 | 1688 | return [_rev_info[0], _rev_info[1]] |
|
1689 | 1689 | return [None, None] |
|
1690 | 1690 | |
|
1691 | 1691 | @landing_rev.setter |
|
1692 | 1692 | def landing_rev(self, val): |
|
1693 | 1693 | if ':' not in val: |
|
1694 | 1694 | raise ValueError('value must be delimited with `:` and consist ' |
|
1695 | 1695 | 'of <rev_type>:<rev>, got %s instead' % val) |
|
1696 | 1696 | self._landing_revision = val |
|
1697 | 1697 | |
|
1698 | 1698 | @hybrid_property |
|
1699 | 1699 | def locked(self): |
|
1700 | 1700 | if self._locked: |
|
1701 | 1701 | user_id, timelocked, reason = self._locked.split(':') |
|
1702 | 1702 | lock_values = int(user_id), timelocked, reason |
|
1703 | 1703 | else: |
|
1704 | 1704 | lock_values = [None, None, None] |
|
1705 | 1705 | return lock_values |
|
1706 | 1706 | |
|
1707 | 1707 | @locked.setter |
|
1708 | 1708 | def locked(self, val): |
|
1709 | 1709 | if val and isinstance(val, (list, tuple)): |
|
1710 | 1710 | self._locked = ':'.join(map(str, val)) |
|
1711 | 1711 | else: |
|
1712 | 1712 | self._locked = None |
|
1713 | 1713 | |
|
1714 | 1714 | @hybrid_property |
|
1715 | 1715 | def changeset_cache(self): |
|
1716 | 1716 | from rhodecode.lib.vcs.backends.base import EmptyCommit |
|
1717 | 1717 | dummy = EmptyCommit().__json__() |
|
1718 | 1718 | if not self._changeset_cache: |
|
1719 | 1719 | return dummy |
|
1720 | 1720 | try: |
|
1721 | 1721 | return json.loads(self._changeset_cache) |
|
1722 | 1722 | except TypeError: |
|
1723 | 1723 | return dummy |
|
1724 | 1724 | except Exception: |
|
1725 | 1725 | log.error(traceback.format_exc()) |
|
1726 | 1726 | return dummy |
|
1727 | 1727 | |
|
1728 | 1728 | @changeset_cache.setter |
|
1729 | 1729 | def changeset_cache(self, val): |
|
1730 | 1730 | try: |
|
1731 | 1731 | self._changeset_cache = json.dumps(val) |
|
1732 | 1732 | except Exception: |
|
1733 | 1733 | log.error(traceback.format_exc()) |
|
1734 | 1734 | |
|
1735 | 1735 | @hybrid_property |
|
1736 | 1736 | def repo_name(self): |
|
1737 | 1737 | return self._repo_name |
|
1738 | 1738 | |
|
1739 | 1739 | @repo_name.setter |
|
1740 | 1740 | def repo_name(self, value): |
|
1741 | 1741 | self._repo_name = value |
|
1742 | 1742 | self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest() |
|
1743 | 1743 | |
|
1744 | 1744 | @classmethod |
|
1745 | 1745 | def normalize_repo_name(cls, repo_name): |
|
1746 | 1746 | """ |
|
1747 | 1747 | Normalizes os specific repo_name to the format internally stored inside |
|
1748 | 1748 | database using URL_SEP |
|
1749 | 1749 | |
|
1750 | 1750 | :param cls: |
|
1751 | 1751 | :param repo_name: |
|
1752 | 1752 | """ |
|
1753 | 1753 | return cls.NAME_SEP.join(repo_name.split(os.sep)) |
|
1754 | 1754 | |
|
1755 | 1755 | @classmethod |
|
1756 | 1756 | def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False): |
|
1757 | 1757 | session = Session() |
|
1758 | 1758 | q = session.query(cls).filter(cls.repo_name == repo_name) |
|
1759 | 1759 | |
|
1760 | 1760 | if cache: |
|
1761 | 1761 | if identity_cache: |
|
1762 | 1762 | val = cls.identity_cache(session, 'repo_name', repo_name) |
|
1763 | 1763 | if val: |
|
1764 | 1764 | return val |
|
1765 | 1765 | else: |
|
1766 | 1766 | cache_key = "get_repo_by_name_%s" % _hash_key(repo_name) |
|
1767 | 1767 | q = q.options( |
|
1768 | 1768 | FromCache("sql_cache_short", cache_key)) |
|
1769 | 1769 | |
|
1770 | 1770 | return q.scalar() |
|
1771 | 1771 | |
|
1772 | 1772 | @classmethod |
|
1773 | 1773 | def get_by_id_or_repo_name(cls, repoid): |
|
1774 | 1774 | if isinstance(repoid, (int, long)): |
|
1775 | 1775 | try: |
|
1776 | 1776 | repo = cls.get(repoid) |
|
1777 | 1777 | except ValueError: |
|
1778 | 1778 | repo = None |
|
1779 | 1779 | else: |
|
1780 | 1780 | repo = cls.get_by_repo_name(repoid) |
|
1781 | 1781 | return repo |
|
1782 | 1782 | |
|
1783 | 1783 | @classmethod |
|
1784 | 1784 | def get_by_full_path(cls, repo_full_path): |
|
1785 | 1785 | repo_name = repo_full_path.split(cls.base_path(), 1)[-1] |
|
1786 | 1786 | repo_name = cls.normalize_repo_name(repo_name) |
|
1787 | 1787 | return cls.get_by_repo_name(repo_name.strip(URL_SEP)) |
|
1788 | 1788 | |
|
1789 | 1789 | @classmethod |
|
1790 | 1790 | def get_repo_forks(cls, repo_id): |
|
1791 | 1791 | return cls.query().filter(Repository.fork_id == repo_id) |
|
1792 | 1792 | |
|
1793 | 1793 | @classmethod |
|
1794 | 1794 | def base_path(cls): |
|
1795 | 1795 | """ |
|
1796 | 1796 | Returns base path when all repos are stored |
|
1797 | 1797 | |
|
1798 | 1798 | :param cls: |
|
1799 | 1799 | """ |
|
1800 | 1800 | q = Session().query(RhodeCodeUi)\ |
|
1801 | 1801 | .filter(RhodeCodeUi.ui_key == cls.NAME_SEP) |
|
1802 | 1802 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) |
|
1803 | 1803 | return q.one().ui_value |
|
1804 | 1804 | |
|
1805 | 1805 | @classmethod |
|
1806 | 1806 | def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None), |
|
1807 | 1807 | case_insensitive=True, archived=False): |
|
1808 | 1808 | q = Repository.query() |
|
1809 | 1809 | |
|
1810 | 1810 | if not archived: |
|
1811 | 1811 | q = q.filter(Repository.archived.isnot(true())) |
|
1812 | 1812 | |
|
1813 | 1813 | if not isinstance(user_id, Optional): |
|
1814 | 1814 | q = q.filter(Repository.user_id == user_id) |
|
1815 | 1815 | |
|
1816 | 1816 | if not isinstance(group_id, Optional): |
|
1817 | 1817 | q = q.filter(Repository.group_id == group_id) |
|
1818 | 1818 | |
|
1819 | 1819 | if case_insensitive: |
|
1820 | 1820 | q = q.order_by(func.lower(Repository.repo_name)) |
|
1821 | 1821 | else: |
|
1822 | 1822 | q = q.order_by(Repository.repo_name) |
|
1823 | 1823 | |
|
1824 | 1824 | return q.all() |
|
1825 | 1825 | |
|
1826 | 1826 | @property |
|
1827 | 1827 | def forks(self): |
|
1828 | 1828 | """ |
|
1829 | 1829 | Return forks of this repo |
|
1830 | 1830 | """ |
|
1831 | 1831 | return Repository.get_repo_forks(self.repo_id) |
|
1832 | 1832 | |
|
1833 | 1833 | @property |
|
1834 | 1834 | def parent(self): |
|
1835 | 1835 | """ |
|
1836 | 1836 | Returns fork parent |
|
1837 | 1837 | """ |
|
1838 | 1838 | return self.fork |
|
1839 | 1839 | |
|
1840 | 1840 | @property |
|
1841 | 1841 | def just_name(self): |
|
1842 | 1842 | return self.repo_name.split(self.NAME_SEP)[-1] |
|
1843 | 1843 | |
|
1844 | 1844 | @property |
|
1845 | 1845 | def groups_with_parents(self): |
|
1846 | 1846 | groups = [] |
|
1847 | 1847 | if self.group is None: |
|
1848 | 1848 | return groups |
|
1849 | 1849 | |
|
1850 | 1850 | cur_gr = self.group |
|
1851 | 1851 | groups.insert(0, cur_gr) |
|
1852 | 1852 | while 1: |
|
1853 | 1853 | gr = getattr(cur_gr, 'parent_group', None) |
|
1854 | 1854 | cur_gr = cur_gr.parent_group |
|
1855 | 1855 | if gr is None: |
|
1856 | 1856 | break |
|
1857 | 1857 | groups.insert(0, gr) |
|
1858 | 1858 | |
|
1859 | 1859 | return groups |
|
1860 | 1860 | |
|
1861 | 1861 | @property |
|
1862 | 1862 | def groups_and_repo(self): |
|
1863 | 1863 | return self.groups_with_parents, self |
|
1864 | 1864 | |
|
1865 | 1865 | @LazyProperty |
|
1866 | 1866 | def repo_path(self): |
|
1867 | 1867 | """ |
|
1868 | 1868 | Returns base full path for that repository means where it actually |
|
1869 | 1869 | exists on a filesystem |
|
1870 | 1870 | """ |
|
1871 | 1871 | q = Session().query(RhodeCodeUi).filter( |
|
1872 | 1872 | RhodeCodeUi.ui_key == self.NAME_SEP) |
|
1873 | 1873 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) |
|
1874 | 1874 | return q.one().ui_value |
|
1875 | 1875 | |
|
1876 | 1876 | @property |
|
1877 | 1877 | def repo_full_path(self): |
|
1878 | 1878 | p = [self.repo_path] |
|
1879 | 1879 | # we need to split the name by / since this is how we store the |
|
1880 | 1880 | # names in the database, but that eventually needs to be converted |
|
1881 | 1881 | # into a valid system path |
|
1882 | 1882 | p += self.repo_name.split(self.NAME_SEP) |
|
1883 | 1883 | return os.path.join(*map(safe_unicode, p)) |
|
1884 | 1884 | |
|
1885 | 1885 | @property |
|
1886 | 1886 | def cache_keys(self): |
|
1887 | 1887 | """ |
|
1888 | 1888 | Returns associated cache keys for that repo |
|
1889 | 1889 | """ |
|
1890 | 1890 | invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format( |
|
1891 | 1891 | repo_id=self.repo_id) |
|
1892 | 1892 | return CacheKey.query()\ |
|
1893 | 1893 | .filter(CacheKey.cache_args == invalidation_namespace)\ |
|
1894 | 1894 | .order_by(CacheKey.cache_key)\ |
|
1895 | 1895 | .all() |
|
1896 | 1896 | |
|
1897 | 1897 | @property |
|
1898 | 1898 | def cached_diffs_relative_dir(self): |
|
1899 | 1899 | """ |
|
1900 | 1900 | Return a relative to the repository store path of cached diffs |
|
1901 | 1901 | used for safe display for users, who shouldn't know the absolute store |
|
1902 | 1902 | path |
|
1903 | 1903 | """ |
|
1904 | 1904 | return os.path.join( |
|
1905 | 1905 | os.path.dirname(self.repo_name), |
|
1906 | 1906 | self.cached_diffs_dir.split(os.path.sep)[-1]) |
|
1907 | 1907 | |
|
1908 | 1908 | @property |
|
1909 | 1909 | def cached_diffs_dir(self): |
|
1910 | 1910 | path = self.repo_full_path |
|
1911 | 1911 | return os.path.join( |
|
1912 | 1912 | os.path.dirname(path), |
|
1913 | 1913 | '.__shadow_diff_cache_repo_{}'.format(self.repo_id)) |
|
1914 | 1914 | |
|
1915 | 1915 | def cached_diffs(self): |
|
1916 | 1916 | diff_cache_dir = self.cached_diffs_dir |
|
1917 | 1917 | if os.path.isdir(diff_cache_dir): |
|
1918 | 1918 | return os.listdir(diff_cache_dir) |
|
1919 | 1919 | return [] |
|
1920 | 1920 | |
|
1921 | 1921 | def shadow_repos(self): |
|
1922 | 1922 | shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id) |
|
1923 | 1923 | return [ |
|
1924 | 1924 | x for x in os.listdir(os.path.dirname(self.repo_full_path)) |
|
1925 | 1925 | if x.startswith(shadow_repos_pattern)] |
|
1926 | 1926 | |
|
1927 | 1927 | def get_new_name(self, repo_name): |
|
1928 | 1928 | """ |
|
1929 | 1929 | returns new full repository name based on assigned group and new new |
|
1930 | 1930 | |
|
1931 | 1931 | :param group_name: |
|
1932 | 1932 | """ |
|
1933 | 1933 | path_prefix = self.group.full_path_splitted if self.group else [] |
|
1934 | 1934 | return self.NAME_SEP.join(path_prefix + [repo_name]) |
|
1935 | 1935 | |
|
1936 | 1936 | @property |
|
1937 | 1937 | def _config(self): |
|
1938 | 1938 | """ |
|
1939 | 1939 | Returns db based config object. |
|
1940 | 1940 | """ |
|
1941 | 1941 | from rhodecode.lib.utils import make_db_config |
|
1942 | 1942 | return make_db_config(clear_session=False, repo=self) |
|
1943 | 1943 | |
|
1944 | 1944 | def permissions(self, with_admins=True, with_owner=True, |
|
1945 | 1945 | expand_from_user_groups=False): |
|
1946 | 1946 | """ |
|
1947 | 1947 | Permissions for repositories |
|
1948 | 1948 | """ |
|
1949 | 1949 | _admin_perm = 'repository.admin' |
|
1950 | 1950 | |
|
1951 | 1951 | owner_row = [] |
|
1952 | 1952 | if with_owner: |
|
1953 | 1953 | usr = AttributeDict(self.user.get_dict()) |
|
1954 | 1954 | usr.owner_row = True |
|
1955 | 1955 | usr.permission = _admin_perm |
|
1956 | 1956 | usr.permission_id = None |
|
1957 | 1957 | owner_row.append(usr) |
|
1958 | 1958 | |
|
1959 | 1959 | super_admin_ids = [] |
|
1960 | 1960 | super_admin_rows = [] |
|
1961 | 1961 | if with_admins: |
|
1962 | 1962 | for usr in User.get_all_super_admins(): |
|
1963 | 1963 | super_admin_ids.append(usr.user_id) |
|
1964 | 1964 | # if this admin is also owner, don't double the record |
|
1965 | 1965 | if usr.user_id == owner_row[0].user_id: |
|
1966 | 1966 | owner_row[0].admin_row = True |
|
1967 | 1967 | else: |
|
1968 | 1968 | usr = AttributeDict(usr.get_dict()) |
|
1969 | 1969 | usr.admin_row = True |
|
1970 | 1970 | usr.permission = _admin_perm |
|
1971 | 1971 | usr.permission_id = None |
|
1972 | 1972 | super_admin_rows.append(usr) |
|
1973 | 1973 | |
|
1974 | 1974 | q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self) |
|
1975 | 1975 | q = q.options(joinedload(UserRepoToPerm.repository), |
|
1976 | 1976 | joinedload(UserRepoToPerm.user), |
|
1977 | 1977 | joinedload(UserRepoToPerm.permission),) |
|
1978 | 1978 | |
|
1979 | 1979 | # get owners and admins and permissions. We do a trick of re-writing |
|
1980 | 1980 | # objects from sqlalchemy to named-tuples due to sqlalchemy session |
|
1981 | 1981 | # has a global reference and changing one object propagates to all |
|
1982 | 1982 | # others. This means if admin is also an owner admin_row that change |
|
1983 | 1983 | # would propagate to both objects |
|
1984 | 1984 | perm_rows = [] |
|
1985 | 1985 | for _usr in q.all(): |
|
1986 | 1986 | usr = AttributeDict(_usr.user.get_dict()) |
|
1987 | 1987 | # if this user is also owner/admin, mark as duplicate record |
|
1988 | 1988 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: |
|
1989 | 1989 | usr.duplicate_perm = True |
|
1990 | 1990 | # also check if this permission is maybe used by branch_permissions |
|
1991 | 1991 | if _usr.branch_perm_entry: |
|
1992 | 1992 | usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry] |
|
1993 | 1993 | |
|
1994 | 1994 | usr.permission = _usr.permission.permission_name |
|
1995 | 1995 | usr.permission_id = _usr.repo_to_perm_id |
|
1996 | 1996 | perm_rows.append(usr) |
|
1997 | 1997 | |
|
1998 | 1998 | # filter the perm rows by 'default' first and then sort them by |
|
1999 | 1999 | # admin,write,read,none permissions sorted again alphabetically in |
|
2000 | 2000 | # each group |
|
2001 | 2001 | perm_rows = sorted(perm_rows, key=display_user_sort) |
|
2002 | 2002 | |
|
2003 | 2003 | user_groups_rows = [] |
|
2004 | 2004 | if expand_from_user_groups: |
|
2005 | 2005 | for ug in self.permission_user_groups(with_members=True): |
|
2006 | 2006 | for user_data in ug.members: |
|
2007 | 2007 | user_groups_rows.append(user_data) |
|
2008 | 2008 | |
|
2009 | 2009 | return super_admin_rows + owner_row + perm_rows + user_groups_rows |
|
2010 | 2010 | |
|
2011 | 2011 | def permission_user_groups(self, with_members=True): |
|
2012 | 2012 | q = UserGroupRepoToPerm.query()\ |
|
2013 | 2013 | .filter(UserGroupRepoToPerm.repository == self) |
|
2014 | 2014 | q = q.options(joinedload(UserGroupRepoToPerm.repository), |
|
2015 | 2015 | joinedload(UserGroupRepoToPerm.users_group), |
|
2016 | 2016 | joinedload(UserGroupRepoToPerm.permission),) |
|
2017 | 2017 | |
|
2018 | 2018 | perm_rows = [] |
|
2019 | 2019 | for _user_group in q.all(): |
|
2020 | 2020 | entry = AttributeDict(_user_group.users_group.get_dict()) |
|
2021 | 2021 | entry.permission = _user_group.permission.permission_name |
|
2022 | 2022 | if with_members: |
|
2023 | 2023 | entry.members = [x.user.get_dict() |
|
2024 | 2024 | for x in _user_group.users_group.members] |
|
2025 | 2025 | perm_rows.append(entry) |
|
2026 | 2026 | |
|
2027 | 2027 | perm_rows = sorted(perm_rows, key=display_user_group_sort) |
|
2028 | 2028 | return perm_rows |
|
2029 | 2029 | |
|
2030 | 2030 | def get_api_data(self, include_secrets=False): |
|
2031 | 2031 | """ |
|
2032 | 2032 | Common function for generating repo api data |
|
2033 | 2033 | |
|
2034 | 2034 | :param include_secrets: See :meth:`User.get_api_data`. |
|
2035 | 2035 | |
|
2036 | 2036 | """ |
|
2037 | 2037 | # TODO: mikhail: Here there is an anti-pattern, we probably need to |
|
2038 | 2038 | # move this methods on models level. |
|
2039 | 2039 | from rhodecode.model.settings import SettingsModel |
|
2040 | 2040 | from rhodecode.model.repo import RepoModel |
|
2041 | 2041 | |
|
2042 | 2042 | repo = self |
|
2043 | 2043 | _user_id, _time, _reason = self.locked |
|
2044 | 2044 | |
|
2045 | 2045 | data = { |
|
2046 | 2046 | 'repo_id': repo.repo_id, |
|
2047 | 2047 | 'repo_name': repo.repo_name, |
|
2048 | 2048 | 'repo_type': repo.repo_type, |
|
2049 | 2049 | 'clone_uri': repo.clone_uri or '', |
|
2050 | 2050 | 'push_uri': repo.push_uri or '', |
|
2051 | 2051 | 'url': RepoModel().get_url(self), |
|
2052 | 2052 | 'private': repo.private, |
|
2053 | 2053 | 'created_on': repo.created_on, |
|
2054 | 2054 | 'description': repo.description_safe, |
|
2055 | 2055 | 'landing_rev': repo.landing_rev, |
|
2056 | 2056 | 'owner': repo.user.username, |
|
2057 | 2057 | 'fork_of': repo.fork.repo_name if repo.fork else None, |
|
2058 | 2058 | 'fork_of_id': repo.fork.repo_id if repo.fork else None, |
|
2059 | 2059 | 'enable_statistics': repo.enable_statistics, |
|
2060 | 2060 | 'enable_locking': repo.enable_locking, |
|
2061 | 2061 | 'enable_downloads': repo.enable_downloads, |
|
2062 | 2062 | 'last_changeset': repo.changeset_cache, |
|
2063 | 2063 | 'locked_by': User.get(_user_id).get_api_data( |
|
2064 | 2064 | include_secrets=include_secrets) if _user_id else None, |
|
2065 | 2065 | 'locked_date': time_to_datetime(_time) if _time else None, |
|
2066 | 2066 | 'lock_reason': _reason if _reason else None, |
|
2067 | 2067 | } |
|
2068 | 2068 | |
|
2069 | 2069 | # TODO: mikhail: should be per-repo settings here |
|
2070 | 2070 | rc_config = SettingsModel().get_all_settings() |
|
2071 | 2071 | repository_fields = str2bool( |
|
2072 | 2072 | rc_config.get('rhodecode_repository_fields')) |
|
2073 | 2073 | if repository_fields: |
|
2074 | 2074 | for f in self.extra_fields: |
|
2075 | 2075 | data[f.field_key_prefixed] = f.field_value |
|
2076 | 2076 | |
|
2077 | 2077 | return data |
|
2078 | 2078 | |
|
2079 | 2079 | @classmethod |
|
2080 | 2080 | def lock(cls, repo, user_id, lock_time=None, lock_reason=None): |
|
2081 | 2081 | if not lock_time: |
|
2082 | 2082 | lock_time = time.time() |
|
2083 | 2083 | if not lock_reason: |
|
2084 | 2084 | lock_reason = cls.LOCK_AUTOMATIC |
|
2085 | 2085 | repo.locked = [user_id, lock_time, lock_reason] |
|
2086 | 2086 | Session().add(repo) |
|
2087 | 2087 | Session().commit() |
|
2088 | 2088 | |
|
2089 | 2089 | @classmethod |
|
2090 | 2090 | def unlock(cls, repo): |
|
2091 | 2091 | repo.locked = None |
|
2092 | 2092 | Session().add(repo) |
|
2093 | 2093 | Session().commit() |
|
2094 | 2094 | |
|
2095 | 2095 | @classmethod |
|
2096 | 2096 | def getlock(cls, repo): |
|
2097 | 2097 | return repo.locked |
|
2098 | 2098 | |
|
2099 | 2099 | def is_user_lock(self, user_id): |
|
2100 | 2100 | if self.lock[0]: |
|
2101 | 2101 | lock_user_id = safe_int(self.lock[0]) |
|
2102 | 2102 | user_id = safe_int(user_id) |
|
2103 | 2103 | # both are ints, and they are equal |
|
2104 | 2104 | return all([lock_user_id, user_id]) and lock_user_id == user_id |
|
2105 | 2105 | |
|
2106 | 2106 | return False |
|
2107 | 2107 | |
|
2108 | 2108 | def get_locking_state(self, action, user_id, only_when_enabled=True): |
|
2109 | 2109 | """ |
|
2110 | 2110 | Checks locking on this repository, if locking is enabled and lock is |
|
2111 | 2111 | present returns a tuple of make_lock, locked, locked_by. |
|
2112 | 2112 | make_lock can have 3 states None (do nothing) True, make lock |
|
2113 | 2113 | False release lock, This value is later propagated to hooks, which |
|
2114 | 2114 | do the locking. Think about this as signals passed to hooks what to do. |
|
2115 | 2115 | |
|
2116 | 2116 | """ |
|
2117 | 2117 | # TODO: johbo: This is part of the business logic and should be moved |
|
2118 | 2118 | # into the RepositoryModel. |
|
2119 | 2119 | |
|
2120 | 2120 | if action not in ('push', 'pull'): |
|
2121 | 2121 | raise ValueError("Invalid action value: %s" % repr(action)) |
|
2122 | 2122 | |
|
2123 | 2123 | # defines if locked error should be thrown to user |
|
2124 | 2124 | currently_locked = False |
|
2125 | 2125 | # defines if new lock should be made, tri-state |
|
2126 | 2126 | make_lock = None |
|
2127 | 2127 | repo = self |
|
2128 | 2128 | user = User.get(user_id) |
|
2129 | 2129 | |
|
2130 | 2130 | lock_info = repo.locked |
|
2131 | 2131 | |
|
2132 | 2132 | if repo and (repo.enable_locking or not only_when_enabled): |
|
2133 | 2133 | if action == 'push': |
|
2134 | 2134 | # check if it's already locked !, if it is compare users |
|
2135 | 2135 | locked_by_user_id = lock_info[0] |
|
2136 | 2136 | if user.user_id == locked_by_user_id: |
|
2137 | 2137 | log.debug( |
|
2138 | 2138 | 'Got `push` action from user %s, now unlocking', user) |
|
2139 | 2139 | # unlock if we have push from user who locked |
|
2140 | 2140 | make_lock = False |
|
2141 | 2141 | else: |
|
2142 | 2142 | # we're not the same user who locked, ban with |
|
2143 | 2143 | # code defined in settings (default is 423 HTTP Locked) ! |
|
2144 | 2144 | log.debug('Repo %s is currently locked by %s', repo, user) |
|
2145 | 2145 | currently_locked = True |
|
2146 | 2146 | elif action == 'pull': |
|
2147 | 2147 | # [0] user [1] date |
|
2148 | 2148 | if lock_info[0] and lock_info[1]: |
|
2149 | 2149 | log.debug('Repo %s is currently locked by %s', repo, user) |
|
2150 | 2150 | currently_locked = True |
|
2151 | 2151 | else: |
|
2152 | 2152 | log.debug('Setting lock on repo %s by %s', repo, user) |
|
2153 | 2153 | make_lock = True |
|
2154 | 2154 | |
|
2155 | 2155 | else: |
|
2156 | 2156 | log.debug('Repository %s do not have locking enabled', repo) |
|
2157 | 2157 | |
|
2158 | 2158 | log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s', |
|
2159 | 2159 | make_lock, currently_locked, lock_info) |
|
2160 | 2160 | |
|
2161 | 2161 | from rhodecode.lib.auth import HasRepoPermissionAny |
|
2162 | 2162 | perm_check = HasRepoPermissionAny('repository.write', 'repository.admin') |
|
2163 | 2163 | if make_lock and not perm_check(repo_name=repo.repo_name, user=user): |
|
2164 | 2164 | # if we don't have at least write permission we cannot make a lock |
|
2165 | 2165 | log.debug('lock state reset back to FALSE due to lack ' |
|
2166 | 2166 | 'of at least read permission') |
|
2167 | 2167 | make_lock = False |
|
2168 | 2168 | |
|
2169 | 2169 | return make_lock, currently_locked, lock_info |
|
2170 | 2170 | |
|
2171 | 2171 | @property |
|
2172 | 2172 | def last_db_change(self): |
|
2173 | 2173 | return self.updated_on |
|
2174 | 2174 | |
|
2175 | 2175 | @property |
|
2176 | 2176 | def clone_uri_hidden(self): |
|
2177 | 2177 | clone_uri = self.clone_uri |
|
2178 | 2178 | if clone_uri: |
|
2179 | 2179 | import urlobject |
|
2180 | 2180 | url_obj = urlobject.URLObject(cleaned_uri(clone_uri)) |
|
2181 | 2181 | if url_obj.password: |
|
2182 | 2182 | clone_uri = url_obj.with_password('*****') |
|
2183 | 2183 | return clone_uri |
|
2184 | 2184 | |
|
2185 | 2185 | @property |
|
2186 | 2186 | def push_uri_hidden(self): |
|
2187 | 2187 | push_uri = self.push_uri |
|
2188 | 2188 | if push_uri: |
|
2189 | 2189 | import urlobject |
|
2190 | 2190 | url_obj = urlobject.URLObject(cleaned_uri(push_uri)) |
|
2191 | 2191 | if url_obj.password: |
|
2192 | 2192 | push_uri = url_obj.with_password('*****') |
|
2193 | 2193 | return push_uri |
|
2194 | 2194 | |
|
2195 | 2195 | def clone_url(self, **override): |
|
2196 | 2196 | from rhodecode.model.settings import SettingsModel |
|
2197 | 2197 | |
|
2198 | 2198 | uri_tmpl = None |
|
2199 | 2199 | if 'with_id' in override: |
|
2200 | 2200 | uri_tmpl = self.DEFAULT_CLONE_URI_ID |
|
2201 | 2201 | del override['with_id'] |
|
2202 | 2202 | |
|
2203 | 2203 | if 'uri_tmpl' in override: |
|
2204 | 2204 | uri_tmpl = override['uri_tmpl'] |
|
2205 | 2205 | del override['uri_tmpl'] |
|
2206 | 2206 | |
|
2207 | 2207 | ssh = False |
|
2208 | 2208 | if 'ssh' in override: |
|
2209 | 2209 | ssh = True |
|
2210 | 2210 | del override['ssh'] |
|
2211 | 2211 | |
|
2212 | 2212 | # we didn't override our tmpl from **overrides |
|
2213 | 2213 | if not uri_tmpl: |
|
2214 | 2214 | rc_config = SettingsModel().get_all_settings(cache=True) |
|
2215 | 2215 | if ssh: |
|
2216 | 2216 | uri_tmpl = rc_config.get( |
|
2217 | 2217 | 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH |
|
2218 | 2218 | else: |
|
2219 | 2219 | uri_tmpl = rc_config.get( |
|
2220 | 2220 | 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI |
|
2221 | 2221 | |
|
2222 | 2222 | request = get_current_request() |
|
2223 | 2223 | return get_clone_url(request=request, |
|
2224 | 2224 | uri_tmpl=uri_tmpl, |
|
2225 | 2225 | repo_name=self.repo_name, |
|
2226 | 2226 | repo_id=self.repo_id, **override) |
|
2227 | 2227 | |
|
2228 | 2228 | def set_state(self, state): |
|
2229 | 2229 | self.repo_state = state |
|
2230 | 2230 | Session().add(self) |
|
2231 | 2231 | #========================================================================== |
|
2232 | 2232 | # SCM PROPERTIES |
|
2233 | 2233 | #========================================================================== |
|
2234 | 2234 | |
|
2235 | 2235 | def get_commit(self, commit_id=None, commit_idx=None, pre_load=None): |
|
2236 | 2236 | return get_commit_safe( |
|
2237 | 2237 | self.scm_instance(), commit_id, commit_idx, pre_load=pre_load) |
|
2238 | 2238 | |
|
2239 | 2239 | def get_changeset(self, rev=None, pre_load=None): |
|
2240 | 2240 | warnings.warn("Use get_commit", DeprecationWarning) |
|
2241 | 2241 | commit_id = None |
|
2242 | 2242 | commit_idx = None |
|
2243 | 2243 | if isinstance(rev, basestring): |
|
2244 | 2244 | commit_id = rev |
|
2245 | 2245 | else: |
|
2246 | 2246 | commit_idx = rev |
|
2247 | 2247 | return self.get_commit(commit_id=commit_id, commit_idx=commit_idx, |
|
2248 | 2248 | pre_load=pre_load) |
|
2249 | 2249 | |
|
2250 | 2250 | def get_landing_commit(self): |
|
2251 | 2251 | """ |
|
2252 | 2252 | Returns landing commit, or if that doesn't exist returns the tip |
|
2253 | 2253 | """ |
|
2254 | 2254 | _rev_type, _rev = self.landing_rev |
|
2255 | 2255 | commit = self.get_commit(_rev) |
|
2256 | 2256 | if isinstance(commit, EmptyCommit): |
|
2257 | 2257 | return self.get_commit() |
|
2258 | 2258 | return commit |
|
2259 | 2259 | |
|
2260 | 2260 | def update_commit_cache(self, cs_cache=None, config=None): |
|
2261 | 2261 | """ |
|
2262 | 2262 | Update cache of last changeset for repository, keys should be:: |
|
2263 | 2263 | |
|
2264 | 2264 | short_id |
|
2265 | 2265 | raw_id |
|
2266 | 2266 | revision |
|
2267 | 2267 | parents |
|
2268 | 2268 | message |
|
2269 | 2269 | date |
|
2270 | 2270 | author |
|
2271 | 2271 | |
|
2272 | 2272 | :param cs_cache: |
|
2273 | 2273 | """ |
|
2274 | 2274 | from rhodecode.lib.vcs.backends.base import BaseChangeset |
|
2275 | 2275 | if cs_cache is None: |
|
2276 | 2276 | # use no-cache version here |
|
2277 | 2277 | scm_repo = self.scm_instance(cache=False, config=config) |
|
2278 | 2278 | |
|
2279 | 2279 | empty = not scm_repo or scm_repo.is_empty() |
|
2280 | 2280 | if not empty: |
|
2281 | 2281 | cs_cache = scm_repo.get_commit( |
|
2282 | 2282 | pre_load=["author", "date", "message", "parents"]) |
|
2283 | 2283 | else: |
|
2284 | 2284 | cs_cache = EmptyCommit() |
|
2285 | 2285 | |
|
2286 | 2286 | if isinstance(cs_cache, BaseChangeset): |
|
2287 | 2287 | cs_cache = cs_cache.__json__() |
|
2288 | 2288 | |
|
2289 | 2289 | def is_outdated(new_cs_cache): |
|
2290 | 2290 | if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or |
|
2291 | 2291 | new_cs_cache['revision'] != self.changeset_cache['revision']): |
|
2292 | 2292 | return True |
|
2293 | 2293 | return False |
|
2294 | 2294 | |
|
2295 | 2295 | # check if we have maybe already latest cached revision |
|
2296 | 2296 | if is_outdated(cs_cache) or not self.changeset_cache: |
|
2297 | 2297 | _default = datetime.datetime.utcnow() |
|
2298 | 2298 | last_change = cs_cache.get('date') or _default |
|
2299 | 2299 | if self.updated_on and self.updated_on > last_change: |
|
2300 | 2300 | # we check if last update is newer than the new value |
|
2301 | 2301 | # if yes, we use the current timestamp instead. Imagine you get |
|
2302 | 2302 | # old commit pushed 1y ago, we'd set last update 1y to ago. |
|
2303 | 2303 | last_change = _default |
|
2304 | 2304 | log.debug('updated repo %s with new cs cache %s', |
|
2305 | 2305 | self.repo_name, cs_cache) |
|
2306 | 2306 | self.updated_on = last_change |
|
2307 | 2307 | self.changeset_cache = cs_cache |
|
2308 | 2308 | Session().add(self) |
|
2309 | 2309 | Session().commit() |
|
2310 | 2310 | else: |
|
2311 | 2311 | log.debug('Skipping update_commit_cache for repo:`%s` ' |
|
2312 | 2312 | 'commit already with latest changes', self.repo_name) |
|
2313 | 2313 | |
|
2314 | 2314 | @property |
|
2315 | 2315 | def tip(self): |
|
2316 | 2316 | return self.get_commit('tip') |
|
2317 | 2317 | |
|
2318 | 2318 | @property |
|
2319 | 2319 | def author(self): |
|
2320 | 2320 | return self.tip.author |
|
2321 | 2321 | |
|
2322 | 2322 | @property |
|
2323 | 2323 | def last_change(self): |
|
2324 | 2324 | return self.scm_instance().last_change |
|
2325 | 2325 | |
|
2326 | 2326 | def get_comments(self, revisions=None): |
|
2327 | 2327 | """ |
|
2328 | 2328 | Returns comments for this repository grouped by revisions |
|
2329 | 2329 | |
|
2330 | 2330 | :param revisions: filter query by revisions only |
|
2331 | 2331 | """ |
|
2332 | 2332 | cmts = ChangesetComment.query()\ |
|
2333 | 2333 | .filter(ChangesetComment.repo == self) |
|
2334 | 2334 | if revisions: |
|
2335 | 2335 | cmts = cmts.filter(ChangesetComment.revision.in_(revisions)) |
|
2336 | 2336 | grouped = collections.defaultdict(list) |
|
2337 | 2337 | for cmt in cmts.all(): |
|
2338 | 2338 | grouped[cmt.revision].append(cmt) |
|
2339 | 2339 | return grouped |
|
2340 | 2340 | |
|
2341 | 2341 | def statuses(self, revisions=None): |
|
2342 | 2342 | """ |
|
2343 | 2343 | Returns statuses for this repository |
|
2344 | 2344 | |
|
2345 | 2345 | :param revisions: list of revisions to get statuses for |
|
2346 | 2346 | """ |
|
2347 | 2347 | statuses = ChangesetStatus.query()\ |
|
2348 | 2348 | .filter(ChangesetStatus.repo == self)\ |
|
2349 | 2349 | .filter(ChangesetStatus.version == 0) |
|
2350 | 2350 | |
|
2351 | 2351 | if revisions: |
|
2352 | 2352 | # Try doing the filtering in chunks to avoid hitting limits |
|
2353 | 2353 | size = 500 |
|
2354 | 2354 | status_results = [] |
|
2355 | 2355 | for chunk in xrange(0, len(revisions), size): |
|
2356 | 2356 | status_results += statuses.filter( |
|
2357 | 2357 | ChangesetStatus.revision.in_( |
|
2358 | 2358 | revisions[chunk: chunk+size]) |
|
2359 | 2359 | ).all() |
|
2360 | 2360 | else: |
|
2361 | 2361 | status_results = statuses.all() |
|
2362 | 2362 | |
|
2363 | 2363 | grouped = {} |
|
2364 | 2364 | |
|
2365 | 2365 | # maybe we have open new pullrequest without a status? |
|
2366 | 2366 | stat = ChangesetStatus.STATUS_UNDER_REVIEW |
|
2367 | 2367 | status_lbl = ChangesetStatus.get_status_lbl(stat) |
|
2368 | 2368 | for pr in PullRequest.query().filter(PullRequest.source_repo == self).all(): |
|
2369 | 2369 | for rev in pr.revisions: |
|
2370 | 2370 | pr_id = pr.pull_request_id |
|
2371 | 2371 | pr_repo = pr.target_repo.repo_name |
|
2372 | 2372 | grouped[rev] = [stat, status_lbl, pr_id, pr_repo] |
|
2373 | 2373 | |
|
2374 | 2374 | for stat in status_results: |
|
2375 | 2375 | pr_id = pr_repo = None |
|
2376 | 2376 | if stat.pull_request: |
|
2377 | 2377 | pr_id = stat.pull_request.pull_request_id |
|
2378 | 2378 | pr_repo = stat.pull_request.target_repo.repo_name |
|
2379 | 2379 | grouped[stat.revision] = [str(stat.status), stat.status_lbl, |
|
2380 | 2380 | pr_id, pr_repo] |
|
2381 | 2381 | return grouped |
|
2382 | 2382 | |
|
2383 | 2383 | # ========================================================================== |
|
2384 | 2384 | # SCM CACHE INSTANCE |
|
2385 | 2385 | # ========================================================================== |
|
2386 | 2386 | |
|
2387 | 2387 | def scm_instance(self, **kwargs): |
|
2388 | 2388 | import rhodecode |
|
2389 | 2389 | |
|
2390 | 2390 | # Passing a config will not hit the cache currently only used |
|
2391 | 2391 | # for repo2dbmapper |
|
2392 | 2392 | config = kwargs.pop('config', None) |
|
2393 | 2393 | cache = kwargs.pop('cache', None) |
|
2394 | 2394 | full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache')) |
|
2395 | 2395 | # if cache is NOT defined use default global, else we have a full |
|
2396 | 2396 | # control over cache behaviour |
|
2397 | 2397 | if cache is None and full_cache and not config: |
|
2398 | 2398 | return self._get_instance_cached() |
|
2399 | 2399 | return self._get_instance(cache=bool(cache), config=config) |
|
2400 | 2400 | |
|
2401 | 2401 | def _get_instance_cached(self): |
|
2402 | 2402 | from rhodecode.lib import rc_cache |
|
2403 | 2403 | |
|
2404 | 2404 | cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id) |
|
2405 | 2405 | invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format( |
|
2406 | 2406 | repo_id=self.repo_id) |
|
2407 | 2407 | region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid) |
|
2408 | 2408 | |
|
2409 | 2409 | @region.conditional_cache_on_arguments(namespace=cache_namespace_uid) |
|
2410 | 2410 | def get_instance_cached(repo_id, context_id): |
|
2411 | 2411 | return self._get_instance() |
|
2412 | 2412 | |
|
2413 | 2413 | # we must use thread scoped cache here, |
|
2414 | 2414 | # because each thread of gevent needs it's own not shared connection and cache |
|
2415 | 2415 | # we also alter `args` so the cache key is individual for every green thread. |
|
2416 | 2416 | inv_context_manager = rc_cache.InvalidationContext( |
|
2417 | 2417 | uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace, |
|
2418 | 2418 | thread_scoped=True) |
|
2419 | 2419 | with inv_context_manager as invalidation_context: |
|
2420 | 2420 | args = (self.repo_id, inv_context_manager.cache_key) |
|
2421 | 2421 | # re-compute and store cache if we get invalidate signal |
|
2422 | 2422 | if invalidation_context.should_invalidate(): |
|
2423 | 2423 | instance = get_instance_cached.refresh(*args) |
|
2424 | 2424 | else: |
|
2425 | 2425 | instance = get_instance_cached(*args) |
|
2426 | 2426 | |
|
2427 | 2427 | log.debug( |
|
2428 | 2428 | 'Repo instance fetched in %.3fs', inv_context_manager.compute_time) |
|
2429 | 2429 | return instance |
|
2430 | 2430 | |
|
2431 | 2431 | def _get_instance(self, cache=True, config=None): |
|
2432 | 2432 | config = config or self._config |
|
2433 | 2433 | custom_wire = { |
|
2434 | 2434 | 'cache': cache # controls the vcs.remote cache |
|
2435 | 2435 | } |
|
2436 | 2436 | repo = get_vcs_instance( |
|
2437 | 2437 | repo_path=safe_str(self.repo_full_path), |
|
2438 | 2438 | config=config, |
|
2439 | 2439 | with_wire=custom_wire, |
|
2440 | 2440 | create=False, |
|
2441 | 2441 | _vcs_alias=self.repo_type) |
|
2442 | 2442 | |
|
2443 | 2443 | return repo |
|
2444 | 2444 | |
|
2445 | 2445 | def __json__(self): |
|
2446 | 2446 | return {'landing_rev': self.landing_rev} |
|
2447 | 2447 | |
|
2448 | 2448 | def get_dict(self): |
|
2449 | 2449 | |
|
2450 | 2450 | # Since we transformed `repo_name` to a hybrid property, we need to |
|
2451 | 2451 | # keep compatibility with the code which uses `repo_name` field. |
|
2452 | 2452 | |
|
2453 | 2453 | result = super(Repository, self).get_dict() |
|
2454 | 2454 | result['repo_name'] = result.pop('_repo_name', None) |
|
2455 | 2455 | return result |
|
2456 | 2456 | |
|
2457 | 2457 | |
|
2458 | 2458 | class RepoGroup(Base, BaseModel): |
|
2459 | 2459 | __tablename__ = 'groups' |
|
2460 | 2460 | __table_args__ = ( |
|
2461 | 2461 | UniqueConstraint('group_name', 'group_parent_id'), |
|
2462 | 2462 | CheckConstraint('group_id != group_parent_id'), |
|
2463 | 2463 | base_table_args, |
|
2464 | 2464 | ) |
|
2465 | 2465 | __mapper_args__ = {'order_by': 'group_name'} |
|
2466 | 2466 | |
|
2467 | 2467 | CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups |
|
2468 | 2468 | |
|
2469 | 2469 | group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
2470 | 2470 | group_name = Column("group_name", String(255), nullable=False, unique=True, default=None) |
|
2471 | 2471 | group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None) |
|
2472 | 2472 | group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None) |
|
2473 | 2473 | enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) |
|
2474 | 2474 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None) |
|
2475 | 2475 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
2476 | 2476 | updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) |
|
2477 | 2477 | personal = Column('personal', Boolean(), nullable=True, unique=None, default=None) |
|
2478 | 2478 | |
|
2479 | 2479 | repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id') |
|
2480 | 2480 | users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all') |
|
2481 | 2481 | parent_group = relationship('RepoGroup', remote_side=group_id) |
|
2482 | 2482 | user = relationship('User') |
|
2483 | 2483 | integrations = relationship('Integration', |
|
2484 | 2484 | cascade="all, delete, delete-orphan") |
|
2485 | 2485 | |
|
2486 | 2486 | def __init__(self, group_name='', parent_group=None): |
|
2487 | 2487 | self.group_name = group_name |
|
2488 | 2488 | self.parent_group = parent_group |
|
2489 | 2489 | |
|
2490 | 2490 | def __unicode__(self): |
|
2491 | 2491 | return u"<%s('id:%s:%s')>" % ( |
|
2492 | 2492 | self.__class__.__name__, self.group_id, self.group_name) |
|
2493 | 2493 | |
|
2494 | 2494 | @hybrid_property |
|
2495 | 2495 | def description_safe(self): |
|
2496 | 2496 | from rhodecode.lib import helpers as h |
|
2497 | 2497 | return h.escape(self.group_description) |
|
2498 | 2498 | |
|
2499 | 2499 | @classmethod |
|
2500 | 2500 | def _generate_choice(cls, repo_group): |
|
2501 | 2501 | from webhelpers.html import literal as _literal |
|
2502 | 2502 | _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k)) |
|
2503 | 2503 | return repo_group.group_id, _name(repo_group.full_path_splitted) |
|
2504 | 2504 | |
|
2505 | 2505 | @classmethod |
|
2506 | 2506 | def groups_choices(cls, groups=None, show_empty_group=True): |
|
2507 | 2507 | if not groups: |
|
2508 | 2508 | groups = cls.query().all() |
|
2509 | 2509 | |
|
2510 | 2510 | repo_groups = [] |
|
2511 | 2511 | if show_empty_group: |
|
2512 | 2512 | repo_groups = [(-1, u'-- %s --' % _('No parent'))] |
|
2513 | 2513 | |
|
2514 | 2514 | repo_groups.extend([cls._generate_choice(x) for x in groups]) |
|
2515 | 2515 | |
|
2516 | 2516 | repo_groups = sorted( |
|
2517 | 2517 | repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0]) |
|
2518 | 2518 | return repo_groups |
|
2519 | 2519 | |
|
2520 | 2520 | @classmethod |
|
2521 | 2521 | def url_sep(cls): |
|
2522 | 2522 | return URL_SEP |
|
2523 | 2523 | |
|
2524 | 2524 | @classmethod |
|
2525 | 2525 | def get_by_group_name(cls, group_name, cache=False, case_insensitive=False): |
|
2526 | 2526 | if case_insensitive: |
|
2527 | 2527 | gr = cls.query().filter(func.lower(cls.group_name) |
|
2528 | 2528 | == func.lower(group_name)) |
|
2529 | 2529 | else: |
|
2530 | 2530 | gr = cls.query().filter(cls.group_name == group_name) |
|
2531 | 2531 | if cache: |
|
2532 | 2532 | name_key = _hash_key(group_name) |
|
2533 | 2533 | gr = gr.options( |
|
2534 | 2534 | FromCache("sql_cache_short", "get_group_%s" % name_key)) |
|
2535 | 2535 | return gr.scalar() |
|
2536 | 2536 | |
|
2537 | 2537 | @classmethod |
|
2538 | 2538 | def get_user_personal_repo_group(cls, user_id): |
|
2539 | 2539 | user = User.get(user_id) |
|
2540 | 2540 | if user.username == User.DEFAULT_USER: |
|
2541 | 2541 | return None |
|
2542 | 2542 | |
|
2543 | 2543 | return cls.query()\ |
|
2544 | 2544 | .filter(cls.personal == true()) \ |
|
2545 | 2545 | .filter(cls.user == user) \ |
|
2546 | 2546 | .order_by(cls.group_id.asc()) \ |
|
2547 | 2547 | .first() |
|
2548 | 2548 | |
|
2549 | 2549 | @classmethod |
|
2550 | 2550 | def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None), |
|
2551 | 2551 | case_insensitive=True): |
|
2552 | 2552 | q = RepoGroup.query() |
|
2553 | 2553 | |
|
2554 | 2554 | if not isinstance(user_id, Optional): |
|
2555 | 2555 | q = q.filter(RepoGroup.user_id == user_id) |
|
2556 | 2556 | |
|
2557 | 2557 | if not isinstance(group_id, Optional): |
|
2558 | 2558 | q = q.filter(RepoGroup.group_parent_id == group_id) |
|
2559 | 2559 | |
|
2560 | 2560 | if case_insensitive: |
|
2561 | 2561 | q = q.order_by(func.lower(RepoGroup.group_name)) |
|
2562 | 2562 | else: |
|
2563 | 2563 | q = q.order_by(RepoGroup.group_name) |
|
2564 | 2564 | return q.all() |
|
2565 | 2565 | |
|
2566 | 2566 | @property |
|
2567 | 2567 | def parents(self): |
|
2568 | 2568 | parents_recursion_limit = 10 |
|
2569 | 2569 | groups = [] |
|
2570 | 2570 | if self.parent_group is None: |
|
2571 | 2571 | return groups |
|
2572 | 2572 | cur_gr = self.parent_group |
|
2573 | 2573 | groups.insert(0, cur_gr) |
|
2574 | 2574 | cnt = 0 |
|
2575 | 2575 | while 1: |
|
2576 | 2576 | cnt += 1 |
|
2577 | 2577 | gr = getattr(cur_gr, 'parent_group', None) |
|
2578 | 2578 | cur_gr = cur_gr.parent_group |
|
2579 | 2579 | if gr is None: |
|
2580 | 2580 | break |
|
2581 | 2581 | if cnt == parents_recursion_limit: |
|
2582 | 2582 | # this will prevent accidental infinit loops |
|
2583 | 2583 | log.error('more than %s parents found for group %s, stopping ' |
|
2584 | 2584 | 'recursive parent fetching', parents_recursion_limit, self) |
|
2585 | 2585 | break |
|
2586 | 2586 | |
|
2587 | 2587 | groups.insert(0, gr) |
|
2588 | 2588 | return groups |
|
2589 | 2589 | |
|
2590 | 2590 | @property |
|
2591 | 2591 | def last_db_change(self): |
|
2592 | 2592 | return self.updated_on |
|
2593 | 2593 | |
|
2594 | 2594 | @property |
|
2595 | 2595 | def children(self): |
|
2596 | 2596 | return RepoGroup.query().filter(RepoGroup.parent_group == self) |
|
2597 | 2597 | |
|
2598 | 2598 | @property |
|
2599 | 2599 | def name(self): |
|
2600 | 2600 | return self.group_name.split(RepoGroup.url_sep())[-1] |
|
2601 | 2601 | |
|
2602 | 2602 | @property |
|
2603 | 2603 | def full_path(self): |
|
2604 | 2604 | return self.group_name |
|
2605 | 2605 | |
|
2606 | 2606 | @property |
|
2607 | 2607 | def full_path_splitted(self): |
|
2608 | 2608 | return self.group_name.split(RepoGroup.url_sep()) |
|
2609 | 2609 | |
|
2610 | 2610 | @property |
|
2611 | 2611 | def repositories(self): |
|
2612 | 2612 | return Repository.query()\ |
|
2613 | 2613 | .filter(Repository.group == self)\ |
|
2614 | 2614 | .order_by(Repository.repo_name) |
|
2615 | 2615 | |
|
2616 | 2616 | @property |
|
2617 | 2617 | def repositories_recursive_count(self): |
|
2618 | 2618 | cnt = self.repositories.count() |
|
2619 | 2619 | |
|
2620 | 2620 | def children_count(group): |
|
2621 | 2621 | cnt = 0 |
|
2622 | 2622 | for child in group.children: |
|
2623 | 2623 | cnt += child.repositories.count() |
|
2624 | 2624 | cnt += children_count(child) |
|
2625 | 2625 | return cnt |
|
2626 | 2626 | |
|
2627 | 2627 | return cnt + children_count(self) |
|
2628 | 2628 | |
|
2629 | 2629 | def _recursive_objects(self, include_repos=True): |
|
2630 | 2630 | all_ = [] |
|
2631 | 2631 | |
|
2632 | 2632 | def _get_members(root_gr): |
|
2633 | 2633 | if include_repos: |
|
2634 | 2634 | for r in root_gr.repositories: |
|
2635 | 2635 | all_.append(r) |
|
2636 | 2636 | childs = root_gr.children.all() |
|
2637 | 2637 | if childs: |
|
2638 | 2638 | for gr in childs: |
|
2639 | 2639 | all_.append(gr) |
|
2640 | 2640 | _get_members(gr) |
|
2641 | 2641 | |
|
2642 | 2642 | _get_members(self) |
|
2643 | 2643 | return [self] + all_ |
|
2644 | 2644 | |
|
2645 | 2645 | def recursive_groups_and_repos(self): |
|
2646 | 2646 | """ |
|
2647 | 2647 | Recursive return all groups, with repositories in those groups |
|
2648 | 2648 | """ |
|
2649 | 2649 | return self._recursive_objects() |
|
2650 | 2650 | |
|
2651 | 2651 | def recursive_groups(self): |
|
2652 | 2652 | """ |
|
2653 | 2653 | Returns all children groups for this group including children of children |
|
2654 | 2654 | """ |
|
2655 | 2655 | return self._recursive_objects(include_repos=False) |
|
2656 | 2656 | |
|
2657 | 2657 | def get_new_name(self, group_name): |
|
2658 | 2658 | """ |
|
2659 | 2659 | returns new full group name based on parent and new name |
|
2660 | 2660 | |
|
2661 | 2661 | :param group_name: |
|
2662 | 2662 | """ |
|
2663 | 2663 | path_prefix = (self.parent_group.full_path_splitted if |
|
2664 | 2664 | self.parent_group else []) |
|
2665 | 2665 | return RepoGroup.url_sep().join(path_prefix + [group_name]) |
|
2666 | 2666 | |
|
2667 | 2667 | def permissions(self, with_admins=True, with_owner=True, |
|
2668 | 2668 | expand_from_user_groups=False): |
|
2669 | 2669 | """ |
|
2670 | 2670 | Permissions for repository groups |
|
2671 | 2671 | """ |
|
2672 | 2672 | _admin_perm = 'group.admin' |
|
2673 | 2673 | |
|
2674 | 2674 | owner_row = [] |
|
2675 | 2675 | if with_owner: |
|
2676 | 2676 | usr = AttributeDict(self.user.get_dict()) |
|
2677 | 2677 | usr.owner_row = True |
|
2678 | 2678 | usr.permission = _admin_perm |
|
2679 | 2679 | owner_row.append(usr) |
|
2680 | 2680 | |
|
2681 | 2681 | super_admin_ids = [] |
|
2682 | 2682 | super_admin_rows = [] |
|
2683 | 2683 | if with_admins: |
|
2684 | 2684 | for usr in User.get_all_super_admins(): |
|
2685 | 2685 | super_admin_ids.append(usr.user_id) |
|
2686 | 2686 | # if this admin is also owner, don't double the record |
|
2687 | 2687 | if usr.user_id == owner_row[0].user_id: |
|
2688 | 2688 | owner_row[0].admin_row = True |
|
2689 | 2689 | else: |
|
2690 | 2690 | usr = AttributeDict(usr.get_dict()) |
|
2691 | 2691 | usr.admin_row = True |
|
2692 | 2692 | usr.permission = _admin_perm |
|
2693 | 2693 | super_admin_rows.append(usr) |
|
2694 | 2694 | |
|
2695 | 2695 | q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self) |
|
2696 | 2696 | q = q.options(joinedload(UserRepoGroupToPerm.group), |
|
2697 | 2697 | joinedload(UserRepoGroupToPerm.user), |
|
2698 | 2698 | joinedload(UserRepoGroupToPerm.permission),) |
|
2699 | 2699 | |
|
2700 | 2700 | # get owners and admins and permissions. We do a trick of re-writing |
|
2701 | 2701 | # objects from sqlalchemy to named-tuples due to sqlalchemy session |
|
2702 | 2702 | # has a global reference and changing one object propagates to all |
|
2703 | 2703 | # others. This means if admin is also an owner admin_row that change |
|
2704 | 2704 | # would propagate to both objects |
|
2705 | 2705 | perm_rows = [] |
|
2706 | 2706 | for _usr in q.all(): |
|
2707 | 2707 | usr = AttributeDict(_usr.user.get_dict()) |
|
2708 | 2708 | # if this user is also owner/admin, mark as duplicate record |
|
2709 | 2709 | if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids: |
|
2710 | 2710 | usr.duplicate_perm = True |
|
2711 | 2711 | usr.permission = _usr.permission.permission_name |
|
2712 | 2712 | perm_rows.append(usr) |
|
2713 | 2713 | |
|
2714 | 2714 | # filter the perm rows by 'default' first and then sort them by |
|
2715 | 2715 | # admin,write,read,none permissions sorted again alphabetically in |
|
2716 | 2716 | # each group |
|
2717 | 2717 | perm_rows = sorted(perm_rows, key=display_user_sort) |
|
2718 | 2718 | |
|
2719 | 2719 | user_groups_rows = [] |
|
2720 | 2720 | if expand_from_user_groups: |
|
2721 | 2721 | for ug in self.permission_user_groups(with_members=True): |
|
2722 | 2722 | for user_data in ug.members: |
|
2723 | 2723 | user_groups_rows.append(user_data) |
|
2724 | 2724 | |
|
2725 | 2725 | return super_admin_rows + owner_row + perm_rows + user_groups_rows |
|
2726 | 2726 | |
|
2727 | 2727 | def permission_user_groups(self, with_members=False): |
|
2728 | 2728 | q = UserGroupRepoGroupToPerm.query()\ |
|
2729 | 2729 | .filter(UserGroupRepoGroupToPerm.group == self) |
|
2730 | 2730 | q = q.options(joinedload(UserGroupRepoGroupToPerm.group), |
|
2731 | 2731 | joinedload(UserGroupRepoGroupToPerm.users_group), |
|
2732 | 2732 | joinedload(UserGroupRepoGroupToPerm.permission),) |
|
2733 | 2733 | |
|
2734 | 2734 | perm_rows = [] |
|
2735 | 2735 | for _user_group in q.all(): |
|
2736 | 2736 | entry = AttributeDict(_user_group.users_group.get_dict()) |
|
2737 | 2737 | entry.permission = _user_group.permission.permission_name |
|
2738 | 2738 | if with_members: |
|
2739 | 2739 | entry.members = [x.user.get_dict() |
|
2740 | 2740 | for x in _user_group.users_group.members] |
|
2741 | 2741 | perm_rows.append(entry) |
|
2742 | 2742 | |
|
2743 | 2743 | perm_rows = sorted(perm_rows, key=display_user_group_sort) |
|
2744 | 2744 | return perm_rows |
|
2745 | 2745 | |
|
2746 | 2746 | def get_api_data(self): |
|
2747 | 2747 | """ |
|
2748 | 2748 | Common function for generating api data |
|
2749 | 2749 | |
|
2750 | 2750 | """ |
|
2751 | 2751 | group = self |
|
2752 | 2752 | data = { |
|
2753 | 2753 | 'group_id': group.group_id, |
|
2754 | 2754 | 'group_name': group.group_name, |
|
2755 | 2755 | 'group_description': group.description_safe, |
|
2756 | 2756 | 'parent_group': group.parent_group.group_name if group.parent_group else None, |
|
2757 | 2757 | 'repositories': [x.repo_name for x in group.repositories], |
|
2758 | 2758 | 'owner': group.user.username, |
|
2759 | 2759 | } |
|
2760 | 2760 | return data |
|
2761 | 2761 | |
|
2762 | 2762 | |
|
2763 | 2763 | class Permission(Base, BaseModel): |
|
2764 | 2764 | __tablename__ = 'permissions' |
|
2765 | 2765 | __table_args__ = ( |
|
2766 | 2766 | Index('p_perm_name_idx', 'permission_name'), |
|
2767 | 2767 | base_table_args, |
|
2768 | 2768 | ) |
|
2769 | 2769 | |
|
2770 | 2770 | PERMS = [ |
|
2771 | 2771 | ('hg.admin', _('RhodeCode Super Administrator')), |
|
2772 | 2772 | |
|
2773 | 2773 | ('repository.none', _('Repository no access')), |
|
2774 | 2774 | ('repository.read', _('Repository read access')), |
|
2775 | 2775 | ('repository.write', _('Repository write access')), |
|
2776 | 2776 | ('repository.admin', _('Repository admin access')), |
|
2777 | 2777 | |
|
2778 | 2778 | ('group.none', _('Repository group no access')), |
|
2779 | 2779 | ('group.read', _('Repository group read access')), |
|
2780 | 2780 | ('group.write', _('Repository group write access')), |
|
2781 | 2781 | ('group.admin', _('Repository group admin access')), |
|
2782 | 2782 | |
|
2783 | 2783 | ('usergroup.none', _('User group no access')), |
|
2784 | 2784 | ('usergroup.read', _('User group read access')), |
|
2785 | 2785 | ('usergroup.write', _('User group write access')), |
|
2786 | 2786 | ('usergroup.admin', _('User group admin access')), |
|
2787 | 2787 | |
|
2788 | 2788 | ('branch.none', _('Branch no permissions')), |
|
2789 | 2789 | ('branch.merge', _('Branch access by web merge')), |
|
2790 | 2790 | ('branch.push', _('Branch access by push')), |
|
2791 | 2791 | ('branch.push_force', _('Branch access by push with force')), |
|
2792 | 2792 | |
|
2793 | 2793 | ('hg.repogroup.create.false', _('Repository Group creation disabled')), |
|
2794 | 2794 | ('hg.repogroup.create.true', _('Repository Group creation enabled')), |
|
2795 | 2795 | |
|
2796 | 2796 | ('hg.usergroup.create.false', _('User Group creation disabled')), |
|
2797 | 2797 | ('hg.usergroup.create.true', _('User Group creation enabled')), |
|
2798 | 2798 | |
|
2799 | 2799 | ('hg.create.none', _('Repository creation disabled')), |
|
2800 | 2800 | ('hg.create.repository', _('Repository creation enabled')), |
|
2801 | 2801 | ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')), |
|
2802 | 2802 | ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')), |
|
2803 | 2803 | |
|
2804 | 2804 | ('hg.fork.none', _('Repository forking disabled')), |
|
2805 | 2805 | ('hg.fork.repository', _('Repository forking enabled')), |
|
2806 | 2806 | |
|
2807 | 2807 | ('hg.register.none', _('Registration disabled')), |
|
2808 | 2808 | ('hg.register.manual_activate', _('User Registration with manual account activation')), |
|
2809 | 2809 | ('hg.register.auto_activate', _('User Registration with automatic account activation')), |
|
2810 | 2810 | |
|
2811 | 2811 | ('hg.password_reset.enabled', _('Password reset enabled')), |
|
2812 | 2812 | ('hg.password_reset.hidden', _('Password reset hidden')), |
|
2813 | 2813 | ('hg.password_reset.disabled', _('Password reset disabled')), |
|
2814 | 2814 | |
|
2815 | 2815 | ('hg.extern_activate.manual', _('Manual activation of external account')), |
|
2816 | 2816 | ('hg.extern_activate.auto', _('Automatic activation of external account')), |
|
2817 | 2817 | |
|
2818 | 2818 | ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')), |
|
2819 | 2819 | ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')), |
|
2820 | 2820 | ] |
|
2821 | 2821 | |
|
2822 | 2822 | # definition of system default permissions for DEFAULT user, created on |
|
2823 | 2823 | # system setup |
|
2824 | 2824 | DEFAULT_USER_PERMISSIONS = [ |
|
2825 | 2825 | # object perms |
|
2826 | 2826 | 'repository.read', |
|
2827 | 2827 | 'group.read', |
|
2828 | 2828 | 'usergroup.read', |
|
2829 | 2829 | # branch, for backward compat we need same value as before so forced pushed |
|
2830 | 2830 | 'branch.push_force', |
|
2831 | 2831 | # global |
|
2832 | 2832 | 'hg.create.repository', |
|
2833 | 2833 | 'hg.repogroup.create.false', |
|
2834 | 2834 | 'hg.usergroup.create.false', |
|
2835 | 2835 | 'hg.create.write_on_repogroup.true', |
|
2836 | 2836 | 'hg.fork.repository', |
|
2837 | 2837 | 'hg.register.manual_activate', |
|
2838 | 2838 | 'hg.password_reset.enabled', |
|
2839 | 2839 | 'hg.extern_activate.auto', |
|
2840 | 2840 | 'hg.inherit_default_perms.true', |
|
2841 | 2841 | ] |
|
2842 | 2842 | |
|
2843 | 2843 | # defines which permissions are more important higher the more important |
|
2844 | 2844 | # Weight defines which permissions are more important. |
|
2845 | 2845 | # The higher number the more important. |
|
2846 | 2846 | PERM_WEIGHTS = { |
|
2847 | 2847 | 'repository.none': 0, |
|
2848 | 2848 | 'repository.read': 1, |
|
2849 | 2849 | 'repository.write': 3, |
|
2850 | 2850 | 'repository.admin': 4, |
|
2851 | 2851 | |
|
2852 | 2852 | 'group.none': 0, |
|
2853 | 2853 | 'group.read': 1, |
|
2854 | 2854 | 'group.write': 3, |
|
2855 | 2855 | 'group.admin': 4, |
|
2856 | 2856 | |
|
2857 | 2857 | 'usergroup.none': 0, |
|
2858 | 2858 | 'usergroup.read': 1, |
|
2859 | 2859 | 'usergroup.write': 3, |
|
2860 | 2860 | 'usergroup.admin': 4, |
|
2861 | 2861 | |
|
2862 | 2862 | 'branch.none': 0, |
|
2863 | 2863 | 'branch.merge': 1, |
|
2864 | 2864 | 'branch.push': 3, |
|
2865 | 2865 | 'branch.push_force': 4, |
|
2866 | 2866 | |
|
2867 | 2867 | 'hg.repogroup.create.false': 0, |
|
2868 | 2868 | 'hg.repogroup.create.true': 1, |
|
2869 | 2869 | |
|
2870 | 2870 | 'hg.usergroup.create.false': 0, |
|
2871 | 2871 | 'hg.usergroup.create.true': 1, |
|
2872 | 2872 | |
|
2873 | 2873 | 'hg.fork.none': 0, |
|
2874 | 2874 | 'hg.fork.repository': 1, |
|
2875 | 2875 | 'hg.create.none': 0, |
|
2876 | 2876 | 'hg.create.repository': 1 |
|
2877 | 2877 | } |
|
2878 | 2878 | |
|
2879 | 2879 | permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
2880 | 2880 | permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None) |
|
2881 | 2881 | permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None) |
|
2882 | 2882 | |
|
2883 | 2883 | def __unicode__(self): |
|
2884 | 2884 | return u"<%s('%s:%s')>" % ( |
|
2885 | 2885 | self.__class__.__name__, self.permission_id, self.permission_name |
|
2886 | 2886 | ) |
|
2887 | 2887 | |
|
2888 | 2888 | @classmethod |
|
2889 | 2889 | def get_by_key(cls, key): |
|
2890 | 2890 | return cls.query().filter(cls.permission_name == key).scalar() |
|
2891 | 2891 | |
|
2892 | 2892 | @classmethod |
|
2893 | 2893 | def get_default_repo_perms(cls, user_id, repo_id=None): |
|
2894 | 2894 | q = Session().query(UserRepoToPerm, Repository, Permission)\ |
|
2895 | 2895 | .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ |
|
2896 | 2896 | .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ |
|
2897 | 2897 | .filter(UserRepoToPerm.user_id == user_id) |
|
2898 | 2898 | if repo_id: |
|
2899 | 2899 | q = q.filter(UserRepoToPerm.repository_id == repo_id) |
|
2900 | 2900 | return q.all() |
|
2901 | 2901 | |
|
2902 | 2902 | @classmethod |
|
2903 | 2903 | def get_default_repo_branch_perms(cls, user_id, repo_id=None): |
|
2904 | 2904 | q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \ |
|
2905 | 2905 | .join( |
|
2906 | 2906 | Permission, |
|
2907 | 2907 | UserToRepoBranchPermission.permission_id == Permission.permission_id) \ |
|
2908 | 2908 | .join( |
|
2909 | 2909 | UserRepoToPerm, |
|
2910 | 2910 | UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \ |
|
2911 | 2911 | .filter(UserRepoToPerm.user_id == user_id) |
|
2912 | 2912 | |
|
2913 | 2913 | if repo_id: |
|
2914 | 2914 | q = q.filter(UserToRepoBranchPermission.repository_id == repo_id) |
|
2915 | 2915 | return q.order_by(UserToRepoBranchPermission.rule_order).all() |
|
2916 | 2916 | |
|
2917 | 2917 | @classmethod |
|
2918 | 2918 | def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None): |
|
2919 | 2919 | q = Session().query(UserGroupRepoToPerm, Repository, Permission)\ |
|
2920 | 2920 | .join( |
|
2921 | 2921 | Permission, |
|
2922 | 2922 | UserGroupRepoToPerm.permission_id == Permission.permission_id)\ |
|
2923 | 2923 | .join( |
|
2924 | 2924 | Repository, |
|
2925 | 2925 | UserGroupRepoToPerm.repository_id == Repository.repo_id)\ |
|
2926 | 2926 | .join( |
|
2927 | 2927 | UserGroup, |
|
2928 | 2928 | UserGroupRepoToPerm.users_group_id == |
|
2929 | 2929 | UserGroup.users_group_id)\ |
|
2930 | 2930 | .join( |
|
2931 | 2931 | UserGroupMember, |
|
2932 | 2932 | UserGroupRepoToPerm.users_group_id == |
|
2933 | 2933 | UserGroupMember.users_group_id)\ |
|
2934 | 2934 | .filter( |
|
2935 | 2935 | UserGroupMember.user_id == user_id, |
|
2936 | 2936 | UserGroup.users_group_active == true()) |
|
2937 | 2937 | if repo_id: |
|
2938 | 2938 | q = q.filter(UserGroupRepoToPerm.repository_id == repo_id) |
|
2939 | 2939 | return q.all() |
|
2940 | 2940 | |
|
2941 | 2941 | @classmethod |
|
2942 | 2942 | def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None): |
|
2943 | 2943 | q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \ |
|
2944 | 2944 | .join( |
|
2945 | 2945 | Permission, |
|
2946 | 2946 | UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \ |
|
2947 | 2947 | .join( |
|
2948 | 2948 | UserGroupRepoToPerm, |
|
2949 | 2949 | UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \ |
|
2950 | 2950 | .join( |
|
2951 | 2951 | UserGroup, |
|
2952 | 2952 | UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \ |
|
2953 | 2953 | .join( |
|
2954 | 2954 | UserGroupMember, |
|
2955 | 2955 | UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \ |
|
2956 | 2956 | .filter( |
|
2957 | 2957 | UserGroupMember.user_id == user_id, |
|
2958 | 2958 | UserGroup.users_group_active == true()) |
|
2959 | 2959 | |
|
2960 | 2960 | if repo_id: |
|
2961 | 2961 | q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id) |
|
2962 | 2962 | return q.order_by(UserGroupToRepoBranchPermission.rule_order).all() |
|
2963 | 2963 | |
|
2964 | 2964 | @classmethod |
|
2965 | 2965 | def get_default_group_perms(cls, user_id, repo_group_id=None): |
|
2966 | 2966 | q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\ |
|
2967 | 2967 | .join( |
|
2968 | 2968 | Permission, |
|
2969 | 2969 | UserRepoGroupToPerm.permission_id == Permission.permission_id)\ |
|
2970 | 2970 | .join( |
|
2971 | 2971 | RepoGroup, |
|
2972 | 2972 | UserRepoGroupToPerm.group_id == RepoGroup.group_id)\ |
|
2973 | 2973 | .filter(UserRepoGroupToPerm.user_id == user_id) |
|
2974 | 2974 | if repo_group_id: |
|
2975 | 2975 | q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id) |
|
2976 | 2976 | return q.all() |
|
2977 | 2977 | |
|
2978 | 2978 | @classmethod |
|
2979 | 2979 | def get_default_group_perms_from_user_group( |
|
2980 | 2980 | cls, user_id, repo_group_id=None): |
|
2981 | 2981 | q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\ |
|
2982 | 2982 | .join( |
|
2983 | 2983 | Permission, |
|
2984 | 2984 | UserGroupRepoGroupToPerm.permission_id == |
|
2985 | 2985 | Permission.permission_id)\ |
|
2986 | 2986 | .join( |
|
2987 | 2987 | RepoGroup, |
|
2988 | 2988 | UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\ |
|
2989 | 2989 | .join( |
|
2990 | 2990 | UserGroup, |
|
2991 | 2991 | UserGroupRepoGroupToPerm.users_group_id == |
|
2992 | 2992 | UserGroup.users_group_id)\ |
|
2993 | 2993 | .join( |
|
2994 | 2994 | UserGroupMember, |
|
2995 | 2995 | UserGroupRepoGroupToPerm.users_group_id == |
|
2996 | 2996 | UserGroupMember.users_group_id)\ |
|
2997 | 2997 | .filter( |
|
2998 | 2998 | UserGroupMember.user_id == user_id, |
|
2999 | 2999 | UserGroup.users_group_active == true()) |
|
3000 | 3000 | if repo_group_id: |
|
3001 | 3001 | q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id) |
|
3002 | 3002 | return q.all() |
|
3003 | 3003 | |
|
3004 | 3004 | @classmethod |
|
3005 | 3005 | def get_default_user_group_perms(cls, user_id, user_group_id=None): |
|
3006 | 3006 | q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\ |
|
3007 | 3007 | .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\ |
|
3008 | 3008 | .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\ |
|
3009 | 3009 | .filter(UserUserGroupToPerm.user_id == user_id) |
|
3010 | 3010 | if user_group_id: |
|
3011 | 3011 | q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id) |
|
3012 | 3012 | return q.all() |
|
3013 | 3013 | |
|
3014 | 3014 | @classmethod |
|
3015 | 3015 | def get_default_user_group_perms_from_user_group( |
|
3016 | 3016 | cls, user_id, user_group_id=None): |
|
3017 | 3017 | TargetUserGroup = aliased(UserGroup, name='target_user_group') |
|
3018 | 3018 | q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\ |
|
3019 | 3019 | .join( |
|
3020 | 3020 | Permission, |
|
3021 | 3021 | UserGroupUserGroupToPerm.permission_id == |
|
3022 | 3022 | Permission.permission_id)\ |
|
3023 | 3023 | .join( |
|
3024 | 3024 | TargetUserGroup, |
|
3025 | 3025 | UserGroupUserGroupToPerm.target_user_group_id == |
|
3026 | 3026 | TargetUserGroup.users_group_id)\ |
|
3027 | 3027 | .join( |
|
3028 | 3028 | UserGroup, |
|
3029 | 3029 | UserGroupUserGroupToPerm.user_group_id == |
|
3030 | 3030 | UserGroup.users_group_id)\ |
|
3031 | 3031 | .join( |
|
3032 | 3032 | UserGroupMember, |
|
3033 | 3033 | UserGroupUserGroupToPerm.user_group_id == |
|
3034 | 3034 | UserGroupMember.users_group_id)\ |
|
3035 | 3035 | .filter( |
|
3036 | 3036 | UserGroupMember.user_id == user_id, |
|
3037 | 3037 | UserGroup.users_group_active == true()) |
|
3038 | 3038 | if user_group_id: |
|
3039 | 3039 | q = q.filter( |
|
3040 | 3040 | UserGroupUserGroupToPerm.user_group_id == user_group_id) |
|
3041 | 3041 | |
|
3042 | 3042 | return q.all() |
|
3043 | 3043 | |
|
3044 | 3044 | |
|
3045 | 3045 | class UserRepoToPerm(Base, BaseModel): |
|
3046 | 3046 | __tablename__ = 'repo_to_perm' |
|
3047 | 3047 | __table_args__ = ( |
|
3048 | 3048 | UniqueConstraint('user_id', 'repository_id', 'permission_id'), |
|
3049 | 3049 | base_table_args |
|
3050 | 3050 | ) |
|
3051 | 3051 | |
|
3052 | 3052 | repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3053 | 3053 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
3054 | 3054 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3055 | 3055 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) |
|
3056 | 3056 | |
|
3057 | 3057 | user = relationship('User') |
|
3058 | 3058 | repository = relationship('Repository') |
|
3059 | 3059 | permission = relationship('Permission') |
|
3060 | 3060 | |
|
3061 | 3061 | branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete, delete-orphan", lazy='joined') |
|
3062 | 3062 | |
|
3063 | 3063 | @classmethod |
|
3064 | 3064 | def create(cls, user, repository, permission): |
|
3065 | 3065 | n = cls() |
|
3066 | 3066 | n.user = user |
|
3067 | 3067 | n.repository = repository |
|
3068 | 3068 | n.permission = permission |
|
3069 | 3069 | Session().add(n) |
|
3070 | 3070 | return n |
|
3071 | 3071 | |
|
3072 | 3072 | def __unicode__(self): |
|
3073 | 3073 | return u'<%s => %s >' % (self.user, self.repository) |
|
3074 | 3074 | |
|
3075 | 3075 | |
|
3076 | 3076 | class UserUserGroupToPerm(Base, BaseModel): |
|
3077 | 3077 | __tablename__ = 'user_user_group_to_perm' |
|
3078 | 3078 | __table_args__ = ( |
|
3079 | 3079 | UniqueConstraint('user_id', 'user_group_id', 'permission_id'), |
|
3080 | 3080 | base_table_args |
|
3081 | 3081 | ) |
|
3082 | 3082 | |
|
3083 | 3083 | user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3084 | 3084 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
3085 | 3085 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3086 | 3086 | user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3087 | 3087 | |
|
3088 | 3088 | user = relationship('User') |
|
3089 | 3089 | user_group = relationship('UserGroup') |
|
3090 | 3090 | permission = relationship('Permission') |
|
3091 | 3091 | |
|
3092 | 3092 | @classmethod |
|
3093 | 3093 | def create(cls, user, user_group, permission): |
|
3094 | 3094 | n = cls() |
|
3095 | 3095 | n.user = user |
|
3096 | 3096 | n.user_group = user_group |
|
3097 | 3097 | n.permission = permission |
|
3098 | 3098 | Session().add(n) |
|
3099 | 3099 | return n |
|
3100 | 3100 | |
|
3101 | 3101 | def __unicode__(self): |
|
3102 | 3102 | return u'<%s => %s >' % (self.user, self.user_group) |
|
3103 | 3103 | |
|
3104 | 3104 | |
|
3105 | 3105 | class UserToPerm(Base, BaseModel): |
|
3106 | 3106 | __tablename__ = 'user_to_perm' |
|
3107 | 3107 | __table_args__ = ( |
|
3108 | 3108 | UniqueConstraint('user_id', 'permission_id'), |
|
3109 | 3109 | base_table_args |
|
3110 | 3110 | ) |
|
3111 | 3111 | |
|
3112 | 3112 | user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3113 | 3113 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
3114 | 3114 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3115 | 3115 | |
|
3116 | 3116 | user = relationship('User') |
|
3117 | 3117 | permission = relationship('Permission', lazy='joined') |
|
3118 | 3118 | |
|
3119 | 3119 | def __unicode__(self): |
|
3120 | 3120 | return u'<%s => %s >' % (self.user, self.permission) |
|
3121 | 3121 | |
|
3122 | 3122 | |
|
3123 | 3123 | class UserGroupRepoToPerm(Base, BaseModel): |
|
3124 | 3124 | __tablename__ = 'users_group_repo_to_perm' |
|
3125 | 3125 | __table_args__ = ( |
|
3126 | 3126 | UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), |
|
3127 | 3127 | base_table_args |
|
3128 | 3128 | ) |
|
3129 | 3129 | |
|
3130 | 3130 | users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3131 | 3131 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3132 | 3132 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3133 | 3133 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) |
|
3134 | 3134 | |
|
3135 | 3135 | users_group = relationship('UserGroup') |
|
3136 | 3136 | permission = relationship('Permission') |
|
3137 | 3137 | repository = relationship('Repository') |
|
3138 | 3138 | user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all') |
|
3139 | 3139 | |
|
3140 | 3140 | @classmethod |
|
3141 | 3141 | def create(cls, users_group, repository, permission): |
|
3142 | 3142 | n = cls() |
|
3143 | 3143 | n.users_group = users_group |
|
3144 | 3144 | n.repository = repository |
|
3145 | 3145 | n.permission = permission |
|
3146 | 3146 | Session().add(n) |
|
3147 | 3147 | return n |
|
3148 | 3148 | |
|
3149 | 3149 | def __unicode__(self): |
|
3150 | 3150 | return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository) |
|
3151 | 3151 | |
|
3152 | 3152 | |
|
3153 | 3153 | class UserGroupUserGroupToPerm(Base, BaseModel): |
|
3154 | 3154 | __tablename__ = 'user_group_user_group_to_perm' |
|
3155 | 3155 | __table_args__ = ( |
|
3156 | 3156 | UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'), |
|
3157 | 3157 | CheckConstraint('target_user_group_id != user_group_id'), |
|
3158 | 3158 | base_table_args |
|
3159 | 3159 | ) |
|
3160 | 3160 | |
|
3161 | 3161 | 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) |
|
3162 | 3162 | target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3163 | 3163 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3164 | 3164 | user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3165 | 3165 | |
|
3166 | 3166 | target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id') |
|
3167 | 3167 | user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id') |
|
3168 | 3168 | permission = relationship('Permission') |
|
3169 | 3169 | |
|
3170 | 3170 | @classmethod |
|
3171 | 3171 | def create(cls, target_user_group, user_group, permission): |
|
3172 | 3172 | n = cls() |
|
3173 | 3173 | n.target_user_group = target_user_group |
|
3174 | 3174 | n.user_group = user_group |
|
3175 | 3175 | n.permission = permission |
|
3176 | 3176 | Session().add(n) |
|
3177 | 3177 | return n |
|
3178 | 3178 | |
|
3179 | 3179 | def __unicode__(self): |
|
3180 | 3180 | return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group) |
|
3181 | 3181 | |
|
3182 | 3182 | |
|
3183 | 3183 | class UserGroupToPerm(Base, BaseModel): |
|
3184 | 3184 | __tablename__ = 'users_group_to_perm' |
|
3185 | 3185 | __table_args__ = ( |
|
3186 | 3186 | UniqueConstraint('users_group_id', 'permission_id',), |
|
3187 | 3187 | base_table_args |
|
3188 | 3188 | ) |
|
3189 | 3189 | |
|
3190 | 3190 | users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3191 | 3191 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3192 | 3192 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3193 | 3193 | |
|
3194 | 3194 | users_group = relationship('UserGroup') |
|
3195 | 3195 | permission = relationship('Permission') |
|
3196 | 3196 | |
|
3197 | 3197 | |
|
3198 | 3198 | class UserRepoGroupToPerm(Base, BaseModel): |
|
3199 | 3199 | __tablename__ = 'user_repo_group_to_perm' |
|
3200 | 3200 | __table_args__ = ( |
|
3201 | 3201 | UniqueConstraint('user_id', 'group_id', 'permission_id'), |
|
3202 | 3202 | base_table_args |
|
3203 | 3203 | ) |
|
3204 | 3204 | |
|
3205 | 3205 | group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3206 | 3206 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
3207 | 3207 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) |
|
3208 | 3208 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3209 | 3209 | |
|
3210 | 3210 | user = relationship('User') |
|
3211 | 3211 | group = relationship('RepoGroup') |
|
3212 | 3212 | permission = relationship('Permission') |
|
3213 | 3213 | |
|
3214 | 3214 | @classmethod |
|
3215 | 3215 | def create(cls, user, repository_group, permission): |
|
3216 | 3216 | n = cls() |
|
3217 | 3217 | n.user = user |
|
3218 | 3218 | n.group = repository_group |
|
3219 | 3219 | n.permission = permission |
|
3220 | 3220 | Session().add(n) |
|
3221 | 3221 | return n |
|
3222 | 3222 | |
|
3223 | 3223 | |
|
3224 | 3224 | class UserGroupRepoGroupToPerm(Base, BaseModel): |
|
3225 | 3225 | __tablename__ = 'users_group_repo_group_to_perm' |
|
3226 | 3226 | __table_args__ = ( |
|
3227 | 3227 | UniqueConstraint('users_group_id', 'group_id'), |
|
3228 | 3228 | base_table_args |
|
3229 | 3229 | ) |
|
3230 | 3230 | |
|
3231 | 3231 | 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) |
|
3232 | 3232 | users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None) |
|
3233 | 3233 | group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None) |
|
3234 | 3234 | permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
3235 | 3235 | |
|
3236 | 3236 | users_group = relationship('UserGroup') |
|
3237 | 3237 | permission = relationship('Permission') |
|
3238 | 3238 | group = relationship('RepoGroup') |
|
3239 | 3239 | |
|
3240 | 3240 | @classmethod |
|
3241 | 3241 | def create(cls, user_group, repository_group, permission): |
|
3242 | 3242 | n = cls() |
|
3243 | 3243 | n.users_group = user_group |
|
3244 | 3244 | n.group = repository_group |
|
3245 | 3245 | n.permission = permission |
|
3246 | 3246 | Session().add(n) |
|
3247 | 3247 | return n |
|
3248 | 3248 | |
|
3249 | 3249 | def __unicode__(self): |
|
3250 | 3250 | return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group) |
|
3251 | 3251 | |
|
3252 | 3252 | |
|
3253 | 3253 | class Statistics(Base, BaseModel): |
|
3254 | 3254 | __tablename__ = 'statistics' |
|
3255 | 3255 | __table_args__ = ( |
|
3256 | 3256 | base_table_args |
|
3257 | 3257 | ) |
|
3258 | 3258 | |
|
3259 | 3259 | stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3260 | 3260 | repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) |
|
3261 | 3261 | stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) |
|
3262 | 3262 | commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data |
|
3263 | 3263 | commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data |
|
3264 | 3264 | languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data |
|
3265 | 3265 | |
|
3266 | 3266 | repository = relationship('Repository', single_parent=True) |
|
3267 | 3267 | |
|
3268 | 3268 | |
|
3269 | 3269 | class UserFollowing(Base, BaseModel): |
|
3270 | 3270 | __tablename__ = 'user_followings' |
|
3271 | 3271 | __table_args__ = ( |
|
3272 | 3272 | UniqueConstraint('user_id', 'follows_repository_id'), |
|
3273 | 3273 | UniqueConstraint('user_id', 'follows_user_id'), |
|
3274 | 3274 | base_table_args |
|
3275 | 3275 | ) |
|
3276 | 3276 | |
|
3277 | 3277 | user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3278 | 3278 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) |
|
3279 | 3279 | follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None) |
|
3280 | 3280 | follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None) |
|
3281 | 3281 | follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) |
|
3282 | 3282 | |
|
3283 | 3283 | user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id') |
|
3284 | 3284 | |
|
3285 | 3285 | follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id') |
|
3286 | 3286 | follows_repository = relationship('Repository', order_by='Repository.repo_name') |
|
3287 | 3287 | |
|
3288 | 3288 | @classmethod |
|
3289 | 3289 | def get_repo_followers(cls, repo_id): |
|
3290 | 3290 | return cls.query().filter(cls.follows_repo_id == repo_id) |
|
3291 | 3291 | |
|
3292 | 3292 | |
|
3293 | 3293 | class CacheKey(Base, BaseModel): |
|
3294 | 3294 | __tablename__ = 'cache_invalidation' |
|
3295 | 3295 | __table_args__ = ( |
|
3296 | 3296 | UniqueConstraint('cache_key'), |
|
3297 | 3297 | Index('key_idx', 'cache_key'), |
|
3298 | 3298 | base_table_args, |
|
3299 | 3299 | ) |
|
3300 | 3300 | |
|
3301 | 3301 | CACHE_TYPE_FEED = 'FEED' |
|
3302 | 3302 | CACHE_TYPE_README = 'README' |
|
3303 | 3303 | # namespaces used to register process/thread aware caches |
|
3304 | 3304 | REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}' |
|
3305 | 3305 | SETTINGS_INVALIDATION_NAMESPACE = 'system_settings' |
|
3306 | 3306 | |
|
3307 | 3307 | cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) |
|
3308 | 3308 | cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None) |
|
3309 | 3309 | cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None) |
|
3310 | 3310 | cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False) |
|
3311 | 3311 | |
|
3312 | 3312 | def __init__(self, cache_key, cache_args=''): |
|
3313 | 3313 | self.cache_key = cache_key |
|
3314 | 3314 | self.cache_args = cache_args |
|
3315 | 3315 | self.cache_active = False |
|
3316 | 3316 | |
|
3317 | 3317 | def __unicode__(self): |
|
3318 | 3318 | return u"<%s('%s:%s[%s]')>" % ( |
|
3319 | 3319 | self.__class__.__name__, |
|
3320 | 3320 | self.cache_id, self.cache_key, self.cache_active) |
|
3321 | 3321 | |
|
3322 | 3322 | def _cache_key_partition(self): |
|
3323 | 3323 | prefix, repo_name, suffix = self.cache_key.partition(self.cache_args) |
|
3324 | 3324 | return prefix, repo_name, suffix |
|
3325 | 3325 | |
|
3326 | 3326 | def get_prefix(self): |
|
3327 | 3327 | """ |
|
3328 | 3328 | Try to extract prefix from existing cache key. The key could consist |
|
3329 | 3329 | of prefix, repo_name, suffix |
|
3330 | 3330 | """ |
|
3331 | 3331 | # this returns prefix, repo_name, suffix |
|
3332 | 3332 | return self._cache_key_partition()[0] |
|
3333 | 3333 | |
|
3334 | 3334 | def get_suffix(self): |
|
3335 | 3335 | """ |
|
3336 | 3336 | get suffix that might have been used in _get_cache_key to |
|
3337 | 3337 | generate self.cache_key. Only used for informational purposes |
|
3338 | 3338 | in repo_edit.mako. |
|
3339 | 3339 | """ |
|
3340 | 3340 | # prefix, repo_name, suffix |
|
3341 | 3341 | return self._cache_key_partition()[2] |
|
3342 | 3342 | |
|
3343 | 3343 | @classmethod |
|
3344 | 3344 | def delete_all_cache(cls): |
|
3345 | 3345 | """ |
|
3346 | 3346 | Delete all cache keys from database. |
|
3347 | 3347 | Should only be run when all instances are down and all entries |
|
3348 | 3348 | thus stale. |
|
3349 | 3349 | """ |
|
3350 | 3350 | cls.query().delete() |
|
3351 | 3351 | Session().commit() |
|
3352 | 3352 | |
|
3353 | 3353 | @classmethod |
|
3354 | 3354 | def set_invalidate(cls, cache_uid, delete=False): |
|
3355 | 3355 | """ |
|
3356 | 3356 | Mark all caches of a repo as invalid in the database. |
|
3357 | 3357 | """ |
|
3358 | 3358 | |
|
3359 | 3359 | try: |
|
3360 | 3360 | qry = Session().query(cls).filter(cls.cache_args == cache_uid) |
|
3361 | 3361 | if delete: |
|
3362 | 3362 | qry.delete() |
|
3363 | 3363 | log.debug('cache objects deleted for cache args %s', |
|
3364 | 3364 | safe_str(cache_uid)) |
|
3365 | 3365 | else: |
|
3366 | 3366 | qry.update({"cache_active": False}) |
|
3367 | 3367 | log.debug('cache objects marked as invalid for cache args %s', |
|
3368 | 3368 | safe_str(cache_uid)) |
|
3369 | 3369 | |
|
3370 | 3370 | Session().commit() |
|
3371 | 3371 | except Exception: |
|
3372 | 3372 | log.exception( |
|
3373 | 3373 | 'Cache key invalidation failed for cache args %s', |
|
3374 | 3374 | safe_str(cache_uid)) |
|
3375 | 3375 | Session().rollback() |
|
3376 | 3376 | |
|
3377 | 3377 | @classmethod |
|
3378 | 3378 | def get_active_cache(cls, cache_key): |
|
3379 | 3379 | inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar() |
|
3380 | 3380 | if inv_obj: |
|
3381 | 3381 | return inv_obj |
|
3382 | 3382 | return None |
|
3383 | 3383 | |
|
3384 | 3384 | |
|
3385 | 3385 | class ChangesetComment(Base, BaseModel): |
|
3386 | 3386 | __tablename__ = 'changeset_comments' |
|
3387 | 3387 | __table_args__ = ( |
|
3388 | 3388 | Index('cc_revision_idx', 'revision'), |
|
3389 | 3389 | base_table_args, |
|
3390 | 3390 | ) |
|
3391 | 3391 | |
|
3392 | 3392 | COMMENT_OUTDATED = u'comment_outdated' |
|
3393 | 3393 | COMMENT_TYPE_NOTE = u'note' |
|
3394 | 3394 | COMMENT_TYPE_TODO = u'todo' |
|
3395 | 3395 | COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO] |
|
3396 | 3396 | |
|
3397 | 3397 | comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True) |
|
3398 | 3398 | repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) |
|
3399 | 3399 | revision = Column('revision', String(40), nullable=True) |
|
3400 | 3400 | pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) |
|
3401 | 3401 | pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True) |
|
3402 | 3402 | line_no = Column('line_no', Unicode(10), nullable=True) |
|
3403 | 3403 | hl_lines = Column('hl_lines', Unicode(512), nullable=True) |
|
3404 | 3404 | f_path = Column('f_path', Unicode(1000), nullable=True) |
|
3405 | 3405 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) |
|
3406 | 3406 | text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False) |
|
3407 | 3407 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
3408 | 3408 | modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
3409 | 3409 | renderer = Column('renderer', Unicode(64), nullable=True) |
|
3410 | 3410 | display_state = Column('display_state', Unicode(128), nullable=True) |
|
3411 | 3411 | |
|
3412 | 3412 | comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE) |
|
3413 | 3413 | resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True) |
|
3414 | 3414 | |
|
3415 | 3415 | resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by') |
|
3416 | 3416 | resolved_by = relationship('ChangesetComment', back_populates='resolved_comment') |
|
3417 | 3417 | |
|
3418 | 3418 | author = relationship('User', lazy='joined') |
|
3419 | 3419 | repo = relationship('Repository') |
|
3420 | 3420 | status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined') |
|
3421 | 3421 | pull_request = relationship('PullRequest', lazy='joined') |
|
3422 | 3422 | pull_request_version = relationship('PullRequestVersion') |
|
3423 | 3423 | |
|
3424 | 3424 | @classmethod |
|
3425 | 3425 | def get_users(cls, revision=None, pull_request_id=None): |
|
3426 | 3426 | """ |
|
3427 | 3427 | Returns user associated with this ChangesetComment. ie those |
|
3428 | 3428 | who actually commented |
|
3429 | 3429 | |
|
3430 | 3430 | :param cls: |
|
3431 | 3431 | :param revision: |
|
3432 | 3432 | """ |
|
3433 | 3433 | q = Session().query(User)\ |
|
3434 | 3434 | .join(ChangesetComment.author) |
|
3435 | 3435 | if revision: |
|
3436 | 3436 | q = q.filter(cls.revision == revision) |
|
3437 | 3437 | elif pull_request_id: |
|
3438 | 3438 | q = q.filter(cls.pull_request_id == pull_request_id) |
|
3439 | 3439 | return q.all() |
|
3440 | 3440 | |
|
3441 | 3441 | @classmethod |
|
3442 | 3442 | def get_index_from_version(cls, pr_version, versions): |
|
3443 | 3443 | num_versions = [x.pull_request_version_id for x in versions] |
|
3444 | 3444 | try: |
|
3445 | 3445 | return num_versions.index(pr_version) +1 |
|
3446 | 3446 | except (IndexError, ValueError): |
|
3447 | 3447 | return |
|
3448 | 3448 | |
|
3449 | 3449 | @property |
|
3450 | 3450 | def outdated(self): |
|
3451 | 3451 | return self.display_state == self.COMMENT_OUTDATED |
|
3452 | 3452 | |
|
3453 | 3453 | def outdated_at_version(self, version): |
|
3454 | 3454 | """ |
|
3455 | 3455 | Checks if comment is outdated for given pull request version |
|
3456 | 3456 | """ |
|
3457 | 3457 | return self.outdated and self.pull_request_version_id != version |
|
3458 | 3458 | |
|
3459 | 3459 | def older_than_version(self, version): |
|
3460 | 3460 | """ |
|
3461 | 3461 | Checks if comment is made from previous version than given |
|
3462 | 3462 | """ |
|
3463 | 3463 | if version is None: |
|
3464 | 3464 | return self.pull_request_version_id is not None |
|
3465 | 3465 | |
|
3466 | 3466 | return self.pull_request_version_id < version |
|
3467 | 3467 | |
|
3468 | 3468 | @property |
|
3469 | 3469 | def resolved(self): |
|
3470 | 3470 | return self.resolved_by[0] if self.resolved_by else None |
|
3471 | 3471 | |
|
3472 | 3472 | @property |
|
3473 | 3473 | def is_todo(self): |
|
3474 | 3474 | return self.comment_type == self.COMMENT_TYPE_TODO |
|
3475 | 3475 | |
|
3476 | 3476 | @property |
|
3477 | 3477 | def is_inline(self): |
|
3478 | 3478 | return self.line_no and self.f_path |
|
3479 | 3479 | |
|
3480 | 3480 | def get_index_version(self, versions): |
|
3481 | 3481 | return self.get_index_from_version( |
|
3482 | 3482 | self.pull_request_version_id, versions) |
|
3483 | 3483 | |
|
3484 | 3484 | def __repr__(self): |
|
3485 | 3485 | if self.comment_id: |
|
3486 | 3486 | return '<DB:Comment #%s>' % self.comment_id |
|
3487 | 3487 | else: |
|
3488 | 3488 | return '<DB:Comment at %#x>' % id(self) |
|
3489 | 3489 | |
|
3490 | 3490 | def get_api_data(self): |
|
3491 | 3491 | comment = self |
|
3492 | 3492 | data = { |
|
3493 | 3493 | 'comment_id': comment.comment_id, |
|
3494 | 3494 | 'comment_type': comment.comment_type, |
|
3495 | 3495 | 'comment_text': comment.text, |
|
3496 | 3496 | 'comment_status': comment.status_change, |
|
3497 | 3497 | 'comment_f_path': comment.f_path, |
|
3498 | 3498 | 'comment_lineno': comment.line_no, |
|
3499 | 3499 | 'comment_author': comment.author, |
|
3500 | 3500 | 'comment_created_on': comment.created_on |
|
3501 | 3501 | } |
|
3502 | 3502 | return data |
|
3503 | 3503 | |
|
3504 | 3504 | def __json__(self): |
|
3505 | 3505 | data = dict() |
|
3506 | 3506 | data.update(self.get_api_data()) |
|
3507 | 3507 | return data |
|
3508 | 3508 | |
|
3509 | 3509 | |
|
3510 | 3510 | class ChangesetStatus(Base, BaseModel): |
|
3511 | 3511 | __tablename__ = 'changeset_statuses' |
|
3512 | 3512 | __table_args__ = ( |
|
3513 | 3513 | Index('cs_revision_idx', 'revision'), |
|
3514 | 3514 | Index('cs_version_idx', 'version'), |
|
3515 | 3515 | UniqueConstraint('repo_id', 'revision', 'version'), |
|
3516 | 3516 | base_table_args |
|
3517 | 3517 | ) |
|
3518 | 3518 | |
|
3519 | 3519 | STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed' |
|
3520 | 3520 | STATUS_APPROVED = 'approved' |
|
3521 | 3521 | STATUS_REJECTED = 'rejected' |
|
3522 | 3522 | STATUS_UNDER_REVIEW = 'under_review' |
|
3523 | 3523 | |
|
3524 | 3524 | STATUSES = [ |
|
3525 | 3525 | (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default |
|
3526 | 3526 | (STATUS_APPROVED, _("Approved")), |
|
3527 | 3527 | (STATUS_REJECTED, _("Rejected")), |
|
3528 | 3528 | (STATUS_UNDER_REVIEW, _("Under Review")), |
|
3529 | 3529 | ] |
|
3530 | 3530 | |
|
3531 | 3531 | changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True) |
|
3532 | 3532 | repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False) |
|
3533 | 3533 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None) |
|
3534 | 3534 | revision = Column('revision', String(40), nullable=False) |
|
3535 | 3535 | status = Column('status', String(128), nullable=False, default=DEFAULT) |
|
3536 | 3536 | changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id')) |
|
3537 | 3537 | modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now) |
|
3538 | 3538 | version = Column('version', Integer(), nullable=False, default=0) |
|
3539 | 3539 | pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) |
|
3540 | 3540 | |
|
3541 | 3541 | author = relationship('User', lazy='joined') |
|
3542 | 3542 | repo = relationship('Repository') |
|
3543 | 3543 | comment = relationship('ChangesetComment', lazy='joined') |
|
3544 | 3544 | pull_request = relationship('PullRequest', lazy='joined') |
|
3545 | 3545 | |
|
3546 | 3546 | def __unicode__(self): |
|
3547 | 3547 | return u"<%s('%s[v%s]:%s')>" % ( |
|
3548 | 3548 | self.__class__.__name__, |
|
3549 | 3549 | self.status, self.version, self.author |
|
3550 | 3550 | ) |
|
3551 | 3551 | |
|
3552 | 3552 | @classmethod |
|
3553 | 3553 | def get_status_lbl(cls, value): |
|
3554 | 3554 | return dict(cls.STATUSES).get(value) |
|
3555 | 3555 | |
|
3556 | 3556 | @property |
|
3557 | 3557 | def status_lbl(self): |
|
3558 | 3558 | return ChangesetStatus.get_status_lbl(self.status) |
|
3559 | 3559 | |
|
3560 | 3560 | def get_api_data(self): |
|
3561 | 3561 | status = self |
|
3562 | 3562 | data = { |
|
3563 | 3563 | 'status_id': status.changeset_status_id, |
|
3564 | 3564 | 'status': status.status, |
|
3565 | 3565 | } |
|
3566 | 3566 | return data |
|
3567 | 3567 | |
|
3568 | 3568 | def __json__(self): |
|
3569 | 3569 | data = dict() |
|
3570 | 3570 | data.update(self.get_api_data()) |
|
3571 | 3571 | return data |
|
3572 | 3572 | |
|
3573 | 3573 | |
|
3574 | 3574 | class _SetState(object): |
|
3575 | 3575 | """ |
|
3576 | 3576 | Context processor allowing changing state for sensitive operation such as |
|
3577 | 3577 | pull request update or merge |
|
3578 | 3578 | """ |
|
3579 | 3579 | |
|
3580 | 3580 | def __init__(self, pull_request, pr_state, back_state=None): |
|
3581 | 3581 | self._pr = pull_request |
|
3582 | 3582 | self._org_state = back_state or pull_request.pull_request_state |
|
3583 | 3583 | self._pr_state = pr_state |
|
3584 | 3584 | |
|
3585 | 3585 | def __enter__(self): |
|
3586 | 3586 | log.debug('StateLock: entering set state context, setting state to: `%s`', |
|
3587 | 3587 | self._pr_state) |
|
3588 | 3588 | self._pr.pull_request_state = self._pr_state |
|
3589 | 3589 | Session().add(self._pr) |
|
3590 | 3590 | Session().commit() |
|
3591 | 3591 | |
|
3592 | 3592 | def __exit__(self, exc_type, exc_val, exc_tb): |
|
3593 | 3593 | log.debug('StateLock: exiting set state context, setting state to: `%s`', |
|
3594 | 3594 | self._org_state) |
|
3595 | 3595 | self._pr.pull_request_state = self._org_state |
|
3596 | 3596 | Session().add(self._pr) |
|
3597 | 3597 | Session().commit() |
|
3598 | 3598 | |
|
3599 | 3599 | |
|
3600 | 3600 | class _PullRequestBase(BaseModel): |
|
3601 | 3601 | """ |
|
3602 | 3602 | Common attributes of pull request and version entries. |
|
3603 | 3603 | """ |
|
3604 | 3604 | |
|
3605 | 3605 | # .status values |
|
3606 | 3606 | STATUS_NEW = u'new' |
|
3607 | 3607 | STATUS_OPEN = u'open' |
|
3608 | 3608 | STATUS_CLOSED = u'closed' |
|
3609 | 3609 | |
|
3610 | 3610 | # available states |
|
3611 | 3611 | STATE_CREATING = u'creating' |
|
3612 | 3612 | STATE_UPDATING = u'updating' |
|
3613 | 3613 | STATE_MERGING = u'merging' |
|
3614 | 3614 | STATE_CREATED = u'created' |
|
3615 | 3615 | |
|
3616 | 3616 | title = Column('title', Unicode(255), nullable=True) |
|
3617 | 3617 | description = Column( |
|
3618 | 3618 | 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), |
|
3619 | 3619 | nullable=True) |
|
3620 | 3620 | description_renderer = Column('description_renderer', Unicode(64), nullable=True) |
|
3621 | 3621 | |
|
3622 | 3622 | # new/open/closed status of pull request (not approve/reject/etc) |
|
3623 | 3623 | status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW) |
|
3624 | 3624 | created_on = Column( |
|
3625 | 3625 | 'created_on', DateTime(timezone=False), nullable=False, |
|
3626 | 3626 | default=datetime.datetime.now) |
|
3627 | 3627 | updated_on = Column( |
|
3628 | 3628 | 'updated_on', DateTime(timezone=False), nullable=False, |
|
3629 | 3629 | default=datetime.datetime.now) |
|
3630 | 3630 | |
|
3631 | 3631 | pull_request_state = Column("pull_request_state", String(255), nullable=True) |
|
3632 | 3632 | |
|
3633 | 3633 | @declared_attr |
|
3634 | 3634 | def user_id(cls): |
|
3635 | 3635 | return Column( |
|
3636 | 3636 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=False, |
|
3637 | 3637 | unique=None) |
|
3638 | 3638 | |
|
3639 | 3639 | # 500 revisions max |
|
3640 | 3640 | _revisions = Column( |
|
3641 | 3641 | 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql')) |
|
3642 | 3642 | |
|
3643 | 3643 | @declared_attr |
|
3644 | 3644 | def source_repo_id(cls): |
|
3645 | 3645 | # TODO: dan: rename column to source_repo_id |
|
3646 | 3646 | return Column( |
|
3647 | 3647 | 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'), |
|
3648 | 3648 | nullable=False) |
|
3649 | 3649 | |
|
3650 | 3650 | _source_ref = Column('org_ref', Unicode(255), nullable=False) |
|
3651 | 3651 | |
|
3652 | 3652 | @hybrid_property |
|
3653 | 3653 | def source_ref(self): |
|
3654 | 3654 | return self._source_ref |
|
3655 | 3655 | |
|
3656 | 3656 | @source_ref.setter |
|
3657 | 3657 | def source_ref(self, val): |
|
3658 | 3658 | parts = (val or '').split(':') |
|
3659 | 3659 | if len(parts) != 3: |
|
3660 | 3660 | raise ValueError( |
|
3661 | 3661 | 'Invalid reference format given: {}, expected X:Y:Z'.format(val)) |
|
3662 | 3662 | self._source_ref = safe_unicode(val) |
|
3663 | 3663 | |
|
3664 | 3664 | _target_ref = Column('other_ref', Unicode(255), nullable=False) |
|
3665 | 3665 | |
|
3666 | 3666 | @hybrid_property |
|
3667 | 3667 | def target_ref(self): |
|
3668 | 3668 | return self._target_ref |
|
3669 | 3669 | |
|
3670 | 3670 | @target_ref.setter |
|
3671 | 3671 | def target_ref(self, val): |
|
3672 | 3672 | parts = (val or '').split(':') |
|
3673 | 3673 | if len(parts) != 3: |
|
3674 | 3674 | raise ValueError( |
|
3675 | 3675 | 'Invalid reference format given: {}, expected X:Y:Z'.format(val)) |
|
3676 | 3676 | self._target_ref = safe_unicode(val) |
|
3677 | 3677 | |
|
3678 | 3678 | @declared_attr |
|
3679 | 3679 | def target_repo_id(cls): |
|
3680 | 3680 | # TODO: dan: rename column to target_repo_id |
|
3681 | 3681 | return Column( |
|
3682 | 3682 | 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'), |
|
3683 | 3683 | nullable=False) |
|
3684 | 3684 | |
|
3685 | 3685 | _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True) |
|
3686 | 3686 | |
|
3687 | 3687 | # TODO: dan: rename column to last_merge_source_rev |
|
3688 | 3688 | _last_merge_source_rev = Column( |
|
3689 | 3689 | 'last_merge_org_rev', String(40), nullable=True) |
|
3690 | 3690 | # TODO: dan: rename column to last_merge_target_rev |
|
3691 | 3691 | _last_merge_target_rev = Column( |
|
3692 | 3692 | 'last_merge_other_rev', String(40), nullable=True) |
|
3693 | 3693 | _last_merge_status = Column('merge_status', Integer(), nullable=True) |
|
3694 | 3694 | merge_rev = Column('merge_rev', String(40), nullable=True) |
|
3695 | 3695 | |
|
3696 | 3696 | reviewer_data = Column( |
|
3697 | 3697 | 'reviewer_data_json', MutationObj.as_mutable( |
|
3698 | 3698 | JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) |
|
3699 | 3699 | |
|
3700 | 3700 | @property |
|
3701 | 3701 | def reviewer_data_json(self): |
|
3702 | 3702 | return json.dumps(self.reviewer_data) |
|
3703 | 3703 | |
|
3704 | 3704 | @hybrid_property |
|
3705 | 3705 | def description_safe(self): |
|
3706 | 3706 | from rhodecode.lib import helpers as h |
|
3707 | 3707 | return h.escape(self.description) |
|
3708 | 3708 | |
|
3709 | 3709 | @hybrid_property |
|
3710 | 3710 | def revisions(self): |
|
3711 | 3711 | return self._revisions.split(':') if self._revisions else [] |
|
3712 | 3712 | |
|
3713 | 3713 | @revisions.setter |
|
3714 | 3714 | def revisions(self, val): |
|
3715 | 3715 | self._revisions = ':'.join(val) |
|
3716 | 3716 | |
|
3717 | 3717 | @hybrid_property |
|
3718 | 3718 | def last_merge_status(self): |
|
3719 | 3719 | return safe_int(self._last_merge_status) |
|
3720 | 3720 | |
|
3721 | 3721 | @last_merge_status.setter |
|
3722 | 3722 | def last_merge_status(self, val): |
|
3723 | 3723 | self._last_merge_status = val |
|
3724 | 3724 | |
|
3725 | 3725 | @declared_attr |
|
3726 | 3726 | def author(cls): |
|
3727 | 3727 | return relationship('User', lazy='joined') |
|
3728 | 3728 | |
|
3729 | 3729 | @declared_attr |
|
3730 | 3730 | def source_repo(cls): |
|
3731 | 3731 | return relationship( |
|
3732 | 3732 | 'Repository', |
|
3733 | 3733 | primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__) |
|
3734 | 3734 | |
|
3735 | 3735 | @property |
|
3736 | 3736 | def source_ref_parts(self): |
|
3737 | 3737 | return self.unicode_to_reference(self.source_ref) |
|
3738 | 3738 | |
|
3739 | 3739 | @declared_attr |
|
3740 | 3740 | def target_repo(cls): |
|
3741 | 3741 | return relationship( |
|
3742 | 3742 | 'Repository', |
|
3743 | 3743 | primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__) |
|
3744 | 3744 | |
|
3745 | 3745 | @property |
|
3746 | 3746 | def target_ref_parts(self): |
|
3747 | 3747 | return self.unicode_to_reference(self.target_ref) |
|
3748 | 3748 | |
|
3749 | 3749 | @property |
|
3750 | 3750 | def shadow_merge_ref(self): |
|
3751 | 3751 | return self.unicode_to_reference(self._shadow_merge_ref) |
|
3752 | 3752 | |
|
3753 | 3753 | @shadow_merge_ref.setter |
|
3754 | 3754 | def shadow_merge_ref(self, ref): |
|
3755 | 3755 | self._shadow_merge_ref = self.reference_to_unicode(ref) |
|
3756 | 3756 | |
|
3757 | 3757 | @staticmethod |
|
3758 | 3758 | def unicode_to_reference(raw): |
|
3759 | 3759 | """ |
|
3760 | 3760 | Convert a unicode (or string) to a reference object. |
|
3761 | 3761 | If unicode evaluates to False it returns None. |
|
3762 | 3762 | """ |
|
3763 | 3763 | if raw: |
|
3764 | 3764 | refs = raw.split(':') |
|
3765 | 3765 | return Reference(*refs) |
|
3766 | 3766 | else: |
|
3767 | 3767 | return None |
|
3768 | 3768 | |
|
3769 | 3769 | @staticmethod |
|
3770 | 3770 | def reference_to_unicode(ref): |
|
3771 | 3771 | """ |
|
3772 | 3772 | Convert a reference object to unicode. |
|
3773 | 3773 | If reference is None it returns None. |
|
3774 | 3774 | """ |
|
3775 | 3775 | if ref: |
|
3776 | 3776 | return u':'.join(ref) |
|
3777 | 3777 | else: |
|
3778 | 3778 | return None |
|
3779 | 3779 | |
|
3780 | 3780 | def get_api_data(self, with_merge_state=True): |
|
3781 | 3781 | from rhodecode.model.pull_request import PullRequestModel |
|
3782 | 3782 | |
|
3783 | 3783 | pull_request = self |
|
3784 | 3784 | if with_merge_state: |
|
3785 | 3785 | merge_status = PullRequestModel().merge_status(pull_request) |
|
3786 | 3786 | merge_state = { |
|
3787 | 3787 | 'status': merge_status[0], |
|
3788 | 3788 | 'message': safe_unicode(merge_status[1]), |
|
3789 | 3789 | } |
|
3790 | 3790 | else: |
|
3791 | 3791 | merge_state = {'status': 'not_available', |
|
3792 | 3792 | 'message': 'not_available'} |
|
3793 | 3793 | |
|
3794 | 3794 | merge_data = { |
|
3795 | 3795 | 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request), |
|
3796 | 3796 | 'reference': ( |
|
3797 | 3797 | pull_request.shadow_merge_ref._asdict() |
|
3798 | 3798 | if pull_request.shadow_merge_ref else None), |
|
3799 | 3799 | } |
|
3800 | 3800 | |
|
3801 | 3801 | data = { |
|
3802 | 3802 | 'pull_request_id': pull_request.pull_request_id, |
|
3803 | 3803 | 'url': PullRequestModel().get_url(pull_request), |
|
3804 | 3804 | 'title': pull_request.title, |
|
3805 | 3805 | 'description': pull_request.description, |
|
3806 | 3806 | 'status': pull_request.status, |
|
3807 | 3807 | 'state': pull_request.pull_request_state, |
|
3808 | 3808 | 'created_on': pull_request.created_on, |
|
3809 | 3809 | 'updated_on': pull_request.updated_on, |
|
3810 | 3810 | 'commit_ids': pull_request.revisions, |
|
3811 | 3811 | 'review_status': pull_request.calculated_review_status(), |
|
3812 | 3812 | 'mergeable': merge_state, |
|
3813 | 3813 | 'source': { |
|
3814 | 3814 | 'clone_url': pull_request.source_repo.clone_url(), |
|
3815 | 3815 | 'repository': pull_request.source_repo.repo_name, |
|
3816 | 3816 | 'reference': { |
|
3817 | 3817 | 'name': pull_request.source_ref_parts.name, |
|
3818 | 3818 | 'type': pull_request.source_ref_parts.type, |
|
3819 | 3819 | 'commit_id': pull_request.source_ref_parts.commit_id, |
|
3820 | 3820 | }, |
|
3821 | 3821 | }, |
|
3822 | 3822 | 'target': { |
|
3823 | 3823 | 'clone_url': pull_request.target_repo.clone_url(), |
|
3824 | 3824 | 'repository': pull_request.target_repo.repo_name, |
|
3825 | 3825 | 'reference': { |
|
3826 | 3826 | 'name': pull_request.target_ref_parts.name, |
|
3827 | 3827 | 'type': pull_request.target_ref_parts.type, |
|
3828 | 3828 | 'commit_id': pull_request.target_ref_parts.commit_id, |
|
3829 | 3829 | }, |
|
3830 | 3830 | }, |
|
3831 | 3831 | 'merge': merge_data, |
|
3832 | 3832 | 'author': pull_request.author.get_api_data(include_secrets=False, |
|
3833 | 3833 | details='basic'), |
|
3834 | 3834 | 'reviewers': [ |
|
3835 | 3835 | { |
|
3836 | 3836 | 'user': reviewer.get_api_data(include_secrets=False, |
|
3837 | 3837 | details='basic'), |
|
3838 | 3838 | 'reasons': reasons, |
|
3839 | 3839 | 'review_status': st[0][1].status if st else 'not_reviewed', |
|
3840 | 3840 | } |
|
3841 | 3841 | for obj, reviewer, reasons, mandatory, st in |
|
3842 | 3842 | pull_request.reviewers_statuses() |
|
3843 | 3843 | ] |
|
3844 | 3844 | } |
|
3845 | 3845 | |
|
3846 | 3846 | return data |
|
3847 | 3847 | |
|
3848 | 3848 | def set_state(self, pull_request_state, final_state=None): |
|
3849 | 3849 | """ |
|
3850 | 3850 | # goes from initial state to updating to initial state. |
|
3851 | 3851 | # initial state can be changed by specifying back_state= |
|
3852 | 3852 | with pull_request_obj.set_state(PullRequest.STATE_UPDATING): |
|
3853 | 3853 | pull_request.merge() |
|
3854 | 3854 | |
|
3855 | 3855 | :param pull_request_state: |
|
3856 | 3856 | :param final_state: |
|
3857 | 3857 | |
|
3858 | 3858 | """ |
|
3859 | 3859 | |
|
3860 | 3860 | return _SetState(self, pull_request_state, back_state=final_state) |
|
3861 | 3861 | |
|
3862 | 3862 | |
|
3863 | 3863 | class PullRequest(Base, _PullRequestBase): |
|
3864 | 3864 | __tablename__ = 'pull_requests' |
|
3865 | 3865 | __table_args__ = ( |
|
3866 | 3866 | base_table_args, |
|
3867 | 3867 | ) |
|
3868 | 3868 | |
|
3869 | 3869 | pull_request_id = Column( |
|
3870 | 3870 | 'pull_request_id', Integer(), nullable=False, primary_key=True) |
|
3871 | 3871 | |
|
3872 | 3872 | def __repr__(self): |
|
3873 | 3873 | if self.pull_request_id: |
|
3874 | 3874 | return '<DB:PullRequest #%s>' % self.pull_request_id |
|
3875 | 3875 | else: |
|
3876 | 3876 | return '<DB:PullRequest at %#x>' % id(self) |
|
3877 | 3877 | |
|
3878 | 3878 | reviewers = relationship('PullRequestReviewers', |
|
3879 | 3879 | cascade="all, delete, delete-orphan") |
|
3880 | 3880 | statuses = relationship('ChangesetStatus', |
|
3881 | 3881 | cascade="all, delete, delete-orphan") |
|
3882 | 3882 | comments = relationship('ChangesetComment', |
|
3883 | 3883 | cascade="all, delete, delete-orphan") |
|
3884 | 3884 | versions = relationship('PullRequestVersion', |
|
3885 | 3885 | cascade="all, delete, delete-orphan", |
|
3886 | 3886 | lazy='dynamic') |
|
3887 | 3887 | |
|
3888 | 3888 | @classmethod |
|
3889 | 3889 | def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj, |
|
3890 | 3890 | internal_methods=None): |
|
3891 | 3891 | |
|
3892 | 3892 | class PullRequestDisplay(object): |
|
3893 | 3893 | """ |
|
3894 | 3894 | Special object wrapper for showing PullRequest data via Versions |
|
3895 | 3895 | It mimics PR object as close as possible. This is read only object |
|
3896 | 3896 | just for display |
|
3897 | 3897 | """ |
|
3898 | 3898 | |
|
3899 | 3899 | def __init__(self, attrs, internal=None): |
|
3900 | 3900 | self.attrs = attrs |
|
3901 | 3901 | # internal have priority over the given ones via attrs |
|
3902 | 3902 | self.internal = internal or ['versions'] |
|
3903 | 3903 | |
|
3904 | 3904 | def __getattr__(self, item): |
|
3905 | 3905 | if item in self.internal: |
|
3906 | 3906 | return getattr(self, item) |
|
3907 | 3907 | try: |
|
3908 | 3908 | return self.attrs[item] |
|
3909 | 3909 | except KeyError: |
|
3910 | 3910 | raise AttributeError( |
|
3911 | 3911 | '%s object has no attribute %s' % (self, item)) |
|
3912 | 3912 | |
|
3913 | 3913 | def __repr__(self): |
|
3914 | 3914 | return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id') |
|
3915 | 3915 | |
|
3916 | 3916 | def versions(self): |
|
3917 | 3917 | return pull_request_obj.versions.order_by( |
|
3918 | 3918 | PullRequestVersion.pull_request_version_id).all() |
|
3919 | 3919 | |
|
3920 | 3920 | def is_closed(self): |
|
3921 | 3921 | return pull_request_obj.is_closed() |
|
3922 | 3922 | |
|
3923 | 3923 | @property |
|
3924 | 3924 | def pull_request_version_id(self): |
|
3925 | 3925 | return getattr(pull_request_obj, 'pull_request_version_id', None) |
|
3926 | 3926 | |
|
3927 | 3927 | attrs = StrictAttributeDict(pull_request_obj.get_api_data()) |
|
3928 | 3928 | |
|
3929 | 3929 | attrs.author = StrictAttributeDict( |
|
3930 | 3930 | pull_request_obj.author.get_api_data()) |
|
3931 | 3931 | if pull_request_obj.target_repo: |
|
3932 | 3932 | attrs.target_repo = StrictAttributeDict( |
|
3933 | 3933 | pull_request_obj.target_repo.get_api_data()) |
|
3934 | 3934 | attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url |
|
3935 | 3935 | |
|
3936 | 3936 | if pull_request_obj.source_repo: |
|
3937 | 3937 | attrs.source_repo = StrictAttributeDict( |
|
3938 | 3938 | pull_request_obj.source_repo.get_api_data()) |
|
3939 | 3939 | attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url |
|
3940 | 3940 | |
|
3941 | 3941 | attrs.source_ref_parts = pull_request_obj.source_ref_parts |
|
3942 | 3942 | attrs.target_ref_parts = pull_request_obj.target_ref_parts |
|
3943 | 3943 | attrs.revisions = pull_request_obj.revisions |
|
3944 | 3944 | |
|
3945 | 3945 | attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref |
|
3946 | 3946 | attrs.reviewer_data = org_pull_request_obj.reviewer_data |
|
3947 | 3947 | attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json |
|
3948 | 3948 | |
|
3949 | 3949 | return PullRequestDisplay(attrs, internal=internal_methods) |
|
3950 | 3950 | |
|
3951 | 3951 | def is_closed(self): |
|
3952 | 3952 | return self.status == self.STATUS_CLOSED |
|
3953 | 3953 | |
|
3954 | 3954 | def __json__(self): |
|
3955 | 3955 | return { |
|
3956 | 3956 | 'revisions': self.revisions, |
|
3957 | 3957 | } |
|
3958 | 3958 | |
|
3959 | 3959 | def calculated_review_status(self): |
|
3960 | 3960 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
3961 | 3961 | return ChangesetStatusModel().calculated_review_status(self) |
|
3962 | 3962 | |
|
3963 | 3963 | def reviewers_statuses(self): |
|
3964 | 3964 | from rhodecode.model.changeset_status import ChangesetStatusModel |
|
3965 | 3965 | return ChangesetStatusModel().reviewers_statuses(self) |
|
3966 | 3966 | |
|
3967 | 3967 | @property |
|
3968 | 3968 | def workspace_id(self): |
|
3969 | 3969 | from rhodecode.model.pull_request import PullRequestModel |
|
3970 | 3970 | return PullRequestModel()._workspace_id(self) |
|
3971 | 3971 | |
|
3972 | 3972 | def get_shadow_repo(self): |
|
3973 | 3973 | workspace_id = self.workspace_id |
|
3974 | 3974 | vcs_obj = self.target_repo.scm_instance() |
|
3975 | 3975 | shadow_repository_path = vcs_obj._get_shadow_repository_path( |
|
3976 | 3976 | self.target_repo.repo_id, workspace_id) |
|
3977 | 3977 | if os.path.isdir(shadow_repository_path): |
|
3978 | 3978 | return vcs_obj._get_shadow_instance(shadow_repository_path) |
|
3979 | 3979 | |
|
3980 | 3980 | |
|
3981 | 3981 | class PullRequestVersion(Base, _PullRequestBase): |
|
3982 | 3982 | __tablename__ = 'pull_request_versions' |
|
3983 | 3983 | __table_args__ = ( |
|
3984 | 3984 | base_table_args, |
|
3985 | 3985 | ) |
|
3986 | 3986 | |
|
3987 | 3987 | pull_request_version_id = Column( |
|
3988 | 3988 | 'pull_request_version_id', Integer(), nullable=False, primary_key=True) |
|
3989 | 3989 | pull_request_id = Column( |
|
3990 | 3990 | 'pull_request_id', Integer(), |
|
3991 | 3991 | ForeignKey('pull_requests.pull_request_id'), nullable=False) |
|
3992 | 3992 | pull_request = relationship('PullRequest') |
|
3993 | 3993 | |
|
3994 | 3994 | def __repr__(self): |
|
3995 | 3995 | if self.pull_request_version_id: |
|
3996 | 3996 | return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id |
|
3997 | 3997 | else: |
|
3998 | 3998 | return '<DB:PullRequestVersion at %#x>' % id(self) |
|
3999 | 3999 | |
|
4000 | 4000 | @property |
|
4001 | 4001 | def reviewers(self): |
|
4002 | 4002 | return self.pull_request.reviewers |
|
4003 | 4003 | |
|
4004 | 4004 | @property |
|
4005 | 4005 | def versions(self): |
|
4006 | 4006 | return self.pull_request.versions |
|
4007 | 4007 | |
|
4008 | 4008 | def is_closed(self): |
|
4009 | 4009 | # calculate from original |
|
4010 | 4010 | return self.pull_request.status == self.STATUS_CLOSED |
|
4011 | 4011 | |
|
4012 | 4012 | def calculated_review_status(self): |
|
4013 | 4013 | return self.pull_request.calculated_review_status() |
|
4014 | 4014 | |
|
4015 | 4015 | def reviewers_statuses(self): |
|
4016 | 4016 | return self.pull_request.reviewers_statuses() |
|
4017 | 4017 | |
|
4018 | 4018 | |
|
4019 | 4019 | class PullRequestReviewers(Base, BaseModel): |
|
4020 | 4020 | __tablename__ = 'pull_request_reviewers' |
|
4021 | 4021 | __table_args__ = ( |
|
4022 | 4022 | base_table_args, |
|
4023 | 4023 | ) |
|
4024 | 4024 | |
|
4025 | 4025 | @hybrid_property |
|
4026 | 4026 | def reasons(self): |
|
4027 | 4027 | if not self._reasons: |
|
4028 | 4028 | return [] |
|
4029 | 4029 | return self._reasons |
|
4030 | 4030 | |
|
4031 | 4031 | @reasons.setter |
|
4032 | 4032 | def reasons(self, val): |
|
4033 | 4033 | val = val or [] |
|
4034 | 4034 | if any(not isinstance(x, basestring) for x in val): |
|
4035 | 4035 | raise Exception('invalid reasons type, must be list of strings') |
|
4036 | 4036 | self._reasons = val |
|
4037 | 4037 | |
|
4038 | 4038 | pull_requests_reviewers_id = Column( |
|
4039 | 4039 | 'pull_requests_reviewers_id', Integer(), nullable=False, |
|
4040 | 4040 | primary_key=True) |
|
4041 | 4041 | pull_request_id = Column( |
|
4042 | 4042 | "pull_request_id", Integer(), |
|
4043 | 4043 | ForeignKey('pull_requests.pull_request_id'), nullable=False) |
|
4044 | 4044 | user_id = Column( |
|
4045 | 4045 | "user_id", Integer(), ForeignKey('users.user_id'), nullable=True) |
|
4046 | 4046 | _reasons = Column( |
|
4047 | 4047 | 'reason', MutationList.as_mutable( |
|
4048 | 4048 | JsonType('list', dialect_map=dict(mysql=UnicodeText(16384))))) |
|
4049 | 4049 | |
|
4050 | 4050 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) |
|
4051 | 4051 | user = relationship('User') |
|
4052 | 4052 | pull_request = relationship('PullRequest') |
|
4053 | 4053 | |
|
4054 | 4054 | rule_data = Column( |
|
4055 | 4055 | 'rule_data_json', |
|
4056 | 4056 | JsonType(dialect_map=dict(mysql=UnicodeText(16384)))) |
|
4057 | 4057 | |
|
4058 | 4058 | def rule_user_group_data(self): |
|
4059 | 4059 | """ |
|
4060 | 4060 | Returns the voting user group rule data for this reviewer |
|
4061 | 4061 | """ |
|
4062 | 4062 | |
|
4063 | 4063 | if self.rule_data and 'vote_rule' in self.rule_data: |
|
4064 | 4064 | user_group_data = {} |
|
4065 | 4065 | if 'rule_user_group_entry_id' in self.rule_data: |
|
4066 | 4066 | # means a group with voting rules ! |
|
4067 | 4067 | user_group_data['id'] = self.rule_data['rule_user_group_entry_id'] |
|
4068 | 4068 | user_group_data['name'] = self.rule_data['rule_name'] |
|
4069 | 4069 | user_group_data['vote_rule'] = self.rule_data['vote_rule'] |
|
4070 | 4070 | |
|
4071 | 4071 | return user_group_data |
|
4072 | 4072 | |
|
4073 | 4073 | def __unicode__(self): |
|
4074 | 4074 | return u"<%s('id:%s')>" % (self.__class__.__name__, |
|
4075 | 4075 | self.pull_requests_reviewers_id) |
|
4076 | 4076 | |
|
4077 | 4077 | |
|
4078 | 4078 | class Notification(Base, BaseModel): |
|
4079 | 4079 | __tablename__ = 'notifications' |
|
4080 | 4080 | __table_args__ = ( |
|
4081 | 4081 | Index('notification_type_idx', 'type'), |
|
4082 | 4082 | base_table_args, |
|
4083 | 4083 | ) |
|
4084 | 4084 | |
|
4085 | 4085 | TYPE_CHANGESET_COMMENT = u'cs_comment' |
|
4086 | 4086 | TYPE_MESSAGE = u'message' |
|
4087 | 4087 | TYPE_MENTION = u'mention' |
|
4088 | 4088 | TYPE_REGISTRATION = u'registration' |
|
4089 | 4089 | TYPE_PULL_REQUEST = u'pull_request' |
|
4090 | 4090 | TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment' |
|
4091 | 4091 | |
|
4092 | 4092 | notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True) |
|
4093 | 4093 | subject = Column('subject', Unicode(512), nullable=True) |
|
4094 | 4094 | body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True) |
|
4095 | 4095 | created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True) |
|
4096 | 4096 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
4097 | 4097 | type_ = Column('type', Unicode(255)) |
|
4098 | 4098 | |
|
4099 | 4099 | created_by_user = relationship('User') |
|
4100 | 4100 | notifications_to_users = relationship('UserNotification', lazy='joined', |
|
4101 | 4101 | cascade="all, delete, delete-orphan") |
|
4102 | 4102 | |
|
4103 | 4103 | @property |
|
4104 | 4104 | def recipients(self): |
|
4105 | 4105 | return [x.user for x in UserNotification.query()\ |
|
4106 | 4106 | .filter(UserNotification.notification == self)\ |
|
4107 | 4107 | .order_by(UserNotification.user_id.asc()).all()] |
|
4108 | 4108 | |
|
4109 | 4109 | @classmethod |
|
4110 | 4110 | def create(cls, created_by, subject, body, recipients, type_=None): |
|
4111 | 4111 | if type_ is None: |
|
4112 | 4112 | type_ = Notification.TYPE_MESSAGE |
|
4113 | 4113 | |
|
4114 | 4114 | notification = cls() |
|
4115 | 4115 | notification.created_by_user = created_by |
|
4116 | 4116 | notification.subject = subject |
|
4117 | 4117 | notification.body = body |
|
4118 | 4118 | notification.type_ = type_ |
|
4119 | 4119 | notification.created_on = datetime.datetime.now() |
|
4120 | 4120 | |
|
4121 | 4121 | # For each recipient link the created notification to his account |
|
4122 | 4122 | for u in recipients: |
|
4123 | 4123 | assoc = UserNotification() |
|
4124 | 4124 | assoc.user_id = u.user_id |
|
4125 | 4125 | assoc.notification = notification |
|
4126 | 4126 | |
|
4127 | 4127 | # if created_by is inside recipients mark his notification |
|
4128 | 4128 | # as read |
|
4129 | 4129 | if u.user_id == created_by.user_id: |
|
4130 | 4130 | assoc.read = True |
|
4131 | 4131 | Session().add(assoc) |
|
4132 | 4132 | |
|
4133 | 4133 | Session().add(notification) |
|
4134 | 4134 | |
|
4135 | 4135 | return notification |
|
4136 | 4136 | |
|
4137 | 4137 | |
|
4138 | 4138 | class UserNotification(Base, BaseModel): |
|
4139 | 4139 | __tablename__ = 'user_to_notification' |
|
4140 | 4140 | __table_args__ = ( |
|
4141 | 4141 | UniqueConstraint('user_id', 'notification_id'), |
|
4142 | 4142 | base_table_args |
|
4143 | 4143 | ) |
|
4144 | 4144 | |
|
4145 | 4145 | user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) |
|
4146 | 4146 | notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True) |
|
4147 | 4147 | read = Column('read', Boolean, default=False) |
|
4148 | 4148 | sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None) |
|
4149 | 4149 | |
|
4150 | 4150 | user = relationship('User', lazy="joined") |
|
4151 | 4151 | notification = relationship('Notification', lazy="joined", |
|
4152 | 4152 | order_by=lambda: Notification.created_on.desc(),) |
|
4153 | 4153 | |
|
4154 | 4154 | def mark_as_read(self): |
|
4155 | 4155 | self.read = True |
|
4156 | 4156 | Session().add(self) |
|
4157 | 4157 | |
|
4158 | 4158 | |
|
4159 | 4159 | class Gist(Base, BaseModel): |
|
4160 | 4160 | __tablename__ = 'gists' |
|
4161 | 4161 | __table_args__ = ( |
|
4162 | 4162 | Index('g_gist_access_id_idx', 'gist_access_id'), |
|
4163 | 4163 | Index('g_created_on_idx', 'created_on'), |
|
4164 | 4164 | base_table_args |
|
4165 | 4165 | ) |
|
4166 | 4166 | |
|
4167 | 4167 | GIST_PUBLIC = u'public' |
|
4168 | 4168 | GIST_PRIVATE = u'private' |
|
4169 | 4169 | DEFAULT_FILENAME = u'gistfile1.txt' |
|
4170 | 4170 | |
|
4171 | 4171 | ACL_LEVEL_PUBLIC = u'acl_public' |
|
4172 | 4172 | ACL_LEVEL_PRIVATE = u'acl_private' |
|
4173 | 4173 | |
|
4174 | 4174 | gist_id = Column('gist_id', Integer(), primary_key=True) |
|
4175 | 4175 | gist_access_id = Column('gist_access_id', Unicode(250)) |
|
4176 | 4176 | gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql')) |
|
4177 | 4177 | gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True) |
|
4178 | 4178 | gist_expires = Column('gist_expires', Float(53), nullable=False) |
|
4179 | 4179 | gist_type = Column('gist_type', Unicode(128), nullable=False) |
|
4180 | 4180 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
4181 | 4181 | modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
4182 | 4182 | acl_level = Column('acl_level', Unicode(128), nullable=True) |
|
4183 | 4183 | |
|
4184 | 4184 | owner = relationship('User') |
|
4185 | 4185 | |
|
4186 | 4186 | def __repr__(self): |
|
4187 | 4187 | return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id) |
|
4188 | 4188 | |
|
4189 | 4189 | @hybrid_property |
|
4190 | 4190 | def description_safe(self): |
|
4191 | 4191 | from rhodecode.lib import helpers as h |
|
4192 | 4192 | return h.escape(self.gist_description) |
|
4193 | 4193 | |
|
4194 | 4194 | @classmethod |
|
4195 | 4195 | def get_or_404(cls, id_): |
|
4196 | 4196 | from pyramid.httpexceptions import HTTPNotFound |
|
4197 | 4197 | |
|
4198 | 4198 | res = cls.query().filter(cls.gist_access_id == id_).scalar() |
|
4199 | 4199 | if not res: |
|
4200 | 4200 | raise HTTPNotFound() |
|
4201 | 4201 | return res |
|
4202 | 4202 | |
|
4203 | 4203 | @classmethod |
|
4204 | 4204 | def get_by_access_id(cls, gist_access_id): |
|
4205 | 4205 | return cls.query().filter(cls.gist_access_id == gist_access_id).scalar() |
|
4206 | 4206 | |
|
4207 | 4207 | def gist_url(self): |
|
4208 | 4208 | from rhodecode.model.gist import GistModel |
|
4209 | 4209 | return GistModel().get_url(self) |
|
4210 | 4210 | |
|
4211 | 4211 | @classmethod |
|
4212 | 4212 | def base_path(cls): |
|
4213 | 4213 | """ |
|
4214 | 4214 | Returns base path when all gists are stored |
|
4215 | 4215 | |
|
4216 | 4216 | :param cls: |
|
4217 | 4217 | """ |
|
4218 | 4218 | from rhodecode.model.gist import GIST_STORE_LOC |
|
4219 | 4219 | q = Session().query(RhodeCodeUi)\ |
|
4220 | 4220 | .filter(RhodeCodeUi.ui_key == URL_SEP) |
|
4221 | 4221 | q = q.options(FromCache("sql_cache_short", "repository_repo_path")) |
|
4222 | 4222 | return os.path.join(q.one().ui_value, GIST_STORE_LOC) |
|
4223 | 4223 | |
|
4224 | 4224 | def get_api_data(self): |
|
4225 | 4225 | """ |
|
4226 | 4226 | Common function for generating gist related data for API |
|
4227 | 4227 | """ |
|
4228 | 4228 | gist = self |
|
4229 | 4229 | data = { |
|
4230 | 4230 | 'gist_id': gist.gist_id, |
|
4231 | 4231 | 'type': gist.gist_type, |
|
4232 | 4232 | 'access_id': gist.gist_access_id, |
|
4233 | 4233 | 'description': gist.gist_description, |
|
4234 | 4234 | 'url': gist.gist_url(), |
|
4235 | 4235 | 'expires': gist.gist_expires, |
|
4236 | 4236 | 'created_on': gist.created_on, |
|
4237 | 4237 | 'modified_at': gist.modified_at, |
|
4238 | 4238 | 'content': None, |
|
4239 | 4239 | 'acl_level': gist.acl_level, |
|
4240 | 4240 | } |
|
4241 | 4241 | return data |
|
4242 | 4242 | |
|
4243 | 4243 | def __json__(self): |
|
4244 | 4244 | data = dict( |
|
4245 | 4245 | ) |
|
4246 | 4246 | data.update(self.get_api_data()) |
|
4247 | 4247 | return data |
|
4248 | 4248 | # SCM functions |
|
4249 | 4249 | |
|
4250 | 4250 | def scm_instance(self, **kwargs): |
|
4251 | 4251 | full_repo_path = os.path.join(self.base_path(), self.gist_access_id) |
|
4252 | 4252 | return get_vcs_instance( |
|
4253 | 4253 | repo_path=safe_str(full_repo_path), create=False) |
|
4254 | 4254 | |
|
4255 | 4255 | |
|
4256 | 4256 | class ExternalIdentity(Base, BaseModel): |
|
4257 | 4257 | __tablename__ = 'external_identities' |
|
4258 | 4258 | __table_args__ = ( |
|
4259 | 4259 | Index('local_user_id_idx', 'local_user_id'), |
|
4260 | 4260 | Index('external_id_idx', 'external_id'), |
|
4261 | 4261 | base_table_args |
|
4262 | 4262 | ) |
|
4263 | 4263 | |
|
4264 | 4264 | external_id = Column('external_id', Unicode(255), default=u'', primary_key=True) |
|
4265 | 4265 | external_username = Column('external_username', Unicode(1024), default=u'') |
|
4266 | 4266 | local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True) |
|
4267 | 4267 | provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True) |
|
4268 | 4268 | access_token = Column('access_token', String(1024), default=u'') |
|
4269 | 4269 | alt_token = Column('alt_token', String(1024), default=u'') |
|
4270 | 4270 | token_secret = Column('token_secret', String(1024), default=u'') |
|
4271 | 4271 | |
|
4272 | 4272 | @classmethod |
|
4273 | 4273 | def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None): |
|
4274 | 4274 | """ |
|
4275 | 4275 | Returns ExternalIdentity instance based on search params |
|
4276 | 4276 | |
|
4277 | 4277 | :param external_id: |
|
4278 | 4278 | :param provider_name: |
|
4279 | 4279 | :return: ExternalIdentity |
|
4280 | 4280 | """ |
|
4281 | 4281 | query = cls.query() |
|
4282 | 4282 | query = query.filter(cls.external_id == external_id) |
|
4283 | 4283 | query = query.filter(cls.provider_name == provider_name) |
|
4284 | 4284 | if local_user_id: |
|
4285 | 4285 | query = query.filter(cls.local_user_id == local_user_id) |
|
4286 | 4286 | return query.first() |
|
4287 | 4287 | |
|
4288 | 4288 | @classmethod |
|
4289 | 4289 | def user_by_external_id_and_provider(cls, external_id, provider_name): |
|
4290 | 4290 | """ |
|
4291 | 4291 | Returns User instance based on search params |
|
4292 | 4292 | |
|
4293 | 4293 | :param external_id: |
|
4294 | 4294 | :param provider_name: |
|
4295 | 4295 | :return: User |
|
4296 | 4296 | """ |
|
4297 | 4297 | query = User.query() |
|
4298 | 4298 | query = query.filter(cls.external_id == external_id) |
|
4299 | 4299 | query = query.filter(cls.provider_name == provider_name) |
|
4300 | 4300 | query = query.filter(User.user_id == cls.local_user_id) |
|
4301 | 4301 | return query.first() |
|
4302 | 4302 | |
|
4303 | 4303 | @classmethod |
|
4304 | 4304 | def by_local_user_id(cls, local_user_id): |
|
4305 | 4305 | """ |
|
4306 | 4306 | Returns all tokens for user |
|
4307 | 4307 | |
|
4308 | 4308 | :param local_user_id: |
|
4309 | 4309 | :return: ExternalIdentity |
|
4310 | 4310 | """ |
|
4311 | 4311 | query = cls.query() |
|
4312 | 4312 | query = query.filter(cls.local_user_id == local_user_id) |
|
4313 | 4313 | return query |
|
4314 | 4314 | |
|
4315 | 4315 | @classmethod |
|
4316 | 4316 | def load_provider_plugin(cls, plugin_id): |
|
4317 | 4317 | from rhodecode.authentication.base import loadplugin |
|
4318 | 4318 | _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id) |
|
4319 | 4319 | auth_plugin = loadplugin(_plugin_id) |
|
4320 | 4320 | return auth_plugin |
|
4321 | 4321 | |
|
4322 | 4322 | |
|
4323 | 4323 | class Integration(Base, BaseModel): |
|
4324 | 4324 | __tablename__ = 'integrations' |
|
4325 | 4325 | __table_args__ = ( |
|
4326 | 4326 | base_table_args |
|
4327 | 4327 | ) |
|
4328 | 4328 | |
|
4329 | 4329 | integration_id = Column('integration_id', Integer(), primary_key=True) |
|
4330 | 4330 | integration_type = Column('integration_type', String(255)) |
|
4331 | 4331 | enabled = Column('enabled', Boolean(), nullable=False) |
|
4332 | 4332 | name = Column('name', String(255), nullable=False) |
|
4333 | 4333 | child_repos_only = Column('child_repos_only', Boolean(), nullable=False, |
|
4334 | 4334 | default=False) |
|
4335 | 4335 | |
|
4336 | 4336 | settings = Column( |
|
4337 | 4337 | 'settings_json', MutationObj.as_mutable( |
|
4338 | 4338 | JsonType(dialect_map=dict(mysql=UnicodeText(16384))))) |
|
4339 | 4339 | repo_id = Column( |
|
4340 | 4340 | 'repo_id', Integer(), ForeignKey('repositories.repo_id'), |
|
4341 | 4341 | nullable=True, unique=None, default=None) |
|
4342 | 4342 | repo = relationship('Repository', lazy='joined') |
|
4343 | 4343 | |
|
4344 | 4344 | repo_group_id = Column( |
|
4345 | 4345 | 'repo_group_id', Integer(), ForeignKey('groups.group_id'), |
|
4346 | 4346 | nullable=True, unique=None, default=None) |
|
4347 | 4347 | repo_group = relationship('RepoGroup', lazy='joined') |
|
4348 | 4348 | |
|
4349 | 4349 | @property |
|
4350 | 4350 | def scope(self): |
|
4351 | 4351 | if self.repo: |
|
4352 | 4352 | return repr(self.repo) |
|
4353 | 4353 | if self.repo_group: |
|
4354 | 4354 | if self.child_repos_only: |
|
4355 | 4355 | return repr(self.repo_group) + ' (child repos only)' |
|
4356 | 4356 | else: |
|
4357 | 4357 | return repr(self.repo_group) + ' (recursive)' |
|
4358 | 4358 | if self.child_repos_only: |
|
4359 | 4359 | return 'root_repos' |
|
4360 | 4360 | return 'global' |
|
4361 | 4361 | |
|
4362 | 4362 | def __repr__(self): |
|
4363 | 4363 | return '<Integration(%r, %r)>' % (self.integration_type, self.scope) |
|
4364 | 4364 | |
|
4365 | 4365 | |
|
4366 | 4366 | class RepoReviewRuleUser(Base, BaseModel): |
|
4367 | 4367 | __tablename__ = 'repo_review_rules_users' |
|
4368 | 4368 | __table_args__ = ( |
|
4369 | 4369 | base_table_args |
|
4370 | 4370 | ) |
|
4371 | 4371 | |
|
4372 | 4372 | repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True) |
|
4373 | 4373 | repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) |
|
4374 | 4374 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False) |
|
4375 | 4375 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) |
|
4376 | 4376 | user = relationship('User') |
|
4377 | 4377 | |
|
4378 | 4378 | def rule_data(self): |
|
4379 | 4379 | return { |
|
4380 | 4380 | 'mandatory': self.mandatory |
|
4381 | 4381 | } |
|
4382 | 4382 | |
|
4383 | 4383 | |
|
4384 | 4384 | class RepoReviewRuleUserGroup(Base, BaseModel): |
|
4385 | 4385 | __tablename__ = 'repo_review_rules_users_groups' |
|
4386 | 4386 | __table_args__ = ( |
|
4387 | 4387 | base_table_args |
|
4388 | 4388 | ) |
|
4389 | 4389 | |
|
4390 | 4390 | VOTE_RULE_ALL = -1 |
|
4391 | 4391 | |
|
4392 | 4392 | repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True) |
|
4393 | 4393 | repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id')) |
|
4394 | 4394 | users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False) |
|
4395 | 4395 | mandatory = Column("mandatory", Boolean(), nullable=False, default=False) |
|
4396 | 4396 | vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL) |
|
4397 | 4397 | users_group = relationship('UserGroup') |
|
4398 | 4398 | |
|
4399 | 4399 | def rule_data(self): |
|
4400 | 4400 | return { |
|
4401 | 4401 | 'mandatory': self.mandatory, |
|
4402 | 4402 | 'vote_rule': self.vote_rule |
|
4403 | 4403 | } |
|
4404 | 4404 | |
|
4405 | 4405 | @property |
|
4406 | 4406 | def vote_rule_label(self): |
|
4407 | 4407 | if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL: |
|
4408 | 4408 | return 'all must vote' |
|
4409 | 4409 | else: |
|
4410 | 4410 | return 'min. vote {}'.format(self.vote_rule) |
|
4411 | 4411 | |
|
4412 | 4412 | |
|
4413 | 4413 | class RepoReviewRule(Base, BaseModel): |
|
4414 | 4414 | __tablename__ = 'repo_review_rules' |
|
4415 | 4415 | __table_args__ = ( |
|
4416 | 4416 | base_table_args |
|
4417 | 4417 | ) |
|
4418 | 4418 | |
|
4419 | 4419 | repo_review_rule_id = Column( |
|
4420 | 4420 | 'repo_review_rule_id', Integer(), primary_key=True) |
|
4421 | 4421 | repo_id = Column( |
|
4422 | 4422 | "repo_id", Integer(), ForeignKey('repositories.repo_id')) |
|
4423 | 4423 | repo = relationship('Repository', backref='review_rules') |
|
4424 | 4424 | |
|
4425 | 4425 | review_rule_name = Column('review_rule_name', String(255)) |
|
4426 | 4426 | _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob |
|
4427 | 4427 | _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob |
|
4428 | 4428 | _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob |
|
4429 | 4429 | |
|
4430 | 4430 | use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False) |
|
4431 | 4431 | forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False) |
|
4432 | 4432 | forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False) |
|
4433 | 4433 | forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False) |
|
4434 | 4434 | |
|
4435 | 4435 | rule_users = relationship('RepoReviewRuleUser') |
|
4436 | 4436 | rule_user_groups = relationship('RepoReviewRuleUserGroup') |
|
4437 | 4437 | |
|
4438 | 4438 | def _validate_pattern(self, value): |
|
4439 | 4439 | re.compile('^' + glob2re(value) + '$') |
|
4440 | 4440 | |
|
4441 | 4441 | @hybrid_property |
|
4442 | 4442 | def source_branch_pattern(self): |
|
4443 | 4443 | return self._branch_pattern or '*' |
|
4444 | 4444 | |
|
4445 | 4445 | @source_branch_pattern.setter |
|
4446 | 4446 | def source_branch_pattern(self, value): |
|
4447 | 4447 | self._validate_pattern(value) |
|
4448 | 4448 | self._branch_pattern = value or '*' |
|
4449 | 4449 | |
|
4450 | 4450 | @hybrid_property |
|
4451 | 4451 | def target_branch_pattern(self): |
|
4452 | 4452 | return self._target_branch_pattern or '*' |
|
4453 | 4453 | |
|
4454 | 4454 | @target_branch_pattern.setter |
|
4455 | 4455 | def target_branch_pattern(self, value): |
|
4456 | 4456 | self._validate_pattern(value) |
|
4457 | 4457 | self._target_branch_pattern = value or '*' |
|
4458 | 4458 | |
|
4459 | 4459 | @hybrid_property |
|
4460 | 4460 | def file_pattern(self): |
|
4461 | 4461 | return self._file_pattern or '*' |
|
4462 | 4462 | |
|
4463 | 4463 | @file_pattern.setter |
|
4464 | 4464 | def file_pattern(self, value): |
|
4465 | 4465 | self._validate_pattern(value) |
|
4466 | 4466 | self._file_pattern = value or '*' |
|
4467 | 4467 | |
|
4468 | 4468 | def matches(self, source_branch, target_branch, files_changed): |
|
4469 | 4469 | """ |
|
4470 | 4470 | Check if this review rule matches a branch/files in a pull request |
|
4471 | 4471 | |
|
4472 | 4472 | :param source_branch: source branch name for the commit |
|
4473 | 4473 | :param target_branch: target branch name for the commit |
|
4474 | 4474 | :param files_changed: list of file paths changed in the pull request |
|
4475 | 4475 | """ |
|
4476 | 4476 | |
|
4477 | 4477 | source_branch = source_branch or '' |
|
4478 | 4478 | target_branch = target_branch or '' |
|
4479 | 4479 | files_changed = files_changed or [] |
|
4480 | 4480 | |
|
4481 | 4481 | branch_matches = True |
|
4482 | 4482 | if source_branch or target_branch: |
|
4483 | 4483 | if self.source_branch_pattern == '*': |
|
4484 | 4484 | source_branch_match = True |
|
4485 | 4485 | else: |
|
4486 | 4486 | if self.source_branch_pattern.startswith('re:'): |
|
4487 | 4487 | source_pattern = self.source_branch_pattern[3:] |
|
4488 | 4488 | else: |
|
4489 | 4489 | source_pattern = '^' + glob2re(self.source_branch_pattern) + '$' |
|
4490 | 4490 | source_branch_regex = re.compile(source_pattern) |
|
4491 | 4491 | source_branch_match = bool(source_branch_regex.search(source_branch)) |
|
4492 | 4492 | if self.target_branch_pattern == '*': |
|
4493 | 4493 | target_branch_match = True |
|
4494 | 4494 | else: |
|
4495 | 4495 | if self.target_branch_pattern.startswith('re:'): |
|
4496 | 4496 | target_pattern = self.target_branch_pattern[3:] |
|
4497 | 4497 | else: |
|
4498 | 4498 | target_pattern = '^' + glob2re(self.target_branch_pattern) + '$' |
|
4499 | 4499 | target_branch_regex = re.compile(target_pattern) |
|
4500 | 4500 | target_branch_match = bool(target_branch_regex.search(target_branch)) |
|
4501 | 4501 | |
|
4502 | 4502 | branch_matches = source_branch_match and target_branch_match |
|
4503 | 4503 | |
|
4504 | 4504 | files_matches = True |
|
4505 | 4505 | if self.file_pattern != '*': |
|
4506 | 4506 | files_matches = False |
|
4507 | 4507 | if self.file_pattern.startswith('re:'): |
|
4508 | 4508 | file_pattern = self.file_pattern[3:] |
|
4509 | 4509 | else: |
|
4510 | 4510 | file_pattern = glob2re(self.file_pattern) |
|
4511 | 4511 | file_regex = re.compile(file_pattern) |
|
4512 | 4512 | for filename in files_changed: |
|
4513 | 4513 | if file_regex.search(filename): |
|
4514 | 4514 | files_matches = True |
|
4515 | 4515 | break |
|
4516 | 4516 | |
|
4517 | 4517 | return branch_matches and files_matches |
|
4518 | 4518 | |
|
4519 | 4519 | @property |
|
4520 | 4520 | def review_users(self): |
|
4521 | 4521 | """ Returns the users which this rule applies to """ |
|
4522 | 4522 | |
|
4523 | 4523 | users = collections.OrderedDict() |
|
4524 | 4524 | |
|
4525 | 4525 | for rule_user in self.rule_users: |
|
4526 | 4526 | if rule_user.user.active: |
|
4527 | 4527 | if rule_user.user not in users: |
|
4528 | 4528 | users[rule_user.user.username] = { |
|
4529 | 4529 | 'user': rule_user.user, |
|
4530 | 4530 | 'source': 'user', |
|
4531 | 4531 | 'source_data': {}, |
|
4532 | 4532 | 'data': rule_user.rule_data() |
|
4533 | 4533 | } |
|
4534 | 4534 | |
|
4535 | 4535 | for rule_user_group in self.rule_user_groups: |
|
4536 | 4536 | source_data = { |
|
4537 | 4537 | 'user_group_id': rule_user_group.users_group.users_group_id, |
|
4538 | 4538 | 'name': rule_user_group.users_group.users_group_name, |
|
4539 | 4539 | 'members': len(rule_user_group.users_group.members) |
|
4540 | 4540 | } |
|
4541 | 4541 | for member in rule_user_group.users_group.members: |
|
4542 | 4542 | if member.user.active: |
|
4543 | 4543 | key = member.user.username |
|
4544 | 4544 | if key in users: |
|
4545 | 4545 | # skip this member as we have him already |
|
4546 | 4546 | # this prevents from override the "first" matched |
|
4547 | 4547 | # users with duplicates in multiple groups |
|
4548 | 4548 | continue |
|
4549 | 4549 | |
|
4550 | 4550 | users[key] = { |
|
4551 | 4551 | 'user': member.user, |
|
4552 | 4552 | 'source': 'user_group', |
|
4553 | 4553 | 'source_data': source_data, |
|
4554 | 4554 | 'data': rule_user_group.rule_data() |
|
4555 | 4555 | } |
|
4556 | 4556 | |
|
4557 | 4557 | return users |
|
4558 | 4558 | |
|
4559 | 4559 | def user_group_vote_rule(self, user_id): |
|
4560 | 4560 | |
|
4561 | 4561 | rules = [] |
|
4562 | 4562 | if not self.rule_user_groups: |
|
4563 | 4563 | return rules |
|
4564 | 4564 | |
|
4565 | 4565 | for user_group in self.rule_user_groups: |
|
4566 | 4566 | user_group_members = [x.user_id for x in user_group.users_group.members] |
|
4567 | 4567 | if user_id in user_group_members: |
|
4568 | 4568 | rules.append(user_group) |
|
4569 | 4569 | return rules |
|
4570 | 4570 | |
|
4571 | 4571 | def __repr__(self): |
|
4572 | 4572 | return '<RepoReviewerRule(id=%r, repo=%r)>' % ( |
|
4573 | 4573 | self.repo_review_rule_id, self.repo) |
|
4574 | 4574 | |
|
4575 | 4575 | |
|
4576 | 4576 | class ScheduleEntry(Base, BaseModel): |
|
4577 | 4577 | __tablename__ = 'schedule_entries' |
|
4578 | 4578 | __table_args__ = ( |
|
4579 | 4579 | UniqueConstraint('schedule_name', name='s_schedule_name_idx'), |
|
4580 | 4580 | UniqueConstraint('task_uid', name='s_task_uid_idx'), |
|
4581 | 4581 | base_table_args, |
|
4582 | 4582 | ) |
|
4583 | 4583 | |
|
4584 | 4584 | schedule_types = ['crontab', 'timedelta', 'integer'] |
|
4585 | 4585 | schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True) |
|
4586 | 4586 | |
|
4587 | 4587 | schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None) |
|
4588 | 4588 | schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None) |
|
4589 | 4589 | schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True) |
|
4590 | 4590 | |
|
4591 | 4591 | _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None) |
|
4592 | 4592 | schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT())))) |
|
4593 | 4593 | |
|
4594 | 4594 | schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None) |
|
4595 | 4595 | schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0) |
|
4596 | 4596 | |
|
4597 | 4597 | # task |
|
4598 | 4598 | task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None) |
|
4599 | 4599 | task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None) |
|
4600 | 4600 | task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT())))) |
|
4601 | 4601 | task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT())))) |
|
4602 | 4602 | |
|
4603 | 4603 | created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now) |
|
4604 | 4604 | updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None) |
|
4605 | 4605 | |
|
4606 | 4606 | @hybrid_property |
|
4607 | 4607 | def schedule_type(self): |
|
4608 | 4608 | return self._schedule_type |
|
4609 | 4609 | |
|
4610 | 4610 | @schedule_type.setter |
|
4611 | 4611 | def schedule_type(self, val): |
|
4612 | 4612 | if val not in self.schedule_types: |
|
4613 | 4613 | raise ValueError('Value must be on of `{}` and got `{}`'.format( |
|
4614 | 4614 | val, self.schedule_type)) |
|
4615 | 4615 | |
|
4616 | 4616 | self._schedule_type = val |
|
4617 | 4617 | |
|
4618 | 4618 | @classmethod |
|
4619 | 4619 | def get_uid(cls, obj): |
|
4620 | 4620 | args = obj.task_args |
|
4621 | 4621 | kwargs = obj.task_kwargs |
|
4622 | 4622 | if isinstance(args, JsonRaw): |
|
4623 | 4623 | try: |
|
4624 | 4624 | args = json.loads(args) |
|
4625 | 4625 | except ValueError: |
|
4626 | 4626 | args = tuple() |
|
4627 | 4627 | |
|
4628 | 4628 | if isinstance(kwargs, JsonRaw): |
|
4629 | 4629 | try: |
|
4630 | 4630 | kwargs = json.loads(kwargs) |
|
4631 | 4631 | except ValueError: |
|
4632 | 4632 | kwargs = dict() |
|
4633 | 4633 | |
|
4634 | 4634 | dot_notation = obj.task_dot_notation |
|
4635 | 4635 | val = '.'.join(map(safe_str, [ |
|
4636 | 4636 | sorted(dot_notation), args, sorted(kwargs.items())])) |
|
4637 | 4637 | return hashlib.sha1(val).hexdigest() |
|
4638 | 4638 | |
|
4639 | 4639 | @classmethod |
|
4640 | 4640 | def get_by_schedule_name(cls, schedule_name): |
|
4641 | 4641 | return cls.query().filter(cls.schedule_name == schedule_name).scalar() |
|
4642 | 4642 | |
|
4643 | 4643 | @classmethod |
|
4644 | 4644 | def get_by_schedule_id(cls, schedule_id): |
|
4645 | 4645 | return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar() |
|
4646 | 4646 | |
|
4647 | 4647 | @property |
|
4648 | 4648 | def task(self): |
|
4649 | 4649 | return self.task_dot_notation |
|
4650 | 4650 | |
|
4651 | 4651 | @property |
|
4652 | 4652 | def schedule(self): |
|
4653 | 4653 | from rhodecode.lib.celerylib.utils import raw_2_schedule |
|
4654 | 4654 | schedule = raw_2_schedule(self.schedule_definition, self.schedule_type) |
|
4655 | 4655 | return schedule |
|
4656 | 4656 | |
|
4657 | 4657 | @property |
|
4658 | 4658 | def args(self): |
|
4659 | 4659 | try: |
|
4660 | 4660 | return list(self.task_args or []) |
|
4661 | 4661 | except ValueError: |
|
4662 | 4662 | return list() |
|
4663 | 4663 | |
|
4664 | 4664 | @property |
|
4665 | 4665 | def kwargs(self): |
|
4666 | 4666 | try: |
|
4667 | 4667 | return dict(self.task_kwargs or {}) |
|
4668 | 4668 | except ValueError: |
|
4669 | 4669 | return dict() |
|
4670 | 4670 | |
|
4671 | 4671 | def _as_raw(self, val): |
|
4672 | 4672 | if hasattr(val, 'de_coerce'): |
|
4673 | 4673 | val = val.de_coerce() |
|
4674 | 4674 | if val: |
|
4675 | 4675 | val = json.dumps(val) |
|
4676 | 4676 | |
|
4677 | 4677 | return val |
|
4678 | 4678 | |
|
4679 | 4679 | @property |
|
4680 | 4680 | def schedule_definition_raw(self): |
|
4681 | 4681 | return self._as_raw(self.schedule_definition) |
|
4682 | 4682 | |
|
4683 | 4683 | @property |
|
4684 | 4684 | def args_raw(self): |
|
4685 | 4685 | return self._as_raw(self.task_args) |
|
4686 | 4686 | |
|
4687 | 4687 | @property |
|
4688 | 4688 | def kwargs_raw(self): |
|
4689 | 4689 | return self._as_raw(self.task_kwargs) |
|
4690 | 4690 | |
|
4691 | 4691 | def __repr__(self): |
|
4692 | 4692 | return '<DB:ScheduleEntry({}:{})>'.format( |
|
4693 | 4693 | self.schedule_entry_id, self.schedule_name) |
|
4694 | 4694 | |
|
4695 | 4695 | |
|
4696 | 4696 | @event.listens_for(ScheduleEntry, 'before_update') |
|
4697 | 4697 | def update_task_uid(mapper, connection, target): |
|
4698 | 4698 | target.task_uid = ScheduleEntry.get_uid(target) |
|
4699 | 4699 | |
|
4700 | 4700 | |
|
4701 | 4701 | @event.listens_for(ScheduleEntry, 'before_insert') |
|
4702 | 4702 | def set_task_uid(mapper, connection, target): |
|
4703 | 4703 | target.task_uid = ScheduleEntry.get_uid(target) |
|
4704 | 4704 | |
|
4705 | 4705 | |
|
4706 | 4706 | class _BaseBranchPerms(BaseModel): |
|
4707 | 4707 | @classmethod |
|
4708 | 4708 | def compute_hash(cls, value): |
|
4709 | 4709 | return sha1_safe(value) |
|
4710 | 4710 | |
|
4711 | 4711 | @hybrid_property |
|
4712 | 4712 | def branch_pattern(self): |
|
4713 | 4713 | return self._branch_pattern or '*' |
|
4714 | 4714 | |
|
4715 | 4715 | @hybrid_property |
|
4716 | 4716 | def branch_hash(self): |
|
4717 | 4717 | return self._branch_hash |
|
4718 | 4718 | |
|
4719 | 4719 | def _validate_glob(self, value): |
|
4720 | 4720 | re.compile('^' + glob2re(value) + '$') |
|
4721 | 4721 | |
|
4722 | 4722 | @branch_pattern.setter |
|
4723 | 4723 | def branch_pattern(self, value): |
|
4724 | 4724 | self._validate_glob(value) |
|
4725 | 4725 | self._branch_pattern = value or '*' |
|
4726 | 4726 | # set the Hash when setting the branch pattern |
|
4727 | 4727 | self._branch_hash = self.compute_hash(self._branch_pattern) |
|
4728 | 4728 | |
|
4729 | 4729 | def matches(self, branch): |
|
4730 | 4730 | """ |
|
4731 | 4731 | Check if this the branch matches entry |
|
4732 | 4732 | |
|
4733 | 4733 | :param branch: branch name for the commit |
|
4734 | 4734 | """ |
|
4735 | 4735 | |
|
4736 | 4736 | branch = branch or '' |
|
4737 | 4737 | |
|
4738 | 4738 | branch_matches = True |
|
4739 | 4739 | if branch: |
|
4740 | 4740 | branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$') |
|
4741 | 4741 | branch_matches = bool(branch_regex.search(branch)) |
|
4742 | 4742 | |
|
4743 | 4743 | return branch_matches |
|
4744 | 4744 | |
|
4745 | 4745 | |
|
4746 | 4746 | class UserToRepoBranchPermission(Base, _BaseBranchPerms): |
|
4747 | 4747 | __tablename__ = 'user_to_repo_branch_permissions' |
|
4748 | 4748 | __table_args__ = ( |
|
4749 | 4749 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
4750 | 4750 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} |
|
4751 | 4751 | ) |
|
4752 | 4752 | |
|
4753 | 4753 | branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True) |
|
4754 | 4754 | |
|
4755 | 4755 | repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) |
|
4756 | 4756 | repo = relationship('Repository', backref='user_branch_perms') |
|
4757 | 4757 | |
|
4758 | 4758 | permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
4759 | 4759 | permission = relationship('Permission') |
|
4760 | 4760 | |
|
4761 | 4761 | rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None) |
|
4762 | 4762 | user_repo_to_perm = relationship('UserRepoToPerm') |
|
4763 | 4763 | |
|
4764 | 4764 | rule_order = Column('rule_order', Integer(), nullable=False) |
|
4765 | 4765 | _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob |
|
4766 | 4766 | _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql')) |
|
4767 | 4767 | |
|
4768 | 4768 | def __unicode__(self): |
|
4769 | 4769 | return u'<UserBranchPermission(%s => %r)>' % ( |
|
4770 | 4770 | self.user_repo_to_perm, self.branch_pattern) |
|
4771 | 4771 | |
|
4772 | 4772 | |
|
4773 | 4773 | class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms): |
|
4774 | 4774 | __tablename__ = 'user_group_to_repo_branch_permissions' |
|
4775 | 4775 | __table_args__ = ( |
|
4776 | 4776 | {'extend_existing': True, 'mysql_engine': 'InnoDB', |
|
4777 | 4777 | 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,} |
|
4778 | 4778 | ) |
|
4779 | 4779 | |
|
4780 | 4780 | branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True) |
|
4781 | 4781 | |
|
4782 | 4782 | repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None) |
|
4783 | 4783 | repo = relationship('Repository', backref='user_group_branch_perms') |
|
4784 | 4784 | |
|
4785 | 4785 | permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None) |
|
4786 | 4786 | permission = relationship('Permission') |
|
4787 | 4787 | |
|
4788 | 4788 | rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None) |
|
4789 | 4789 | user_group_repo_to_perm = relationship('UserGroupRepoToPerm') |
|
4790 | 4790 | |
|
4791 | 4791 | rule_order = Column('rule_order', Integer(), nullable=False) |
|
4792 | 4792 | _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob |
|
4793 | 4793 | _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql')) |
|
4794 | 4794 | |
|
4795 | 4795 | def __unicode__(self): |
|
4796 | 4796 | return u'<UserBranchPermission(%s => %r)>' % ( |
|
4797 | 4797 | self.user_group_repo_to_perm, self.branch_pattern) |
|
4798 | 4798 | |
|
4799 | 4799 | |
|
4800 | class UserBookmark(Base, BaseModel): | |
|
4801 | __tablename__ = 'user_bookmarks' | |
|
4802 | __table_args__ = ( | |
|
4803 | UniqueConstraint('user_id', 'bookmark_repo_id'), | |
|
4804 | UniqueConstraint('user_id', 'bookmark_repo_group_id'), | |
|
4805 | UniqueConstraint('user_id', 'bookmark_position'), | |
|
4806 | base_table_args | |
|
4807 | ) | |
|
4808 | ||
|
4809 | user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) | |
|
4810 | user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) | |
|
4811 | position = Column("bookmark_position", Integer(), nullable=False) | |
|
4812 | title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None) | |
|
4813 | redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None) | |
|
4814 | created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now) | |
|
4815 | ||
|
4816 | bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None) | |
|
4817 | bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None) | |
|
4818 | ||
|
4819 | user = relationship("User") | |
|
4820 | ||
|
4821 | repository = relationship("Repository") | |
|
4822 | repository_group = relationship("RepoGroup") | |
|
4823 | ||
|
4824 | @classmethod | |
|
4825 | def get_by_position_for_user(cls, position, user_id): | |
|
4826 | return cls.query() \ | |
|
4827 | .filter(UserBookmark.user_id == user_id) \ | |
|
4828 | .filter(UserBookmark.position == position).scalar() | |
|
4829 | ||
|
4830 | @classmethod | |
|
4831 | def get_bookmarks_for_user(cls, user_id): | |
|
4832 | return cls.query() \ | |
|
4833 | .filter(UserBookmark.user_id == user_id) \ | |
|
4834 | .options(joinedload(UserBookmark.repository)) \ | |
|
4835 | .options(joinedload(UserBookmark.repository_group)) \ | |
|
4836 | .order_by(UserBookmark.position.asc()) \ | |
|
4837 | .all() | |
|
4838 | ||
|
4839 | def __unicode__(self): | |
|
4840 | return u'<UserBookmark(%d @ %r)>' % (self.position, self.redirect_url) | |
|
4841 | ||
|
4842 | ||
|
4800 | 4843 | class DbMigrateVersion(Base, BaseModel): |
|
4801 | 4844 | __tablename__ = 'db_migrate_version' |
|
4802 | 4845 | __table_args__ = ( |
|
4803 | 4846 | base_table_args, |
|
4804 | 4847 | ) |
|
4805 | 4848 | |
|
4806 | 4849 | repository_id = Column('repository_id', String(250), primary_key=True) |
|
4807 | 4850 | repository_path = Column('repository_path', Text) |
|
4808 | 4851 | version = Column('version', Integer) |
|
4809 | 4852 | |
|
4810 | 4853 | @classmethod |
|
4811 | 4854 | def set_version(cls, version): |
|
4812 | 4855 | """ |
|
4813 | 4856 | Helper for forcing a different version, usually for debugging purposes via ishell. |
|
4814 | 4857 | """ |
|
4815 | 4858 | ver = DbMigrateVersion.query().first() |
|
4816 | 4859 | ver.version = version |
|
4817 | 4860 | Session().commit() |
|
4818 | 4861 | |
|
4819 | 4862 | |
|
4820 | 4863 | class DbSession(Base, BaseModel): |
|
4821 | 4864 | __tablename__ = 'db_session' |
|
4822 | 4865 | __table_args__ = ( |
|
4823 | 4866 | base_table_args, |
|
4824 | 4867 | ) |
|
4825 | 4868 | |
|
4826 | 4869 | def __repr__(self): |
|
4827 | 4870 | return '<DB:DbSession({})>'.format(self.id) |
|
4828 | 4871 | |
|
4829 | 4872 | id = Column('id', Integer()) |
|
4830 | 4873 | namespace = Column('namespace', String(255), primary_key=True) |
|
4831 | 4874 | accessed = Column('accessed', DateTime, nullable=False) |
|
4832 | 4875 | created = Column('created', DateTime, nullable=False) |
|
4833 | 4876 | data = Column('data', PickleType, nullable=False) |
@@ -1,304 +1,324 b'' | |||
|
1 | 1 | //LOGIN |
|
2 | 2 | |
|
3 | 3 | |
|
4 | 4 | .loginbox { |
|
5 | 5 | max-width: 65%; |
|
6 | 6 | margin: @pagepadding auto; |
|
7 | 7 | font-family: @text-light; |
|
8 | 8 | border: @border-thickness solid @grey5; |
|
9 | 9 | box-sizing: border-box; |
|
10 | 10 | |
|
11 | 11 | @media (max-width:1200px) { |
|
12 | 12 | max-width: 85%; |
|
13 | 13 | } |
|
14 | 14 | |
|
15 | 15 | @media (max-width:768px) { |
|
16 | 16 | max-width: 100%; |
|
17 | 17 | width: 100%; |
|
18 | 18 | margin: 0; |
|
19 | 19 | } |
|
20 | 20 | |
|
21 | 21 | .title { |
|
22 | 22 | float: none; |
|
23 | 23 | } |
|
24 | 24 | |
|
25 | 25 | .header { |
|
26 | 26 | width: 100%; |
|
27 | 27 | padding: 0 35px; |
|
28 | 28 | box-sizing: border-box; |
|
29 | 29 | |
|
30 | 30 | .title { |
|
31 | 31 | padding: 0; |
|
32 | 32 | } |
|
33 | 33 | } |
|
34 | 34 | |
|
35 | 35 | .loginwrapper { |
|
36 | 36 | float: left; |
|
37 | 37 | height: 100%; |
|
38 | 38 | width: 100%; |
|
39 | 39 | padding: 35px 55px 35px 35px; |
|
40 | 40 | background-color: white; |
|
41 | 41 | box-sizing: border-box; |
|
42 | 42 | |
|
43 | 43 | @media (max-width:414px) { |
|
44 | 44 | padding: 35px; |
|
45 | 45 | } |
|
46 | 46 | } |
|
47 | 47 | |
|
48 | 48 | .left-column { |
|
49 | 49 | float: left; |
|
50 | 50 | position: relative; |
|
51 | 51 | width: 50%; |
|
52 | 52 | height: 100%; |
|
53 | 53 | |
|
54 | 54 | @media (max-width:414px) { |
|
55 | 55 | display:none; |
|
56 | 56 | } |
|
57 | 57 | } |
|
58 | 58 | |
|
59 | 59 | .right-column { |
|
60 | 60 | float: right; |
|
61 | 61 | position: relative; |
|
62 | 62 | width: 50%; |
|
63 | 63 | |
|
64 | 64 | @media (max-width:414px) { |
|
65 | 65 | width: 100%; |
|
66 | 66 | } |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | .sign-in-image { |
|
70 | 70 | display: block; |
|
71 | 71 | width: 65%; |
|
72 | 72 | margin: 5% auto; |
|
73 | 73 | } |
|
74 | 74 | |
|
75 | 75 | .sign-in-title { |
|
76 | 76 | |
|
77 | 77 | h4 { |
|
78 | 78 | margin: @padding*2 0; |
|
79 | 79 | } |
|
80 | 80 | } |
|
81 | 81 | |
|
82 | 82 | .form { |
|
83 | 83 | label { |
|
84 | 84 | display: block; |
|
85 | 85 | } |
|
86 | 86 | |
|
87 | 87 | input { |
|
88 | 88 | width: 100%; |
|
89 | 89 | margin: 0 10% @padding 0; |
|
90 | 90 | .box-sizing(border-box) ; |
|
91 | 91 | |
|
92 | 92 | &[type="checkbox"] { |
|
93 | 93 | clear: both; |
|
94 | 94 | width: auto; |
|
95 | 95 | margin: 0 1em @padding 0; |
|
96 | 96 | } |
|
97 | 97 | } |
|
98 | 98 | |
|
99 | 99 | .checkbox { |
|
100 | 100 | display: inline; |
|
101 | 101 | width: auto; |
|
102 | 102 | } |
|
103 | 103 | |
|
104 | 104 | .sign-in { |
|
105 | 105 | clear: both; |
|
106 | 106 | width: 100%; |
|
107 | 107 | margin: @padding 0; |
|
108 | 108 | } |
|
109 | 109 | .pwd_reset { |
|
110 | 110 | font-weight: normal; |
|
111 | 111 | } |
|
112 | 112 | .new_account { |
|
113 | 113 | font-weight: bold; |
|
114 | 114 | } |
|
115 | 115 | } |
|
116 | 116 | .register_message, |
|
117 | 117 | .activation_msg { |
|
118 | 118 | padding: 0 0 @padding; |
|
119 | 119 | } |
|
120 | 120 | |
|
121 | 121 | .buttons, |
|
122 | 122 | .links { |
|
123 | 123 | padding: 0; |
|
124 | 124 | } |
|
125 | 125 | |
|
126 | 126 | .buttons { |
|
127 | 127 | input { |
|
128 | 128 | margin-right: 0; |
|
129 | 129 | .box-sizing(border-box); |
|
130 | 130 | } |
|
131 | 131 | |
|
132 | 132 | #sign_up, #send { |
|
133 | 133 | width: 100%; |
|
134 | 134 | } |
|
135 | 135 | } |
|
136 | 136 | |
|
137 | 137 | .fields { |
|
138 | 138 | .field.field-compact { |
|
139 | 139 | margin-bottom: 0px; |
|
140 | 140 | } |
|
141 | 141 | |
|
142 | 142 | .buttons { |
|
143 | 143 | margin: 0; |
|
144 | 144 | } |
|
145 | 145 | |
|
146 | 146 | .field { |
|
147 | 147 | margin-bottom: 15px; |
|
148 | 148 | |
|
149 | 149 | input { |
|
150 | 150 | width: 100%; |
|
151 | 151 | .box-sizing(border-box); |
|
152 | 152 | } |
|
153 | 153 | |
|
154 | 154 | .input { |
|
155 | 155 | margin-left: 0; |
|
156 | 156 | } |
|
157 | 157 | |
|
158 | 158 | .label { |
|
159 | 159 | padding-top: 0; |
|
160 | 160 | } |
|
161 | 161 | } |
|
162 | 162 | } |
|
163 | 163 | |
|
164 | 164 | .checkbox { |
|
165 | 165 | margin: 0 0 @textmargin 0; |
|
166 | 166 | |
|
167 | 167 | input[type="checkbox"] { |
|
168 | 168 | width: auto; |
|
169 | 169 | } |
|
170 | 170 | |
|
171 | 171 | label { |
|
172 | 172 | padding: 0; |
|
173 | 173 | min-height: 0; |
|
174 | 174 | } |
|
175 | 175 | } |
|
176 | 176 | |
|
177 | 177 | .activation_msg { |
|
178 | 178 | padding: @padding 0 0; |
|
179 | 179 | color: @grey4; |
|
180 | 180 | } |
|
181 | 181 | |
|
182 | 182 | .links { |
|
183 | 183 | float: right; |
|
184 | 184 | margin: 0; |
|
185 | 185 | padding: 0; |
|
186 | 186 | line-height: 1; |
|
187 | 187 | |
|
188 | 188 | p { |
|
189 | 189 | float: right; |
|
190 | 190 | margin: 0; |
|
191 | 191 | line-height: 1.5em; |
|
192 | 192 | } |
|
193 | 193 | } |
|
194 | 194 | |
|
195 | 195 | p.help-block { |
|
196 | 196 | margin-left: 0; |
|
197 | 197 | } |
|
198 | 198 | } |
|
199 | 199 | |
|
200 | 200 | .user-menu.submenu { |
|
201 | 201 | right: 0; |
|
202 | 202 | left: auto; |
|
203 | min-width: 290px; | |
|
203 | 204 | } |
|
205 | ||
|
206 | ||
|
207 | .user-menu { | |
|
208 | .bookmark-items { | |
|
209 | padding: 4px 2px; | |
|
210 | color: @grey3; | |
|
211 | border-bottom: @grey3; | |
|
212 | ||
|
213 | a { | |
|
214 | padding: 0 !important; | |
|
215 | color: @rcblue !important; | |
|
216 | } | |
|
217 | } | |
|
218 | a.bookmark-item { | |
|
219 | color: @rcblue !important; | |
|
220 | } | |
|
221 | } | |
|
222 | ||
|
223 | ||
|
204 | 224 | #quick_login { |
|
205 | 225 | left: auto; |
|
206 | 226 | right: 0; |
|
207 | 227 | padding: @menupadding; |
|
208 | 228 | z-index: 999; |
|
209 | 229 | overflow: hidden; |
|
210 | 230 | background-color: @grey6; |
|
211 | 231 | color: @grey2; |
|
212 | 232 | |
|
213 | 233 | h4 { |
|
214 | 234 | margin-bottom: 12px; |
|
215 | 235 | } |
|
216 | 236 | |
|
217 | 237 | .form { |
|
218 | 238 | width: auto; |
|
219 | 239 | } |
|
220 | 240 | |
|
221 | 241 | label, .field { |
|
222 | 242 | margin-bottom: 0; |
|
223 | 243 | } |
|
224 | 244 | |
|
225 | 245 | .label { |
|
226 | 246 | padding-top: 0; |
|
227 | 247 | } |
|
228 | 248 | |
|
229 | 249 | input { |
|
230 | 250 | min-width: 215px; |
|
231 | 251 | margin: 8px 0 @padding; |
|
232 | 252 | } |
|
233 | 253 | |
|
234 | 254 | input[type="submit"] { |
|
235 | 255 | &:extend(.btn-primary); |
|
236 | 256 | width:100%; |
|
237 | 257 | min-width: 0; |
|
238 | 258 | } |
|
239 | 259 | |
|
240 | 260 | .forgot_password, |
|
241 | 261 | .buttons .register { |
|
242 | 262 | a { |
|
243 | 263 | color: @rcblue; |
|
244 | 264 | |
|
245 | 265 | &:hover { |
|
246 | 266 | color: @rcdarkblue; |
|
247 | 267 | } |
|
248 | 268 | } |
|
249 | 269 | } |
|
250 | 270 | |
|
251 | 271 | .buttons { |
|
252 | 272 | margin: 0; |
|
253 | 273 | } |
|
254 | 274 | |
|
255 | 275 | .buttons a { |
|
256 | 276 | padding: 8px 0; |
|
257 | 277 | line-height: 1.4em; |
|
258 | 278 | color: @grey4; |
|
259 | 279 | |
|
260 | 280 | &:hover { |
|
261 | 281 | color: @grey2; |
|
262 | 282 | } |
|
263 | 283 | } |
|
264 | 284 | |
|
265 | 285 | #sign_in { |
|
266 | 286 | margin-bottom: 0 |
|
267 | 287 | } |
|
268 | 288 | |
|
269 | 289 | .big_gravatar { |
|
270 | 290 | float: left; |
|
271 | 291 | display: block; |
|
272 | 292 | margin-top: .5em; |
|
273 | 293 | } |
|
274 | 294 | |
|
275 | 295 | .full_name, |
|
276 | 296 | .email { |
|
277 | 297 | margin: 0 0 0 65px; |
|
278 | 298 | } |
|
279 | 299 | |
|
280 | 300 | .email { |
|
281 | 301 | font-family: @text-light; |
|
282 | 302 | } |
|
283 | 303 | |
|
284 | 304 | ol.links { |
|
285 | 305 | clear:both; |
|
286 | 306 | margin: 0; |
|
287 | 307 | padding: @padding 0 0 0; |
|
288 | 308 | |
|
289 | 309 | li { |
|
290 | 310 | border-top: @border-thickness solid @grey5; |
|
291 | 311 | |
|
292 | 312 | input { |
|
293 | 313 | margin: @padding 0 0 0; |
|
294 | 314 | } |
|
295 | 315 | } |
|
296 | 316 | } |
|
297 | 317 | } |
|
298 | 318 | .submenu #quick_login li:hover { |
|
299 | 319 | background-color: transparent; |
|
300 | 320 | } |
|
301 | 321 | |
|
302 | 322 | #quick_login_link:hover + #quick_login { |
|
303 | 323 | display: block; |
|
304 | 324 | } |
@@ -1,683 +1,684 b'' | |||
|
1 | 1 | // navigation.less |
|
2 | 2 | // For use in RhodeCode applications; |
|
3 | 3 | // see style guide documentation for guidelines. |
|
4 | 4 | |
|
5 | 5 | // HEADER NAVIGATION |
|
6 | 6 | |
|
7 | 7 | .horizontal-list { |
|
8 | 8 | float: right; |
|
9 | 9 | display: block; |
|
10 | 10 | margin: 0; |
|
11 | 11 | padding: 0; |
|
12 | 12 | -webkit-padding-start: 0; |
|
13 | 13 | text-align: left; |
|
14 | 14 | font-size: @navigation-fontsize; |
|
15 | 15 | color: @grey6; |
|
16 | 16 | z-index:10; |
|
17 | 17 | |
|
18 | 18 | li { |
|
19 | 19 | line-height: 1em; |
|
20 | 20 | list-style-type: none; |
|
21 | 21 | |
|
22 | 22 | a { |
|
23 | 23 | padding: 0 .5em; |
|
24 | 24 | |
|
25 | 25 | &.menu_link_notifications { |
|
26 | 26 | .pill(7px,@rcblue); |
|
27 | 27 | display: inline; |
|
28 | 28 | margin: 0 7px 0 .7em; |
|
29 | 29 | font-size: @basefontsize; |
|
30 | 30 | color: white; |
|
31 | 31 | |
|
32 | 32 | &.empty { |
|
33 | 33 | background-color: @grey4; |
|
34 | 34 | } |
|
35 | 35 | |
|
36 | 36 | &:hover { |
|
37 | 37 | background-color: @rcdarkblue; |
|
38 | 38 | } |
|
39 | 39 | } |
|
40 | 40 | } |
|
41 | 41 | .pill_container { |
|
42 | 42 | margin: 1.25em 0px 0px 0px; |
|
43 | 43 | float: right; |
|
44 | 44 | } |
|
45 | 45 | |
|
46 | 46 | &#quick_login_li { |
|
47 | 47 | &:hover { |
|
48 | 48 | color: @grey5; |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | a.menu_link_notifications { |
|
52 | 52 | color: white; |
|
53 | 53 | } |
|
54 | 54 | |
|
55 | 55 | .user { |
|
56 | 56 | padding-bottom: 10px; |
|
57 | 57 | } |
|
58 | 58 | |
|
59 | 59 | &.open { |
|
60 | 60 | .user { |
|
61 | 61 | border-bottom: 5px solid @rcblue; |
|
62 | 62 | } |
|
63 | 63 | } |
|
64 | 64 | } |
|
65 | 65 | |
|
66 | 66 | &:before { content: none; } |
|
67 | 67 | |
|
68 | 68 | &:last-child { |
|
69 | 69 | .menulabel { |
|
70 | 70 | padding-right: 0; |
|
71 | 71 | border-right: none; |
|
72 | 72 | |
|
73 | 73 | .show_more { |
|
74 | 74 | padding-right: 0; |
|
75 | 75 | } |
|
76 | 76 | } |
|
77 | 77 | |
|
78 | 78 | &> a { |
|
79 | 79 | border-bottom: none; |
|
80 | 80 | } |
|
81 | 81 | } |
|
82 | 82 | |
|
83 | 83 | &.active { |
|
84 | 84 | border-bottom: 5px solid @rcblue; |
|
85 | 85 | } |
|
86 | 86 | |
|
87 | 87 | &.open { |
|
88 | 88 | |
|
89 | 89 | a { |
|
90 | 90 | color: white; |
|
91 | 91 | } |
|
92 | 92 | } |
|
93 | 93 | |
|
94 | 94 | &:focus { |
|
95 | 95 | outline: none; |
|
96 | 96 | } |
|
97 | 97 | |
|
98 | 98 | ul li { |
|
99 | 99 | display: block; |
|
100 | 100 | |
|
101 | 101 | &:last-child> a { |
|
102 | 102 | border-bottom: none; |
|
103 | 103 | } |
|
104 | 104 | |
|
105 | 105 | ul li:last-child a { |
|
106 | 106 | /* we don't expect more then 3 levels of submenu and the third |
|
107 | 107 | level can have different html structure */ |
|
108 | 108 | border-bottom: none; |
|
109 | 109 | } |
|
110 | 110 | } |
|
111 | 111 | } |
|
112 | 112 | |
|
113 | 113 | > li { |
|
114 | 114 | float: left; |
|
115 | 115 | display: block; |
|
116 | 116 | padding: 0; |
|
117 | 117 | |
|
118 | 118 | > a, |
|
119 | 119 | &.has_select2 a { |
|
120 | 120 | display: block; |
|
121 | 121 | padding: 10px 0 2px; |
|
122 | 122 | } |
|
123 | 123 | |
|
124 | 124 | .menulabel { |
|
125 | 125 | padding: 0 .5em; |
|
126 | 126 | line-height: 1em; |
|
127 | 127 | // for this specifically we do not use a variable |
|
128 | 128 | border-right: 1px solid @grey4; |
|
129 | 129 | } |
|
130 | 130 | |
|
131 | 131 | .pr_notifications { |
|
132 | 132 | padding-left: .5em; |
|
133 | 133 | } |
|
134 | 134 | |
|
135 | 135 | .pr_notifications + .menulabel { |
|
136 | 136 | display:inline; |
|
137 | 137 | padding-left: 0; |
|
138 | 138 | } |
|
139 | 139 | |
|
140 | 140 | &:hover, |
|
141 | 141 | &.open, |
|
142 | 142 | &.active { |
|
143 | 143 | a { |
|
144 | 144 | color: @grey1; |
|
145 | 145 | } |
|
146 | 146 | } |
|
147 | 147 | } |
|
148 | 148 | |
|
149 | 149 | pre { |
|
150 | 150 | margin: 0; |
|
151 | 151 | padding: 0; |
|
152 | 152 | } |
|
153 | 153 | |
|
154 | 154 | .select2-container, |
|
155 | 155 | .menulink.childs { |
|
156 | 156 | position: relative; |
|
157 | 157 | } |
|
158 | 158 | |
|
159 | 159 | #quick_login { |
|
160 | 160 | |
|
161 | 161 | li a { |
|
162 | 162 | padding: .5em 0; |
|
163 | 163 | border-bottom: none; |
|
164 | 164 | color: @grey2; |
|
165 | 165 | |
|
166 | 166 | &:hover { color: @grey1; } |
|
167 | 167 | } |
|
168 | 168 | } |
|
169 | 169 | |
|
170 | 170 | #quick_login_link { |
|
171 | 171 | display: inline-block; |
|
172 | 172 | |
|
173 | 173 | .gravatar { |
|
174 | 174 | border: 1px solid @grey5; |
|
175 | 175 | } |
|
176 | 176 | |
|
177 | 177 | .gravatar-login { |
|
178 | 178 | height: 20px; |
|
179 | 179 | width: 20px; |
|
180 | 180 | margin: -8px 0; |
|
181 | 181 | padding: 0; |
|
182 | 182 | } |
|
183 | 183 | |
|
184 | 184 | &:hover .user { |
|
185 | 185 | color: @grey6; |
|
186 | 186 | } |
|
187 | 187 | } |
|
188 | 188 | } |
|
189 | 189 | .header .horizontal-list { |
|
190 | 190 | |
|
191 | 191 | li { |
|
192 | 192 | |
|
193 | 193 | &#quick_login_li { |
|
194 | 194 | padding-left: .5em; |
|
195 | 195 | |
|
196 | 196 | &:hover #quick_login_link { |
|
197 | 197 | color: inherit; |
|
198 | 198 | } |
|
199 | 199 | |
|
200 | 200 | .menu_link_user { |
|
201 | 201 | padding: 0 2px; |
|
202 | 202 | } |
|
203 | 203 | } |
|
204 | 204 | list-style-type: none; |
|
205 | 205 | } |
|
206 | 206 | |
|
207 | 207 | > li { |
|
208 | 208 | |
|
209 | 209 | a { |
|
210 | 210 | padding: 18px 0 12px 0; |
|
211 | 211 | color: @nav-grey; |
|
212 | 212 | |
|
213 | 213 | &.menu_link_notifications { |
|
214 | 214 | padding: 1px 8px; |
|
215 | 215 | } |
|
216 | 216 | } |
|
217 | 217 | |
|
218 | 218 | &:hover, |
|
219 | 219 | &.open, |
|
220 | 220 | &.active { |
|
221 | 221 | .pill_container a { |
|
222 | 222 | // don't select text for the pill container, it has it' own |
|
223 | 223 | // hover behaviour |
|
224 | 224 | color: @nav-grey; |
|
225 | 225 | } |
|
226 | 226 | } |
|
227 | 227 | |
|
228 | 228 | &:hover, |
|
229 | 229 | &.open, |
|
230 | 230 | &.active { |
|
231 | 231 | a { |
|
232 | 232 | color: @grey6; |
|
233 | 233 | } |
|
234 | 234 | } |
|
235 | 235 | |
|
236 | 236 | .select2-dropdown-open a { |
|
237 | 237 | color: @grey6; |
|
238 | 238 | } |
|
239 | 239 | |
|
240 | 240 | .repo-switcher { |
|
241 | 241 | padding-left: 0; |
|
242 | 242 | |
|
243 | 243 | .menulabel { |
|
244 | 244 | padding-left: 0; |
|
245 | 245 | } |
|
246 | 246 | } |
|
247 | 247 | } |
|
248 | 248 | |
|
249 | 249 | li ul li { |
|
250 | 250 | background-color:@grey2; |
|
251 | 251 | |
|
252 | 252 | a { |
|
253 | 253 | padding: .5em 0; |
|
254 | 254 | border-bottom: @border-thickness solid @border-default-color; |
|
255 | 255 | color: @grey6; |
|
256 | 256 | } |
|
257 | 257 | |
|
258 | 258 | &:last-child a, &.last a{ |
|
259 | 259 | border-bottom: none; |
|
260 | 260 | } |
|
261 | 261 | |
|
262 | 262 | &:hover { |
|
263 | 263 | background-color: @grey3; |
|
264 | 264 | } |
|
265 | 265 | } |
|
266 | 266 | |
|
267 | 267 | .submenu { |
|
268 | 268 | margin-top: 5px; |
|
269 | 269 | } |
|
270 | 270 | } |
|
271 | 271 | |
|
272 | 272 | // SUBMENUS |
|
273 | 273 | .navigation .submenu { |
|
274 | 274 | display: none; |
|
275 | 275 | } |
|
276 | 276 | |
|
277 | 277 | .navigation li.open { |
|
278 | 278 | .submenu { |
|
279 | 279 | display: block; |
|
280 | 280 | } |
|
281 | 281 | } |
|
282 | 282 | |
|
283 | 283 | .navigation li:last-child .submenu { |
|
284 | 284 | right: -20px; |
|
285 | 285 | left: auto; |
|
286 | 286 | } |
|
287 | 287 | |
|
288 | 288 | .submenu { |
|
289 | 289 | position: absolute; |
|
290 | 290 | top: 100%; |
|
291 | 291 | left: 0; |
|
292 | 292 | min-width: 150px; |
|
293 | 293 | margin: 6px 0 0; |
|
294 | 294 | padding: 0; |
|
295 | 295 | text-align: left; |
|
296 | 296 | font-family: @text-light; |
|
297 | 297 | border-radius: @border-radius; |
|
298 | 298 | z-index: 20; |
|
299 | 299 | |
|
300 | 300 | li { |
|
301 | 301 | display: block; |
|
302 | 302 | margin: 0; |
|
303 | 303 | padding: 0 .5em; |
|
304 | 304 | line-height: 1em; |
|
305 | 305 | color: @grey3; |
|
306 | 306 | background-color: @grey6; |
|
307 | 307 | list-style-type: none; |
|
308 | 308 | |
|
309 | 309 | a { |
|
310 | 310 | display: block; |
|
311 | 311 | width: 100%; |
|
312 | 312 | padding: .5em 0; |
|
313 | 313 | border-right: none; |
|
314 | 314 | border-bottom: @border-thickness solid white; |
|
315 | 315 | color: @grey3; |
|
316 | 316 | } |
|
317 | 317 | |
|
318 | 318 | ul { |
|
319 | 319 | display: none; |
|
320 | 320 | position: absolute; |
|
321 | 321 | top: 0; |
|
322 | 322 | right: 100%; |
|
323 | 323 | padding: 0; |
|
324 | 324 | z-index: 30; |
|
325 | 325 | } |
|
326 | 326 | &:hover { |
|
327 | 327 | background-color: @grey5; |
|
328 | 328 | -webkit-transition: background .3s; |
|
329 | 329 | -moz-transition: background .3s; |
|
330 | 330 | -o-transition: background .3s; |
|
331 | 331 | transition: background .3s; |
|
332 | 332 | |
|
333 | 333 | ul { |
|
334 | 334 | display: block; |
|
335 | 335 | } |
|
336 | 336 | } |
|
337 | 337 | } |
|
338 | ||
|
338 | 339 | } |
|
339 | 340 | |
|
340 | 341 | |
|
341 | 342 | |
|
342 | 343 | |
|
343 | 344 | // repo dropdown |
|
344 | 345 | .quick_repo_menu { |
|
345 | 346 | width: 15px; |
|
346 | 347 | text-align: center; |
|
347 | 348 | position: relative; |
|
348 | 349 | cursor: pointer; |
|
349 | 350 | |
|
350 | 351 | div { |
|
351 | 352 | overflow: visible !important; |
|
352 | 353 | } |
|
353 | 354 | |
|
354 | 355 | &.sorting { |
|
355 | 356 | cursor: auto; |
|
356 | 357 | } |
|
357 | 358 | |
|
358 | 359 | &:hover { |
|
359 | 360 | .menu_items_container { |
|
360 | 361 | position: absolute; |
|
361 | 362 | display: block; |
|
362 | 363 | } |
|
363 | 364 | .menu_items { |
|
364 | 365 | display: block; |
|
365 | 366 | } |
|
366 | 367 | } |
|
367 | 368 | |
|
368 | 369 | i { |
|
369 | 370 | margin: 0; |
|
370 | 371 | color: @grey4; |
|
371 | 372 | } |
|
372 | 373 | |
|
373 | 374 | .menu_items_container { |
|
374 | 375 | position: absolute; |
|
375 | 376 | top: 0; |
|
376 | 377 | left: 100%; |
|
377 | 378 | margin: 0; |
|
378 | 379 | padding: 0; |
|
379 | 380 | list-style: none; |
|
380 | 381 | background-color: @grey6; |
|
381 | 382 | z-index: 999; |
|
382 | 383 | text-align: left; |
|
383 | 384 | |
|
384 | 385 | a { |
|
385 | 386 | color: @grey2; |
|
386 | 387 | } |
|
387 | 388 | |
|
388 | 389 | ul.menu_items { |
|
389 | 390 | margin: 0; |
|
390 | 391 | padding: 0; |
|
391 | 392 | } |
|
392 | 393 | |
|
393 | 394 | li { |
|
394 | 395 | margin: 0; |
|
395 | 396 | padding: 0; |
|
396 | 397 | line-height: 1em; |
|
397 | 398 | list-style-type: none; |
|
398 | 399 | |
|
399 | 400 | a { |
|
400 | 401 | display: block; |
|
401 | 402 | height: 16px; |
|
402 | 403 | padding: 8px; //must add up to td height (28px) |
|
403 | 404 | width: 120px; // set width |
|
404 | 405 | |
|
405 | 406 | &:hover { |
|
406 | 407 | background-color: @grey5; |
|
407 | 408 | -webkit-transition: background .3s; |
|
408 | 409 | -moz-transition: background .3s; |
|
409 | 410 | -o-transition: background .3s; |
|
410 | 411 | transition: background .3s; |
|
411 | 412 | } |
|
412 | 413 | } |
|
413 | 414 | } |
|
414 | 415 | } |
|
415 | 416 | } |
|
416 | 417 | |
|
417 | 418 | // Header Repository Switcher |
|
418 | 419 | // Select2 Dropdown |
|
419 | 420 | #select2-drop.select2-drop.repo-switcher-dropdown { |
|
420 | 421 | width: auto !important; |
|
421 | 422 | margin-top: 5px; |
|
422 | 423 | padding: 1em 0; |
|
423 | 424 | text-align: left; |
|
424 | 425 | .border-radius-bottom(@border-radius); |
|
425 | 426 | border-color: transparent; |
|
426 | 427 | color: @grey6; |
|
427 | 428 | background-color: @grey2; |
|
428 | 429 | |
|
429 | 430 | input { |
|
430 | 431 | min-width: 90%; |
|
431 | 432 | } |
|
432 | 433 | |
|
433 | 434 | ul.select2-result-sub { |
|
434 | 435 | |
|
435 | 436 | li { |
|
436 | 437 | line-height: 1em; |
|
437 | 438 | |
|
438 | 439 | &:hover, |
|
439 | 440 | &.select2-highlighted { |
|
440 | 441 | background-color: @grey3; |
|
441 | 442 | } |
|
442 | 443 | } |
|
443 | 444 | |
|
444 | 445 | &:before { content: none; } |
|
445 | 446 | } |
|
446 | 447 | |
|
447 | 448 | ul.select2-results { |
|
448 | 449 | min-width: 200px; |
|
449 | 450 | margin: 0; |
|
450 | 451 | padding: 0; |
|
451 | 452 | list-style-type: none; |
|
452 | 453 | overflow-x: visible; |
|
453 | 454 | overflow-y: scroll; |
|
454 | 455 | |
|
455 | 456 | li { |
|
456 | 457 | padding: 0 8px; |
|
457 | 458 | line-height: 1em; |
|
458 | 459 | color: @grey6; |
|
459 | 460 | |
|
460 | 461 | &>.select2-result-label { |
|
461 | 462 | padding: 8px 0; |
|
462 | 463 | border-bottom: @border-thickness solid @grey3; |
|
463 | 464 | white-space: nowrap; |
|
464 | 465 | color: @grey5; |
|
465 | 466 | cursor: pointer; |
|
466 | 467 | } |
|
467 | 468 | |
|
468 | 469 | &.select2-result-with-children { |
|
469 | 470 | margin: 0; |
|
470 | 471 | padding: 0; |
|
471 | 472 | } |
|
472 | 473 | |
|
473 | 474 | &.select2-result-unselectable > .select2-result-label { |
|
474 | 475 | margin: 0 8px; |
|
475 | 476 | } |
|
476 | 477 | |
|
477 | 478 | } |
|
478 | 479 | } |
|
479 | 480 | |
|
480 | 481 | ul.select2-result-sub { |
|
481 | 482 | margin: 0; |
|
482 | 483 | padding: 0; |
|
483 | 484 | |
|
484 | 485 | li { |
|
485 | 486 | display: block; |
|
486 | 487 | margin: 0; |
|
487 | 488 | border-right: none; |
|
488 | 489 | line-height: 1em; |
|
489 | 490 | font-family: @text-light; |
|
490 | 491 | color: @grey2; |
|
491 | 492 | list-style-type: none; |
|
492 | 493 | |
|
493 | 494 | &:hover { |
|
494 | 495 | background-color: @grey3; |
|
495 | 496 | } |
|
496 | 497 | } |
|
497 | 498 | } |
|
498 | 499 | } |
|
499 | 500 | |
|
500 | 501 | |
|
501 | 502 | #context-bar { |
|
502 | 503 | display: block; |
|
503 | 504 | margin: 0 auto; |
|
504 | 505 | padding: 0 @header-padding; |
|
505 | 506 | background-color: @grey6; |
|
506 | 507 | border-bottom: @border-thickness solid @grey5; |
|
507 | 508 | |
|
508 | 509 | .clear { |
|
509 | 510 | clear: both; |
|
510 | 511 | } |
|
511 | 512 | } |
|
512 | 513 | |
|
513 | 514 | ul#context-pages { |
|
514 | 515 | li { |
|
515 | 516 | line-height: 1em; |
|
516 | 517 | list-style-type: none; |
|
517 | 518 | |
|
518 | 519 | a { |
|
519 | 520 | color: @grey3; |
|
520 | 521 | } |
|
521 | 522 | |
|
522 | 523 | &.active { |
|
523 | 524 | // special case, non-variable color |
|
524 | 525 | border-bottom: 4px solid @nav-grey; |
|
525 | 526 | |
|
526 | 527 | a { |
|
527 | 528 | color: @grey1; |
|
528 | 529 | } |
|
529 | 530 | } |
|
530 | 531 | } |
|
531 | 532 | } |
|
532 | 533 | |
|
533 | 534 | // PAGINATION |
|
534 | 535 | |
|
535 | 536 | .pagination { |
|
536 | 537 | border: @border-thickness solid @rcblue; |
|
537 | 538 | color: @rcblue; |
|
538 | 539 | |
|
539 | 540 | .current { |
|
540 | 541 | color: @grey4; |
|
541 | 542 | } |
|
542 | 543 | } |
|
543 | 544 | |
|
544 | 545 | .dataTables_processing { |
|
545 | 546 | text-align: center; |
|
546 | 547 | font-size: 1.1em; |
|
547 | 548 | position: relative; |
|
548 | 549 | top: 95px; |
|
549 | 550 | } |
|
550 | 551 | |
|
551 | 552 | .dataTables_paginate, .pagination-wh { |
|
552 | 553 | text-align: left; |
|
553 | 554 | display: inline-block; |
|
554 | 555 | border-left: 1px solid @rcblue; |
|
555 | 556 | float: none; |
|
556 | 557 | overflow: hidden; |
|
557 | 558 | |
|
558 | 559 | .paginate_button, .pager_curpage, |
|
559 | 560 | .pager_link, .pg-previous, .pg-next, .pager_dotdot { |
|
560 | 561 | display: inline-block; |
|
561 | 562 | padding: @menupadding/4 @menupadding; |
|
562 | 563 | border: 1px solid @rcblue; |
|
563 | 564 | border-left: 0; |
|
564 | 565 | color: @rcblue; |
|
565 | 566 | cursor: pointer; |
|
566 | 567 | float: left; |
|
567 | 568 | } |
|
568 | 569 | |
|
569 | 570 | .pager_curpage, .pager_dotdot, |
|
570 | 571 | .paginate_button.current, .paginate_button.disabled, |
|
571 | 572 | .disabled { |
|
572 | 573 | color: @grey3; |
|
573 | 574 | cursor: default; |
|
574 | 575 | } |
|
575 | 576 | |
|
576 | 577 | .ellipsis { |
|
577 | 578 | display: inline-block; |
|
578 | 579 | text-align: left; |
|
579 | 580 | padding: @menupadding/4 @menupadding; |
|
580 | 581 | border: 1px solid @rcblue; |
|
581 | 582 | border-left: 0; |
|
582 | 583 | float: left; |
|
583 | 584 | } |
|
584 | 585 | } |
|
585 | 586 | |
|
586 | 587 | // SIDEBAR |
|
587 | 588 | |
|
588 | 589 | .sidebar { |
|
589 | 590 | .block-left; |
|
590 | 591 | clear: left; |
|
591 | 592 | max-width: @sidebar-width; |
|
592 | 593 | margin-right: @sidebarpadding; |
|
593 | 594 | padding-right: @sidebarpadding; |
|
594 | 595 | font-family: @text-regular; |
|
595 | 596 | color: @grey1; |
|
596 | 597 | |
|
597 | 598 | &#graph_nodes { |
|
598 | 599 | clear:both; |
|
599 | 600 | width: auto; |
|
600 | 601 | margin-left: -100px; |
|
601 | 602 | padding: 0; |
|
602 | 603 | border: none; |
|
603 | 604 | } |
|
604 | 605 | |
|
605 | 606 | .nav-pills { |
|
606 | 607 | margin: 0; |
|
607 | 608 | } |
|
608 | 609 | |
|
609 | 610 | .nav { |
|
610 | 611 | list-style: none; |
|
611 | 612 | padding: 0; |
|
612 | 613 | |
|
613 | 614 | li { |
|
614 | 615 | padding-bottom: @menupadding; |
|
615 | 616 | line-height: 1em; |
|
616 | 617 | color: @grey4; |
|
617 | 618 | list-style-type: none; |
|
618 | 619 | |
|
619 | 620 | &.active a { |
|
620 | 621 | color: @grey2; |
|
621 | 622 | } |
|
622 | 623 | |
|
623 | 624 | a { |
|
624 | 625 | color: @grey4; |
|
625 | 626 | } |
|
626 | 627 | } |
|
627 | 628 | |
|
628 | 629 | } |
|
629 | 630 | } |
|
630 | 631 | |
|
631 | 632 | .main_filter_help_box { |
|
632 | 633 | padding: 7px 7px; |
|
633 | 634 | border-top: 1px solid @grey4; |
|
634 | 635 | border-right: 1px solid @grey4; |
|
635 | 636 | border-bottom: 1px solid @grey4; |
|
636 | 637 | display: inline-block; |
|
637 | 638 | vertical-align: top; |
|
638 | 639 | margin-left: -7px; |
|
639 | 640 | background: @grey3; |
|
640 | 641 | } |
|
641 | 642 | |
|
642 | 643 | .main_filter_input_box { |
|
643 | 644 | display: inline-block; |
|
644 | 645 | } |
|
645 | 646 | |
|
646 | 647 | .main_filter_box { |
|
647 | 648 | margin: 9px 0 0 0; |
|
648 | 649 | } |
|
649 | 650 | |
|
650 | 651 | #main_filter_help { |
|
651 | 652 | background: @grey3; |
|
652 | 653 | border: 1px solid black; |
|
653 | 654 | position: absolute; |
|
654 | 655 | white-space: pre-wrap; |
|
655 | 656 | z-index: 9999; |
|
656 | 657 | color: @nav-grey; |
|
657 | 658 | margin: 1px 7px; |
|
658 | 659 | padding: 0 2px; |
|
659 | 660 | } |
|
660 | 661 | |
|
661 | 662 | .main_filter_input { |
|
662 | 663 | padding: 5px; |
|
663 | 664 | min-width: 220px; |
|
664 | 665 | color: @nav-grey; |
|
665 | 666 | background: @grey3; |
|
666 | 667 | min-height: 18px; |
|
667 | 668 | } |
|
668 | 669 | |
|
669 | 670 | .main_filter_input::placeholder { |
|
670 | 671 | color: @nav-grey; |
|
671 | 672 | opacity: 1; |
|
672 | 673 | } |
|
673 | 674 | |
|
674 | 675 | .notice-box { |
|
675 | 676 | display:block !important; |
|
676 | 677 | padding: 9px 0 !important; |
|
677 | 678 | } |
|
678 | 679 | |
|
679 | 680 | .menulabel-notice { |
|
680 | 681 | border: 1px solid @color5; |
|
681 | 682 | padding:7px 10px; |
|
682 | 683 | color: @color5; |
|
683 | 684 | } |
@@ -1,507 +1,509 b'' | |||
|
1 | 1 | |
|
2 | 2 | // tables.less |
|
3 | 3 | // For use in RhodeCode application tables; |
|
4 | 4 | // see style guide documentation for guidelines. |
|
5 | 5 | |
|
6 | 6 | // TABLES |
|
7 | 7 | |
|
8 | 8 | .rctable, |
|
9 | 9 | table.rctable, |
|
10 | 10 | table.dataTable { |
|
11 | 11 | clear:both; |
|
12 | 12 | width: 100%; |
|
13 | 13 | margin: 0 auto @padding; |
|
14 | 14 | padding: 0; |
|
15 | 15 | vertical-align: baseline; |
|
16 | 16 | line-height:1.5em; |
|
17 | 17 | border: none; |
|
18 | 18 | outline: none; |
|
19 | 19 | border-collapse: collapse; |
|
20 | 20 | border-spacing: 0; |
|
21 | 21 | color: @grey2; |
|
22 | 22 | |
|
23 | 23 | b { |
|
24 | 24 | font-weight: normal; |
|
25 | 25 | } |
|
26 | 26 | |
|
27 | 27 | em { |
|
28 | 28 | font-weight: bold; |
|
29 | 29 | font-style: normal; |
|
30 | 30 | } |
|
31 | 31 | |
|
32 | 32 | th, |
|
33 | 33 | td { |
|
34 | 34 | height: auto; |
|
35 | 35 | max-width: 20%; |
|
36 | 36 | padding: .65em 1em .65em 0; |
|
37 | 37 | vertical-align: middle; |
|
38 | 38 | border-bottom: @border-thickness solid @grey5; |
|
39 | 39 | white-space: normal; |
|
40 | 40 | |
|
41 | 41 | &.td-radio, |
|
42 | 42 | &.td-checkbox { |
|
43 | 43 | padding-right: 0; |
|
44 | 44 | text-align: center; |
|
45 | 45 | |
|
46 | 46 | input { |
|
47 | 47 | margin: 0 1em; |
|
48 | 48 | } |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | &.truncate-wrap { |
|
52 | 52 | white-space: nowrap !important; |
|
53 | 53 | } |
|
54 | 54 | |
|
55 | 55 | pre { |
|
56 | 56 | margin: 0; |
|
57 | 57 | } |
|
58 | 58 | |
|
59 | 59 | .show_more { |
|
60 | 60 | height: inherit; |
|
61 | 61 | } |
|
62 | 62 | } |
|
63 | 63 | |
|
64 | 64 | .expired td { |
|
65 | 65 | background-color: @grey7; |
|
66 | 66 | } |
|
67 | 67 | .inactive td { |
|
68 | 68 | background-color: @grey6; |
|
69 | 69 | } |
|
70 | 70 | th { |
|
71 | 71 | text-align: left; |
|
72 | 72 | font-weight: @text-semibold-weight; |
|
73 | 73 | font-family: @text-semibold; |
|
74 | 74 | } |
|
75 | 75 | |
|
76 | 76 | .hl { |
|
77 | 77 | td { |
|
78 | 78 | background-color: lighten(@alert4,25%); |
|
79 | 79 | } |
|
80 | 80 | } |
|
81 | 81 | |
|
82 | 82 | // Special Data Cell Types |
|
83 | 83 | // See style guide for desciptions and examples. |
|
84 | 84 | |
|
85 | 85 | td { |
|
86 | 86 | |
|
87 | 87 | &.user { |
|
88 | 88 | padding-left: 1em; |
|
89 | 89 | } |
|
90 | 90 | |
|
91 | 91 | &.td-rss { |
|
92 | 92 | width: 20px; |
|
93 | 93 | min-width: 0; |
|
94 | 94 | margin: 0; |
|
95 | 95 | } |
|
96 | 96 | |
|
97 | 97 | &.quick_repo_menu { |
|
98 | 98 | width: 15px; |
|
99 | 99 | text-align: center; |
|
100 | 100 | |
|
101 | 101 | &:hover { |
|
102 | 102 | background-color: @grey5; |
|
103 | 103 | } |
|
104 | 104 | } |
|
105 | 105 | |
|
106 | 106 | &.td-hash { |
|
107 | 107 | min-width: 80px; |
|
108 | 108 | width: 200px; |
|
109 | 109 | |
|
110 | 110 | .obsolete { |
|
111 | 111 | text-decoration: line-through; |
|
112 | 112 | color: lighten(@grey2,25%); |
|
113 | 113 | } |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | &.td-time { |
|
117 | 117 | width: 160px; |
|
118 | 118 | white-space: nowrap; |
|
119 | 119 | } |
|
120 | 120 | |
|
121 | 121 | &.annotate{ |
|
122 | 122 | padding-right: 0; |
|
123 | 123 | |
|
124 | 124 | div.annotatediv{ |
|
125 | 125 | margin: 0 0.7em; |
|
126 | 126 | } |
|
127 | 127 | } |
|
128 | 128 | |
|
129 | 129 | &.tags-col { |
|
130 | 130 | padding-right: 0; |
|
131 | 131 | } |
|
132 | 132 | |
|
133 | 133 | &.td-description { |
|
134 | 134 | min-width: 350px; |
|
135 | 135 | |
|
136 | 136 | &.truncate, .truncate-wrap { |
|
137 | 137 | white-space: nowrap; |
|
138 | 138 | overflow: hidden; |
|
139 | 139 | text-overflow: ellipsis; |
|
140 | 140 | max-width: 350px; |
|
141 | 141 | } |
|
142 | 142 | } |
|
143 | 143 | |
|
144 | 144 | &.td-componentname { |
|
145 | 145 | white-space: nowrap; |
|
146 | 146 | } |
|
147 | 147 | |
|
148 | 148 | &.td-name { |
|
149 | 149 | |
|
150 | 150 | } |
|
151 | 151 | |
|
152 | 152 | &.td-journalaction { |
|
153 | 153 | min-width: 300px; |
|
154 | 154 | |
|
155 | 155 | .journal_action_params { |
|
156 | 156 | // waiting for feedback |
|
157 | 157 | } |
|
158 | 158 | } |
|
159 | 159 | |
|
160 | 160 | &.td-active { |
|
161 | 161 | padding-left: .65em; |
|
162 | 162 | } |
|
163 | 163 | |
|
164 | 164 | &.td-url { |
|
165 | 165 | white-space: nowrap; |
|
166 | 166 | } |
|
167 | 167 | |
|
168 | 168 | &.td-comments { |
|
169 | 169 | min-width: 3em; |
|
170 | 170 | } |
|
171 | 171 | |
|
172 | 172 | &.td-buttons { |
|
173 | 173 | padding: .3em 0; |
|
174 | 174 | } |
|
175 | ||
|
175 | &.td-align-top { | |
|
176 | vertical-align: text-top | |
|
177 | } | |
|
176 | 178 | &.td-action { |
|
177 | 179 | // this is for the remove/delete/edit buttons |
|
178 | 180 | padding-right: 0; |
|
179 | 181 | min-width: 95px; |
|
180 | 182 | text-transform: capitalize; |
|
181 | 183 | |
|
182 | 184 | i { |
|
183 | 185 | display: none; |
|
184 | 186 | } |
|
185 | 187 | } |
|
186 | 188 | |
|
187 | 189 | // TODO: lisa: this needs to be cleaned up with the buttons |
|
188 | 190 | .grid_edit, |
|
189 | 191 | .grid_delete { |
|
190 | 192 | display: inline-block; |
|
191 | 193 | margin: 0 @padding/3 0 0; |
|
192 | 194 | font-family: @text-light; |
|
193 | 195 | |
|
194 | 196 | i { |
|
195 | 197 | display: none; |
|
196 | 198 | } |
|
197 | 199 | } |
|
198 | 200 | |
|
199 | 201 | .grid_edit + .grid_delete { |
|
200 | 202 | border-left: @border-thickness solid @grey5; |
|
201 | 203 | padding-left: @padding/2; |
|
202 | 204 | } |
|
203 | 205 | |
|
204 | 206 | &.td-compare { |
|
205 | 207 | |
|
206 | 208 | input { |
|
207 | 209 | margin-right: 1em; |
|
208 | 210 | } |
|
209 | 211 | |
|
210 | 212 | .compare-radio-button { |
|
211 | 213 | margin: 0 1em 0 0; |
|
212 | 214 | } |
|
213 | 215 | |
|
214 | 216 | |
|
215 | 217 | } |
|
216 | 218 | |
|
217 | 219 | &.td-tags { |
|
218 | 220 | padding: .5em 1em .5em 0; |
|
219 | 221 | width: 140px; |
|
220 | 222 | |
|
221 | 223 | .tag { |
|
222 | 224 | margin: 1px; |
|
223 | 225 | float: left; |
|
224 | 226 | } |
|
225 | 227 | } |
|
226 | 228 | |
|
227 | 229 | .icon-svn, .icon-hg, .icon-git { |
|
228 | 230 | font-size: 1.4em; |
|
229 | 231 | } |
|
230 | 232 | |
|
231 | 233 | &.collapse_commit, |
|
232 | 234 | &.expand_commit { |
|
233 | 235 | padding-right: 0; |
|
234 | 236 | padding-left: 1em; |
|
235 | 237 | cursor: pointer; |
|
236 | 238 | width: 20px; |
|
237 | 239 | } |
|
238 | 240 | } |
|
239 | 241 | |
|
240 | 242 | .perm_admin_row { |
|
241 | 243 | color: @grey4; |
|
242 | 244 | background-color: @grey6; |
|
243 | 245 | } |
|
244 | 246 | |
|
245 | 247 | .noborder { |
|
246 | 248 | border: none; |
|
247 | 249 | |
|
248 | 250 | td { |
|
249 | 251 | border: none; |
|
250 | 252 | } |
|
251 | 253 | } |
|
252 | 254 | } |
|
253 | 255 | .rctable.audit-log { |
|
254 | 256 | td { |
|
255 | 257 | vertical-align: top; |
|
256 | 258 | } |
|
257 | 259 | } |
|
258 | 260 | |
|
259 | 261 | // TRUNCATING |
|
260 | 262 | // TODO: lisaq: should this possibly be moved out of tables.less? |
|
261 | 263 | // for truncated text |
|
262 | 264 | // used inside of table cells and in code block headers |
|
263 | 265 | .truncate-wrap { |
|
264 | 266 | white-space: nowrap !important; |
|
265 | 267 | |
|
266 | 268 | //truncated text |
|
267 | 269 | .truncate { |
|
268 | 270 | max-width: 450px; |
|
269 | 271 | width: 300px; |
|
270 | 272 | overflow: hidden; |
|
271 | 273 | text-overflow: ellipsis; |
|
272 | 274 | -o-text-overflow: ellipsis; |
|
273 | 275 | -ms-text-overflow: ellipsis; |
|
274 | 276 | |
|
275 | 277 | &.autoexpand { |
|
276 | 278 | width: 120px; |
|
277 | 279 | margin-right: 200px; |
|
278 | 280 | } |
|
279 | 281 | } |
|
280 | 282 | &:hover .truncate.autoexpand { |
|
281 | 283 | overflow: visible; |
|
282 | 284 | } |
|
283 | 285 | |
|
284 | 286 | .tags-truncate { |
|
285 | 287 | width: 150px; |
|
286 | 288 | height: 22px; |
|
287 | 289 | overflow: hidden; |
|
288 | 290 | |
|
289 | 291 | .tag { |
|
290 | 292 | display: inline-block; |
|
291 | 293 | } |
|
292 | 294 | |
|
293 | 295 | &.truncate { |
|
294 | 296 | height: 22px; |
|
295 | 297 | max-height:2em; |
|
296 | 298 | width: 140px; |
|
297 | 299 | } |
|
298 | 300 | } |
|
299 | 301 | } |
|
300 | 302 | |
|
301 | 303 | .apikeys_wrap { |
|
302 | 304 | margin-bottom: @padding; |
|
303 | 305 | |
|
304 | 306 | table.rctable td:first-child { |
|
305 | 307 | width: 340px; |
|
306 | 308 | } |
|
307 | 309 | } |
|
308 | 310 | |
|
309 | 311 | |
|
310 | 312 | |
|
311 | 313 | // SPECIAL CASES |
|
312 | 314 | |
|
313 | 315 | // Repository Followers |
|
314 | 316 | table.rctable.followers_data { |
|
315 | 317 | width: 75%; |
|
316 | 318 | margin: 0; |
|
317 | 319 | } |
|
318 | 320 | |
|
319 | 321 | // Repository List |
|
320 | 322 | // Group Members List |
|
321 | 323 | table.rctable.group_members, |
|
322 | 324 | table#repo_list_table { |
|
323 | 325 | min-width: 600px; |
|
324 | 326 | } |
|
325 | 327 | |
|
326 | 328 | // Keyboard mappings |
|
327 | 329 | table.keyboard-mappings { |
|
328 | 330 | th { |
|
329 | 331 | text-align: left; |
|
330 | 332 | font-weight: @text-semibold-weight; |
|
331 | 333 | font-family: @text-semibold; |
|
332 | 334 | } |
|
333 | 335 | } |
|
334 | 336 | |
|
335 | 337 | // Branches, Tags, and Bookmarks |
|
336 | 338 | #obj_list_table.dataTable { |
|
337 | 339 | td.td-time { |
|
338 | 340 | padding-right: 1em; |
|
339 | 341 | } |
|
340 | 342 | } |
|
341 | 343 | |
|
342 | 344 | // User Admin |
|
343 | 345 | .rctable.useremails, |
|
344 | 346 | .rctable.account_emails { |
|
345 | 347 | .tag, |
|
346 | 348 | .btn { |
|
347 | 349 | float: right; |
|
348 | 350 | } |
|
349 | 351 | .btn { //to line up with tags |
|
350 | 352 | margin-right: 1.65em; |
|
351 | 353 | } |
|
352 | 354 | } |
|
353 | 355 | |
|
354 | 356 | // User List |
|
355 | 357 | #user_list_table { |
|
356 | 358 | |
|
357 | 359 | td.td-user { |
|
358 | 360 | min-width: 100px; |
|
359 | 361 | } |
|
360 | 362 | } |
|
361 | 363 | |
|
362 | 364 | // Pull Request List Table |
|
363 | 365 | #pull_request_list_table.dataTable { |
|
364 | 366 | |
|
365 | 367 | //TODO: lisa: This needs to be removed once the description is adjusted |
|
366 | 368 | // for using an expand_commit button (see issue 765) |
|
367 | 369 | td { |
|
368 | 370 | vertical-align: middle; |
|
369 | 371 | } |
|
370 | 372 | } |
|
371 | 373 | |
|
372 | 374 | // Settings (no border) |
|
373 | 375 | table.rctable.dl-settings { |
|
374 | 376 | td { |
|
375 | 377 | border: none; |
|
376 | 378 | vertical-align: baseline; |
|
377 | 379 | } |
|
378 | 380 | } |
|
379 | 381 | |
|
380 | 382 | |
|
381 | 383 | // Statistics |
|
382 | 384 | table.trending_language_tbl { |
|
383 | 385 | width: 100%; |
|
384 | 386 | line-height: 1em; |
|
385 | 387 | |
|
386 | 388 | td div { |
|
387 | 389 | overflow: visible; |
|
388 | 390 | } |
|
389 | 391 | } |
|
390 | 392 | |
|
391 | 393 | .trending_language_tbl, .trending_language_tbl td { |
|
392 | 394 | border: 0; |
|
393 | 395 | margin: 0; |
|
394 | 396 | padding: 0; |
|
395 | 397 | background: transparent; |
|
396 | 398 | } |
|
397 | 399 | |
|
398 | 400 | .trending_language_tbl, .trending_language_tbl tr { |
|
399 | 401 | border-spacing: 0 3px; |
|
400 | 402 | } |
|
401 | 403 | |
|
402 | 404 | .trending_language { |
|
403 | 405 | position: relative; |
|
404 | 406 | width: 100%; |
|
405 | 407 | height: 19px; |
|
406 | 408 | overflow: hidden; |
|
407 | 409 | background-color: @grey6; |
|
408 | 410 | |
|
409 | 411 | span, b{ |
|
410 | 412 | position: absolute; |
|
411 | 413 | display: block; |
|
412 | 414 | height: 12px; |
|
413 | 415 | margin-bottom: 0px; |
|
414 | 416 | white-space: pre; |
|
415 | 417 | padding: floor(@basefontsize/4); |
|
416 | 418 | top: 0; |
|
417 | 419 | left: 0; |
|
418 | 420 | } |
|
419 | 421 | |
|
420 | 422 | span{ |
|
421 | 423 | color: @text-color; |
|
422 | 424 | z-index: 0; |
|
423 | 425 | min-width: 20px; |
|
424 | 426 | } |
|
425 | 427 | |
|
426 | 428 | b { |
|
427 | 429 | z-index: 1; |
|
428 | 430 | overflow: hidden; |
|
429 | 431 | background-color: @rcblue; |
|
430 | 432 | color: #FFF; |
|
431 | 433 | text-decoration: none; |
|
432 | 434 | } |
|
433 | 435 | |
|
434 | 436 | } |
|
435 | 437 | |
|
436 | 438 | // Changesets |
|
437 | 439 | #changesets.rctable { |
|
438 | 440 | |
|
439 | 441 | // td must be fixed height for graph |
|
440 | 442 | td { |
|
441 | 443 | height: 32px; |
|
442 | 444 | padding: 0 1em 0 0; |
|
443 | 445 | vertical-align: middle; |
|
444 | 446 | white-space: nowrap; |
|
445 | 447 | |
|
446 | 448 | &.td-description { |
|
447 | 449 | white-space: normal; |
|
448 | 450 | } |
|
449 | 451 | |
|
450 | 452 | &.expand_commit { |
|
451 | 453 | padding-right: 0; |
|
452 | 454 | cursor: pointer; |
|
453 | 455 | width: 20px; |
|
454 | 456 | } |
|
455 | 457 | } |
|
456 | 458 | } |
|
457 | 459 | |
|
458 | 460 | // Compare |
|
459 | 461 | table.compare_view_commits { |
|
460 | 462 | margin-top: @space; |
|
461 | 463 | |
|
462 | 464 | td.td-time { |
|
463 | 465 | padding-left: .5em; |
|
464 | 466 | } |
|
465 | 467 | |
|
466 | 468 | // special case to not show hover actions on hidden indicator |
|
467 | 469 | tr.compare_select_hidden:hover { |
|
468 | 470 | cursor: inherit; |
|
469 | 471 | |
|
470 | 472 | td { |
|
471 | 473 | background-color: inherit; |
|
472 | 474 | } |
|
473 | 475 | } |
|
474 | 476 | |
|
475 | 477 | tr:hover { |
|
476 | 478 | cursor: pointer; |
|
477 | 479 | |
|
478 | 480 | td { |
|
479 | 481 | background-color: lighten(@alert4,25%); |
|
480 | 482 | } |
|
481 | 483 | } |
|
482 | 484 | |
|
483 | 485 | |
|
484 | 486 | } |
|
485 | 487 | |
|
486 | 488 | .file_history { |
|
487 | 489 | td.td-actions { |
|
488 | 490 | text-align: right; |
|
489 | 491 | } |
|
490 | 492 | } |
|
491 | 493 | |
|
492 | 494 | |
|
493 | 495 | // Gist List |
|
494 | 496 | #gist_list_table { |
|
495 | 497 | td { |
|
496 | 498 | vertical-align: middle; |
|
497 | 499 | |
|
498 | 500 | div{ |
|
499 | 501 | display: inline-block; |
|
500 | 502 | vertical-align: middle; |
|
501 | 503 | } |
|
502 | 504 | |
|
503 | 505 | img{ |
|
504 | 506 | vertical-align: middle; |
|
505 | 507 | } |
|
506 | 508 | } |
|
507 | 509 | } |
@@ -1,544 +1,548 b'' | |||
|
1 | 1 | // |
|
2 | 2 | // Typography |
|
3 | 3 | // modified from Bootstrap |
|
4 | 4 | // -------------------------------------------------- |
|
5 | 5 | |
|
6 | 6 | // Base |
|
7 | 7 | body { |
|
8 | 8 | font-size: @basefontsize; |
|
9 | 9 | font-family: @text-light; |
|
10 | 10 | letter-spacing: .02em; |
|
11 | 11 | color: @grey2; |
|
12 | 12 | } |
|
13 | 13 | |
|
14 | 14 | #content, label{ |
|
15 | 15 | font-size: @basefontsize; |
|
16 | 16 | } |
|
17 | 17 | |
|
18 | 18 | label { |
|
19 | 19 | color: @grey2; |
|
20 | 20 | } |
|
21 | 21 | |
|
22 | 22 | ::selection { background: @rchighlightblue; } |
|
23 | 23 | |
|
24 | 24 | // Headings |
|
25 | 25 | // ------------------------- |
|
26 | 26 | |
|
27 | 27 | h1, h2, h3, h4, h5, h6, |
|
28 | 28 | .h1, .h2, .h3, .h4, .h5, .h6 { |
|
29 | 29 | margin: 0 0 @textmargin 0; |
|
30 | 30 | padding: 0; |
|
31 | 31 | line-height: 1.8em; |
|
32 | 32 | color: @text-color; |
|
33 | 33 | a { |
|
34 | 34 | color: @rcblue; |
|
35 | 35 | } |
|
36 | 36 | } |
|
37 | 37 | |
|
38 | 38 | h1, .h1 { font-size: 1.54em; font-weight: @text-bold-weight; font-family: @text-bold; } |
|
39 | 39 | h2, .h2 { font-size: 1.23em; font-weight: @text-semibold-weight; font-family: @text-semibold; } |
|
40 | 40 | h3, .h3 { font-size: 1.23em; font-family: @text-regular; } |
|
41 | 41 | h4, .h4 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; } |
|
42 | 42 | h5, .h5 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; } |
|
43 | 43 | h6, .h6 { font-size: 1em; font-weight: @text-bold-weight; font-family: @text-bold; } |
|
44 | 44 | |
|
45 | 45 | // Breadcrumbs |
|
46 | 46 | .breadcrumbs { |
|
47 | 47 | font-size: @repo-title-fontsize; |
|
48 | 48 | margin: 0; |
|
49 | 49 | } |
|
50 | 50 | |
|
51 | 51 | .breadcrumbs_light { |
|
52 | 52 | float:left; |
|
53 | 53 | font-size: 1.3em; |
|
54 | 54 | line-height: 38px; |
|
55 | 55 | } |
|
56 | 56 | |
|
57 | 57 | // Body text |
|
58 | 58 | // ------------------------- |
|
59 | 59 | |
|
60 | 60 | p { |
|
61 | 61 | margin: 0 0 @textmargin 0; |
|
62 | 62 | padding: 0; |
|
63 | 63 | line-height: 2em; |
|
64 | 64 | } |
|
65 | 65 | |
|
66 | 66 | .lead { |
|
67 | 67 | margin-bottom: @textmargin; |
|
68 | 68 | font-weight: 300; |
|
69 | 69 | line-height: 1.4; |
|
70 | 70 | |
|
71 | 71 | @media (min-width: @screen-sm-min) { |
|
72 | 72 | font-size: (@basefontsize * 1.5); |
|
73 | 73 | } |
|
74 | 74 | } |
|
75 | 75 | |
|
76 | 76 | a, |
|
77 | 77 | .link { |
|
78 | 78 | color: @rcblue; |
|
79 | 79 | text-decoration: none; |
|
80 | 80 | outline: none; |
|
81 | 81 | cursor: pointer; |
|
82 | 82 | |
|
83 | 83 | &:focus { |
|
84 | 84 | outline: none; |
|
85 | 85 | } |
|
86 | 86 | |
|
87 | 87 | &:hover { |
|
88 | 88 | color: @rcdarkblue; |
|
89 | 89 | } |
|
90 | 90 | } |
|
91 | 91 | |
|
92 | 92 | img { |
|
93 | 93 | border: none; |
|
94 | 94 | outline: none; |
|
95 | 95 | } |
|
96 | 96 | |
|
97 | 97 | strong { |
|
98 | 98 | font-weight: @text-bold-weight; |
|
99 | 99 | font-family: @text-bold; |
|
100 | 100 | } |
|
101 | 101 | |
|
102 | 102 | em { |
|
103 | 103 | font-family: @text-italic; |
|
104 | 104 | font-style: italic; |
|
105 | 105 | } |
|
106 | 106 | |
|
107 | 107 | strong em, |
|
108 | 108 | em strong { |
|
109 | 109 | font-style: italic; |
|
110 | 110 | font-weight: @text-bold-italic-weight; |
|
111 | 111 | font-family: @text-bold-italic; |
|
112 | 112 | } |
|
113 | 113 | |
|
114 | 114 | //TODO: lisa: b and i are depreciated, but we are still using them in places. |
|
115 | 115 | // Should probably make some decision whether to keep or lose these. |
|
116 | 116 | b { |
|
117 | 117 | |
|
118 | 118 | } |
|
119 | 119 | |
|
120 | 120 | i { |
|
121 | 121 | font-style: normal; |
|
122 | 122 | } |
|
123 | 123 | |
|
124 | 124 | label { |
|
125 | 125 | color: @text-color; |
|
126 | 126 | |
|
127 | 127 | input[type="checkbox"] { |
|
128 | 128 | margin-right: 1em; |
|
129 | 129 | } |
|
130 | 130 | input[type="radio"] { |
|
131 | 131 | margin-right: 1em; |
|
132 | 132 | } |
|
133 | 133 | } |
|
134 | 134 | |
|
135 | 135 | code, |
|
136 | 136 | .code { |
|
137 | 137 | font-size: .95em; |
|
138 | 138 | font-family: @text-code; |
|
139 | 139 | color: @grey3; |
|
140 | 140 | |
|
141 | 141 | a { |
|
142 | 142 | color: lighten(@rcblue,10%) |
|
143 | 143 | } |
|
144 | 144 | } |
|
145 | 145 | |
|
146 | 146 | pre { |
|
147 | 147 | margin: 0; |
|
148 | 148 | padding: 0; |
|
149 | 149 | border: 0; |
|
150 | 150 | outline: 0; |
|
151 | 151 | font-size: @basefontsize*.95; |
|
152 | 152 | line-height: 1.4em; |
|
153 | 153 | font-family: @text-code; |
|
154 | 154 | color: @grey3; |
|
155 | 155 | } |
|
156 | 156 | |
|
157 | 157 | // Emphasis & misc |
|
158 | 158 | // ------------------------- |
|
159 | 159 | |
|
160 | 160 | small, |
|
161 | 161 | .small { |
|
162 | 162 | font-size: 75%; |
|
163 | 163 | font-weight: normal; |
|
164 | 164 | line-height: 1em; |
|
165 | 165 | } |
|
166 | 166 | |
|
167 | 167 | mark, |
|
168 | 168 | .mark { |
|
169 | 169 | padding: .2em; |
|
170 | 170 | } |
|
171 | 171 | |
|
172 | 172 | // Alignment |
|
173 | 173 | .text-left { text-align: left; } |
|
174 | 174 | .text-right { text-align: right; } |
|
175 | 175 | .text-center { text-align: center; } |
|
176 | 176 | .text-justify { text-align: justify; } |
|
177 | 177 | .text-nowrap { white-space: nowrap; } |
|
178 | 178 | |
|
179 | 179 | // Transformation |
|
180 | 180 | .text-lowercase { text-transform: lowercase; } |
|
181 | 181 | .text-uppercase { text-transform: uppercase; } |
|
182 | 182 | .text-capitalize { text-transform: capitalize; } |
|
183 | 183 | |
|
184 | 184 | // Contextual colors |
|
185 | 185 | .text-muted { |
|
186 | 186 | color: @grey4; |
|
187 | 187 | } |
|
188 | 188 | .text-primary { |
|
189 | 189 | color: @rcblue; |
|
190 | 190 | } |
|
191 | 191 | .text-success { |
|
192 | 192 | color: @alert1; |
|
193 | 193 | } |
|
194 | 194 | .text-info { |
|
195 | 195 | color: @alert4; |
|
196 | 196 | } |
|
197 | 197 | .text-warning { |
|
198 | 198 | color: @alert3; |
|
199 | 199 | } |
|
200 | 200 | .text-danger { |
|
201 | 201 | color: @alert2; |
|
202 | 202 | } |
|
203 | 203 | |
|
204 | 204 | // Contextual backgrounds |
|
205 | 205 | .bg-primary { |
|
206 | 206 | background-color: white; |
|
207 | 207 | } |
|
208 | 208 | .bg-success { |
|
209 | 209 | background-color: @alert1; |
|
210 | 210 | } |
|
211 | 211 | .bg-info { |
|
212 | 212 | background-color: @alert4; |
|
213 | 213 | } |
|
214 | 214 | .bg-warning { |
|
215 | 215 | background-color: @alert3; |
|
216 | 216 | } |
|
217 | 217 | .bg-danger { |
|
218 | 218 | background-color: @alert2; |
|
219 | 219 | } |
|
220 | 220 | |
|
221 | 221 | |
|
222 | 222 | // Page header |
|
223 | 223 | // ------------------------- |
|
224 | 224 | |
|
225 | 225 | .page-header { |
|
226 | 226 | margin: @pagepadding 0 @textmargin; |
|
227 | 227 | border-bottom: @border-thickness solid @grey5; |
|
228 | 228 | } |
|
229 | 229 | |
|
230 | 230 | .title { |
|
231 | 231 | clear: both; |
|
232 | 232 | float: left; |
|
233 | 233 | width: 100%; |
|
234 | 234 | margin: @pagepadding/2 0 @pagepadding; |
|
235 | 235 | |
|
236 | 236 | .breadcrumbs { |
|
237 | 237 | float: left; |
|
238 | 238 | clear: both; |
|
239 | 239 | width: 700px; |
|
240 | 240 | margin: 0; |
|
241 | 241 | |
|
242 | 242 | .q_filter_box { |
|
243 | 243 | margin-right: @padding; |
|
244 | 244 | } |
|
245 | 245 | } |
|
246 | 246 | |
|
247 | 247 | h1 a { |
|
248 | 248 | color: @rcblue; |
|
249 | 249 | } |
|
250 | 250 | |
|
251 | 251 | input{ |
|
252 | 252 | margin-right: @padding; |
|
253 | 253 | } |
|
254 | 254 | |
|
255 | 255 | h5, .h5 { |
|
256 | 256 | color: @grey1; |
|
257 | 257 | margin-bottom: @space; |
|
258 | 258 | |
|
259 | 259 | span { |
|
260 | 260 | display: inline-block; |
|
261 | 261 | } |
|
262 | 262 | } |
|
263 | 263 | |
|
264 | 264 | p { |
|
265 | 265 | margin-bottom: 0; |
|
266 | 266 | } |
|
267 | 267 | |
|
268 | 268 | .links { |
|
269 | 269 | float: right; |
|
270 | 270 | display: inline; |
|
271 | 271 | margin: 0; |
|
272 | 272 | padding-left: 0; |
|
273 | 273 | list-style: none; |
|
274 | 274 | text-align: right; |
|
275 | 275 | |
|
276 | 276 | li { |
|
277 | 277 | float: right; |
|
278 | 278 | list-style-type: none; |
|
279 | 279 | } |
|
280 | 280 | |
|
281 | 281 | a { |
|
282 | 282 | display: inline-block; |
|
283 | 283 | margin-left: @textmargin/2; |
|
284 | 284 | } |
|
285 | 285 | } |
|
286 | 286 | |
|
287 | 287 | .title-content { |
|
288 | 288 | float: left; |
|
289 | 289 | margin: 0; |
|
290 | 290 | padding: 0; |
|
291 | 291 | |
|
292 | 292 | & + .breadcrumbs { |
|
293 | 293 | margin-top: @padding; |
|
294 | 294 | } |
|
295 | 295 | |
|
296 | 296 | & + .links { |
|
297 | 297 | margin-top: -@button-padding; |
|
298 | 298 | |
|
299 | 299 | & + .breadcrumbs { |
|
300 | 300 | margin-top: @padding; |
|
301 | 301 | } |
|
302 | 302 | } |
|
303 | 303 | } |
|
304 | 304 | |
|
305 | 305 | .title-main { |
|
306 | 306 | font-size: @repo-title-fontsize; |
|
307 | 307 | } |
|
308 | 308 | |
|
309 | 309 | .title-description { |
|
310 | 310 | margin-top: .5em; |
|
311 | 311 | } |
|
312 | 312 | |
|
313 | 313 | .q_filter_box { |
|
314 | 314 | width: 200px; |
|
315 | 315 | } |
|
316 | 316 | |
|
317 | 317 | } |
|
318 | 318 | |
|
319 | 319 | #readme .title { |
|
320 | 320 | text-transform: none; |
|
321 | 321 | } |
|
322 | 322 | |
|
323 | 323 | // Lists |
|
324 | 324 | // ------------------------- |
|
325 | 325 | |
|
326 | 326 | // Unordered and Ordered lists |
|
327 | 327 | ul, |
|
328 | 328 | ol { |
|
329 | 329 | margin-top: 0; |
|
330 | 330 | margin-bottom: @textmargin; |
|
331 | 331 | ul, |
|
332 | 332 | ol { |
|
333 | 333 | margin-bottom: 0; |
|
334 | 334 | } |
|
335 | 335 | } |
|
336 | 336 | |
|
337 | 337 | li { |
|
338 | 338 | line-height: 2em; |
|
339 | 339 | } |
|
340 | 340 | |
|
341 | 341 | ul li { |
|
342 | 342 | position: relative; |
|
343 | 343 | list-style-type: disc; |
|
344 | 344 | |
|
345 | 345 | p:first-child { |
|
346 | 346 | display:inline; |
|
347 | 347 | } |
|
348 | 348 | } |
|
349 | 349 | |
|
350 | 350 | // List options |
|
351 | 351 | |
|
352 | 352 | // Unstyled keeps list items block level, just removes default browser padding and list-style |
|
353 | 353 | .list-unstyled { |
|
354 | 354 | padding-left: 0; |
|
355 | 355 | list-style: none; |
|
356 | 356 | li:before { content: none; } |
|
357 | 357 | } |
|
358 | 358 | |
|
359 | 359 | // Inline turns list items into inline-block |
|
360 | 360 | .list-inline { |
|
361 | 361 | .list-unstyled(); |
|
362 | 362 | margin-left: -5px; |
|
363 | 363 | |
|
364 | 364 | > li { |
|
365 | 365 | display: inline-block; |
|
366 | 366 | padding-left: 5px; |
|
367 | 367 | padding-right: 5px; |
|
368 | 368 | } |
|
369 | 369 | } |
|
370 | 370 | |
|
371 | 371 | // Description Lists |
|
372 | 372 | |
|
373 | 373 | dl { |
|
374 | 374 | margin-top: 0; // Remove browser default |
|
375 | 375 | margin-bottom: @textmargin; |
|
376 | 376 | } |
|
377 | 377 | |
|
378 | 378 | dt, |
|
379 | 379 | dd { |
|
380 | 380 | line-height: 1.4em; |
|
381 | 381 | } |
|
382 | 382 | |
|
383 | 383 | dt { |
|
384 | 384 | margin: @textmargin 0 0 0; |
|
385 | 385 | font-weight: @text-bold-weight; |
|
386 | 386 | font-family: @text-bold; |
|
387 | 387 | } |
|
388 | 388 | |
|
389 | 389 | dd { |
|
390 | 390 | margin-left: 0; // Undo browser default |
|
391 | 391 | } |
|
392 | 392 | |
|
393 | 393 | // Horizontal description lists |
|
394 | 394 | // Defaults to being stacked without any of the below styles applied, until the |
|
395 | 395 | // grid breakpoint is reached (default of ~768px). |
|
396 | 396 | // These are used in forms as well; see style guide. |
|
397 | 397 | // TODO: lisa: These should really not be used in forms. |
|
398 | 398 | |
|
399 | 399 | .dl-horizontal { |
|
400 | 400 | |
|
401 | 401 | overflow: hidden; |
|
402 | 402 | margin-bottom: @space; |
|
403 | 403 | |
|
404 | 404 | dt, dd { |
|
405 | 405 | float: left; |
|
406 | 406 | margin: 5px 0 5px 0; |
|
407 | 407 | } |
|
408 | 408 | |
|
409 | 409 | dt { |
|
410 | 410 | clear: left; |
|
411 | 411 | width: @label-width - @form-vertical-margin; |
|
412 | 412 | } |
|
413 | 413 | |
|
414 | 414 | dd { |
|
415 | 415 | &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present |
|
416 | 416 | margin-left: @form-vertical-margin; |
|
417 | 417 | max-width: @form-max-width - (@label-width - @form-vertical-margin) - @form-vertical-margin; |
|
418 | 418 | } |
|
419 | 419 | |
|
420 | 420 | pre { |
|
421 | 421 | margin: 0; |
|
422 | 422 | } |
|
423 | 423 | |
|
424 | 424 | &.settings { |
|
425 | 425 | dt { |
|
426 | 426 | text-align: left; |
|
427 | 427 | } |
|
428 | 428 | } |
|
429 | 429 | |
|
430 | 430 | @media (min-width: 768px) { |
|
431 | 431 | dt { |
|
432 | 432 | float: left; |
|
433 | 433 | width: 185px; |
|
434 | 434 | clear: left; |
|
435 | 435 | text-align: right; |
|
436 | 436 | } |
|
437 | 437 | dd { |
|
438 | 438 | margin-left: 20px; |
|
439 | 439 | } |
|
440 | 440 | } |
|
441 | 441 | } |
|
442 | 442 | |
|
443 | 443 | |
|
444 | 444 | // Misc |
|
445 | 445 | // ------------------------- |
|
446 | 446 | |
|
447 | 447 | // Abbreviations and acronyms |
|
448 | 448 | abbr[title], |
|
449 | 449 | abbr[data-original-title] { |
|
450 | 450 | cursor: help; |
|
451 | 451 | border-bottom: @border-thickness dotted @grey4; |
|
452 | 452 | } |
|
453 | 453 | .initialism { |
|
454 | 454 | font-size: 90%; |
|
455 | 455 | text-transform: uppercase; |
|
456 | 456 | } |
|
457 | 457 | |
|
458 | 458 | // Blockquotes |
|
459 | 459 | blockquote { |
|
460 | 460 | padding: 1em 2em; |
|
461 | 461 | margin: 0 0 2em; |
|
462 | 462 | font-size: @basefontsize; |
|
463 | 463 | border-left: 2px solid @grey6; |
|
464 | 464 | |
|
465 | 465 | p, |
|
466 | 466 | ul, |
|
467 | 467 | ol { |
|
468 | 468 | &:last-child { |
|
469 | 469 | margin-bottom: 0; |
|
470 | 470 | } |
|
471 | 471 | } |
|
472 | 472 | |
|
473 | 473 | footer, |
|
474 | 474 | small, |
|
475 | 475 | .small { |
|
476 | 476 | display: block; |
|
477 | 477 | font-size: 80%; |
|
478 | 478 | |
|
479 | 479 | &:before { |
|
480 | 480 | content: '\2014 \00A0'; // em dash, nbsp |
|
481 | 481 | } |
|
482 | 482 | } |
|
483 | 483 | } |
|
484 | 484 | |
|
485 | 485 | // Opposite alignment of blockquote |
|
486 | 486 | // |
|
487 | 487 | .blockquote-reverse, |
|
488 | 488 | blockquote.pull-right { |
|
489 | 489 | padding-right: 15px; |
|
490 | 490 | padding-left: 0; |
|
491 | 491 | border-right: 5px solid @grey6; |
|
492 | 492 | border-left: 0; |
|
493 | 493 | text-align: right; |
|
494 | 494 | |
|
495 | 495 | // Account for citation |
|
496 | 496 | footer, |
|
497 | 497 | small, |
|
498 | 498 | .small { |
|
499 | 499 | &:before { content: ''; } |
|
500 | 500 | &:after { |
|
501 | 501 | content: '\00A0 \2014'; // nbsp, em dash |
|
502 | 502 | } |
|
503 | 503 | } |
|
504 | 504 | } |
|
505 | 505 | |
|
506 | 506 | // Addresses |
|
507 | 507 | address { |
|
508 | 508 | margin-bottom: 2em; |
|
509 | 509 | font-style: normal; |
|
510 | 510 | line-height: 1.8em; |
|
511 | 511 | } |
|
512 | 512 | |
|
513 | 513 | .error-message { |
|
514 | 514 | display: block; |
|
515 | 515 | margin: @padding/3 0; |
|
516 | 516 | color: @alert2; |
|
517 | 517 | } |
|
518 | 518 | |
|
519 | 519 | .issue-tracker-link { |
|
520 | 520 | color: @rcblue; |
|
521 | 521 | } |
|
522 | 522 | |
|
523 | 523 | .info_text{ |
|
524 | 524 | font-size: @basefontsize; |
|
525 | 525 | color: @grey4; |
|
526 | 526 | font-family: @text-regular; |
|
527 | 527 | } |
|
528 | 528 | |
|
529 | .help-block-inline { | |
|
530 | margin: 0; | |
|
531 | } | |
|
532 | ||
|
529 | 533 | // help block text |
|
530 | 534 | .help-block { |
|
531 | 535 | display: block; |
|
532 | 536 | margin: 0 0 @padding; |
|
533 | 537 | color: @grey4; |
|
534 | 538 | font-family: @text-light; |
|
535 | 539 | &.pre-formatting { |
|
536 | 540 | white-space: pre-wrap; |
|
537 | 541 | } |
|
538 | 542 | } |
|
539 | 543 | |
|
540 | 544 | .error-message { |
|
541 | 545 | display: block; |
|
542 | 546 | margin: @padding/3 0; |
|
543 | 547 | color: @alert2; |
|
544 | 548 | } |
@@ -1,101 +1,133 b'' | |||
|
1 | 1 | // Global keyboard bindings |
|
2 | 2 | |
|
3 | 3 | function setRCMouseBindings(repoName, repoLandingRev) { |
|
4 | 4 | |
|
5 | 5 | /** custom callback for supressing mousetrap from firing */ |
|
6 | 6 | Mousetrap.stopCallback = function(e, element) { |
|
7 | 7 | // if the element has the class "mousetrap" then no need to stop |
|
8 | 8 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { |
|
9 | 9 | return false; |
|
10 | 10 | } |
|
11 | 11 | |
|
12 | 12 | // stop for input, select, and textarea |
|
13 | 13 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; |
|
14 | 14 | }; |
|
15 | 15 | |
|
16 | 16 | // general help "?" |
|
17 | 17 | Mousetrap.bind(['?'], function(e) { |
|
18 | 18 | $('#help_kb').modal({}); |
|
19 | 19 | }); |
|
20 | 20 | |
|
21 | 21 | // / open the quick filter |
|
22 | 22 | Mousetrap.bind(['/'], function(e) { |
|
23 | 23 | $('#main_filter').get(0).focus(); |
|
24 | 24 | |
|
25 | 25 | // return false to prevent default browser behavior |
|
26 | 26 | // and stop event from bubbling |
|
27 | 27 | return false; |
|
28 | 28 | }); |
|
29 | 29 | |
|
30 | 30 | // ctrl/command+b, show the the main bar |
|
31 | 31 | Mousetrap.bind(['command+b', 'ctrl+b'], function(e) { |
|
32 | 32 | var $headerInner = $('#header-inner'), |
|
33 | 33 | $content = $('#content'); |
|
34 | 34 | if ($headerInner.hasClass('hover') && $content.hasClass('hover')) { |
|
35 | 35 | $headerInner.removeClass('hover'); |
|
36 | 36 | $content.removeClass('hover'); |
|
37 | 37 | } else { |
|
38 | 38 | $headerInner.addClass('hover'); |
|
39 | 39 | $content.addClass('hover'); |
|
40 | 40 | } |
|
41 | 41 | return false; |
|
42 | 42 | }); |
|
43 | 43 | |
|
44 | 44 | // general nav g + action |
|
45 | 45 | Mousetrap.bind(['g h'], function(e) { |
|
46 | 46 | window.location = pyroutes.url('home'); |
|
47 | 47 | }); |
|
48 | 48 | Mousetrap.bind(['g g'], function(e) { |
|
49 | 49 | window.location = pyroutes.url('gists_show', {'private': 1}); |
|
50 | 50 | }); |
|
51 | 51 | Mousetrap.bind(['g G'], function(e) { |
|
52 | 52 | window.location = pyroutes.url('gists_show', {'public': 1}); |
|
53 | 53 | }); |
|
54 | ||
|
55 | Mousetrap.bind(['g 0'], function(e) { | |
|
56 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 0}); | |
|
57 | }); | |
|
58 | Mousetrap.bind(['g 1'], function(e) { | |
|
59 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 1}); | |
|
60 | }); | |
|
61 | Mousetrap.bind(['g 2'], function(e) { | |
|
62 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 2}); | |
|
63 | }); | |
|
64 | Mousetrap.bind(['g 3'], function(e) { | |
|
65 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 3}); | |
|
66 | }); | |
|
67 | Mousetrap.bind(['g 4'], function(e) { | |
|
68 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 4}); | |
|
69 | }); | |
|
70 | Mousetrap.bind(['g 5'], function(e) { | |
|
71 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 5}); | |
|
72 | }); | |
|
73 | Mousetrap.bind(['g 6'], function(e) { | |
|
74 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 6}); | |
|
75 | }); | |
|
76 | Mousetrap.bind(['g 7'], function(e) { | |
|
77 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 7}); | |
|
78 | }); | |
|
79 | Mousetrap.bind(['g 8'], function(e) { | |
|
80 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 8}); | |
|
81 | }); | |
|
82 | Mousetrap.bind(['g 9'], function(e) { | |
|
83 | window.location = pyroutes.url('my_account_goto_bookmark', {'bookmark_id': 9}); | |
|
84 | }); | |
|
85 | ||
|
54 | 86 | Mousetrap.bind(['n g'], function(e) { |
|
55 | 87 | window.location = pyroutes.url('gists_new'); |
|
56 | 88 | }); |
|
57 | 89 | Mousetrap.bind(['n r'], function(e) { |
|
58 | 90 | window.location = pyroutes.url('repo_new'); |
|
59 | 91 | }); |
|
60 | 92 | |
|
61 | if (repoName && repoName != '') { | |
|
93 | if (repoName && repoName !== '') { | |
|
62 | 94 | // nav in repo context |
|
63 | 95 | Mousetrap.bind(['g s'], function(e) { |
|
64 | 96 | window.location = pyroutes.url( |
|
65 | 97 | 'repo_summary', {'repo_name': repoName}); |
|
66 | 98 | }); |
|
67 | 99 | Mousetrap.bind(['g c'], function(e) { |
|
68 | 100 | window.location = pyroutes.url( |
|
69 | 101 | 'repo_changelog', {'repo_name': repoName}); |
|
70 | 102 | }); |
|
71 | 103 | Mousetrap.bind(['g F'], function(e) { |
|
72 | 104 | window.location = pyroutes.url( |
|
73 | 105 | 'repo_files', |
|
74 | 106 | { |
|
75 | 107 | 'repo_name': repoName, |
|
76 | 108 | 'commit_id': repoLandingRev, |
|
77 | 109 | 'f_path': '', |
|
78 | 110 | 'search': '1' |
|
79 | 111 | }); |
|
80 | 112 | }); |
|
81 | 113 | Mousetrap.bind(['g f'], function(e) { |
|
82 | 114 | window.location = pyroutes.url( |
|
83 | 115 | 'repo_files', |
|
84 | 116 | { |
|
85 | 117 | 'repo_name': repoName, |
|
86 | 118 | 'commit_id': repoLandingRev, |
|
87 | 119 | 'f_path': '' |
|
88 | 120 | }); |
|
89 | 121 | }); |
|
90 | 122 | Mousetrap.bind(['g o'], function(e) { |
|
91 | 123 | window.location = pyroutes.url( |
|
92 | 124 | 'edit_repo', {'repo_name': repoName}); |
|
93 | 125 | }); |
|
94 | 126 | Mousetrap.bind(['g O'], function(e) { |
|
95 | 127 | window.location = pyroutes.url( |
|
96 | 128 | 'edit_repo_perms', {'repo_name': repoName}); |
|
97 | 129 | }); |
|
98 | 130 | } |
|
99 | 131 | } |
|
100 | 132 | |
|
101 | 133 | setRCMouseBindings(templateContext.repo_name, templateContext.repo_landing_commit); |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now