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