##// END OF EJS Templates
model: trivial typo fixes
Thomas De Schampheleire -
r4919:494b04a8 default
parent child Browse files
Show More
@@ -1,189 +1,189 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 kallithea.model.changeset_status
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Changeset status conttroller
6 Changeset status controller
7 7
8 8 This file was forked by the Kallithea project in July 2014.
9 9 Original author and date, and relevant copyright and licensing information is below:
10 10 :created_on: Apr 30, 2012
11 11 :author: marcink
12 12 :copyright: (c) 2013 RhodeCode GmbH, and others.
13 13 :license: GPLv3, see LICENSE.md for more details.
14 14 """
15 15 # This program is free software: you can redistribute it and/or modify
16 16 # it under the terms of the GNU General Public License as published by
17 17 # the Free Software Foundation, either version 3 of the License, or
18 18 # (at your option) any later version.
19 19 #
20 20 # This program is distributed in the hope that it will be useful,
21 21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 23 # GNU General Public License for more details.
24 24 #
25 25 # You should have received a copy of the GNU General Public License
26 26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27 27
28 28
29 29 import logging
30 30 from collections import defaultdict
31 31 from sqlalchemy.orm import joinedload
32 32
33 33 from kallithea.model import BaseModel
34 34 from kallithea.model.db import ChangesetStatus, PullRequest
35 35 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39
40 40 class ChangesetStatusModel(BaseModel):
41 41
42 42 cls = ChangesetStatus
43 43
44 44 def __get_changeset_status(self, changeset_status):
45 45 return self._get_instance(ChangesetStatus, changeset_status)
46 46
47 47 def __get_pull_request(self, pull_request):
48 48 return self._get_instance(PullRequest, pull_request)
49 49
50 50 def _get_status_query(self, repo, revision, pull_request,
51 51 with_revisions=False):
52 52 repo = self._get_repo(repo)
53 53
54 54 q = ChangesetStatus.query()\
55 55 .filter(ChangesetStatus.repo == repo)
56 56 if not with_revisions:
57 57 q = q.filter(ChangesetStatus.version == 0)
58 58
59 59 if revision:
60 60 q = q.filter(ChangesetStatus.revision == revision)
61 61 elif pull_request:
62 62 pull_request = self.__get_pull_request(pull_request)
63 63 q = q.filter(ChangesetStatus.pull_request == pull_request)
64 64 else:
65 65 raise Exception('Please specify revision or pull_request')
66 66 q = q.order_by(ChangesetStatus.version.asc())
67 67 return q
68 68
69 69 def calculate_pull_request_result(self, pull_request):
70 70 """
71 71 Policy: approve if consensus. Only approve and reject counts as valid votes.
72 72 """
73 73
74 74 # collect latest votes from all voters
75 75 cs_statuses = dict()
76 76 for st in reversed(self.get_statuses(pull_request.org_repo,
77 77 pull_request=pull_request,
78 78 with_revisions=True)):
79 79 cs_statuses[st.author.username] = st
80 80 # collect votes from official reviewers
81 81 pull_request_reviewers = []
82 82 pull_request_pending_reviewers = []
83 83 approved_votes = 0
84 84 for r in pull_request.reviewers:
85 85 st = cs_statuses.get(r.user.username)
86 86 if st and st.status == ChangesetStatus.STATUS_APPROVED:
87 87 approved_votes += 1
88 88 if not st or st.status in (ChangesetStatus.STATUS_NOT_REVIEWED,
89 89 ChangesetStatus.STATUS_UNDER_REVIEW):
90 90 st = None
91 91 pull_request_pending_reviewers.append(r.user)
92 92 pull_request_reviewers.append((r.user, st))
93 93
94 94 # calculate result
95 95 result = ChangesetStatus.STATUS_UNDER_REVIEW
96 96 if approved_votes and approved_votes == len(pull_request.reviewers):
97 97 result = ChangesetStatus.STATUS_APPROVED
98 98
99 99 return (pull_request_reviewers,
100 100 pull_request_pending_reviewers,
101 101 result)
102 102
103 103 def get_statuses(self, repo, revision=None, pull_request=None,
104 104 with_revisions=False):
105 105 q = self._get_status_query(repo, revision, pull_request,
106 106 with_revisions)
107 107 q = q.options(joinedload('author'))
108 108 return q.all()
109 109
110 110 def get_status(self, repo, revision=None, pull_request=None, as_str=True):
111 111 """
112 112 Returns latest status of changeset for given revision or for given
113 113 pull request. Statuses are versioned inside a table itself and
114 114 version == 0 is always the current one
115 115
116 116 :param repo:
117 117 :param revision: 40char hash or None
118 118 :param pull_request: pull_request reference
119 119 :param as_str: return status as string not object
120 120 """
121 121 q = self._get_status_query(repo, revision, pull_request)
122 122
123 123 # need to use first here since there can be multiple statuses
124 124 # returned from pull_request
125 125 status = q.first()
126 126 if as_str:
127 127 return str(status.status) if status else ChangesetStatus.DEFAULT
128 128 return status
129 129
130 130 def set_status(self, repo, status, user, comment, revision=None,
131 131 pull_request=None, dont_allow_on_closed_pull_request=False):
132 132 """
133 133 Creates new status for changeset or updates the old ones bumping their
134 134 version, leaving the current status at the value of 'status'.
135 135
136 136 :param repo:
137 137 :param status:
138 138 :param user:
139 139 :param comment:
140 140 :param revision:
141 141 :param pull_request:
142 142 :param dont_allow_on_closed_pull_request: don't allow a status change
143 143 if last status was for pull request and it's closed. We shouldn't
144 144 mess around this manually
145 145 """
146 146 repo = self._get_repo(repo)
147 147
148 148 q = ChangesetStatus.query()
149 149 if revision is not None:
150 150 assert pull_request is None
151 151 q = q.filter(ChangesetStatus.repo == repo)
152 152 q = q.filter(ChangesetStatus.revision == revision)
153 153 revisions = [revision]
154 154 else:
155 155 assert pull_request is not None
156 156 pull_request = self.__get_pull_request(pull_request)
157 157 repo = pull_request.org_repo
158 158 q = q.filter(ChangesetStatus.repo == repo)
159 159 q = q.filter(ChangesetStatus.revision.in_(pull_request.revisions))
160 160 revisions = pull_request.revisions
161 161 cur_statuses = q.all()
162 162
163 163 #if statuses exists and last is associated with a closed pull request
164 164 # we need to check if we can allow this status change
165 165 if (dont_allow_on_closed_pull_request and cur_statuses
166 166 and getattr(cur_statuses[0].pull_request, 'status', '')
167 167 == PullRequest.STATUS_CLOSED):
168 168 raise StatusChangeOnClosedPullRequestError(
169 169 'Changing status on closed pull request is not allowed'
170 170 )
171 171
172 172 #update all current statuses with older version
173 173 for st in cur_statuses:
174 174 st.version += 1
175 175 self.sa.add(st)
176 176
177 177 new_statuses = []
178 178 for rev in revisions:
179 179 new_status = ChangesetStatus()
180 180 new_status.version = 0 # default
181 181 new_status.author = self._get_user(user)
182 182 new_status.repo = self._get_repo(repo)
183 183 new_status.status = status
184 184 new_status.comment = comment
185 185 new_status.revision = rev
186 186 new_status.pull_request = pull_request
187 187 new_statuses.append(new_status)
188 188 self.sa.add(new_status)
189 189 return new_statuses
@@ -1,2521 +1,2521 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.db
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 Database Models for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 08, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import os
29 29 import time
30 30 import logging
31 31 import datetime
32 32 import traceback
33 33 import hashlib
34 34 import collections
35 35 import functools
36 36
37 37 from sqlalchemy import *
38 38 from sqlalchemy.ext.hybrid import hybrid_property
39 39 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
40 40 from beaker.cache import cache_region, region_invalidate
41 41 from webob.exc import HTTPNotFound
42 42
43 43 from pylons.i18n.translation import lazy_ugettext as _
44 44
45 45 from kallithea import DB_PREFIX
46 46 from kallithea.lib.vcs import get_backend
47 47 from kallithea.lib.vcs.utils.helpers import get_scm
48 48 from kallithea.lib.vcs.exceptions import VCSError
49 49 from kallithea.lib.vcs.utils.lazy import LazyProperty
50 50 from kallithea.lib.vcs.backends.base import EmptyChangeset
51 51
52 52 from kallithea.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
53 53 safe_unicode, remove_prefix, time_to_datetime, aslist, Optional, safe_int, \
54 54 get_clone_url, urlreadable
55 55 from kallithea.lib.compat import json
56 56 from kallithea.lib.caching_query import FromCache
57 57
58 58 from kallithea.model.meta import Base, Session
59 59
60 60 URL_SEP = '/'
61 61 log = logging.getLogger(__name__)
62 62
63 63 #==============================================================================
64 64 # BASE CLASSES
65 65 #==============================================================================
66 66
67 67 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
68 68
69 69
70 70 class BaseModel(object):
71 71 """
72 Base Model for all classess
72 Base Model for all classes
73 73 """
74 74
75 75 @classmethod
76 76 def _get_keys(cls):
77 77 """return column names for this model """
78 78 return class_mapper(cls).c.keys()
79 79
80 80 def get_dict(self):
81 81 """
82 82 return dict with keys and values corresponding
83 83 to this model data """
84 84
85 85 d = {}
86 86 for k in self._get_keys():
87 87 d[k] = getattr(self, k)
88 88
89 89 # also use __json__() if present to get additional fields
90 90 _json_attr = getattr(self, '__json__', None)
91 91 if _json_attr:
92 92 # update with attributes from __json__
93 93 if callable(_json_attr):
94 94 _json_attr = _json_attr()
95 95 for k, val in _json_attr.iteritems():
96 96 d[k] = val
97 97 return d
98 98
99 99 def get_appstruct(self):
100 """return list with keys and values tupples corresponding
100 """return list with keys and values tuples corresponding
101 101 to this model data """
102 102
103 103 l = []
104 104 for k in self._get_keys():
105 105 l.append((k, getattr(self, k),))
106 106 return l
107 107
108 108 def populate_obj(self, populate_dict):
109 109 """populate model with data from given populate_dict"""
110 110
111 111 for k in self._get_keys():
112 112 if k in populate_dict:
113 113 setattr(self, k, populate_dict[k])
114 114
115 115 @classmethod
116 116 def query(cls):
117 117 return Session().query(cls)
118 118
119 119 @classmethod
120 120 def get(cls, id_):
121 121 if id_:
122 122 return cls.query().get(id_)
123 123
124 124 @classmethod
125 125 def get_or_404(cls, id_):
126 126 try:
127 127 id_ = int(id_)
128 128 except (TypeError, ValueError):
129 129 raise HTTPNotFound
130 130
131 131 res = cls.query().get(id_)
132 132 if not res:
133 133 raise HTTPNotFound
134 134 return res
135 135
136 136 @classmethod
137 137 def getAll(cls):
138 138 # deprecated and left for backward compatibility
139 139 return cls.get_all()
140 140
141 141 @classmethod
142 142 def get_all(cls):
143 143 return cls.query().all()
144 144
145 145 @classmethod
146 146 def delete(cls, id_):
147 147 obj = cls.query().get(id_)
148 148 Session().delete(obj)
149 149
150 150 def __repr__(self):
151 151 if hasattr(self, '__unicode__'):
152 152 # python repr needs to return str
153 153 try:
154 154 return safe_str(self.__unicode__())
155 155 except UnicodeDecodeError:
156 156 pass
157 157 return '<DB:%s>' % (self.__class__.__name__)
158 158
159 159
160 160 class Setting(Base, BaseModel):
161 161 __tablename__ = DB_PREFIX + 'settings'
162 162
163 163 __table_args__ = (
164 164 UniqueConstraint('app_settings_name'),
165 165 {'extend_existing': True, 'mysql_engine': 'InnoDB',
166 166 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
167 167 )
168 168
169 169 SETTINGS_TYPES = {
170 170 'str': safe_str,
171 171 'int': safe_int,
172 172 'unicode': safe_unicode,
173 173 'bool': str2bool,
174 174 'list': functools.partial(aslist, sep=',')
175 175 }
176 176 DEFAULT_UPDATE_URL = ''
177 177
178 178 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
179 179 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
180 180 _app_settings_value = Column("app_settings_value", String(4096, convert_unicode=False), nullable=True, unique=None, default=None)
181 181 _app_settings_type = Column("app_settings_type", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
182 182
183 183 def __init__(self, key='', val='', type='unicode'):
184 184 self.app_settings_name = key
185 185 self.app_settings_value = val
186 186 self.app_settings_type = type
187 187
188 188 @validates('_app_settings_value')
189 189 def validate_settings_value(self, key, val):
190 190 assert type(val) == unicode
191 191 return val
192 192
193 193 @hybrid_property
194 194 def app_settings_value(self):
195 195 v = self._app_settings_value
196 196 _type = self.app_settings_type
197 197 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
198 198 return converter(v)
199 199
200 200 @app_settings_value.setter
201 201 def app_settings_value(self, val):
202 202 """
203 203 Setter that will always make sure we use unicode in app_settings_value
204 204
205 205 :param val:
206 206 """
207 207 self._app_settings_value = safe_unicode(val)
208 208
209 209 @hybrid_property
210 210 def app_settings_type(self):
211 211 return self._app_settings_type
212 212
213 213 @app_settings_type.setter
214 214 def app_settings_type(self, val):
215 215 if val not in self.SETTINGS_TYPES:
216 216 raise Exception('type must be one of %s got %s'
217 217 % (self.SETTINGS_TYPES.keys(), val))
218 218 self._app_settings_type = val
219 219
220 220 def __unicode__(self):
221 221 return u"<%s('%s:%s[%s]')>" % (
222 222 self.__class__.__name__,
223 223 self.app_settings_name, self.app_settings_value, self.app_settings_type
224 224 )
225 225
226 226 @classmethod
227 227 def get_by_name(cls, key):
228 228 return cls.query()\
229 229 .filter(cls.app_settings_name == key).scalar()
230 230
231 231 @classmethod
232 232 def get_by_name_or_create(cls, key, val='', type='unicode'):
233 233 res = cls.get_by_name(key)
234 234 if not res:
235 235 res = cls(key, val, type)
236 236 return res
237 237
238 238 @classmethod
239 239 def create_or_update(cls, key, val=Optional(''), type=Optional('unicode')):
240 240 """
241 241 Creates or updates Kallithea setting. If updates are triggered, it will only
242 242 update parameters that are explicitly set. Optional instance will be skipped.
243 243
244 244 :param key:
245 245 :param val:
246 246 :param type:
247 247 :return:
248 248 """
249 249 res = cls.get_by_name(key)
250 250 if not res:
251 251 val = Optional.extract(val)
252 252 type = Optional.extract(type)
253 253 res = cls(key, val, type)
254 254 else:
255 255 res.app_settings_name = key
256 256 if not isinstance(val, Optional):
257 257 # update if set
258 258 res.app_settings_value = val
259 259 if not isinstance(type, Optional):
260 260 # update if set
261 261 res.app_settings_type = type
262 262 return res
263 263
264 264 @classmethod
265 265 def get_app_settings(cls, cache=False):
266 266
267 267 ret = cls.query()
268 268
269 269 if cache:
270 270 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
271 271
272 272 if not ret:
273 273 raise Exception('Could not get application settings !')
274 274 settings = {}
275 275 for each in ret:
276 276 settings[each.app_settings_name] = \
277 277 each.app_settings_value
278 278
279 279 return settings
280 280
281 281 @classmethod
282 282 def get_auth_plugins(cls, cache=False):
283 283 auth_plugins = cls.get_by_name("auth_plugins").app_settings_value
284 284 return auth_plugins
285 285
286 286 @classmethod
287 287 def get_auth_settings(cls, cache=False):
288 288 ret = cls.query()\
289 289 .filter(cls.app_settings_name.startswith('auth_')).all()
290 290 fd = {}
291 291 for row in ret:
292 292 fd[row.app_settings_name] = row.app_settings_value
293 293 return fd
294 294
295 295 @classmethod
296 296 def get_default_repo_settings(cls, cache=False, strip_prefix=False):
297 297 ret = cls.query()\
298 298 .filter(cls.app_settings_name.startswith('default_')).all()
299 299 fd = {}
300 300 for row in ret:
301 301 key = row.app_settings_name
302 302 if strip_prefix:
303 303 key = remove_prefix(key, prefix='default_')
304 304 fd.update({key: row.app_settings_value})
305 305
306 306 return fd
307 307
308 308 @classmethod
309 309 def get_server_info(cls):
310 310 import pkg_resources
311 311 import platform
312 312 import kallithea
313 313 from kallithea.lib.utils import check_git_version
314 314 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
315 315 info = {
316 316 'modules': sorted(mods, key=lambda k: k[0].lower()),
317 317 'py_version': platform.python_version(),
318 318 'platform': safe_unicode(platform.platform()),
319 319 'kallithea_version': kallithea.__version__,
320 320 'git_version': safe_unicode(check_git_version()),
321 321 'git_path': kallithea.CONFIG.get('git_path')
322 322 }
323 323 return info
324 324
325 325
326 326 class Ui(Base, BaseModel):
327 327 __tablename__ = DB_PREFIX + 'ui'
328 328 __table_args__ = (
329 329 UniqueConstraint('ui_key'),
330 330 {'extend_existing': True, 'mysql_engine': 'InnoDB',
331 331 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
332 332 )
333 333
334 334 HOOK_UPDATE = 'changegroup.update'
335 335 HOOK_REPO_SIZE = 'changegroup.repo_size'
336 336 HOOK_PUSH = 'changegroup.push_logger'
337 337 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
338 338 HOOK_PULL = 'outgoing.pull_logger'
339 339 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
340 340
341 341 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
342 342 ui_section = Column("ui_section", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
343 343 ui_key = Column("ui_key", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
344 344 ui_value = Column("ui_value", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
345 345 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
346 346
347 347 # def __init__(self, section='', key='', value=''):
348 348 # self.ui_section = section
349 349 # self.ui_key = key
350 350 # self.ui_value = value
351 351
352 352 @classmethod
353 353 def get_by_key(cls, key):
354 354 return cls.query().filter(cls.ui_key == key).scalar()
355 355
356 356 @classmethod
357 357 def get_builtin_hooks(cls):
358 358 q = cls.query()
359 359 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
360 360 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
361 361 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
362 362 return q.all()
363 363
364 364 @classmethod
365 365 def get_custom_hooks(cls):
366 366 q = cls.query()
367 367 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
368 368 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
369 369 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
370 370 q = q.filter(cls.ui_section == 'hooks')
371 371 return q.all()
372 372
373 373 @classmethod
374 374 def get_repos_location(cls):
375 375 return cls.get_by_key('/').ui_value
376 376
377 377 @classmethod
378 378 def create_or_update_hook(cls, key, val):
379 379 new_ui = cls.get_by_key(key) or cls()
380 380 new_ui.ui_section = 'hooks'
381 381 new_ui.ui_active = True
382 382 new_ui.ui_key = key
383 383 new_ui.ui_value = val
384 384
385 385 Session().add(new_ui)
386 386
387 387 def __repr__(self):
388 388 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
389 389 self.ui_key, self.ui_value)
390 390
391 391
392 392 class User(Base, BaseModel):
393 393 __tablename__ = 'users'
394 394 __table_args__ = (
395 395 UniqueConstraint('username'), UniqueConstraint('email'),
396 396 Index('u_username_idx', 'username'),
397 397 Index('u_email_idx', 'email'),
398 398 {'extend_existing': True, 'mysql_engine': 'InnoDB',
399 399 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
400 400 )
401 401 DEFAULT_USER = 'default'
402 402 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
403 403
404 404 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
405 405 username = Column("username", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
406 406 password = Column("password", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
407 407 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
408 408 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
409 409 name = Column("firstname", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
410 410 lastname = Column("lastname", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
411 411 _email = Column("email", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
412 412 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
413 413 extern_type = Column("extern_type", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
414 414 extern_name = Column("extern_name", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
415 415 api_key = Column("api_key", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
416 416 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
417 417 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
418 418 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
419 419
420 420 user_log = relationship('UserLog')
421 421 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
422 422
423 423 repositories = relationship('Repository')
424 424 repo_groups = relationship('RepoGroup')
425 425 user_groups = relationship('UserGroup')
426 426 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
427 427 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
428 428
429 429 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
430 430 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
431 431
432 432 group_member = relationship('UserGroupMember', cascade='all')
433 433
434 434 notifications = relationship('UserNotification', cascade='all')
435 435 # notifications assigned to this user
436 436 user_created_notifications = relationship('Notification', cascade='all')
437 437 # comments created by this user
438 438 user_comments = relationship('ChangesetComment', cascade='all')
439 439 #extra emails for this user
440 440 user_emails = relationship('UserEmailMap', cascade='all')
441 441 #extra api keys
442 442 user_api_keys = relationship('UserApiKeys', cascade='all')
443 443
444 444
445 445 @hybrid_property
446 446 def email(self):
447 447 return self._email
448 448
449 449 @email.setter
450 450 def email(self, val):
451 451 self._email = val.lower() if val else None
452 452
453 453 @property
454 454 def firstname(self):
455 455 # alias for future
456 456 return self.name
457 457
458 458 @property
459 459 def emails(self):
460 460 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
461 461 return [self.email] + [x.email for x in other]
462 462
463 463 @property
464 464 def api_keys(self):
465 465 other = UserApiKeys.query().filter(UserApiKeys.user==self).all()
466 466 return [self.api_key] + [x.api_key for x in other]
467 467
468 468 @property
469 469 def ip_addresses(self):
470 470 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
471 471 return [x.ip_addr for x in ret]
472 472
473 473 @property
474 474 def username_and_name(self):
475 475 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
476 476
477 477 @property
478 478 def full_name(self):
479 479 return '%s %s' % (self.firstname, self.lastname)
480 480
481 481 @property
482 482 def full_name_or_username(self):
483 483 return ('%s %s' % (self.firstname, self.lastname)
484 484 if (self.firstname and self.lastname) else self.username)
485 485
486 486 @property
487 487 def full_contact(self):
488 488 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
489 489
490 490 @property
491 491 def short_contact(self):
492 492 return '%s %s' % (self.firstname, self.lastname)
493 493
494 494 @property
495 495 def is_admin(self):
496 496 return self.admin
497 497
498 498 @property
499 499 def AuthUser(self):
500 500 """
501 501 Returns instance of AuthUser for this user
502 502 """
503 503 from kallithea.lib.auth import AuthUser
504 504 return AuthUser(user_id=self.user_id, api_key=self.api_key,
505 505 username=self.username)
506 506
507 507 @hybrid_property
508 508 def user_data(self):
509 509 if not self._user_data:
510 510 return {}
511 511
512 512 try:
513 513 return json.loads(self._user_data)
514 514 except TypeError:
515 515 return {}
516 516
517 517 @user_data.setter
518 518 def user_data(self, val):
519 519 try:
520 520 self._user_data = json.dumps(val)
521 521 except Exception:
522 522 log.error(traceback.format_exc())
523 523
524 524 def __unicode__(self):
525 525 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
526 526 self.user_id, self.username)
527 527
528 528 @classmethod
529 529 def get_by_username(cls, username, case_insensitive=False, cache=False):
530 530 if case_insensitive:
531 531 q = cls.query().filter(cls.username.ilike(username))
532 532 else:
533 533 q = cls.query().filter(cls.username == username)
534 534
535 535 if cache:
536 536 q = q.options(FromCache(
537 537 "sql_cache_short",
538 538 "get_user_%s" % _hash_key(username)
539 539 )
540 540 )
541 541 return q.scalar()
542 542
543 543 @classmethod
544 544 def get_by_api_key(cls, api_key, cache=False, fallback=True):
545 545 q = cls.query().filter(cls.api_key == api_key)
546 546
547 547 if cache:
548 548 q = q.options(FromCache("sql_cache_short",
549 549 "get_api_key_%s" % api_key))
550 550 res = q.scalar()
551 551
552 552 if fallback and not res:
553 553 #fallback to additional keys
554 554 _res = UserApiKeys.query()\
555 555 .filter(UserApiKeys.api_key == api_key)\
556 556 .filter(or_(UserApiKeys.expires == -1,
557 557 UserApiKeys.expires >= time.time()))\
558 558 .first()
559 559 if _res:
560 560 res = _res.user
561 561 return res
562 562
563 563 @classmethod
564 564 def get_by_email(cls, email, case_insensitive=False, cache=False):
565 565 if case_insensitive:
566 566 q = cls.query().filter(cls.email.ilike(email))
567 567 else:
568 568 q = cls.query().filter(cls.email == email)
569 569
570 570 if cache:
571 571 q = q.options(FromCache("sql_cache_short",
572 572 "get_email_key_%s" % email))
573 573
574 574 ret = q.scalar()
575 575 if ret is None:
576 576 q = UserEmailMap.query()
577 577 # try fetching in alternate email map
578 578 if case_insensitive:
579 579 q = q.filter(UserEmailMap.email.ilike(email))
580 580 else:
581 581 q = q.filter(UserEmailMap.email == email)
582 582 q = q.options(joinedload(UserEmailMap.user))
583 583 if cache:
584 584 q = q.options(FromCache("sql_cache_short",
585 585 "get_email_map_key_%s" % email))
586 586 ret = getattr(q.scalar(), 'user', None)
587 587
588 588 return ret
589 589
590 590 @classmethod
591 591 def get_from_cs_author(cls, author):
592 592 """
593 593 Tries to get User objects out of commit author string
594 594
595 595 :param author:
596 596 """
597 597 from kallithea.lib.helpers import email, author_name
598 598 # Valid email in the attribute passed, see if they're in the system
599 599 _email = email(author)
600 600 if _email:
601 601 user = cls.get_by_email(_email, case_insensitive=True)
602 602 if user:
603 603 return user
604 604 # Maybe we can match by username?
605 605 _author = author_name(author)
606 606 user = cls.get_by_username(_author, case_insensitive=True)
607 607 if user:
608 608 return user
609 609
610 610 def update_lastlogin(self):
611 611 """Update user lastlogin"""
612 612 self.last_login = datetime.datetime.now()
613 613 Session().add(self)
614 614 log.debug('updated user %s lastlogin' % self.username)
615 615
616 616 @classmethod
617 617 def get_first_admin(cls):
618 618 user = User.query().filter(User.admin == True).first()
619 619 if user is None:
620 620 raise Exception('Missing administrative account!')
621 621 return user
622 622
623 623 @classmethod
624 624 def get_default_user(cls, cache=False):
625 625 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
626 626 if user is None:
627 627 raise Exception('Missing default account!')
628 628 return user
629 629
630 630 def get_api_data(self, details=False):
631 631 """
632 632 Common function for generating user related data for API
633 633 """
634 634 user = self
635 635 data = dict(
636 636 user_id=user.user_id,
637 637 username=user.username,
638 638 firstname=user.name,
639 639 lastname=user.lastname,
640 640 email=user.email,
641 641 emails=user.emails,
642 642 active=user.active,
643 643 admin=user.admin,
644 644 )
645 645 if details:
646 646 data.update(dict(
647 647 extern_type=user.extern_type,
648 648 extern_name=user.extern_name,
649 649 api_key=user.api_key,
650 650 api_keys=user.api_keys,
651 651 last_login=user.last_login,
652 652 ip_addresses=user.ip_addresses
653 653 ))
654 654 return data
655 655
656 656 def __json__(self):
657 657 data = dict(
658 658 full_name=self.full_name,
659 659 full_name_or_username=self.full_name_or_username,
660 660 short_contact=self.short_contact,
661 661 full_contact=self.full_contact
662 662 )
663 663 data.update(self.get_api_data())
664 664 return data
665 665
666 666
667 667 class UserApiKeys(Base, BaseModel):
668 668 __tablename__ = 'user_api_keys'
669 669 __table_args__ = (
670 670 Index('uak_api_key_idx', 'api_key'),
671 671 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
672 672 UniqueConstraint('api_key'),
673 673 {'extend_existing': True, 'mysql_engine': 'InnoDB',
674 674 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
675 675 )
676 676 __mapper_args__ = {}
677 677
678 678 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
679 679 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
680 680 api_key = Column("api_key", String(255, convert_unicode=False), nullable=False, unique=True)
681 681 description = Column('description', UnicodeText(1024))
682 682 expires = Column('expires', Float(53), nullable=False)
683 683 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
684 684
685 685 user = relationship('User')
686 686
687 687 @property
688 688 def expired(self):
689 689 if self.expires == -1:
690 690 return False
691 691 return time.time() > self.expires
692 692
693 693
694 694 class UserEmailMap(Base, BaseModel):
695 695 __tablename__ = 'user_email_map'
696 696 __table_args__ = (
697 697 Index('uem_email_idx', 'email'),
698 698 UniqueConstraint('email'),
699 699 {'extend_existing': True, 'mysql_engine': 'InnoDB',
700 700 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
701 701 )
702 702 __mapper_args__ = {}
703 703
704 704 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
705 705 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
706 706 _email = Column("email", String(255, convert_unicode=False), nullable=True, unique=False, default=None)
707 707 user = relationship('User')
708 708
709 709 @validates('_email')
710 710 def validate_email(self, key, email):
711 711 # check if this email is not main one
712 712 main_email = Session().query(User).filter(User.email == email).scalar()
713 713 if main_email is not None:
714 714 raise AttributeError('email %s is present is user table' % email)
715 715 return email
716 716
717 717 @hybrid_property
718 718 def email(self):
719 719 return self._email
720 720
721 721 @email.setter
722 722 def email(self, val):
723 723 self._email = val.lower() if val else None
724 724
725 725
726 726 class UserIpMap(Base, BaseModel):
727 727 __tablename__ = 'user_ip_map'
728 728 __table_args__ = (
729 729 UniqueConstraint('user_id', 'ip_addr'),
730 730 {'extend_existing': True, 'mysql_engine': 'InnoDB',
731 731 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
732 732 )
733 733 __mapper_args__ = {}
734 734
735 735 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
736 736 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
737 737 ip_addr = Column("ip_addr", String(255, convert_unicode=False), nullable=True, unique=False, default=None)
738 738 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
739 739 user = relationship('User')
740 740
741 741 @classmethod
742 742 def _get_ip_range(cls, ip_addr):
743 743 from kallithea.lib import ipaddr
744 744 net = ipaddr.IPNetwork(address=ip_addr)
745 745 return [str(net.network), str(net.broadcast)]
746 746
747 747 def __json__(self):
748 748 return dict(
749 749 ip_addr=self.ip_addr,
750 750 ip_range=self._get_ip_range(self.ip_addr)
751 751 )
752 752
753 753 def __unicode__(self):
754 754 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
755 755 self.user_id, self.ip_addr)
756 756
757 757 class UserLog(Base, BaseModel):
758 758 __tablename__ = 'user_logs'
759 759 __table_args__ = (
760 760 {'extend_existing': True, 'mysql_engine': 'InnoDB',
761 761 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
762 762 )
763 763 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
764 764 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
765 765 username = Column("username", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
766 766 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
767 767 repository_name = Column("repository_name", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
768 768 user_ip = Column("user_ip", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
769 769 action = Column("action", UnicodeText(1200000, convert_unicode=False), nullable=True, unique=None, default=None)
770 770 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
771 771
772 772 def __unicode__(self):
773 773 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
774 774 self.repository_name,
775 775 self.action)
776 776
777 777 @property
778 778 def action_as_day(self):
779 779 return datetime.date(*self.action_date.timetuple()[:3])
780 780
781 781 user = relationship('User')
782 782 repository = relationship('Repository', cascade='')
783 783
784 784
785 785 class UserGroup(Base, BaseModel):
786 786 __tablename__ = 'users_groups'
787 787 __table_args__ = (
788 788 {'extend_existing': True, 'mysql_engine': 'InnoDB',
789 789 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
790 790 )
791 791
792 792 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
793 793 users_group_name = Column("users_group_name", String(255, convert_unicode=False), nullable=False, unique=True, default=None)
794 794 user_group_description = Column("user_group_description", String(10000, convert_unicode=False), nullable=True, unique=None, default=None)
795 795 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
796 796 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
797 797 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
798 798 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
799 799 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
800 800
801 801 members = relationship('UserGroupMember', cascade="all, delete-orphan")
802 802 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
803 803 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
804 804 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
805 805 user_user_group_to_perm = relationship('UserUserGroupToPerm ', cascade='all')
806 806 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
807 807
808 808 user = relationship('User')
809 809
810 810 @hybrid_property
811 811 def group_data(self):
812 812 if not self._group_data:
813 813 return {}
814 814
815 815 try:
816 816 return json.loads(self._group_data)
817 817 except TypeError:
818 818 return {}
819 819
820 820 @group_data.setter
821 821 def group_data(self, val):
822 822 try:
823 823 self._group_data = json.dumps(val)
824 824 except Exception:
825 825 log.error(traceback.format_exc())
826 826
827 827 def __unicode__(self):
828 828 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
829 829 self.users_group_id,
830 830 self.users_group_name)
831 831
832 832 @classmethod
833 833 def get_by_group_name(cls, group_name, cache=False,
834 834 case_insensitive=False):
835 835 if case_insensitive:
836 836 q = cls.query().filter(cls.users_group_name.ilike(group_name))
837 837 else:
838 838 q = cls.query().filter(cls.users_group_name == group_name)
839 839 if cache:
840 840 q = q.options(FromCache(
841 841 "sql_cache_short",
842 842 "get_group_%s" % _hash_key(group_name)
843 843 )
844 844 )
845 845 return q.scalar()
846 846
847 847 @classmethod
848 848 def get(cls, user_group_id, cache=False):
849 849 user_group = cls.query()
850 850 if cache:
851 851 user_group = user_group.options(FromCache("sql_cache_short",
852 852 "get_users_group_%s" % user_group_id))
853 853 return user_group.get(user_group_id)
854 854
855 855 def get_api_data(self, with_members=True):
856 856 user_group = self
857 857
858 858 data = dict(
859 859 users_group_id=user_group.users_group_id,
860 860 group_name=user_group.users_group_name,
861 861 group_description=user_group.user_group_description,
862 862 active=user_group.users_group_active,
863 863 owner=user_group.user.username,
864 864 )
865 865 if with_members:
866 866 members = []
867 867 for user in user_group.members:
868 868 user = user.user
869 869 members.append(user.get_api_data())
870 870 data['members'] = members
871 871
872 872 return data
873 873
874 874
875 875 class UserGroupMember(Base, BaseModel):
876 876 __tablename__ = 'users_groups_members'
877 877 __table_args__ = (
878 878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
879 879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
880 880 )
881 881
882 882 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
883 883 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
884 884 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
885 885
886 886 user = relationship('User')
887 887 users_group = relationship('UserGroup')
888 888
889 889 def __init__(self, gr_id='', u_id=''):
890 890 self.users_group_id = gr_id
891 891 self.user_id = u_id
892 892
893 893
894 894 class RepositoryField(Base, BaseModel):
895 895 __tablename__ = 'repositories_fields'
896 896 __table_args__ = (
897 897 UniqueConstraint('repository_id', 'field_key'), # no-multi field
898 898 {'extend_existing': True, 'mysql_engine': 'InnoDB',
899 899 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
900 900 )
901 901 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
902 902
903 903 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
904 904 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
905 905 field_key = Column("field_key", String(250, convert_unicode=False))
906 906 field_label = Column("field_label", String(1024, convert_unicode=False), nullable=False)
907 907 field_value = Column("field_value", String(10000, convert_unicode=False), nullable=False)
908 908 field_desc = Column("field_desc", String(1024, convert_unicode=False), nullable=False)
909 909 field_type = Column("field_type", String(255), nullable=False, unique=None)
910 910 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
911 911
912 912 repository = relationship('Repository')
913 913
914 914 @property
915 915 def field_key_prefixed(self):
916 916 return 'ex_%s' % self.field_key
917 917
918 918 @classmethod
919 919 def un_prefix_key(cls, key):
920 920 if key.startswith(cls.PREFIX):
921 921 return key[len(cls.PREFIX):]
922 922 return key
923 923
924 924 @classmethod
925 925 def get_by_key_name(cls, key, repo):
926 926 row = cls.query()\
927 927 .filter(cls.repository == repo)\
928 928 .filter(cls.field_key == key).scalar()
929 929 return row
930 930
931 931
932 932 class Repository(Base, BaseModel):
933 933 __tablename__ = 'repositories'
934 934 __table_args__ = (
935 935 UniqueConstraint('repo_name'),
936 936 Index('r_repo_name_idx', 'repo_name'),
937 937 {'extend_existing': True, 'mysql_engine': 'InnoDB',
938 938 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
939 939 )
940 940 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
941 941 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
942 942
943 943 STATE_CREATED = 'repo_state_created'
944 944 STATE_PENDING = 'repo_state_pending'
945 945 STATE_ERROR = 'repo_state_error'
946 946
947 947 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
948 948 repo_name = Column("repo_name", String(255, convert_unicode=False), nullable=False, unique=True, default=None)
949 949 repo_state = Column("repo_state", String(255), nullable=True)
950 950
951 951 clone_uri = Column("clone_uri", String(255, convert_unicode=False), nullable=True, unique=False, default=None)
952 952 repo_type = Column("repo_type", String(255, convert_unicode=False), nullable=False, unique=False, default=None)
953 953 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
954 954 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
955 955 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
956 956 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
957 957 description = Column("description", String(10000, convert_unicode=False), nullable=True, unique=None, default=None)
958 958 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
959 959 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
960 960 _landing_revision = Column("landing_revision", String(255, convert_unicode=False), nullable=False, unique=False, default=None)
961 961 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
962 962 _locked = Column("locked", String(255, convert_unicode=False), nullable=True, unique=False, default=None)
963 963 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) #JSON data
964 964
965 965 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
966 966 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
967 967
968 968 user = relationship('User')
969 969 fork = relationship('Repository', remote_side=repo_id)
970 970 group = relationship('RepoGroup')
971 971 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
972 972 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
973 973 stats = relationship('Statistics', cascade='all', uselist=False)
974 974
975 975 followers = relationship('UserFollowing',
976 976 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
977 977 cascade='all')
978 978 extra_fields = relationship('RepositoryField',
979 979 cascade="all, delete-orphan")
980 980
981 981 logs = relationship('UserLog')
982 982 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
983 983
984 984 pull_requests_org = relationship('PullRequest',
985 985 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
986 986 cascade="all, delete-orphan")
987 987
988 988 pull_requests_other = relationship('PullRequest',
989 989 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
990 990 cascade="all, delete-orphan")
991 991
992 992 def __unicode__(self):
993 993 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
994 994 safe_unicode(self.repo_name))
995 995
996 996 @hybrid_property
997 997 def landing_rev(self):
998 998 # always should return [rev_type, rev]
999 999 if self._landing_revision:
1000 1000 _rev_info = self._landing_revision.split(':')
1001 1001 if len(_rev_info) < 2:
1002 1002 _rev_info.insert(0, 'rev')
1003 1003 return [_rev_info[0], _rev_info[1]]
1004 1004 return [None, None]
1005 1005
1006 1006 @landing_rev.setter
1007 1007 def landing_rev(self, val):
1008 1008 if ':' not in val:
1009 1009 raise ValueError('value must be delimited with `:` and consist '
1010 1010 'of <rev_type>:<rev>, got %s instead' % val)
1011 1011 self._landing_revision = val
1012 1012
1013 1013 @hybrid_property
1014 1014 def locked(self):
1015 1015 # always should return [user_id, timelocked]
1016 1016 if self._locked:
1017 1017 _lock_info = self._locked.split(':')
1018 1018 return int(_lock_info[0]), _lock_info[1]
1019 1019 return [None, None]
1020 1020
1021 1021 @locked.setter
1022 1022 def locked(self, val):
1023 1023 if val and isinstance(val, (list, tuple)):
1024 1024 self._locked = ':'.join(map(str, val))
1025 1025 else:
1026 1026 self._locked = None
1027 1027
1028 1028 @hybrid_property
1029 1029 def changeset_cache(self):
1030 1030 from kallithea.lib.vcs.backends.base import EmptyChangeset
1031 1031 dummy = EmptyChangeset().__json__()
1032 1032 if not self._changeset_cache:
1033 1033 return dummy
1034 1034 try:
1035 1035 return json.loads(self._changeset_cache)
1036 1036 except TypeError:
1037 1037 return dummy
1038 1038
1039 1039 @changeset_cache.setter
1040 1040 def changeset_cache(self, val):
1041 1041 try:
1042 1042 self._changeset_cache = json.dumps(val)
1043 1043 except Exception:
1044 1044 log.error(traceback.format_exc())
1045 1045
1046 1046 @classmethod
1047 1047 def url_sep(cls):
1048 1048 return URL_SEP
1049 1049
1050 1050 @classmethod
1051 1051 def normalize_repo_name(cls, repo_name):
1052 1052 """
1053 1053 Normalizes os specific repo_name to the format internally stored inside
1054 dabatabase using URL_SEP
1054 database using URL_SEP
1055 1055
1056 1056 :param cls:
1057 1057 :param repo_name:
1058 1058 """
1059 1059 return cls.url_sep().join(repo_name.split(os.sep))
1060 1060
1061 1061 @classmethod
1062 1062 def get_by_repo_name(cls, repo_name):
1063 1063 q = Session().query(cls).filter(cls.repo_name == repo_name)
1064 1064 q = q.options(joinedload(Repository.fork))\
1065 1065 .options(joinedload(Repository.user))\
1066 1066 .options(joinedload(Repository.group))
1067 1067 return q.scalar()
1068 1068
1069 1069 @classmethod
1070 1070 def get_by_full_path(cls, repo_full_path):
1071 1071 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1072 1072 repo_name = cls.normalize_repo_name(repo_name)
1073 1073 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1074 1074
1075 1075 @classmethod
1076 1076 def get_repo_forks(cls, repo_id):
1077 1077 return cls.query().filter(Repository.fork_id == repo_id)
1078 1078
1079 1079 @classmethod
1080 1080 def base_path(cls):
1081 1081 """
1082 1082 Returns base path where all repos are stored
1083 1083
1084 1084 :param cls:
1085 1085 """
1086 1086 q = Session().query(Ui)\
1087 1087 .filter(Ui.ui_key == cls.url_sep())
1088 1088 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1089 1089 return q.one().ui_value
1090 1090
1091 1091 @property
1092 1092 def forks(self):
1093 1093 """
1094 1094 Return forks of this repo
1095 1095 """
1096 1096 return Repository.get_repo_forks(self.repo_id)
1097 1097
1098 1098 @property
1099 1099 def parent(self):
1100 1100 """
1101 1101 Returns fork parent
1102 1102 """
1103 1103 return self.fork
1104 1104
1105 1105 @property
1106 1106 def just_name(self):
1107 1107 return self.repo_name.split(Repository.url_sep())[-1]
1108 1108
1109 1109 @property
1110 1110 def groups_with_parents(self):
1111 1111 groups = []
1112 1112 if self.group is None:
1113 1113 return groups
1114 1114
1115 1115 cur_gr = self.group
1116 1116 groups.insert(0, cur_gr)
1117 1117 while 1:
1118 1118 gr = getattr(cur_gr, 'parent_group', None)
1119 1119 cur_gr = cur_gr.parent_group
1120 1120 if gr is None:
1121 1121 break
1122 1122 groups.insert(0, gr)
1123 1123
1124 1124 return groups
1125 1125
1126 1126 @property
1127 1127 def groups_and_repo(self):
1128 1128 return self.groups_with_parents, self.just_name, self.repo_name
1129 1129
1130 1130 @LazyProperty
1131 1131 def repo_path(self):
1132 1132 """
1133 1133 Returns base full path for that repository means where it actually
1134 1134 exists on a filesystem
1135 1135 """
1136 1136 q = Session().query(Ui).filter(Ui.ui_key ==
1137 1137 Repository.url_sep())
1138 1138 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1139 1139 return q.one().ui_value
1140 1140
1141 1141 @property
1142 1142 def repo_full_path(self):
1143 1143 p = [self.repo_path]
1144 1144 # we need to split the name by / since this is how we store the
1145 1145 # names in the database, but that eventually needs to be converted
1146 1146 # into a valid system path
1147 1147 p += self.repo_name.split(Repository.url_sep())
1148 1148 return os.path.join(*map(safe_unicode, p))
1149 1149
1150 1150 @property
1151 1151 def cache_keys(self):
1152 1152 """
1153 1153 Returns associated cache keys for that repo
1154 1154 """
1155 1155 return CacheInvalidation.query()\
1156 1156 .filter(CacheInvalidation.cache_args == self.repo_name)\
1157 1157 .order_by(CacheInvalidation.cache_key)\
1158 1158 .all()
1159 1159
1160 1160 def get_new_name(self, repo_name):
1161 1161 """
1162 1162 returns new full repository name based on assigned group and new new
1163 1163
1164 1164 :param group_name:
1165 1165 """
1166 1166 path_prefix = self.group.full_path_splitted if self.group else []
1167 1167 return Repository.url_sep().join(path_prefix + [repo_name])
1168 1168
1169 1169 @property
1170 1170 def _ui(self):
1171 1171 """
1172 1172 Creates an db based ui object for this repository
1173 1173 """
1174 1174 from kallithea.lib.utils import make_ui
1175 1175 return make_ui('db', clear_session=False)
1176 1176
1177 1177 @classmethod
1178 1178 def is_valid(cls, repo_name):
1179 1179 """
1180 1180 returns True if given repo name is a valid filesystem repository
1181 1181
1182 1182 :param cls:
1183 1183 :param repo_name:
1184 1184 """
1185 1185 from kallithea.lib.utils import is_valid_repo
1186 1186
1187 1187 return is_valid_repo(repo_name, cls.base_path())
1188 1188
1189 1189 def get_api_data(self):
1190 1190 """
1191 1191 Common function for generating repo api data
1192 1192
1193 1193 """
1194 1194 repo = self
1195 1195 data = dict(
1196 1196 repo_id=repo.repo_id,
1197 1197 repo_name=repo.repo_name,
1198 1198 repo_type=repo.repo_type,
1199 1199 clone_uri=repo.clone_uri,
1200 1200 private=repo.private,
1201 1201 created_on=repo.created_on,
1202 1202 description=repo.description,
1203 1203 landing_rev=repo.landing_rev,
1204 1204 owner=repo.user.username,
1205 1205 fork_of=repo.fork.repo_name if repo.fork else None,
1206 1206 enable_statistics=repo.enable_statistics,
1207 1207 enable_locking=repo.enable_locking,
1208 1208 enable_downloads=repo.enable_downloads,
1209 1209 last_changeset=repo.changeset_cache,
1210 1210 locked_by=User.get(self.locked[0]).get_api_data() \
1211 1211 if self.locked[0] else None,
1212 1212 locked_date=time_to_datetime(self.locked[1]) \
1213 1213 if self.locked[1] else None
1214 1214 )
1215 1215 rc_config = Setting.get_app_settings()
1216 1216 repository_fields = str2bool(rc_config.get('repository_fields'))
1217 1217 if repository_fields:
1218 1218 for f in self.extra_fields:
1219 1219 data[f.field_key_prefixed] = f.field_value
1220 1220
1221 1221 return data
1222 1222
1223 1223 @classmethod
1224 1224 def lock(cls, repo, user_id, lock_time=None):
1225 1225 if lock_time is not None:
1226 1226 lock_time = time.time()
1227 1227 repo.locked = [user_id, lock_time]
1228 1228 Session().add(repo)
1229 1229 Session().commit()
1230 1230
1231 1231 @classmethod
1232 1232 def unlock(cls, repo):
1233 1233 repo.locked = None
1234 1234 Session().add(repo)
1235 1235 Session().commit()
1236 1236
1237 1237 @classmethod
1238 1238 def getlock(cls, repo):
1239 1239 return repo.locked
1240 1240
1241 1241 @property
1242 1242 def last_db_change(self):
1243 1243 return self.updated_on
1244 1244
1245 1245 @property
1246 1246 def clone_uri_hidden(self):
1247 1247 clone_uri = self.clone_uri
1248 1248 if clone_uri:
1249 1249 import urlobject
1250 1250 url_obj = urlobject.URLObject(self.clone_uri)
1251 1251 if url_obj.password:
1252 1252 clone_uri = url_obj.with_password('*****')
1253 1253 return clone_uri
1254 1254
1255 1255 def clone_url(self, **override):
1256 1256 import kallithea.lib.helpers as h
1257 1257 qualified_home_url = h.canonical_url('home')
1258 1258
1259 1259 uri_tmpl = None
1260 1260 if 'with_id' in override:
1261 1261 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1262 1262 del override['with_id']
1263 1263
1264 1264 if 'uri_tmpl' in override:
1265 1265 uri_tmpl = override['uri_tmpl']
1266 1266 del override['uri_tmpl']
1267 1267
1268 1268 # we didn't override our tmpl from **overrides
1269 1269 if not uri_tmpl:
1270 1270 uri_tmpl = self.DEFAULT_CLONE_URI
1271 1271 try:
1272 1272 from pylons import tmpl_context as c
1273 1273 uri_tmpl = c.clone_uri_tmpl
1274 1274 except AttributeError:
1275 1275 # in any case if we call this outside of request context,
1276 1276 # ie, not having tmpl_context set up
1277 1277 pass
1278 1278
1279 1279 return get_clone_url(uri_tmpl=uri_tmpl,
1280 1280 qualifed_home_url=qualified_home_url,
1281 1281 repo_name=self.repo_name,
1282 1282 repo_id=self.repo_id, **override)
1283 1283
1284 1284 def set_state(self, state):
1285 1285 self.repo_state = state
1286 1286 Session().add(self)
1287 1287 #==========================================================================
1288 1288 # SCM PROPERTIES
1289 1289 #==========================================================================
1290 1290
1291 1291 def get_changeset(self, rev=None):
1292 1292 return get_changeset_safe(self.scm_instance, rev)
1293 1293
1294 1294 def get_landing_changeset(self):
1295 1295 """
1296 1296 Returns landing changeset, or if that doesn't exist returns the tip
1297 1297 """
1298 1298 _rev_type, _rev = self.landing_rev
1299 1299 cs = self.get_changeset(_rev)
1300 1300 if isinstance(cs, EmptyChangeset):
1301 1301 return self.get_changeset()
1302 1302 return cs
1303 1303
1304 1304 def update_changeset_cache(self, cs_cache=None):
1305 1305 """
1306 1306 Update cache of last changeset for repository, keys should be::
1307 1307
1308 1308 short_id
1309 1309 raw_id
1310 1310 revision
1311 1311 message
1312 1312 date
1313 1313 author
1314 1314
1315 1315 :param cs_cache:
1316 1316 """
1317 1317 from kallithea.lib.vcs.backends.base import BaseChangeset
1318 1318 if cs_cache is None:
1319 1319 cs_cache = EmptyChangeset()
1320 1320 # use no-cache version here
1321 1321 scm_repo = self.scm_instance_no_cache()
1322 1322 if scm_repo:
1323 1323 cs_cache = scm_repo.get_changeset()
1324 1324
1325 1325 if isinstance(cs_cache, BaseChangeset):
1326 1326 cs_cache = cs_cache.__json__()
1327 1327
1328 1328 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1329 1329 _default = datetime.datetime.fromtimestamp(0)
1330 1330 last_change = cs_cache.get('date') or _default
1331 1331 log.debug('updated repo %s with new cs cache %s'
1332 1332 % (self.repo_name, cs_cache))
1333 1333 self.updated_on = last_change
1334 1334 self.changeset_cache = cs_cache
1335 1335 Session().add(self)
1336 1336 Session().commit()
1337 1337 else:
1338 1338 log.debug('changeset_cache for %s already up to date with %s'
1339 1339 % (self.repo_name, cs_cache['raw_id']))
1340 1340
1341 1341 @property
1342 1342 def tip(self):
1343 1343 return self.get_changeset('tip')
1344 1344
1345 1345 @property
1346 1346 def author(self):
1347 1347 return self.tip.author
1348 1348
1349 1349 @property
1350 1350 def last_change(self):
1351 1351 return self.scm_instance.last_change
1352 1352
1353 1353 def get_comments(self, revisions=None):
1354 1354 """
1355 1355 Returns comments for this repository grouped by revisions
1356 1356
1357 1357 :param revisions: filter query by revisions only
1358 1358 """
1359 1359 cmts = ChangesetComment.query()\
1360 1360 .filter(ChangesetComment.repo == self)
1361 1361 if revisions is not None:
1362 1362 if not revisions:
1363 1363 return [] # don't use sql 'in' on empty set
1364 1364 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1365 1365 grouped = collections.defaultdict(list)
1366 1366 for cmt in cmts.all():
1367 1367 grouped[cmt.revision].append(cmt)
1368 1368 return grouped
1369 1369
1370 1370 def statuses(self, revisions):
1371 1371 """
1372 1372 Returns statuses for this repository.
1373 1373 PRs without any votes do _not_ show up as unreviewed.
1374 1374
1375 1375 :param revisions: list of revisions to get statuses for
1376 1376 """
1377 1377 if not revisions:
1378 1378 return {}
1379 1379
1380 1380 statuses = ChangesetStatus.query()\
1381 1381 .filter(ChangesetStatus.repo == self)\
1382 1382 .filter(ChangesetStatus.version == 0)\
1383 1383 .filter(ChangesetStatus.revision.in_(revisions))
1384 1384
1385 1385 grouped = {}
1386 1386 for stat in statuses.all():
1387 1387 pr_id = pr_repo = None
1388 1388 if stat.pull_request:
1389 1389 pr_id = stat.pull_request.pull_request_id
1390 1390 pr_repo = stat.pull_request.other_repo.repo_name
1391 1391 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1392 1392 pr_id, pr_repo]
1393 1393 return grouped
1394 1394
1395 1395 def _repo_size(self):
1396 1396 from kallithea.lib import helpers as h
1397 1397 log.debug('calculating repository size...')
1398 1398 return h.format_byte_size(self.scm_instance.size)
1399 1399
1400 1400 #==========================================================================
1401 1401 # SCM CACHE INSTANCE
1402 1402 #==========================================================================
1403 1403
1404 1404 def set_invalidate(self):
1405 1405 """
1406 1406 Mark caches of this repo as invalid.
1407 1407 """
1408 1408 CacheInvalidation.set_invalidate(self.repo_name)
1409 1409
1410 1410 def scm_instance_no_cache(self):
1411 1411 return self.__get_instance()
1412 1412
1413 1413 @property
1414 1414 def scm_instance(self):
1415 1415 import kallithea
1416 1416 full_cache = str2bool(kallithea.CONFIG.get('vcs_full_cache'))
1417 1417 if full_cache:
1418 1418 return self.scm_instance_cached()
1419 1419 return self.__get_instance()
1420 1420
1421 1421 def scm_instance_cached(self, valid_cache_keys=None):
1422 1422 @cache_region('long_term')
1423 1423 def _c(repo_name):
1424 1424 return self.__get_instance()
1425 1425 rn = self.repo_name
1426 1426
1427 1427 valid = CacheInvalidation.test_and_set_valid(rn, None, valid_cache_keys=valid_cache_keys)
1428 1428 if not valid:
1429 1429 log.debug('Cache for %s invalidated, getting new object' % (rn))
1430 1430 region_invalidate(_c, None, rn)
1431 1431 else:
1432 1432 log.debug('Getting scm_instance of %s from cache' % (rn))
1433 1433 return _c(rn)
1434 1434
1435 1435 def __get_instance(self):
1436 1436 repo_full_path = self.repo_full_path
1437 1437
1438 1438 alias = get_scm(repo_full_path)[0]
1439 1439 log.debug('Creating instance of %s repository from %s'
1440 1440 % (alias, repo_full_path))
1441 1441 backend = get_backend(alias)
1442 1442
1443 1443 if alias == 'hg':
1444 1444 repo = backend(safe_str(repo_full_path), create=False,
1445 1445 baseui=self._ui)
1446 1446 else:
1447 1447 repo = backend(repo_full_path, create=False)
1448 1448
1449 1449 return repo
1450 1450
1451 1451 def __json__(self):
1452 1452 return dict(landing_rev = self.landing_rev)
1453 1453
1454 1454 class RepoGroup(Base, BaseModel):
1455 1455 __tablename__ = 'groups'
1456 1456 __table_args__ = (
1457 1457 UniqueConstraint('group_name', 'group_parent_id'),
1458 1458 CheckConstraint('group_id != group_parent_id'),
1459 1459 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1460 1460 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1461 1461 )
1462 1462 __mapper_args__ = {'order_by': 'group_name'}
1463 1463
1464 1464 SEP = ' &raquo; '
1465 1465
1466 1466 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1467 1467 group_name = Column("group_name", String(255, convert_unicode=False), nullable=False, unique=True, default=None)
1468 1468 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1469 1469 group_description = Column("group_description", String(10000, convert_unicode=False), nullable=True, unique=None, default=None)
1470 1470 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1471 1471 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1472 1472 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1473 1473
1474 1474 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1475 1475 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1476 1476 parent_group = relationship('RepoGroup', remote_side=group_id)
1477 1477 user = relationship('User')
1478 1478
1479 1479 def __init__(self, group_name='', parent_group=None):
1480 1480 self.group_name = group_name
1481 1481 self.parent_group = parent_group
1482 1482
1483 1483 def __unicode__(self):
1484 1484 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
1485 1485 self.group_name)
1486 1486
1487 1487 @classmethod
1488 1488 def _generate_choice(cls, repo_group):
1489 1489 from webhelpers.html import literal as _literal
1490 1490 _name = lambda k: _literal(cls.SEP.join(k))
1491 1491 return repo_group.group_id, _name(repo_group.full_path_splitted)
1492 1492
1493 1493 @classmethod
1494 1494 def groups_choices(cls, groups=None, show_empty_group=True):
1495 1495 if not groups:
1496 1496 groups = cls.query().all()
1497 1497
1498 1498 repo_groups = []
1499 1499 if show_empty_group:
1500 1500 repo_groups = [('-1', u'-- %s --' % _('top level'))]
1501 1501
1502 1502 repo_groups.extend([cls._generate_choice(x) for x in groups])
1503 1503
1504 1504 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(cls.SEP)[0])
1505 1505 return repo_groups
1506 1506
1507 1507 @classmethod
1508 1508 def url_sep(cls):
1509 1509 return URL_SEP
1510 1510
1511 1511 @classmethod
1512 1512 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1513 1513 if case_insensitive:
1514 1514 gr = cls.query()\
1515 1515 .filter(cls.group_name.ilike(group_name))
1516 1516 else:
1517 1517 gr = cls.query()\
1518 1518 .filter(cls.group_name == group_name)
1519 1519 if cache:
1520 1520 gr = gr.options(FromCache(
1521 1521 "sql_cache_short",
1522 1522 "get_group_%s" % _hash_key(group_name)
1523 1523 )
1524 1524 )
1525 1525 return gr.scalar()
1526 1526
1527 1527 @property
1528 1528 def parents(self):
1529 1529 parents_recursion_limit = 10
1530 1530 groups = []
1531 1531 if self.parent_group is None:
1532 1532 return groups
1533 1533 cur_gr = self.parent_group
1534 1534 groups.insert(0, cur_gr)
1535 1535 cnt = 0
1536 1536 while 1:
1537 1537 cnt += 1
1538 1538 gr = getattr(cur_gr, 'parent_group', None)
1539 1539 cur_gr = cur_gr.parent_group
1540 1540 if gr is None:
1541 1541 break
1542 1542 if cnt == parents_recursion_limit:
1543 # this will prevent accidental infinit loops
1543 # this will prevent accidental infinite loops
1544 1544 log.error(('more than %s parents found for group %s, stopping '
1545 1545 'recursive parent fetching' % (parents_recursion_limit, self)))
1546 1546 break
1547 1547
1548 1548 groups.insert(0, gr)
1549 1549 return groups
1550 1550
1551 1551 @property
1552 1552 def children(self):
1553 1553 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1554 1554
1555 1555 @property
1556 1556 def name(self):
1557 1557 return self.group_name.split(RepoGroup.url_sep())[-1]
1558 1558
1559 1559 @property
1560 1560 def full_path(self):
1561 1561 return self.group_name
1562 1562
1563 1563 @property
1564 1564 def full_path_splitted(self):
1565 1565 return self.group_name.split(RepoGroup.url_sep())
1566 1566
1567 1567 @property
1568 1568 def repositories(self):
1569 1569 return Repository.query()\
1570 1570 .filter(Repository.group == self)\
1571 1571 .order_by(Repository.repo_name)
1572 1572
1573 1573 @property
1574 1574 def repositories_recursive_count(self):
1575 1575 cnt = self.repositories.count()
1576 1576
1577 1577 def children_count(group):
1578 1578 cnt = 0
1579 1579 for child in group.children:
1580 1580 cnt += child.repositories.count()
1581 1581 cnt += children_count(child)
1582 1582 return cnt
1583 1583
1584 1584 return cnt + children_count(self)
1585 1585
1586 1586 def _recursive_objects(self, include_repos=True):
1587 1587 all_ = []
1588 1588
1589 1589 def _get_members(root_gr):
1590 1590 if include_repos:
1591 1591 for r in root_gr.repositories:
1592 1592 all_.append(r)
1593 1593 childs = root_gr.children.all()
1594 1594 if childs:
1595 1595 for gr in childs:
1596 1596 all_.append(gr)
1597 1597 _get_members(gr)
1598 1598
1599 1599 _get_members(self)
1600 1600 return [self] + all_
1601 1601
1602 1602 def recursive_groups_and_repos(self):
1603 1603 """
1604 1604 Recursive return all groups, with repositories in those groups
1605 1605 """
1606 1606 return self._recursive_objects()
1607 1607
1608 1608 def recursive_groups(self):
1609 1609 """
1610 1610 Returns all children groups for this group including children of children
1611 1611 """
1612 1612 return self._recursive_objects(include_repos=False)
1613 1613
1614 1614 def get_new_name(self, group_name):
1615 1615 """
1616 1616 returns new full group name based on parent and new name
1617 1617
1618 1618 :param group_name:
1619 1619 """
1620 1620 path_prefix = (self.parent_group.full_path_splitted if
1621 1621 self.parent_group else [])
1622 1622 return RepoGroup.url_sep().join(path_prefix + [group_name])
1623 1623
1624 1624 def get_api_data(self):
1625 1625 """
1626 1626 Common function for generating api data
1627 1627
1628 1628 """
1629 1629 group = self
1630 1630 data = dict(
1631 1631 group_id=group.group_id,
1632 1632 group_name=group.group_name,
1633 1633 group_description=group.group_description,
1634 1634 parent_group=group.parent_group.group_name if group.parent_group else None,
1635 1635 repositories=[x.repo_name for x in group.repositories],
1636 1636 owner=group.user.username
1637 1637 )
1638 1638 return data
1639 1639
1640 1640
1641 1641 class Permission(Base, BaseModel):
1642 1642 __tablename__ = 'permissions'
1643 1643 __table_args__ = (
1644 1644 Index('p_perm_name_idx', 'permission_name'),
1645 1645 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1646 1646 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1647 1647 )
1648 1648 PERMS = [
1649 1649 ('hg.admin', _('Kallithea Administrator')),
1650 1650
1651 1651 ('repository.none', _('Repository no access')),
1652 1652 ('repository.read', _('Repository read access')),
1653 1653 ('repository.write', _('Repository write access')),
1654 1654 ('repository.admin', _('Repository admin access')),
1655 1655
1656 1656 ('group.none', _('Repository group no access')),
1657 1657 ('group.read', _('Repository group read access')),
1658 1658 ('group.write', _('Repository group write access')),
1659 1659 ('group.admin', _('Repository group admin access')),
1660 1660
1661 1661 ('usergroup.none', _('User group no access')),
1662 1662 ('usergroup.read', _('User group read access')),
1663 1663 ('usergroup.write', _('User group write access')),
1664 1664 ('usergroup.admin', _('User group admin access')),
1665 1665
1666 1666 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
1667 1667 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
1668 1668
1669 1669 ('hg.usergroup.create.false', _('User Group creation disabled')),
1670 1670 ('hg.usergroup.create.true', _('User Group creation enabled')),
1671 1671
1672 1672 ('hg.create.none', _('Repository creation disabled')),
1673 1673 ('hg.create.repository', _('Repository creation enabled')),
1674 1674 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
1675 1675 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
1676 1676
1677 1677 ('hg.fork.none', _('Repository forking disabled')),
1678 1678 ('hg.fork.repository', _('Repository forking enabled')),
1679 1679
1680 1680 ('hg.register.none', _('Registration disabled')),
1681 1681 ('hg.register.manual_activate', _('User Registration with manual account activation')),
1682 1682 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
1683 1683
1684 1684 ('hg.extern_activate.manual', _('Manual activation of external account')),
1685 1685 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1686 1686
1687 1687 ]
1688 1688
1689 1689 #definition of system default permissions for DEFAULT user
1690 1690 DEFAULT_USER_PERMISSIONS = [
1691 1691 'repository.read',
1692 1692 'group.read',
1693 1693 'usergroup.read',
1694 1694 'hg.create.repository',
1695 1695 'hg.create.write_on_repogroup.true',
1696 1696 'hg.fork.repository',
1697 1697 'hg.register.manual_activate',
1698 1698 'hg.extern_activate.auto',
1699 1699 ]
1700 1700
1701 1701 # defines which permissions are more important higher the more important
1702 1702 # Weight defines which permissions are more important.
1703 1703 # The higher number the more important.
1704 1704 PERM_WEIGHTS = {
1705 1705 'repository.none': 0,
1706 1706 'repository.read': 1,
1707 1707 'repository.write': 3,
1708 1708 'repository.admin': 4,
1709 1709
1710 1710 'group.none': 0,
1711 1711 'group.read': 1,
1712 1712 'group.write': 3,
1713 1713 'group.admin': 4,
1714 1714
1715 1715 'usergroup.none': 0,
1716 1716 'usergroup.read': 1,
1717 1717 'usergroup.write': 3,
1718 1718 'usergroup.admin': 4,
1719 1719 'hg.repogroup.create.false': 0,
1720 1720 'hg.repogroup.create.true': 1,
1721 1721
1722 1722 'hg.usergroup.create.false': 0,
1723 1723 'hg.usergroup.create.true': 1,
1724 1724
1725 1725 'hg.fork.none': 0,
1726 1726 'hg.fork.repository': 1,
1727 1727 'hg.create.none': 0,
1728 1728 'hg.create.repository': 1
1729 1729 }
1730 1730
1731 1731 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1732 1732 permission_name = Column("permission_name", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
1733 1733 permission_longname = Column("permission_longname", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
1734 1734
1735 1735 def __unicode__(self):
1736 1736 return u"<%s('%s:%s')>" % (
1737 1737 self.__class__.__name__, self.permission_id, self.permission_name
1738 1738 )
1739 1739
1740 1740 @classmethod
1741 1741 def get_by_key(cls, key):
1742 1742 return cls.query().filter(cls.permission_name == key).scalar()
1743 1743
1744 1744 @classmethod
1745 1745 def get_default_perms(cls, default_user_id):
1746 1746 q = Session().query(UserRepoToPerm, Repository, cls)\
1747 1747 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1748 1748 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1749 1749 .filter(UserRepoToPerm.user_id == default_user_id)
1750 1750
1751 1751 return q.all()
1752 1752
1753 1753 @classmethod
1754 1754 def get_default_group_perms(cls, default_user_id):
1755 1755 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1756 1756 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1757 1757 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1758 1758 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1759 1759
1760 1760 return q.all()
1761 1761
1762 1762 @classmethod
1763 1763 def get_default_user_group_perms(cls, default_user_id):
1764 1764 q = Session().query(UserUserGroupToPerm, UserGroup, cls)\
1765 1765 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
1766 1766 .join((cls, UserUserGroupToPerm.permission_id == cls.permission_id))\
1767 1767 .filter(UserUserGroupToPerm.user_id == default_user_id)
1768 1768
1769 1769 return q.all()
1770 1770
1771 1771
1772 1772 class UserRepoToPerm(Base, BaseModel):
1773 1773 __tablename__ = 'repo_to_perm'
1774 1774 __table_args__ = (
1775 1775 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1776 1776 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1777 1777 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1778 1778 )
1779 1779 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1780 1780 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1781 1781 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1782 1782 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1783 1783
1784 1784 user = relationship('User')
1785 1785 repository = relationship('Repository')
1786 1786 permission = relationship('Permission')
1787 1787
1788 1788 @classmethod
1789 1789 def create(cls, user, repository, permission):
1790 1790 n = cls()
1791 1791 n.user = user
1792 1792 n.repository = repository
1793 1793 n.permission = permission
1794 1794 Session().add(n)
1795 1795 return n
1796 1796
1797 1797 def __unicode__(self):
1798 1798 return u'<%s => %s >' % (self.user, self.repository)
1799 1799
1800 1800
1801 1801 class UserUserGroupToPerm(Base, BaseModel):
1802 1802 __tablename__ = 'user_user_group_to_perm'
1803 1803 __table_args__ = (
1804 1804 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1805 1805 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1806 1806 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1807 1807 )
1808 1808 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1809 1809 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1810 1810 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1811 1811 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1812 1812
1813 1813 user = relationship('User')
1814 1814 user_group = relationship('UserGroup')
1815 1815 permission = relationship('Permission')
1816 1816
1817 1817 @classmethod
1818 1818 def create(cls, user, user_group, permission):
1819 1819 n = cls()
1820 1820 n.user = user
1821 1821 n.user_group = user_group
1822 1822 n.permission = permission
1823 1823 Session().add(n)
1824 1824 return n
1825 1825
1826 1826 def __unicode__(self):
1827 1827 return u'<%s => %s >' % (self.user, self.user_group)
1828 1828
1829 1829
1830 1830 class UserToPerm(Base, BaseModel):
1831 1831 __tablename__ = 'user_to_perm'
1832 1832 __table_args__ = (
1833 1833 UniqueConstraint('user_id', 'permission_id'),
1834 1834 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1835 1835 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1836 1836 )
1837 1837 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1838 1838 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1839 1839 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1840 1840
1841 1841 user = relationship('User')
1842 1842 permission = relationship('Permission')
1843 1843
1844 1844 def __unicode__(self):
1845 1845 return u'<%s => %s >' % (self.user, self.permission)
1846 1846
1847 1847
1848 1848 class UserGroupRepoToPerm(Base, BaseModel):
1849 1849 __tablename__ = 'users_group_repo_to_perm'
1850 1850 __table_args__ = (
1851 1851 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1852 1852 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1853 1853 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1854 1854 )
1855 1855 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1856 1856 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1857 1857 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1858 1858 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1859 1859
1860 1860 users_group = relationship('UserGroup')
1861 1861 permission = relationship('Permission')
1862 1862 repository = relationship('Repository')
1863 1863
1864 1864 @classmethod
1865 1865 def create(cls, users_group, repository, permission):
1866 1866 n = cls()
1867 1867 n.users_group = users_group
1868 1868 n.repository = repository
1869 1869 n.permission = permission
1870 1870 Session().add(n)
1871 1871 return n
1872 1872
1873 1873 def __unicode__(self):
1874 1874 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
1875 1875
1876 1876
1877 1877 class UserGroupUserGroupToPerm(Base, BaseModel):
1878 1878 __tablename__ = 'user_group_user_group_to_perm'
1879 1879 __table_args__ = (
1880 1880 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1881 1881 CheckConstraint('target_user_group_id != user_group_id'),
1882 1882 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1883 1883 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1884 1884 )
1885 1885 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1886 1886 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1887 1887 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1888 1888 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1889 1889
1890 1890 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1891 1891 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1892 1892 permission = relationship('Permission')
1893 1893
1894 1894 @classmethod
1895 1895 def create(cls, target_user_group, user_group, permission):
1896 1896 n = cls()
1897 1897 n.target_user_group = target_user_group
1898 1898 n.user_group = user_group
1899 1899 n.permission = permission
1900 1900 Session().add(n)
1901 1901 return n
1902 1902
1903 1903 def __unicode__(self):
1904 1904 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
1905 1905
1906 1906
1907 1907 class UserGroupToPerm(Base, BaseModel):
1908 1908 __tablename__ = 'users_group_to_perm'
1909 1909 __table_args__ = (
1910 1910 UniqueConstraint('users_group_id', 'permission_id',),
1911 1911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1912 1912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1913 1913 )
1914 1914 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1915 1915 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1916 1916 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1917 1917
1918 1918 users_group = relationship('UserGroup')
1919 1919 permission = relationship('Permission')
1920 1920
1921 1921
1922 1922 class UserRepoGroupToPerm(Base, BaseModel):
1923 1923 __tablename__ = 'user_repo_group_to_perm'
1924 1924 __table_args__ = (
1925 1925 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1926 1926 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1927 1927 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1928 1928 )
1929 1929
1930 1930 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1931 1931 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1932 1932 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1933 1933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1934 1934
1935 1935 user = relationship('User')
1936 1936 group = relationship('RepoGroup')
1937 1937 permission = relationship('Permission')
1938 1938
1939 1939 @classmethod
1940 1940 def create(cls, user, repository_group, permission):
1941 1941 n = cls()
1942 1942 n.user = user
1943 1943 n.group = repository_group
1944 1944 n.permission = permission
1945 1945 Session().add(n)
1946 1946 return n
1947 1947
1948 1948
1949 1949 class UserGroupRepoGroupToPerm(Base, BaseModel):
1950 1950 __tablename__ = 'users_group_repo_group_to_perm'
1951 1951 __table_args__ = (
1952 1952 UniqueConstraint('users_group_id', 'group_id'),
1953 1953 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1954 1954 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1955 1955 )
1956 1956
1957 1957 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1958 1958 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1959 1959 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1960 1960 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1961 1961
1962 1962 users_group = relationship('UserGroup')
1963 1963 permission = relationship('Permission')
1964 1964 group = relationship('RepoGroup')
1965 1965
1966 1966 @classmethod
1967 1967 def create(cls, user_group, repository_group, permission):
1968 1968 n = cls()
1969 1969 n.users_group = user_group
1970 1970 n.group = repository_group
1971 1971 n.permission = permission
1972 1972 Session().add(n)
1973 1973 return n
1974 1974
1975 1975
1976 1976 class Statistics(Base, BaseModel):
1977 1977 __tablename__ = 'statistics'
1978 1978 __table_args__ = (
1979 1979 UniqueConstraint('repository_id'),
1980 1980 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1981 1981 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1982 1982 )
1983 1983 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1984 1984 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1985 1985 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1986 1986 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1987 1987 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1988 1988 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1989 1989
1990 1990 repository = relationship('Repository', single_parent=True)
1991 1991
1992 1992
1993 1993 class UserFollowing(Base, BaseModel):
1994 1994 __tablename__ = 'user_followings'
1995 1995 __table_args__ = (
1996 1996 UniqueConstraint('user_id', 'follows_repository_id'),
1997 1997 UniqueConstraint('user_id', 'follows_user_id'),
1998 1998 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1999 1999 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2000 2000 )
2001 2001
2002 2002 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2003 2003 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2004 2004 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2005 2005 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2006 2006 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2007 2007
2008 2008 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2009 2009
2010 2010 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2011 2011 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2012 2012
2013 2013 @classmethod
2014 2014 def get_repo_followers(cls, repo_id):
2015 2015 return cls.query().filter(cls.follows_repo_id == repo_id)
2016 2016
2017 2017
2018 2018 class CacheInvalidation(Base, BaseModel):
2019 2019 __tablename__ = 'cache_invalidation'
2020 2020 __table_args__ = (
2021 2021 UniqueConstraint('cache_key'),
2022 2022 Index('key_idx', 'cache_key'),
2023 2023 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2024 2024 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2025 2025 )
2026 2026 # cache_id, not used
2027 2027 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 2028 # cache_key as created by _get_cache_key
2029 2029 cache_key = Column("cache_key", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
2030 2030 # cache_args is a repo_name
2031 2031 cache_args = Column("cache_args", String(255, convert_unicode=False), nullable=True, unique=None, default=None)
2032 2032 # instance sets cache_active True when it is caching,
2033 2033 # other instances set cache_active to False to indicate that this cache is invalid
2034 2034 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2035 2035
2036 2036 def __init__(self, cache_key, repo_name=''):
2037 2037 self.cache_key = cache_key
2038 2038 self.cache_args = repo_name
2039 2039 self.cache_active = False
2040 2040
2041 2041 def __unicode__(self):
2042 2042 return u"<%s('%s:%s[%s]')>" % (self.__class__.__name__,
2043 2043 self.cache_id, self.cache_key, self.cache_active)
2044 2044
2045 2045 def _cache_key_partition(self):
2046 2046 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2047 2047 return prefix, repo_name, suffix
2048 2048
2049 2049 def get_prefix(self):
2050 2050 """
2051 2051 get prefix that might have been used in _get_cache_key to
2052 2052 generate self.cache_key. Only used for informational purposes
2053 2053 in repo_edit.html.
2054 2054 """
2055 2055 # prefix, repo_name, suffix
2056 2056 return self._cache_key_partition()[0]
2057 2057
2058 2058 def get_suffix(self):
2059 2059 """
2060 2060 get suffix that might have been used in _get_cache_key to
2061 2061 generate self.cache_key. Only used for informational purposes
2062 2062 in repo_edit.html.
2063 2063 """
2064 2064 # prefix, repo_name, suffix
2065 2065 return self._cache_key_partition()[2]
2066 2066
2067 2067 @classmethod
2068 2068 def clear_cache(cls):
2069 2069 """
2070 2070 Delete all cache keys from database.
2071 2071 Should only be run when all instances are down and all entries thus stale.
2072 2072 """
2073 2073 cls.query().delete()
2074 2074 Session().commit()
2075 2075
2076 2076 @classmethod
2077 2077 def _get_cache_key(cls, key):
2078 2078 """
2079 2079 Wrapper for generating a unique cache key for this instance and "key".
2080 2080 key must / will start with a repo_name which will be stored in .cache_args .
2081 2081 """
2082 2082 import kallithea
2083 2083 prefix = kallithea.CONFIG.get('instance_id', '')
2084 2084 return "%s%s" % (prefix, key)
2085 2085
2086 2086 @classmethod
2087 2087 def set_invalidate(cls, repo_name, delete=False):
2088 2088 """
2089 2089 Mark all caches of a repo as invalid in the database.
2090 2090 """
2091 2091 inv_objs = Session().query(cls).filter(cls.cache_args == repo_name).all()
2092 2092 log.debug('for repo %s got %s invalidation objects'
2093 2093 % (safe_str(repo_name), inv_objs))
2094 2094
2095 2095 for inv_obj in inv_objs:
2096 2096 log.debug('marking %s key for invalidation based on repo_name=%s'
2097 2097 % (inv_obj, safe_str(repo_name)))
2098 2098 if delete:
2099 2099 Session().delete(inv_obj)
2100 2100 else:
2101 2101 inv_obj.cache_active = False
2102 2102 Session().add(inv_obj)
2103 2103 Session().commit()
2104 2104
2105 2105 @classmethod
2106 2106 def test_and_set_valid(cls, repo_name, kind, valid_cache_keys=None):
2107 2107 """
2108 2108 Mark this cache key as active and currently cached.
2109 2109 Return True if the existing cache registration still was valid.
2110 2110 Return False to indicate that it had been invalidated and caches should be refreshed.
2111 2111 """
2112 2112
2113 2113 key = (repo_name + '_' + kind) if kind else repo_name
2114 2114 cache_key = cls._get_cache_key(key)
2115 2115
2116 2116 if valid_cache_keys and cache_key in valid_cache_keys:
2117 2117 return True
2118 2118
2119 2119 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2120 2120 if not inv_obj:
2121 2121 inv_obj = CacheInvalidation(cache_key, repo_name)
2122 2122 if inv_obj.cache_active:
2123 2123 return True
2124 2124 inv_obj.cache_active = True
2125 2125 Session().add(inv_obj)
2126 2126 Session().commit()
2127 2127 return False
2128 2128
2129 2129 @classmethod
2130 2130 def get_valid_cache_keys(cls):
2131 2131 """
2132 2132 Return opaque object with information of which caches still are valid
2133 2133 and can be used without checking for invalidation.
2134 2134 """
2135 2135 return set(inv_obj.cache_key for inv_obj in cls.query().filter(cls.cache_active).all())
2136 2136
2137 2137
2138 2138 class ChangesetComment(Base, BaseModel):
2139 2139 __tablename__ = 'changeset_comments'
2140 2140 __table_args__ = (
2141 2141 Index('cc_revision_idx', 'revision'),
2142 2142 Index('cc_pull_request_id_idx', 'pull_request_id'),
2143 2143 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2144 2144 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2145 2145 )
2146 2146 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2147 2147 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2148 2148 revision = Column('revision', String(40), nullable=True)
2149 2149 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2150 2150 line_no = Column('line_no', Unicode(10), nullable=True)
2151 2151 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2152 2152 f_path = Column('f_path', Unicode(1000), nullable=True)
2153 2153 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2154 2154 text = Column('text', UnicodeText(25000), nullable=False)
2155 2155 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2156 2156 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2157 2157
2158 2158 author = relationship('User')
2159 2159 repo = relationship('Repository')
2160 2160 # status_change is frequently used directly in templates - make it a lazy
2161 2161 # join to avoid fetching each related ChangesetStatus on demand.
2162 2162 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
2163 2163 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
2164 2164 pull_request = relationship('PullRequest')
2165 2165
2166 2166 @classmethod
2167 2167 def get_users(cls, revision=None, pull_request_id=None):
2168 2168 """
2169 2169 Returns user associated with this ChangesetComment. ie those
2170 2170 who actually commented
2171 2171
2172 2172 :param cls:
2173 2173 :param revision:
2174 2174 """
2175 2175 q = Session().query(User)\
2176 2176 .join(ChangesetComment.author)
2177 2177 if revision is not None:
2178 2178 q = q.filter(cls.revision == revision)
2179 2179 elif pull_request_id is not None:
2180 2180 q = q.filter(cls.pull_request_id == pull_request_id)
2181 2181 return q.all()
2182 2182
2183 2183
2184 2184 class ChangesetStatus(Base, BaseModel):
2185 2185 __tablename__ = 'changeset_statuses'
2186 2186 __table_args__ = (
2187 2187 Index('cs_revision_idx', 'revision'),
2188 2188 Index('cs_version_idx', 'version'),
2189 2189 Index('cs_pull_request_id_idx', 'pull_request_id'),
2190 2190 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
2191 2191 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
2192 2192 UniqueConstraint('repo_id', 'revision', 'version'),
2193 2193 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2194 2194 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2195 2195 )
2196 2196 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2197 2197 STATUS_APPROVED = 'approved'
2198 2198 STATUS_REJECTED = 'rejected'
2199 2199 STATUS_UNDER_REVIEW = 'under_review'
2200 2200
2201 2201 STATUSES = [
2202 2202 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2203 2203 (STATUS_APPROVED, _("Approved")),
2204 2204 (STATUS_REJECTED, _("Rejected")),
2205 2205 (STATUS_UNDER_REVIEW, _("Under Review")),
2206 2206 ]
2207 2207
2208 2208 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2209 2209 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2210 2210 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2211 2211 revision = Column('revision', String(40), nullable=False)
2212 2212 status = Column('status', String(128), nullable=False, default=DEFAULT)
2213 2213 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2214 2214 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2215 2215 version = Column('version', Integer(), nullable=False, default=0)
2216 2216 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2217 2217
2218 2218 author = relationship('User')
2219 2219 repo = relationship('Repository')
2220 2220 comment = relationship('ChangesetComment')
2221 2221 pull_request = relationship('PullRequest')
2222 2222
2223 2223 def __unicode__(self):
2224 2224 return u"<%s('%s:%s')>" % (
2225 2225 self.__class__.__name__,
2226 2226 self.status, self.author
2227 2227 )
2228 2228
2229 2229 @classmethod
2230 2230 def get_status_lbl(cls, value):
2231 2231 return dict(cls.STATUSES).get(value)
2232 2232
2233 2233 @property
2234 2234 def status_lbl(self):
2235 2235 return ChangesetStatus.get_status_lbl(self.status)
2236 2236
2237 2237
2238 2238 class PullRequest(Base, BaseModel):
2239 2239 __tablename__ = 'pull_requests'
2240 2240 __table_args__ = (
2241 2241 Index('pr_org_repo_id_idx', 'org_repo_id'),
2242 2242 Index('pr_other_repo_id_idx', 'other_repo_id'),
2243 2243 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2244 2244 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2245 2245 )
2246 2246
2247 2247 # values for .status
2248 2248 STATUS_NEW = u'new'
2249 2249 STATUS_CLOSED = u'closed'
2250 2250
2251 2251 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
2252 2252 title = Column('title', Unicode(255), nullable=True)
2253 2253 description = Column('description', UnicodeText(10240), nullable=True)
2254 2254 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2255 2255 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2256 2256 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2257 2257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2258 2258 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
2259 2259 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2260 2260 org_ref = Column('org_ref', Unicode(255), nullable=False)
2261 2261 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2262 2262 other_ref = Column('other_ref', Unicode(255), nullable=False)
2263 2263
2264 2264 @hybrid_property
2265 2265 def revisions(self):
2266 2266 return self._revisions.split(':')
2267 2267
2268 2268 @revisions.setter
2269 2269 def revisions(self, val):
2270 2270 self._revisions = safe_unicode(':'.join(val))
2271 2271
2272 2272 @property
2273 2273 def org_ref_parts(self):
2274 2274 return self.org_ref.split(':')
2275 2275
2276 2276 @property
2277 2277 def other_ref_parts(self):
2278 2278 return self.other_ref.split(':')
2279 2279
2280 2280 author = relationship('User')
2281 2281 reviewers = relationship('PullRequestReviewers',
2282 2282 cascade="all, delete-orphan")
2283 2283 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2284 2284 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2285 2285 statuses = relationship('ChangesetStatus')
2286 2286 comments = relationship('ChangesetComment',
2287 2287 cascade="all, delete-orphan")
2288 2288
2289 2289 def is_closed(self):
2290 2290 return self.status == self.STATUS_CLOSED
2291 2291
2292 2292 @property
2293 2293 def last_review_status(self):
2294 2294 return str(self.statuses[-1].status) if self.statuses else ''
2295 2295
2296 2296 def user_review_status(self, user_id):
2297 2297 """Return the user's latest status votes on PR"""
2298 2298 # note: no filtering on repo - that would be redundant
2299 2299 status = ChangesetStatus.query()\
2300 2300 .filter(ChangesetStatus.pull_request == self)\
2301 2301 .filter(ChangesetStatus.user_id == user_id)\
2302 2302 .order_by(ChangesetStatus.version)\
2303 2303 .first()
2304 2304 return str(status.status) if status else ''
2305 2305
2306 2306 def __json__(self):
2307 2307 return dict(
2308 2308 revisions=self.revisions
2309 2309 )
2310 2310
2311 2311 def url(self, **kwargs):
2312 2312 canonical = kwargs.pop('canonical', None)
2313 2313 import kallithea.lib.helpers as h
2314 2314 b = self.org_ref_parts[1]
2315 2315 if b != self.other_ref_parts[1]:
2316 2316 s = '/_/' + b
2317 2317 else:
2318 2318 s = '/_/' + self.title
2319 2319 kwargs['extra'] = urlreadable(s)
2320 2320 if canonical:
2321 2321 return h.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2322 2322 pull_request_id=self.pull_request_id, **kwargs)
2323 2323 return h.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2324 2324 pull_request_id=self.pull_request_id, **kwargs)
2325 2325
2326 2326 class PullRequestReviewers(Base, BaseModel):
2327 2327 __tablename__ = 'pull_request_reviewers'
2328 2328 __table_args__ = (
2329 2329 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2330 2330 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2331 2331 )
2332 2332
2333 2333 def __init__(self, user=None, pull_request=None):
2334 2334 self.user = user
2335 2335 self.pull_request = pull_request
2336 2336
2337 2337 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
2338 2338 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2339 2339 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
2340 2340
2341 2341 user = relationship('User')
2342 2342 pull_request = relationship('PullRequest')
2343 2343
2344 2344
2345 2345 class Notification(Base, BaseModel):
2346 2346 __tablename__ = 'notifications'
2347 2347 __table_args__ = (
2348 2348 Index('notification_type_idx', 'type'),
2349 2349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2350 2350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2351 2351 )
2352 2352
2353 2353 TYPE_CHANGESET_COMMENT = u'cs_comment'
2354 2354 TYPE_MESSAGE = u'message'
2355 2355 TYPE_MENTION = u'mention'
2356 2356 TYPE_REGISTRATION = u'registration'
2357 2357 TYPE_PULL_REQUEST = u'pull_request'
2358 2358 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
2359 2359
2360 2360 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
2361 2361 subject = Column('subject', Unicode(512), nullable=True)
2362 2362 body = Column('body', UnicodeText(50000), nullable=True)
2363 2363 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
2364 2364 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2365 2365 type_ = Column('type', Unicode(255))
2366 2366
2367 2367 created_by_user = relationship('User')
2368 2368 notifications_to_users = relationship('UserNotification', cascade="all, delete-orphan")
2369 2369
2370 2370 @property
2371 2371 def recipients(self):
2372 2372 return [x.user for x in UserNotification.query()\
2373 2373 .filter(UserNotification.notification == self)\
2374 2374 .order_by(UserNotification.user_id.asc()).all()]
2375 2375
2376 2376 @classmethod
2377 2377 def create(cls, created_by, subject, body, recipients, type_=None):
2378 2378 if type_ is None:
2379 2379 type_ = Notification.TYPE_MESSAGE
2380 2380
2381 2381 notification = cls()
2382 2382 notification.created_by_user = created_by
2383 2383 notification.subject = subject
2384 2384 notification.body = body
2385 2385 notification.type_ = type_
2386 2386 notification.created_on = datetime.datetime.now()
2387 2387
2388 2388 for u in recipients:
2389 2389 assoc = UserNotification()
2390 2390 assoc.notification = notification
2391 2391 assoc.user_id = u.user_id
2392 2392 Session().add(assoc)
2393 2393 Session().add(notification)
2394 2394 Session().flush() # assign notificaiton.notification_id
2395 2395 return notification
2396 2396
2397 2397 @property
2398 2398 def description(self):
2399 2399 from kallithea.model.notification import NotificationModel
2400 2400 return NotificationModel().make_description(self)
2401 2401
2402 2402
2403 2403 class UserNotification(Base, BaseModel):
2404 2404 __tablename__ = 'user_to_notification'
2405 2405 __table_args__ = (
2406 2406 UniqueConstraint('user_id', 'notification_id'),
2407 2407 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2408 2408 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2409 2409 )
2410 2410 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
2411 2411 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
2412 2412 read = Column('read', Boolean, default=False)
2413 2413 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
2414 2414
2415 2415 user = relationship('User')
2416 2416 notification = relationship('Notification')
2417 2417
2418 2418 def mark_as_read(self):
2419 2419 self.read = True
2420 2420 Session().add(self)
2421 2421
2422 2422
2423 2423 class Gist(Base, BaseModel):
2424 2424 __tablename__ = 'gists'
2425 2425 __table_args__ = (
2426 2426 Index('g_gist_access_id_idx', 'gist_access_id'),
2427 2427 Index('g_created_on_idx', 'created_on'),
2428 2428 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2429 2429 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2430 2430 )
2431 2431 GIST_PUBLIC = u'public'
2432 2432 GIST_PRIVATE = u'private'
2433 2433 DEFAULT_FILENAME = u'gistfile1.txt'
2434 2434
2435 2435 gist_id = Column('gist_id', Integer(), primary_key=True)
2436 2436 gist_access_id = Column('gist_access_id', Unicode(250))
2437 2437 gist_description = Column('gist_description', UnicodeText(1024))
2438 2438 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
2439 2439 gist_expires = Column('gist_expires', Float(53), nullable=False)
2440 2440 gist_type = Column('gist_type', Unicode(128), nullable=False)
2441 2441 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2442 2442 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2443 2443
2444 2444 owner = relationship('User')
2445 2445
2446 2446 def __repr__(self):
2447 2447 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
2448 2448
2449 2449 @classmethod
2450 2450 def get_or_404(cls, id_):
2451 2451 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2452 2452 if not res:
2453 2453 raise HTTPNotFound
2454 2454 return res
2455 2455
2456 2456 @classmethod
2457 2457 def get_by_access_id(cls, gist_access_id):
2458 2458 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2459 2459
2460 2460 def gist_url(self):
2461 2461 import kallithea
2462 2462 alias_url = kallithea.CONFIG.get('gist_alias_url')
2463 2463 if alias_url:
2464 2464 return alias_url.replace('{gistid}', self.gist_access_id)
2465 2465
2466 2466 import kallithea.lib.helpers as h
2467 2467 return h.canonical_url('gist', gist_id=self.gist_access_id)
2468 2468
2469 2469 @classmethod
2470 2470 def base_path(cls):
2471 2471 """
2472 2472 Returns base path where all gists are stored
2473 2473
2474 2474 :param cls:
2475 2475 """
2476 2476 from kallithea.model.gist import GIST_STORE_LOC
2477 2477 q = Session().query(Ui)\
2478 2478 .filter(Ui.ui_key == URL_SEP)
2479 2479 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
2480 2480 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
2481 2481
2482 2482 def get_api_data(self):
2483 2483 """
2484 2484 Common function for generating gist related data for API
2485 2485 """
2486 2486 gist = self
2487 2487 data = dict(
2488 2488 gist_id=gist.gist_id,
2489 2489 type=gist.gist_type,
2490 2490 access_id=gist.gist_access_id,
2491 2491 description=gist.gist_description,
2492 2492 url=gist.gist_url(),
2493 2493 expires=gist.gist_expires,
2494 2494 created_on=gist.created_on,
2495 2495 )
2496 2496 return data
2497 2497
2498 2498 def __json__(self):
2499 2499 data = dict(
2500 2500 )
2501 2501 data.update(self.get_api_data())
2502 2502 return data
2503 2503 ## SCM functions
2504 2504
2505 2505 @property
2506 2506 def scm_instance(self):
2507 2507 from kallithea.lib.vcs import get_repo
2508 2508 base_path = self.base_path()
2509 2509 return get_repo(os.path.join(*map(safe_str,
2510 2510 [base_path, self.gist_access_id])))
2511 2511
2512 2512
2513 2513 class DbMigrateVersion(Base, BaseModel):
2514 2514 __tablename__ = 'db_migrate_version'
2515 2515 __table_args__ = (
2516 2516 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2517 2517 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2518 2518 )
2519 2519 repository_id = Column('repository_id', String(250), primary_key=True)
2520 2520 repository_path = Column('repository_path', Text)
2521 2521 version = Column('version', Integer)
@@ -1,525 +1,525 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 this is forms validation classes
15 these are form validation classes
16 16 http://formencode.org/module-formencode.validators.html
17 for list off all availible validators
17 for list of all available validators
18 18
19 19 we can create our own validators
20 20
21 21 The table below outlines the options which can be used in a schema in addition to the validators themselves
22 22 pre_validators [] These validators will be applied before the schema
23 23 chained_validators [] These validators will be applied after the schema
24 24 allow_extra_fields False If True, then it is not an error when keys that aren't associated with a validator are present
25 25 filter_extra_fields False If True, then keys that aren't associated with a validator are removed
26 26 if_key_missing NoDefault If this is given, then any keys that aren't available but are expected will be replaced with this value (and then validated). This does not override a present .if_missing attribute on validators. NoDefault is a special FormEncode class to mean that no default values has been specified and therefore missing keys shouldn't take a default value.
27 27 ignore_key_missing False If True, then missing keys will be missing in the result, if the validator doesn't have .if_missing on it already
28 28
29 29
30 30 <name> = formencode.validators.<name of validator>
31 31 <name> must equal form name
32 32 list=[1,2,3,4,5]
33 33 for SELECT use formencode.All(OneOf(list), Int())
34 34
35 35 """
36 36 import logging
37 37
38 38 import formencode
39 39 from formencode import All
40 40
41 41 from pylons.i18n.translation import _
42 42
43 43 from kallithea import BACKENDS
44 44 from kallithea.model import validators as v
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class LoginForm(formencode.Schema):
50 50 allow_extra_fields = True
51 51 filter_extra_fields = True
52 52 username = v.UnicodeString(
53 53 strip=True,
54 54 min=1,
55 55 not_empty=True,
56 56 messages={
57 57 'empty': _(u'Please enter a login'),
58 58 'tooShort': _(u'Enter a value %(min)i characters long or more')}
59 59 )
60 60
61 61 password = v.UnicodeString(
62 62 strip=False,
63 63 min=3,
64 64 not_empty=True,
65 65 messages={
66 66 'empty': _(u'Please enter a password'),
67 67 'tooShort': _(u'Enter %(min)i characters or more')}
68 68 )
69 69
70 70 remember = v.StringBoolean(if_missing=False)
71 71
72 72 chained_validators = [v.ValidAuth()]
73 73
74 74
75 75 def PasswordChangeForm(username):
76 76 class _PasswordChangeForm(formencode.Schema):
77 77 allow_extra_fields = True
78 78 filter_extra_fields = True
79 79
80 80 current_password = v.ValidOldPassword(username)(not_empty=True)
81 81 new_password = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
82 82 new_password_confirmation = All(v.ValidPassword(), v.UnicodeString(strip=False, min=6))
83 83
84 84 chained_validators = [v.ValidPasswordsMatch('new_password',
85 85 'new_password_confirmation')]
86 86 return _PasswordChangeForm
87 87
88 88
89 89 def UserForm(edit=False, old_data={}):
90 90 class _UserForm(formencode.Schema):
91 91 allow_extra_fields = True
92 92 filter_extra_fields = True
93 93 username = All(v.UnicodeString(strip=True, min=1, not_empty=True),
94 94 v.ValidUsername(edit, old_data))
95 95 if edit:
96 96 new_password = All(
97 97 v.ValidPassword(),
98 98 v.UnicodeString(strip=False, min=6, not_empty=False)
99 99 )
100 100 password_confirmation = All(
101 101 v.ValidPassword(),
102 102 v.UnicodeString(strip=False, min=6, not_empty=False),
103 103 )
104 104 admin = v.StringBoolean(if_missing=False)
105 105 else:
106 106 password = All(
107 107 v.ValidPassword(),
108 108 v.UnicodeString(strip=False, min=6, not_empty=True)
109 109 )
110 110 password_confirmation = All(
111 111 v.ValidPassword(),
112 112 v.UnicodeString(strip=False, min=6, not_empty=False)
113 113 )
114 114
115 115 active = v.StringBoolean(if_missing=False)
116 116 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
117 117 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
118 118 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
119 119 extern_name = v.UnicodeString(strip=True)
120 120 extern_type = v.UnicodeString(strip=True)
121 121 chained_validators = [v.ValidPasswordsMatch()]
122 122 return _UserForm
123 123
124 124
125 125 def UserGroupForm(edit=False, old_data={}, available_members=[]):
126 126 class _UserGroupForm(formencode.Schema):
127 127 allow_extra_fields = True
128 128 filter_extra_fields = True
129 129
130 130 users_group_name = All(
131 131 v.UnicodeString(strip=True, min=1, not_empty=True),
132 132 v.ValidUserGroup(edit, old_data)
133 133 )
134 134 user_group_description = v.UnicodeString(strip=True, min=1,
135 135 not_empty=False)
136 136
137 137 users_group_active = v.StringBoolean(if_missing=False)
138 138
139 139 if edit:
140 140 users_group_members = v.OneOf(
141 141 available_members, hideList=False, testValueList=True,
142 142 if_missing=None, not_empty=False
143 143 )
144 144
145 145 return _UserGroupForm
146 146
147 147
148 148 def RepoGroupForm(edit=False, old_data={}, available_groups=[],
149 149 can_create_in_root=False):
150 150 class _RepoGroupForm(formencode.Schema):
151 151 allow_extra_fields = True
152 152 filter_extra_fields = False
153 153
154 154 group_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
155 155 v.SlugifyName(),
156 156 v.ValidRegex(msg=_('Name must not contain only digits'))(r'(?!^\d+$)^.+$'))
157 157 group_description = v.UnicodeString(strip=True, min=1,
158 158 not_empty=False)
159 159 group_copy_permissions = v.StringBoolean(if_missing=False)
160 160
161 161 if edit:
162 162 #FIXME: do a special check that we cannot move a group to one of
163 163 #its children
164 164 pass
165 165 group_parent_id = All(v.CanCreateGroup(can_create_in_root),
166 166 v.OneOf(available_groups, hideList=False,
167 167 testValueList=True,
168 168 if_missing=None, not_empty=True))
169 169 enable_locking = v.StringBoolean(if_missing=False)
170 170 chained_validators = [v.ValidRepoGroup(edit, old_data)]
171 171
172 172 return _RepoGroupForm
173 173
174 174
175 175 def RegisterForm(edit=False, old_data={}):
176 176 class _RegisterForm(formencode.Schema):
177 177 allow_extra_fields = True
178 178 filter_extra_fields = True
179 179 username = All(
180 180 v.ValidUsername(edit, old_data),
181 181 v.UnicodeString(strip=True, min=1, not_empty=True)
182 182 )
183 183 password = All(
184 184 v.ValidPassword(),
185 185 v.UnicodeString(strip=False, min=6, not_empty=True)
186 186 )
187 187 password_confirmation = All(
188 188 v.ValidPassword(),
189 189 v.UnicodeString(strip=False, min=6, not_empty=True)
190 190 )
191 191 active = v.StringBoolean(if_missing=False)
192 192 firstname = v.UnicodeString(strip=True, min=1, not_empty=False)
193 193 lastname = v.UnicodeString(strip=True, min=1, not_empty=False)
194 194 email = All(v.Email(not_empty=True), v.UniqSystemEmail(old_data))
195 195
196 196 chained_validators = [v.ValidPasswordsMatch()]
197 197
198 198 return _RegisterForm
199 199
200 200
201 201 def PasswordResetForm():
202 202 class _PasswordResetForm(formencode.Schema):
203 203 allow_extra_fields = True
204 204 filter_extra_fields = True
205 205 email = All(v.ValidSystemEmail(), v.Email(not_empty=True))
206 206 return _PasswordResetForm
207 207
208 208
209 209 def RepoForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
210 210 repo_groups=[], landing_revs=[]):
211 211 class _RepoForm(formencode.Schema):
212 212 allow_extra_fields = True
213 213 filter_extra_fields = False
214 214 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
215 215 v.SlugifyName())
216 216 repo_group = All(v.CanWriteGroup(old_data),
217 217 v.OneOf(repo_groups, hideList=True))
218 218 repo_type = v.OneOf(supported_backends, required=False,
219 219 if_missing=old_data.get('repo_type'))
220 220 repo_description = v.UnicodeString(strip=True, min=1, not_empty=False)
221 221 repo_private = v.StringBoolean(if_missing=False)
222 222 repo_landing_rev = v.OneOf(landing_revs, hideList=True)
223 223 repo_copy_permissions = v.StringBoolean(if_missing=False)
224 224 clone_uri = All(v.UnicodeString(strip=True, min=1, not_empty=False))
225 225
226 226 repo_enable_statistics = v.StringBoolean(if_missing=False)
227 227 repo_enable_downloads = v.StringBoolean(if_missing=False)
228 228 repo_enable_locking = v.StringBoolean(if_missing=False)
229 229
230 230 if edit:
231 231 #this is repo owner
232 232 user = All(v.UnicodeString(not_empty=True), v.ValidRepoUser())
233 233 clone_uri_change = v.UnicodeString(not_empty=False, if_missing=v.Missing)
234 234
235 235 chained_validators = [v.ValidCloneUri(),
236 236 v.ValidRepoName(edit, old_data)]
237 237 return _RepoForm
238 238
239 239
240 240 def RepoPermsForm():
241 241 class _RepoPermsForm(formencode.Schema):
242 242 allow_extra_fields = True
243 243 filter_extra_fields = False
244 244 chained_validators = [v.ValidPerms(type_='repo')]
245 245 return _RepoPermsForm
246 246
247 247
248 248 def RepoGroupPermsForm(valid_recursive_choices):
249 249 class _RepoGroupPermsForm(formencode.Schema):
250 250 allow_extra_fields = True
251 251 filter_extra_fields = False
252 252 recursive = v.OneOf(valid_recursive_choices)
253 253 chained_validators = [v.ValidPerms(type_='repo_group')]
254 254 return _RepoGroupPermsForm
255 255
256 256
257 257 def UserGroupPermsForm():
258 258 class _UserPermsForm(formencode.Schema):
259 259 allow_extra_fields = True
260 260 filter_extra_fields = False
261 261 chained_validators = [v.ValidPerms(type_='user_group')]
262 262 return _UserPermsForm
263 263
264 264
265 265 def RepoFieldForm():
266 266 class _RepoFieldForm(formencode.Schema):
267 267 filter_extra_fields = True
268 268 allow_extra_fields = True
269 269
270 270 new_field_key = All(v.FieldKey(),
271 271 v.UnicodeString(strip=True, min=3, not_empty=True))
272 272 new_field_value = v.UnicodeString(not_empty=False, if_missing='')
273 273 new_field_type = v.OneOf(['str', 'unicode', 'list', 'tuple'],
274 274 if_missing='str')
275 275 new_field_label = v.UnicodeString(not_empty=False)
276 276 new_field_desc = v.UnicodeString(not_empty=False)
277 277
278 278 return _RepoFieldForm
279 279
280 280
281 281 def RepoForkForm(edit=False, old_data={}, supported_backends=BACKENDS.keys(),
282 282 repo_groups=[], landing_revs=[]):
283 283 class _RepoForkForm(formencode.Schema):
284 284 allow_extra_fields = True
285 285 filter_extra_fields = False
286 286 repo_name = All(v.UnicodeString(strip=True, min=1, not_empty=True),
287 287 v.SlugifyName())
288 288 repo_group = All(v.CanWriteGroup(),
289 289 v.OneOf(repo_groups, hideList=True))
290 290 repo_type = All(v.ValidForkType(old_data), v.OneOf(supported_backends))
291 291 description = v.UnicodeString(strip=True, min=1, not_empty=True)
292 292 private = v.StringBoolean(if_missing=False)
293 293 copy_permissions = v.StringBoolean(if_missing=False)
294 294 update_after_clone = v.StringBoolean(if_missing=False)
295 295 fork_parent_id = v.UnicodeString()
296 296 chained_validators = [v.ValidForkName(edit, old_data)]
297 297 landing_rev = v.OneOf(landing_revs, hideList=True)
298 298
299 299 return _RepoForkForm
300 300
301 301
302 302 def ApplicationSettingsForm():
303 303 class _ApplicationSettingsForm(formencode.Schema):
304 304 allow_extra_fields = True
305 305 filter_extra_fields = False
306 306 title = v.UnicodeString(strip=True, not_empty=False)
307 307 realm = v.UnicodeString(strip=True, min=1, not_empty=True)
308 308 ga_code = v.UnicodeString(strip=True, min=1, not_empty=False)
309 309 captcha_public_key = v.UnicodeString(strip=True, min=1, not_empty=False)
310 310 captcha_private_key = v.UnicodeString(strip=True, min=1, not_empty=False)
311 311
312 312 return _ApplicationSettingsForm
313 313
314 314
315 315 def ApplicationVisualisationForm():
316 316 class _ApplicationVisualisationForm(formencode.Schema):
317 317 allow_extra_fields = True
318 318 filter_extra_fields = False
319 319 show_public_icon = v.StringBoolean(if_missing=False)
320 320 show_private_icon = v.StringBoolean(if_missing=False)
321 321 stylify_metatags = v.StringBoolean(if_missing=False)
322 322
323 323 repository_fields = v.StringBoolean(if_missing=False)
324 324 lightweight_journal = v.StringBoolean(if_missing=False)
325 325 dashboard_items = v.Int(min=5, not_empty=True)
326 326 admin_grid_items = v.Int(min=5, not_empty=True)
327 327 show_version = v.StringBoolean(if_missing=False)
328 328 use_gravatar = v.StringBoolean(if_missing=False)
329 329 gravatar_url = v.UnicodeString(min=3)
330 330 clone_uri_tmpl = v.UnicodeString(min=3)
331 331
332 332 return _ApplicationVisualisationForm
333 333
334 334
335 335 def ApplicationUiSettingsForm():
336 336 class _ApplicationUiSettingsForm(formencode.Schema):
337 337 allow_extra_fields = True
338 338 filter_extra_fields = False
339 339 web_push_ssl = v.StringBoolean(if_missing=False)
340 340 paths_root_path = All(
341 341 v.ValidPath(),
342 342 v.UnicodeString(strip=True, min=1, not_empty=True)
343 343 )
344 344 hooks_changegroup_update = v.StringBoolean(if_missing=False)
345 345 hooks_changegroup_repo_size = v.StringBoolean(if_missing=False)
346 346 hooks_changegroup_push_logger = v.StringBoolean(if_missing=False)
347 347 hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False)
348 348
349 349 extensions_largefiles = v.StringBoolean(if_missing=False)
350 350 extensions_hgsubversion = v.StringBoolean(if_missing=False)
351 351 extensions_hggit = v.StringBoolean(if_missing=False)
352 352
353 353 return _ApplicationUiSettingsForm
354 354
355 355
356 356 def DefaultPermissionsForm(repo_perms_choices, group_perms_choices,
357 357 user_group_perms_choices, create_choices,
358 358 create_on_write_choices, repo_group_create_choices,
359 359 user_group_create_choices, fork_choices,
360 360 register_choices, extern_activate_choices):
361 361 class _DefaultPermissionsForm(formencode.Schema):
362 362 allow_extra_fields = True
363 363 filter_extra_fields = True
364 364 overwrite_default_repo = v.StringBoolean(if_missing=False)
365 365 overwrite_default_group = v.StringBoolean(if_missing=False)
366 366 overwrite_default_user_group = v.StringBoolean(if_missing=False)
367 367 anonymous = v.StringBoolean(if_missing=False)
368 368 default_repo_perm = v.OneOf(repo_perms_choices)
369 369 default_group_perm = v.OneOf(group_perms_choices)
370 370 default_user_group_perm = v.OneOf(user_group_perms_choices)
371 371
372 372 default_repo_create = v.OneOf(create_choices)
373 373 create_on_write = v.OneOf(create_on_write_choices)
374 374 default_user_group_create = v.OneOf(user_group_create_choices)
375 375 #default_repo_group_create = v.OneOf(repo_group_create_choices) #not impl. yet
376 376 default_fork = v.OneOf(fork_choices)
377 377
378 378 default_register = v.OneOf(register_choices)
379 379 default_extern_activate = v.OneOf(extern_activate_choices)
380 380 return _DefaultPermissionsForm
381 381
382 382
383 383 def CustomDefaultPermissionsForm():
384 384 class _CustomDefaultPermissionsForm(formencode.Schema):
385 385 filter_extra_fields = True
386 386 allow_extra_fields = True
387 387 inherit_default_permissions = v.StringBoolean(if_missing=False)
388 388
389 389 create_repo_perm = v.StringBoolean(if_missing=False)
390 390 create_user_group_perm = v.StringBoolean(if_missing=False)
391 391 #create_repo_group_perm Impl. later
392 392
393 393 fork_repo_perm = v.StringBoolean(if_missing=False)
394 394
395 395 return _CustomDefaultPermissionsForm
396 396
397 397
398 398 def DefaultsForm(edit=False, old_data={}, supported_backends=BACKENDS.keys()):
399 399 class _DefaultsForm(formencode.Schema):
400 400 allow_extra_fields = True
401 401 filter_extra_fields = True
402 402 default_repo_type = v.OneOf(supported_backends)
403 403 default_repo_private = v.StringBoolean(if_missing=False)
404 404 default_repo_enable_statistics = v.StringBoolean(if_missing=False)
405 405 default_repo_enable_downloads = v.StringBoolean(if_missing=False)
406 406 default_repo_enable_locking = v.StringBoolean(if_missing=False)
407 407
408 408 return _DefaultsForm
409 409
410 410
411 411 def AuthSettingsForm(current_active_modules):
412 412 class _AuthSettingsForm(formencode.Schema):
413 413 allow_extra_fields = True
414 414 filter_extra_fields = True
415 415 auth_plugins = All(v.ValidAuthPlugins(),
416 416 v.UniqueListFromString()(not_empty=True))
417 417
418 418 def __init__(self, *args, **kwargs):
419 419 # The auth plugins tell us what form validators they use
420 420 if current_active_modules:
421 421 import kallithea.lib.auth_modules
422 422 from kallithea.lib.auth_modules import LazyFormencode
423 423 for module in current_active_modules:
424 424 plugin = kallithea.lib.auth_modules.loadplugin(module)
425 425 plugin_name = plugin.name
426 426 for sv in plugin.plugin_settings():
427 427 newk = "auth_%s_%s" % (plugin_name, sv["name"])
428 428 # can be a LazyFormencode object from plugin settings
429 429 validator = sv["validator"]
430 430 if isinstance(validator, LazyFormencode):
431 431 validator = validator()
432 432 #init all lazy validators from formencode.All
433 433 if isinstance(validator, All):
434 434 init_validators = []
435 435 for validator in validator.validators:
436 436 if isinstance(validator, LazyFormencode):
437 437 validator = validator()
438 438 init_validators.append(validator)
439 439 validator.validators = init_validators
440 440
441 441 self.add_field(newk, validator)
442 442 formencode.Schema.__init__(self, *args, **kwargs)
443 443
444 444 return _AuthSettingsForm
445 445
446 446
447 447 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices,
448 448 tls_kind_choices):
449 449 class _LdapSettingsForm(formencode.Schema):
450 450 allow_extra_fields = True
451 451 filter_extra_fields = True
452 452 #pre_validators = [LdapLibValidator]
453 453 ldap_active = v.StringBoolean(if_missing=False)
454 454 ldap_host = v.UnicodeString(strip=True,)
455 455 ldap_port = v.Number(strip=True,)
456 456 ldap_tls_kind = v.OneOf(tls_kind_choices)
457 457 ldap_tls_reqcert = v.OneOf(tls_reqcert_choices)
458 458 ldap_dn_user = v.UnicodeString(strip=True,)
459 459 ldap_dn_pass = v.UnicodeString(strip=True,)
460 460 ldap_base_dn = v.UnicodeString(strip=True,)
461 461 ldap_filter = v.UnicodeString(strip=True,)
462 462 ldap_search_scope = v.OneOf(search_scope_choices)
463 463 ldap_attr_login = v.AttrLoginValidator()(not_empty=True)
464 464 ldap_attr_firstname = v.UnicodeString(strip=True,)
465 465 ldap_attr_lastname = v.UnicodeString(strip=True,)
466 466 ldap_attr_email = v.UnicodeString(strip=True,)
467 467
468 468 return _LdapSettingsForm
469 469
470 470
471 471 def UserExtraEmailForm():
472 472 class _UserExtraEmailForm(formencode.Schema):
473 473 email = All(v.UniqSystemEmail(), v.Email(not_empty=True))
474 474 return _UserExtraEmailForm
475 475
476 476
477 477 def UserExtraIpForm():
478 478 class _UserExtraIpForm(formencode.Schema):
479 479 ip = v.ValidIp()(not_empty=True)
480 480 return _UserExtraIpForm
481 481
482 482
483 483 def PullRequestForm(repo_id):
484 484 class _PullRequestForm(formencode.Schema):
485 485 allow_extra_fields = True
486 486 filter_extra_fields = True
487 487
488 488 org_repo = v.UnicodeString(strip=True, required=True)
489 489 org_ref = v.UnicodeString(strip=True, required=True)
490 490 other_repo = v.UnicodeString(strip=True, required=True)
491 491 other_ref = v.UnicodeString(strip=True, required=True)
492 492 review_members = v.Set()
493 493
494 494 pullrequest_title = v.UnicodeString(strip=True, required=True)
495 495 pullrequest_desc = v.UnicodeString(strip=True, required=False)
496 496
497 497 return _PullRequestForm
498 498
499 499
500 500 def PullRequestPostForm():
501 501 class _PullRequestPostForm(formencode.Schema):
502 502 allow_extra_fields = True
503 503 filter_extra_fields = True
504 504
505 505 pullrequest_title = v.UnicodeString(strip=True, required=True)
506 506 pullrequest_desc = v.UnicodeString(strip=True, required=False)
507 507 review_members = v.Set()
508 508 updaterev = v.UnicodeString(strip=True, required=False, if_missing=None)
509 509
510 510 return _PullRequestPostForm
511 511
512 512
513 513 def GistForm(lifetime_options):
514 514 class _GistForm(formencode.Schema):
515 515
516 516 filename = All(v.BasePath()(),
517 517 v.UnicodeString(strip=True, required=False))
518 518 description = v.UnicodeString(required=False, if_missing=u'')
519 519 lifetime = v.OneOf(lifetime_options)
520 520 mimetype = v.UnicodeString(required=False, if_missing=None)
521 521 content = v.UnicodeString(required=True, not_empty=True)
522 522 public = v.UnicodeString(required=False, if_missing=u'')
523 523 private = v.UnicodeString(required=False, if_missing=u'')
524 524
525 525 return _GistForm
@@ -1,886 +1,886 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.scm
16 16 ~~~~~~~~~~~~~~~~~~~
17 17
18 18 Scm model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 9, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 from __future__ import with_statement
29 29 import os
30 30 import re
31 31 import time
32 32 import traceback
33 33 import logging
34 34 import cStringIO
35 35 import pkg_resources
36 36 from os.path import join as jn
37 37
38 38 from sqlalchemy import func
39 39 from pylons.i18n.translation import _
40 40
41 41 import kallithea
42 42 from kallithea.lib.vcs import get_backend
43 43 from kallithea.lib.vcs.exceptions import RepositoryError
44 44 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 45 from kallithea.lib.vcs.nodes import FileNode
46 46 from kallithea.lib.vcs.backends.base import EmptyChangeset
47 47
48 48 from kallithea import BACKENDS
49 49 from kallithea.lib import helpers as h
50 50 from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url,\
51 51 _set_extras
52 52 from kallithea.lib.auth import HasRepoPermissionAny, HasRepoGroupPermissionAny,\
53 53 HasUserGroupPermissionAny
54 54 from kallithea.lib.utils import get_filesystem_repos, make_ui, \
55 55 action_logger
56 56 from kallithea.model import BaseModel
57 57 from kallithea.model.db import Repository, Ui, CacheInvalidation, \
58 58 UserFollowing, UserLog, User, RepoGroup, PullRequest
59 59 from kallithea.lib.hooks import log_push_action
60 60 from kallithea.lib.exceptions import NonRelativePathError, IMCCommitError
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 class UserTemp(object):
66 66 def __init__(self, user_id):
67 67 self.user_id = user_id
68 68
69 69 def __repr__(self):
70 70 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
71 71
72 72
73 73 class RepoTemp(object):
74 74 def __init__(self, repo_id):
75 75 self.repo_id = repo_id
76 76
77 77 def __repr__(self):
78 78 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
79 79
80 80
81 81 class CachedRepoList(object):
82 82 """
83 83 Cached repo list. Uses super-fast in-memory cache after initialization.
84 84 """
85 85
86 86 def __init__(self, db_repo_list, repos_path, order_by=None, perm_set=None):
87 87 self.db_repo_list = db_repo_list
88 88 self.repos_path = repos_path
89 89 self.order_by = order_by
90 90 self.reversed = (order_by or '').startswith('-')
91 91 if not perm_set:
92 92 perm_set = ['repository.read', 'repository.write',
93 93 'repository.admin']
94 94 self.perm_set = perm_set
95 95
96 96 def __len__(self):
97 97 return len(self.db_repo_list)
98 98
99 99 def __repr__(self):
100 100 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
101 101
102 102 def __iter__(self):
103 103 # pre-propagated valid_cache_keys to save executing select statements
104 104 # for each repo
105 105 valid_cache_keys = CacheInvalidation.get_valid_cache_keys()
106 106
107 107 for dbr in self.db_repo_list:
108 108 scmr = dbr.scm_instance_cached(valid_cache_keys)
109 109 # check permission at this level
110 110 if not HasRepoPermissionAny(
111 111 *self.perm_set)(dbr.repo_name, 'get repo check'):
112 112 continue
113 113
114 114 try:
115 115 last_change = scmr.last_change
116 116 tip = h.get_changeset_safe(scmr, 'tip')
117 117 except Exception:
118 118 log.error(
119 119 '%s this repository is present in database but it '
120 120 'cannot be created as an scm instance, org_exc:%s'
121 121 % (dbr.repo_name, traceback.format_exc())
122 122 )
123 123 continue
124 124
125 125 tmp_d = {}
126 126 tmp_d['name'] = dbr.repo_name
127 127 tmp_d['name_sort'] = tmp_d['name'].lower()
128 128 tmp_d['raw_name'] = tmp_d['name'].lower()
129 129 tmp_d['description'] = dbr.description
130 130 tmp_d['description_sort'] = tmp_d['description'].lower()
131 131 tmp_d['last_change'] = last_change
132 132 tmp_d['last_change_sort'] = time.mktime(last_change.timetuple())
133 133 tmp_d['tip'] = tip.raw_id
134 134 tmp_d['tip_sort'] = tip.revision
135 135 tmp_d['rev'] = tip.revision
136 136 tmp_d['contact'] = dbr.user.full_contact
137 137 tmp_d['contact_sort'] = tmp_d['contact']
138 138 tmp_d['owner_sort'] = tmp_d['contact']
139 139 tmp_d['repo_archives'] = list(scmr._get_archives())
140 140 tmp_d['last_msg'] = tip.message
141 141 tmp_d['author'] = tip.author
142 142 tmp_d['dbrepo'] = dbr.get_dict()
143 143 tmp_d['dbrepo_fork'] = dbr.fork.get_dict() if dbr.fork else {}
144 144 yield tmp_d
145 145
146 146
147 147 class SimpleCachedRepoList(CachedRepoList):
148 148 """
149 149 Lighter version of CachedRepoList without the scm initialisation
150 150 """
151 151
152 152 def __iter__(self):
153 153 for dbr in self.db_repo_list:
154 154 # check permission at this level
155 155 if not HasRepoPermissionAny(
156 156 *self.perm_set)(dbr.repo_name, 'get repo check'):
157 157 continue
158 158
159 159 tmp_d = {
160 160 'name': dbr.repo_name,
161 161 'dbrepo': dbr.get_dict(),
162 162 'dbrepo_fork': dbr.fork.get_dict() if dbr.fork else {}
163 163 }
164 164 yield tmp_d
165 165
166 166
167 167 class _PermCheckIterator(object):
168 168 def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None):
169 169 """
170 170 Creates iterator from given list of objects, additionally
171 171 checking permission for them from perm_set var
172 172
173 173 :param obj_list: list of db objects
174 174 :param obj_attr: attribute of object to pass into perm_checker
175 175 :param perm_set: list of permissions to check
176 176 :param perm_checker: callable to check permissions against
177 177 """
178 178 self.obj_list = obj_list
179 179 self.obj_attr = obj_attr
180 180 self.perm_set = perm_set
181 181 self.perm_checker = perm_checker
182 182 self.extra_kwargs = extra_kwargs or {}
183 183
184 184 def __len__(self):
185 185 return len(self.obj_list)
186 186
187 187 def __repr__(self):
188 188 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
189 189
190 190 def __iter__(self):
191 191 for db_obj in self.obj_list:
192 192 # check permission at this level
193 193 name = getattr(db_obj, self.obj_attr, None)
194 194 if not self.perm_checker(*self.perm_set)(
195 195 name, self.__class__.__name__, **self.extra_kwargs):
196 196 continue
197 197
198 198 yield db_obj
199 199
200 200
201 201 class RepoList(_PermCheckIterator):
202 202
203 203 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
204 204 if not perm_set:
205 205 perm_set = ['repository.read', 'repository.write', 'repository.admin']
206 206
207 207 super(RepoList, self).__init__(obj_list=db_repo_list,
208 208 obj_attr='repo_name', perm_set=perm_set,
209 209 perm_checker=HasRepoPermissionAny,
210 210 extra_kwargs=extra_kwargs)
211 211
212 212
213 213 class RepoGroupList(_PermCheckIterator):
214 214
215 215 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
216 216 if not perm_set:
217 217 perm_set = ['group.read', 'group.write', 'group.admin']
218 218
219 219 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
220 220 obj_attr='group_name', perm_set=perm_set,
221 221 perm_checker=HasRepoGroupPermissionAny,
222 222 extra_kwargs=extra_kwargs)
223 223
224 224
225 225 class UserGroupList(_PermCheckIterator):
226 226
227 227 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
228 228 if not perm_set:
229 229 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
230 230
231 231 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
232 232 obj_attr='users_group_name', perm_set=perm_set,
233 233 perm_checker=HasUserGroupPermissionAny,
234 234 extra_kwargs=extra_kwargs)
235 235
236 236
237 237 class ScmModel(BaseModel):
238 238 """
239 239 Generic Scm Model
240 240 """
241 241
242 242 def __get_repo(self, instance):
243 243 cls = Repository
244 244 if isinstance(instance, cls):
245 245 return instance
246 246 elif isinstance(instance, int) or safe_str(instance).isdigit():
247 247 return cls.get(instance)
248 248 elif isinstance(instance, basestring):
249 249 return cls.get_by_repo_name(instance)
250 250 elif instance:
251 251 raise Exception('given object must be int, basestr or Instance'
252 252 ' of %s got %s' % (type(cls), type(instance)))
253 253
254 254 @LazyProperty
255 255 def repos_path(self):
256 256 """
257 257 Gets the repositories root path from database
258 258 """
259 259
260 260 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
261 261
262 262 return q.ui_value
263 263
264 264 def repo_scan(self, repos_path=None):
265 265 """
266 266 Listing of repositories in given path. This path should not be a
267 267 repository itself. Return a dictionary of repository objects
268 268
269 269 :param repos_path: path to directory containing repositories
270 270 """
271 271
272 272 if repos_path is None:
273 273 repos_path = self.repos_path
274 274
275 275 log.info('scanning for repositories in %s' % repos_path)
276 276
277 277 baseui = make_ui('db')
278 278 repos = {}
279 279
280 280 for name, path in get_filesystem_repos(repos_path, recursive=True):
281 281 # name need to be decomposed and put back together using the /
282 282 # since this is internal storage separator for kallithea
283 283 name = Repository.normalize_repo_name(name)
284 284
285 285 try:
286 286 if name in repos:
287 287 raise RepositoryError('Duplicate repository name %s '
288 288 'found in %s' % (name, path))
289 289 else:
290 290
291 291 klass = get_backend(path[0])
292 292
293 293 if path[0] == 'hg' and path[0] in BACKENDS.keys():
294 294 repos[name] = klass(safe_str(path[1]), baseui=baseui)
295 295
296 296 if path[0] == 'git' and path[0] in BACKENDS.keys():
297 297 repos[name] = klass(path[1])
298 298 except OSError:
299 299 continue
300 300 log.debug('found %s paths with repositories' % (len(repos)))
301 301 return repos
302 302
303 303 def get_repos(self, all_repos=None, sort_key=None, simple=False):
304 304 """
305 305 Get all repos from db and for each repo create its
306 306 backend instance and fill that backed with information from database
307 307
308 308 :param all_repos: list of repository names as strings
309 309 give specific repositories list, good for filtering
310 310
311 311 :param sort_key: initial sorting of repos
312 312 :param simple: use SimpleCachedList - one without the SCM info
313 313 """
314 314 if all_repos is None:
315 315 all_repos = self.sa.query(Repository)\
316 316 .filter(Repository.group_id == None)\
317 317 .order_by(func.lower(Repository.repo_name)).all()
318 318 if simple:
319 319 repo_iter = SimpleCachedRepoList(all_repos,
320 320 repos_path=self.repos_path,
321 321 order_by=sort_key)
322 322 else:
323 323 repo_iter = CachedRepoList(all_repos,
324 324 repos_path=self.repos_path,
325 325 order_by=sort_key)
326 326
327 327 return repo_iter
328 328
329 329 def get_repo_groups(self, all_groups=None):
330 330 if all_groups is None:
331 331 all_groups = RepoGroup.query()\
332 332 .filter(RepoGroup.group_parent_id == None).all()
333 333 return [x for x in RepoGroupList(all_groups)]
334 334
335 335 def mark_for_invalidation(self, repo_name, delete=False):
336 336 """
337 337 Mark caches of this repo invalid in the database.
338 338
339 339 :param repo_name: the repo for which caches should be marked invalid
340 340 """
341 341 CacheInvalidation.set_invalidate(repo_name, delete=delete)
342 342 repo = Repository.get_by_repo_name(repo_name)
343 343 if repo:
344 344 repo.update_changeset_cache()
345 345
346 346 def toggle_following_repo(self, follow_repo_id, user_id):
347 347
348 348 f = self.sa.query(UserFollowing)\
349 349 .filter(UserFollowing.follows_repo_id == follow_repo_id)\
350 350 .filter(UserFollowing.user_id == user_id).scalar()
351 351
352 352 if f is not None:
353 353 try:
354 354 self.sa.delete(f)
355 355 action_logger(UserTemp(user_id),
356 356 'stopped_following_repo',
357 357 RepoTemp(follow_repo_id))
358 358 return
359 359 except Exception:
360 360 log.error(traceback.format_exc())
361 361 raise
362 362
363 363 try:
364 364 f = UserFollowing()
365 365 f.user_id = user_id
366 366 f.follows_repo_id = follow_repo_id
367 367 self.sa.add(f)
368 368
369 369 action_logger(UserTemp(user_id),
370 370 'started_following_repo',
371 371 RepoTemp(follow_repo_id))
372 372 except Exception:
373 373 log.error(traceback.format_exc())
374 374 raise
375 375
376 376 def toggle_following_user(self, follow_user_id, user_id):
377 377 f = self.sa.query(UserFollowing)\
378 378 .filter(UserFollowing.follows_user_id == follow_user_id)\
379 379 .filter(UserFollowing.user_id == user_id).scalar()
380 380
381 381 if f is not None:
382 382 try:
383 383 self.sa.delete(f)
384 384 return
385 385 except Exception:
386 386 log.error(traceback.format_exc())
387 387 raise
388 388
389 389 try:
390 390 f = UserFollowing()
391 391 f.user_id = user_id
392 392 f.follows_user_id = follow_user_id
393 393 self.sa.add(f)
394 394 except Exception:
395 395 log.error(traceback.format_exc())
396 396 raise
397 397
398 398 def is_following_repo(self, repo_name, user_id, cache=False):
399 399 r = self.sa.query(Repository)\
400 400 .filter(Repository.repo_name == repo_name).scalar()
401 401
402 402 f = self.sa.query(UserFollowing)\
403 403 .filter(UserFollowing.follows_repository == r)\
404 404 .filter(UserFollowing.user_id == user_id).scalar()
405 405
406 406 return f is not None
407 407
408 408 def is_following_user(self, username, user_id, cache=False):
409 409 u = User.get_by_username(username)
410 410
411 411 f = self.sa.query(UserFollowing)\
412 412 .filter(UserFollowing.follows_user == u)\
413 413 .filter(UserFollowing.user_id == user_id).scalar()
414 414
415 415 return f is not None
416 416
417 417 def get_followers(self, repo):
418 418 repo = self._get_repo(repo)
419 419
420 420 return self.sa.query(UserFollowing)\
421 421 .filter(UserFollowing.follows_repository == repo).count()
422 422
423 423 def get_forks(self, repo):
424 424 repo = self._get_repo(repo)
425 425 return self.sa.query(Repository)\
426 426 .filter(Repository.fork == repo).count()
427 427
428 428 def get_pull_requests(self, repo):
429 429 repo = self._get_repo(repo)
430 430 return self.sa.query(PullRequest)\
431 431 .filter(PullRequest.other_repo == repo)\
432 432 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
433 433
434 434 def mark_as_fork(self, repo, fork, user):
435 435 repo = self.__get_repo(repo)
436 436 fork = self.__get_repo(fork)
437 437 if fork and repo.repo_id == fork.repo_id:
438 438 raise Exception("Cannot set repository as fork of itself")
439 439
440 440 if fork and repo.repo_type != fork.repo_type:
441 441 raise RepositoryError("Cannot set repository as fork of repository with other type")
442 442
443 443 repo.fork = fork
444 444 self.sa.add(repo)
445 445 return repo
446 446
447 447 def _handle_rc_scm_extras(self, username, repo_name, repo_alias,
448 448 action=None):
449 449 from kallithea import CONFIG
450 450 from kallithea.lib.base import _get_ip_addr
451 451 try:
452 452 from pylons import request
453 453 environ = request.environ
454 454 except TypeError:
455 455 # we might use this outside of request context, let's fake the
456 456 # environ data
457 457 from webob import Request
458 458 environ = Request.blank('').environ
459 459 extras = {
460 460 'ip': _get_ip_addr(environ),
461 461 'username': username,
462 462 'action': action or 'push_local',
463 463 'repository': repo_name,
464 464 'scm': repo_alias,
465 465 'config': CONFIG['__file__'],
466 466 'server_url': get_server_url(environ),
467 467 'make_lock': None,
468 468 'locked_by': [None, None]
469 469 }
470 470 _set_extras(extras)
471 471
472 472 def _handle_push(self, repo, username, action, repo_name, revisions):
473 473 """
474 474 Triggers push action hooks
475 475
476 476 :param repo: SCM repo
477 477 :param username: username who pushes
478 :param action: push/push_loca/push_remote
478 :param action: push/push_local/push_remote
479 479 :param repo_name: name of repo
480 480 :param revisions: list of revisions that we pushed
481 481 """
482 482 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
483 483 _scm_repo = repo._repo
484 484 # trigger push hook
485 485 if repo.alias == 'hg':
486 486 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
487 487 elif repo.alias == 'git':
488 488 log_push_action(None, _scm_repo, _git_revs=revisions)
489 489
490 490 def _get_IMC_module(self, scm_type):
491 491 """
492 492 Returns InMemoryCommit class based on scm_type
493 493
494 494 :param scm_type:
495 495 """
496 496 if scm_type == 'hg':
497 497 from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
498 498 return MercurialInMemoryChangeset
499 499
500 500 if scm_type == 'git':
501 501 from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
502 502 return GitInMemoryChangeset
503 503
504 504 raise Exception('Invalid scm_type, must be one of hg,git got %s'
505 505 % (scm_type,))
506 506
507 507 def pull_changes(self, repo, username):
508 508 dbrepo = self.__get_repo(repo)
509 509 clone_uri = dbrepo.clone_uri
510 510 if not clone_uri:
511 511 raise Exception("This repository doesn't have a clone uri")
512 512
513 513 repo = dbrepo.scm_instance
514 514 repo_name = dbrepo.repo_name
515 515 try:
516 516 if repo.alias == 'git':
517 517 repo.fetch(clone_uri)
518 518 # git doesn't really have something like post-fetch action
519 519 # we fake that now. #TODO: extract fetched revisions somehow
520 520 # here
521 521 self._handle_push(repo,
522 522 username=username,
523 523 action='push_remote',
524 524 repo_name=repo_name,
525 525 revisions=[])
526 526 else:
527 527 self._handle_rc_scm_extras(username, dbrepo.repo_name,
528 528 repo.alias, action='push_remote')
529 529 repo.pull(clone_uri)
530 530
531 531 self.mark_for_invalidation(repo_name)
532 532 except Exception:
533 533 log.error(traceback.format_exc())
534 534 raise
535 535
536 536 def commit_change(self, repo, repo_name, cs, user, author, message,
537 537 content, f_path):
538 538 """
539 539 Commits changes
540 540
541 541 :param repo: SCM instance
542 542
543 543 """
544 544 user = self._get_user(user)
545 545 IMC = self._get_IMC_module(repo.alias)
546 546
547 547 # decoding here will force that we have proper encoded values
548 548 # in any other case this will throw exceptions and deny commit
549 549 content = safe_str(content)
550 550 path = safe_str(f_path)
551 551 # message and author needs to be unicode
552 552 # proper backend should then translate that into required type
553 553 message = safe_unicode(message)
554 554 author = safe_unicode(author)
555 555 imc = IMC(repo)
556 556 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
557 557 try:
558 558 tip = imc.commit(message=message, author=author,
559 559 parents=[cs], branch=cs.branch)
560 560 except Exception, e:
561 561 log.error(traceback.format_exc())
562 562 raise IMCCommitError(str(e))
563 563 finally:
564 564 # always clear caches, if commit fails we want fresh object also
565 565 self.mark_for_invalidation(repo_name)
566 566 self._handle_push(repo,
567 567 username=user.username,
568 568 action='push_local',
569 569 repo_name=repo_name,
570 570 revisions=[tip.raw_id])
571 571 return tip
572 572
573 573 def _sanitize_path(self, f_path):
574 574 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
575 575 raise NonRelativePathError('%s is not an relative path' % f_path)
576 576 if f_path:
577 577 f_path = os.path.normpath(f_path)
578 578 return f_path
579 579
580 580 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
581 581 """
582 582 Recursively walk root dir and return a set of all paths found.
583 583
584 584 :param repo_name: name of repository
585 585 :param revision: revision for which to list nodes
586 586 :param root_path: root path to list
587 587 :param flat: return as a list, if False returns a dict with description
588 588
589 589 """
590 590 _files = list()
591 591 _dirs = list()
592 592 try:
593 593 _repo = self.__get_repo(repo_name)
594 594 changeset = _repo.scm_instance.get_changeset(revision)
595 595 root_path = root_path.lstrip('/')
596 596 for topnode, dirs, files in changeset.walk(root_path):
597 597 for f in files:
598 598 _files.append(f.path if flat else {"name": f.path,
599 599 "type": "file"})
600 600 for d in dirs:
601 601 _dirs.append(d.path if flat else {"name": d.path,
602 602 "type": "dir"})
603 603 except RepositoryError:
604 604 log.debug(traceback.format_exc())
605 605 raise
606 606
607 607 return _dirs, _files
608 608
609 609 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
610 610 author=None, trigger_push_hook=True):
611 611 """
612 612 Commits specified nodes to repo.
613 613
614 :param user: Kallithea User object or user_id, the commiter
614 :param user: Kallithea User object or user_id, the committer
615 615 :param repo: Kallithea Repository object
616 616 :param message: commit message
617 617 :param nodes: mapping {filename:{'content':content},...}
618 618 :param parent_cs: parent changeset, can be empty than it's initial commit
619 :param author: author of commit, cna be different that commiter only for git
619 :param author: author of commit, cna be different that committer only for git
620 620 :param trigger_push_hook: trigger push hooks
621 621
622 :returns: new commited changeset
622 :returns: new committed changeset
623 623 """
624 624
625 625 user = self._get_user(user)
626 626 scm_instance = repo.scm_instance_no_cache()
627 627
628 628 processed_nodes = []
629 629 for f_path in nodes:
630 630 f_path = self._sanitize_path(f_path)
631 631 content = nodes[f_path]['content']
632 632 f_path = safe_str(f_path)
633 633 # decoding here will force that we have proper encoded values
634 634 # in any other case this will throw exceptions and deny commit
635 635 if isinstance(content, (basestring,)):
636 636 content = safe_str(content)
637 637 elif isinstance(content, (file, cStringIO.OutputType,)):
638 638 content = content.read()
639 639 else:
640 640 raise Exception('Content is of unrecognized type %s' % (
641 641 type(content)
642 642 ))
643 643 processed_nodes.append((f_path, content))
644 644
645 645 message = safe_unicode(message)
646 commiter = user.full_contact
647 author = safe_unicode(author) if author else commiter
646 committer = user.full_contact
647 author = safe_unicode(author) if author else committer
648 648
649 649 IMC = self._get_IMC_module(scm_instance.alias)
650 650 imc = IMC(scm_instance)
651 651
652 652 if not parent_cs:
653 653 parent_cs = EmptyChangeset(alias=scm_instance.alias)
654 654
655 655 if isinstance(parent_cs, EmptyChangeset):
656 656 # EmptyChangeset means we we're editing empty repository
657 657 parents = None
658 658 else:
659 659 parents = [parent_cs]
660 660 # add multiple nodes
661 661 for path, content in processed_nodes:
662 662 imc.add(FileNode(path, content=content))
663 663
664 664 tip = imc.commit(message=message,
665 665 author=author,
666 666 parents=parents,
667 667 branch=parent_cs.branch)
668 668
669 669 self.mark_for_invalidation(repo.repo_name)
670 670 if trigger_push_hook:
671 671 self._handle_push(scm_instance,
672 672 username=user.username,
673 673 action='push_local',
674 674 repo_name=repo.repo_name,
675 675 revisions=[tip.raw_id])
676 676 return tip
677 677
678 678 def update_nodes(self, user, repo, message, nodes, parent_cs=None,
679 679 author=None, trigger_push_hook=True):
680 680 user = self._get_user(user)
681 681 scm_instance = repo.scm_instance_no_cache()
682 682
683 683 message = safe_unicode(message)
684 commiter = user.full_contact
685 author = safe_unicode(author) if author else commiter
684 committer = user.full_contact
685 author = safe_unicode(author) if author else committer
686 686
687 687 imc_class = self._get_IMC_module(scm_instance.alias)
688 688 imc = imc_class(scm_instance)
689 689
690 690 if not parent_cs:
691 691 parent_cs = EmptyChangeset(alias=scm_instance.alias)
692 692
693 693 if isinstance(parent_cs, EmptyChangeset):
694 694 # EmptyChangeset means we we're editing empty repository
695 695 parents = None
696 696 else:
697 697 parents = [parent_cs]
698 698
699 699 # add multiple nodes
700 700 for _filename, data in nodes.items():
701 701 # new filename, can be renamed from the old one
702 702 filename = self._sanitize_path(data['filename'])
703 703 old_filename = self._sanitize_path(_filename)
704 704 content = data['content']
705 705
706 706 filenode = FileNode(old_filename, content=content)
707 707 op = data['op']
708 708 if op == 'add':
709 709 imc.add(filenode)
710 710 elif op == 'del':
711 711 imc.remove(filenode)
712 712 elif op == 'mod':
713 713 if filename != old_filename:
714 714 #TODO: handle renames, needs vcs lib changes
715 715 imc.remove(filenode)
716 716 imc.add(FileNode(filename, content=content))
717 717 else:
718 718 imc.change(filenode)
719 719
720 720 # commit changes
721 721 tip = imc.commit(message=message,
722 722 author=author,
723 723 parents=parents,
724 724 branch=parent_cs.branch)
725 725
726 726 self.mark_for_invalidation(repo.repo_name)
727 727 if trigger_push_hook:
728 728 self._handle_push(scm_instance,
729 729 username=user.username,
730 730 action='push_local',
731 731 repo_name=repo.repo_name,
732 732 revisions=[tip.raw_id])
733 733
734 734 def delete_nodes(self, user, repo, message, nodes, parent_cs=None,
735 735 author=None, trigger_push_hook=True):
736 736 """
737 737 Deletes specified nodes from repo.
738 738
739 :param user: Kallithea User object or user_id, the commiter
739 :param user: Kallithea User object or user_id, the committer
740 740 :param repo: Kallithea Repository object
741 741 :param message: commit message
742 742 :param nodes: mapping {filename:{'content':content},...}
743 743 :param parent_cs: parent changeset, can be empty than it's initial commit
744 :param author: author of commit, cna be different that commiter only for git
744 :param author: author of commit, cna be different that committer only for git
745 745 :param trigger_push_hook: trigger push hooks
746 746
747 :returns: new commited changeset after deletion
747 :returns: new committed changeset after deletion
748 748 """
749 749
750 750 user = self._get_user(user)
751 751 scm_instance = repo.scm_instance_no_cache()
752 752
753 753 processed_nodes = []
754 754 for f_path in nodes:
755 755 f_path = self._sanitize_path(f_path)
756 756 # content can be empty but for compatibility it allows same dicts
757 757 # structure as add_nodes
758 758 content = nodes[f_path].get('content')
759 759 processed_nodes.append((f_path, content))
760 760
761 761 message = safe_unicode(message)
762 commiter = user.full_contact
763 author = safe_unicode(author) if author else commiter
762 committer = user.full_contact
763 author = safe_unicode(author) if author else committer
764 764
765 765 IMC = self._get_IMC_module(scm_instance.alias)
766 766 imc = IMC(scm_instance)
767 767
768 768 if not parent_cs:
769 769 parent_cs = EmptyChangeset(alias=scm_instance.alias)
770 770
771 771 if isinstance(parent_cs, EmptyChangeset):
772 772 # EmptyChangeset means we we're editing empty repository
773 773 parents = None
774 774 else:
775 775 parents = [parent_cs]
776 776 # add multiple nodes
777 777 for path, content in processed_nodes:
778 778 imc.remove(FileNode(path, content=content))
779 779
780 780 tip = imc.commit(message=message,
781 781 author=author,
782 782 parents=parents,
783 783 branch=parent_cs.branch)
784 784
785 785 self.mark_for_invalidation(repo.repo_name)
786 786 if trigger_push_hook:
787 787 self._handle_push(scm_instance,
788 788 username=user.username,
789 789 action='push_local',
790 790 repo_name=repo.repo_name,
791 791 revisions=[tip.raw_id])
792 792 return tip
793 793
794 794 def get_unread_journal(self):
795 795 return self.sa.query(UserLog).count()
796 796
797 797 def get_repo_landing_revs(self, repo=None):
798 798 """
799 799 Generates select option with tags branches and bookmarks (for hg only)
800 800 grouped by type
801 801
802 802 :param repo:
803 803 """
804 804
805 805 hist_l = []
806 806 choices = []
807 807 repo = self.__get_repo(repo)
808 808 hist_l.append(['rev:tip', _('latest tip')])
809 809 choices.append('rev:tip')
810 810 if not repo:
811 811 return choices, hist_l
812 812
813 813 repo = repo.scm_instance
814 814
815 815 branches_group = ([(u'branch:%s' % k, k) for k, v in
816 816 repo.branches.iteritems()], _("Branches"))
817 817 hist_l.append(branches_group)
818 818 choices.extend([x[0] for x in branches_group[0]])
819 819
820 820 if repo.alias == 'hg':
821 821 bookmarks_group = ([(u'book:%s' % k, k) for k, v in
822 822 repo.bookmarks.iteritems()], _("Bookmarks"))
823 823 hist_l.append(bookmarks_group)
824 824 choices.extend([x[0] for x in bookmarks_group[0]])
825 825
826 826 tags_group = ([(u'tag:%s' % k, k) for k, v in
827 827 repo.tags.iteritems()], _("Tags"))
828 828 hist_l.append(tags_group)
829 829 choices.extend([x[0] for x in tags_group[0]])
830 830
831 831 return choices, hist_l
832 832
833 833 def install_git_hook(self, repo, force_create=False):
834 834 """
835 835 Creates a kallithea hook inside a git repository
836 836
837 837 :param repo: Instance of VCS repo
838 838 :param force_create: Create even if same name hook exists
839 839 """
840 840
841 841 loc = jn(repo.path, 'hooks')
842 842 if not repo.bare:
843 843 loc = jn(repo.path, '.git', 'hooks')
844 844 if not os.path.isdir(loc):
845 845 os.makedirs(loc)
846 846
847 847 tmpl_post = pkg_resources.resource_string(
848 848 'kallithea', jn('config', 'post_receive_tmpl.py')
849 849 )
850 850 tmpl_pre = pkg_resources.resource_string(
851 851 'kallithea', jn('config', 'pre_receive_tmpl.py')
852 852 )
853 853
854 854 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
855 855 _hook_file = jn(loc, '%s-receive' % h_type)
856 856 has_hook = False
857 857 log.debug('Installing git hook in repo %s' % repo)
858 858 if os.path.exists(_hook_file):
859 859 # let's take a look at this hook, maybe it's kallithea ?
860 860 log.debug('hook exists, checking if it is from kallithea')
861 861 with open(_hook_file, 'rb') as f:
862 862 data = f.read()
863 863 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
864 864 % 'KALLITHEA_HOOK_VER').search(data)
865 865 if matches:
866 866 try:
867 867 ver = matches.groups()[0]
868 868 log.debug('got %s it is kallithea' % (ver))
869 869 has_hook = True
870 870 except Exception:
871 871 log.error(traceback.format_exc())
872 872 else:
873 873 # there is no hook in this dir, so we want to create one
874 874 has_hook = True
875 875
876 876 if has_hook or force_create:
877 877 log.debug('writing %s hook file !' % (h_type,))
878 878 try:
879 879 with open(_hook_file, 'wb') as f:
880 880 tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
881 881 f.write(tmpl)
882 882 os.chmod(_hook_file, 0755)
883 883 except IOError, e:
884 884 log.error('error writing %s: %s' % (_hook_file, e))
885 885 else:
886 886 log.debug('skipping writing hook file')
@@ -1,469 +1,469 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.model.user
16 16 ~~~~~~~~~~~~~~~~~~~~
17 17
18 18 users model for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 9, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import logging
30 30 import traceback
31 31 from pylons.i18n.translation import _
32 32
33 33 from sqlalchemy.exc import DatabaseError
34 34
35 35 from kallithea import EXTERN_TYPE_INTERNAL
36 36 from kallithea.lib.utils2 import safe_unicode, generate_api_key, get_current_authuser
37 37 from kallithea.lib.caching_query import FromCache
38 38 from kallithea.model import BaseModel
39 39 from kallithea.model.db import User, UserToPerm, Notification, \
40 40 UserEmailMap, UserIpMap
41 41 from kallithea.lib.exceptions import DefaultUserException, \
42 42 UserOwnsReposException
43 43 from kallithea.model.meta import Session
44 44
45 45
46 46 log = logging.getLogger(__name__)
47 47
48 48
49 49 class UserModel(BaseModel):
50 50 cls = User
51 51
52 52 def get(self, user_id, cache=False):
53 53 user = self.sa.query(User)
54 54 if cache:
55 55 user = user.options(FromCache("sql_cache_short",
56 56 "get_user_%s" % user_id))
57 57 return user.get(user_id)
58 58
59 59 def get_user(self, user):
60 60 return self._get_user(user)
61 61
62 62 def get_by_username(self, username, cache=False, case_insensitive=False):
63 63
64 64 if case_insensitive:
65 65 user = self.sa.query(User).filter(User.username.ilike(username))
66 66 else:
67 67 user = self.sa.query(User)\
68 68 .filter(User.username == username)
69 69 if cache:
70 70 user = user.options(FromCache("sql_cache_short",
71 71 "get_user_%s" % username))
72 72 return user.scalar()
73 73
74 74 def get_by_email(self, email, cache=False, case_insensitive=False):
75 75 return User.get_by_email(email, case_insensitive, cache)
76 76
77 77 def get_by_api_key(self, api_key, cache=False):
78 78 return User.get_by_api_key(api_key, cache)
79 79
80 80 def create(self, form_data, cur_user=None):
81 81 if not cur_user:
82 82 cur_user = getattr(get_current_authuser(), 'username', None)
83 83
84 84 from kallithea.lib.hooks import log_create_user, check_allowed_create_user
85 85 _fd = form_data
86 86 user_data = {
87 87 'username': _fd['username'], 'password': _fd['password'],
88 88 'email': _fd['email'], 'firstname': _fd['firstname'], 'lastname': _fd['lastname'],
89 89 'active': _fd['active'], 'admin': False
90 90 }
91 91 # raises UserCreationError if it's not allowed
92 92 check_allowed_create_user(user_data, cur_user)
93 93 from kallithea.lib.auth import get_crypt_password
94 94
95 95 new_user = User()
96 96 for k, v in form_data.items():
97 97 if k == 'password':
98 98 v = get_crypt_password(v)
99 99 if k == 'firstname':
100 100 k = 'name'
101 101 setattr(new_user, k, v)
102 102
103 103 new_user.api_key = generate_api_key(form_data['username'])
104 104 self.sa.add(new_user)
105 105
106 106 log_create_user(new_user.get_dict(), cur_user)
107 107 return new_user
108 108
109 109 def create_or_update(self, username, password, email, firstname='',
110 110 lastname='', active=True, admin=False,
111 111 extern_type=None, extern_name=None, cur_user=None):
112 112 """
113 113 Creates a new instance if not found, or updates current one
114 114
115 115 :param username:
116 116 :param password:
117 117 :param email:
118 118 :param active:
119 119 :param firstname:
120 120 :param lastname:
121 121 :param active:
122 122 :param admin:
123 123 :param extern_name:
124 124 :param extern_type:
125 125 :param cur_user:
126 126 """
127 127 if not cur_user:
128 128 cur_user = getattr(get_current_authuser(), 'username', None)
129 129
130 130 from kallithea.lib.auth import get_crypt_password, check_password
131 131 from kallithea.lib.hooks import log_create_user, check_allowed_create_user
132 132 user_data = {
133 133 'username': username, 'password': password,
134 134 'email': email, 'firstname': firstname, 'lastname': lastname,
135 135 'active': active, 'admin': admin
136 136 }
137 137 # raises UserCreationError if it's not allowed
138 138 check_allowed_create_user(user_data, cur_user)
139 139
140 140 log.debug('Checking for %s account in Kallithea database' % username)
141 141 user = User.get_by_username(username, case_insensitive=True)
142 142 if user is None:
143 143 log.debug('creating new user %s' % username)
144 144 new_user = User()
145 145 edit = False
146 146 else:
147 147 log.debug('updating user %s' % username)
148 148 new_user = user
149 149 edit = True
150 150
151 151 try:
152 152 new_user.username = username
153 153 new_user.admin = admin
154 154 new_user.email = email
155 155 new_user.active = active
156 156 new_user.extern_name = safe_unicode(extern_name) if extern_name else None
157 157 new_user.extern_type = safe_unicode(extern_type) if extern_type else None
158 158 new_user.name = firstname
159 159 new_user.lastname = lastname
160 160
161 161 if not edit:
162 162 new_user.api_key = generate_api_key(username)
163 163
164 164 # set password only if creating an user or password is changed
165 165 password_change = new_user.password and not check_password(password,
166 166 new_user.password)
167 167 if not edit or password_change:
168 168 reason = 'new password' if edit else 'new user'
169 169 log.debug('Updating password reason=>%s' % (reason,))
170 170 new_user.password = get_crypt_password(password) if password else None
171 171
172 172 self.sa.add(new_user)
173 173
174 174 if not edit:
175 175 log_create_user(new_user.get_dict(), cur_user)
176 176 return new_user
177 177 except (DatabaseError,):
178 178 log.error(traceback.format_exc())
179 179 raise
180 180
181 181 def create_registration(self, form_data):
182 182 from kallithea.model.notification import NotificationModel
183 183 import kallithea.lib.helpers as h
184 184
185 185 form_data['admin'] = False
186 186 form_data['extern_name'] = EXTERN_TYPE_INTERNAL
187 187 form_data['extern_type'] = EXTERN_TYPE_INTERNAL
188 188 new_user = self.create(form_data)
189 189
190 190 self.sa.add(new_user)
191 191 self.sa.flush()
192 192
193 193 # notification to admins
194 194 subject = _('New user registration')
195 195 body = ('New user registration\n'
196 196 '---------------------\n'
197 197 '- Username: %s\n'
198 198 '- Full Name: %s\n'
199 199 '- Email: %s\n')
200 200 body = body % (new_user.username, new_user.full_name, new_user.email)
201 201 edit_url = h.canonical_url('edit_user', id=new_user.user_id)
202 202 email_kwargs = {'registered_user_url': edit_url, 'new_username': new_user.username}
203 203 NotificationModel().create(created_by=new_user, subject=subject,
204 204 body=body, recipients=None,
205 205 type_=Notification.TYPE_REGISTRATION,
206 206 email_kwargs=email_kwargs)
207 207
208 208 def update(self, user_id, form_data, skip_attrs=[]):
209 209 from kallithea.lib.auth import get_crypt_password
210 210
211 211 user = self.get(user_id, cache=False)
212 212 if user.username == User.DEFAULT_USER:
213 213 raise DefaultUserException(
214 214 _("You can't Edit this user since it's "
215 215 "crucial for entire application"))
216 216
217 217 for k, v in form_data.items():
218 218 if k in skip_attrs:
219 219 continue
220 220 if k == 'new_password' and v:
221 221 user.password = get_crypt_password(v)
222 222 else:
223 223 # old legacy thing orm models store firstname as name,
224 224 # need proper refactor to username
225 225 if k == 'firstname':
226 226 k = 'name'
227 227 setattr(user, k, v)
228 228 self.sa.add(user)
229 229
230 230 def update_user(self, user, **kwargs):
231 231 from kallithea.lib.auth import get_crypt_password
232 232
233 233 user = self._get_user(user)
234 234 if user.username == User.DEFAULT_USER:
235 235 raise DefaultUserException(
236 236 _("You can't Edit this user since it's"
237 237 " crucial for entire application")
238 238 )
239 239
240 240 for k, v in kwargs.items():
241 241 if k == 'password' and v:
242 242 v = get_crypt_password(v)
243 243
244 244 setattr(user, k, v)
245 245 self.sa.add(user)
246 246 return user
247 247
248 248 def delete(self, user, cur_user=None):
249 249 if not cur_user:
250 250 cur_user = getattr(get_current_authuser(), 'username', None)
251 251 user = self._get_user(user)
252 252
253 253 if user.username == User.DEFAULT_USER:
254 254 raise DefaultUserException(
255 255 _(u"You can't remove this user since it's"
256 256 " crucial for entire application")
257 257 )
258 258 if user.repositories:
259 259 repos = [x.repo_name for x in user.repositories]
260 260 raise UserOwnsReposException(
261 261 _(u'User "%s" still owns %s repositories and cannot be '
262 262 'removed. Switch owners or remove those repositories: %s')
263 263 % (user.username, len(repos), ', '.join(repos))
264 264 )
265 265 if user.repo_groups:
266 266 repogroups = [x.group_name for x in user.repo_groups]
267 267 raise UserOwnsReposException(
268 268 _(u'User "%s" still owns %s repository groups and cannot be '
269 269 'removed. Switch owners or remove those repository groups: %s')
270 270 % (user.username, len(repogroups), ', '.join(repogroups))
271 271 )
272 272 if user.user_groups:
273 273 usergroups = [x.users_group_name for x in user.user_groups]
274 274 raise UserOwnsReposException(
275 275 _(u'User "%s" still owns %s user groups and cannot be '
276 276 'removed. Switch owners or remove those user groups: %s')
277 277 % (user.username, len(usergroups), ', '.join(usergroups))
278 278 )
279 279 self.sa.delete(user)
280 280
281 281 from kallithea.lib.hooks import log_delete_user
282 282 log_delete_user(user.get_dict(), cur_user)
283 283
284 284 def reset_password_link(self, data):
285 285 from kallithea.lib.celerylib import tasks, run_task
286 286 from kallithea.model.notification import EmailNotificationModel
287 287 import kallithea.lib.helpers as h
288 288
289 289 user_email = data['email']
290 290 user = User.get_by_email(user_email)
291 291 if user:
292 292 log.debug('password reset user found %s' % user)
293 293 link = h.canonical_url('reset_password_confirmation', key=user.api_key)
294 294 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
295 295 body = EmailNotificationModel().get_email_tmpl(reg_type,
296 296 'txt',
297 297 user=user.short_contact,
298 298 reset_url=link)
299 299 html_body = EmailNotificationModel().get_email_tmpl(reg_type,
300 300 'html',
301 301 user=user.short_contact,
302 302 reset_url=link)
303 303 log.debug('sending email')
304 304 run_task(tasks.send_email, [user_email],
305 305 _("Password reset link"), body, html_body)
306 306 log.info('send new password mail to %s' % user_email)
307 307 else:
308 308 log.debug("password reset email %s not found" % user_email)
309 309
310 310 return True
311 311
312 312 def reset_password(self, data):
313 313 from kallithea.lib.celerylib import tasks, run_task
314 314 from kallithea.lib import auth
315 315 user_email = data['email']
316 316 user = User.get_by_email(user_email)
317 317 new_passwd = auth.PasswordGenerator().gen_password(8,
318 318 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
319 319 if user:
320 320 user.password = auth.get_crypt_password(new_passwd)
321 321 Session().add(user)
322 322 Session().commit()
323 323 log.info('change password for %s' % user_email)
324 324 if new_passwd is None:
325 325 raise Exception('unable to generate new password')
326 326
327 327 run_task(tasks.send_email, [user_email],
328 328 _('Your new password'),
329 329 _('Your new Kallithea password:%s') % (new_passwd,))
330 330 log.info('send new password mail to %s' % user_email)
331 331
332 332 return True
333 333
334 334 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
335 335 """
336 336 Fetches auth_user by user_id,or api_key if present.
337 337 Fills auth_user attributes with those taken from database.
338 Additionally sets is_authenitated if lookup fails
338 Additionally sets is_authenticated if lookup fails
339 339 present in database
340 340
341 341 :param auth_user: instance of user to set attributes
342 342 :param user_id: user id to fetch by
343 343 :param api_key: api key to fetch by
344 344 :param username: username to fetch by
345 345 """
346 346 if user_id is None and api_key is None and username is None:
347 347 raise Exception('You need to pass user_id, api_key or username')
348 348
349 349 dbuser = None
350 350 if user_id is not None:
351 351 dbuser = self.get(user_id)
352 352 elif api_key is not None:
353 353 dbuser = self.get_by_api_key(api_key)
354 354 elif username is not None:
355 355 dbuser = self.get_by_username(username)
356 356
357 357 if dbuser is not None and dbuser.active:
358 358 log.debug('filling %s data' % dbuser)
359 359 for k, v in dbuser.get_dict().iteritems():
360 360 if k not in ['api_keys', 'permissions']:
361 361 setattr(auth_user, k, v)
362 362 return True
363 363 return False
364 364
365 365 def has_perm(self, user, perm):
366 366 perm = self._get_perm(perm)
367 367 user = self._get_user(user)
368 368
369 369 return UserToPerm.query().filter(UserToPerm.user == user)\
370 370 .filter(UserToPerm.permission == perm).scalar() is not None
371 371
372 372 def grant_perm(self, user, perm):
373 373 """
374 374 Grant user global permissions
375 375
376 376 :param user:
377 377 :param perm:
378 378 """
379 379 user = self._get_user(user)
380 380 perm = self._get_perm(perm)
381 381 # if this permission is already granted skip it
382 382 _perm = UserToPerm.query()\
383 383 .filter(UserToPerm.user == user)\
384 384 .filter(UserToPerm.permission == perm)\
385 385 .scalar()
386 386 if _perm:
387 387 return
388 388 new = UserToPerm()
389 389 new.user = user
390 390 new.permission = perm
391 391 self.sa.add(new)
392 392 return new
393 393
394 394 def revoke_perm(self, user, perm):
395 395 """
396 396 Revoke users global permissions
397 397
398 398 :param user:
399 399 :param perm:
400 400 """
401 401 user = self._get_user(user)
402 402 perm = self._get_perm(perm)
403 403
404 404 obj = UserToPerm.query()\
405 405 .filter(UserToPerm.user == user)\
406 406 .filter(UserToPerm.permission == perm)\
407 407 .scalar()
408 408 if obj:
409 409 self.sa.delete(obj)
410 410
411 411 def add_extra_email(self, user, email):
412 412 """
413 413 Adds email address to UserEmailMap
414 414
415 415 :param user:
416 416 :param email:
417 417 """
418 418 from kallithea.model import forms
419 419 form = forms.UserExtraEmailForm()()
420 420 data = form.to_python(dict(email=email))
421 421 user = self._get_user(user)
422 422
423 423 obj = UserEmailMap()
424 424 obj.user = user
425 425 obj.email = data['email']
426 426 self.sa.add(obj)
427 427 return obj
428 428
429 429 def delete_extra_email(self, user, email_id):
430 430 """
431 431 Removes email address from UserEmailMap
432 432
433 433 :param user:
434 434 :param email_id:
435 435 """
436 436 user = self._get_user(user)
437 437 obj = UserEmailMap.query().get(email_id)
438 438 if obj:
439 439 self.sa.delete(obj)
440 440
441 441 def add_extra_ip(self, user, ip):
442 442 """
443 443 Adds ip address to UserIpMap
444 444
445 445 :param user:
446 446 :param ip:
447 447 """
448 448 from kallithea.model import forms
449 449 form = forms.UserExtraIpForm()()
450 450 data = form.to_python(dict(ip=ip))
451 451 user = self._get_user(user)
452 452
453 453 obj = UserIpMap()
454 454 obj.user = user
455 455 obj.ip_addr = data['ip']
456 456 self.sa.add(obj)
457 457 return obj
458 458
459 459 def delete_extra_ip(self, user, ip_id):
460 460 """
461 461 Removes ip address from UserIpMap
462 462
463 463 :param user:
464 464 :param ip_id:
465 465 """
466 466 user = self._get_user(user)
467 467 obj = UserIpMap.query().get(ip_id)
468 468 if obj:
469 469 self.sa.delete(obj)
@@ -1,905 +1,905 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Set of generic validators
16 16 """
17 17
18 18 import os
19 19 import re
20 20 import formencode
21 21 import logging
22 22 from collections import defaultdict
23 23 from pylons.i18n.translation import _
24 24 from webhelpers.pylonslib.secure_form import authentication_token
25 25 import sqlalchemy
26 26
27 27 from formencode.validators import (
28 28 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
29 29 NotEmpty, IPAddress, CIDR, String, FancyValidator
30 30 )
31 31 from kallithea.lib.compat import OrderedSet
32 32 from kallithea.lib import ipaddr
33 33 from kallithea.lib.utils import repo_name_slug
34 34 from kallithea.lib.utils2 import safe_int, str2bool, aslist
35 35 from kallithea.model.db import RepoGroup, Repository, UserGroup, User,\
36 36 ChangesetStatus
37 37 from kallithea.lib.exceptions import LdapImportError
38 38 from kallithea.config.routing import ADMIN_PREFIX
39 39 from kallithea.lib.auth import HasRepoGroupPermissionAny, HasPermissionAny
40 40
41 41 # silence warnings and pylint
42 42 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
43 43 NotEmpty, IPAddress, CIDR, String, FancyValidator
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47 class _Missing(object):
48 48 pass
49 49
50 50 Missing = _Missing()
51 51
52 52
53 53 class StateObj(object):
54 54 """
55 55 this is needed to translate the messages using _() in validators
56 56 """
57 57 _ = staticmethod(_)
58 58
59 59
60 60 def M(self, key, state=None, **kwargs):
61 61 """
62 62 returns string from self.message based on given key,
63 63 passed kw params are used to substitute %(named)s params inside
64 64 translated strings
65 65
66 66 :param msg:
67 67 :param state:
68 68 """
69 69 if state is None:
70 70 state = StateObj()
71 71 else:
72 72 state._ = staticmethod(_)
73 73 #inject validator into state object
74 74 return self.message(key, state, **kwargs)
75 75
76 76
77 77 def UniqueListFromString():
78 78 class _UniqueListFromString(formencode.FancyValidator):
79 79 """
80 80 Split value on ',' and make unique while preserving order
81 81 """
82 82 messages = dict(
83 83 empty=_('Value cannot be an empty list'),
84 84 missing_value=_('Value cannot be an empty list'),
85 85 )
86 86
87 87 def _to_python(self, value, state):
88 88 value = aslist(value, ',')
89 89 seen = set()
90 90 return [c for c in value if not (c in seen or seen.add(c))]
91 91
92 92 def empty_value(self, value):
93 93 return []
94 94
95 95 return _UniqueListFromString
96 96
97 97
98 98 def ValidUsername(edit=False, old_data={}):
99 99 class _validator(formencode.validators.FancyValidator):
100 100 messages = {
101 101 'username_exists': _(u'Username "%(username)s" already exists'),
102 102 'system_invalid_username':
103 103 _(u'Username "%(username)s" is forbidden'),
104 104 'invalid_username':
105 105 _(u'Username may only contain alphanumeric characters '
106 106 'underscores, periods or dashes and must begin with '
107 107 'alphanumeric character or underscore')
108 108 }
109 109
110 110 def validate_python(self, value, state):
111 111 if value in ['default', 'new_user']:
112 112 msg = M(self, 'system_invalid_username', state, username=value)
113 113 raise formencode.Invalid(msg, value, state)
114 114 #check if user is unique
115 115 old_un = None
116 116 if edit:
117 117 old_un = User.get(old_data.get('user_id')).username
118 118
119 119 if old_un != value or not edit:
120 120 if User.get_by_username(value, case_insensitive=True):
121 121 msg = M(self, 'username_exists', state, username=value)
122 122 raise formencode.Invalid(msg, value, state)
123 123
124 124 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
125 125 msg = M(self, 'invalid_username', state)
126 126 raise formencode.Invalid(msg, value, state)
127 127 return _validator
128 128
129 129
130 130 def ValidRegex(msg=None):
131 131 class _validator(formencode.validators.Regex):
132 132 messages = dict(invalid=msg or _('The input is not valid'))
133 133 return _validator
134 134
135 135
136 136 def ValidRepoUser():
137 137 class _validator(formencode.validators.FancyValidator):
138 138 messages = {
139 139 'invalid_username': _(u'Username %(username)s is not valid')
140 140 }
141 141
142 142 def validate_python(self, value, state):
143 143 try:
144 144 User.query().filter(User.active == True)\
145 145 .filter(User.username == value).one()
146 146 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
147 147 msg = M(self, 'invalid_username', state, username=value)
148 148 raise formencode.Invalid(msg, value, state,
149 149 error_dict=dict(username=msg)
150 150 )
151 151
152 152 return _validator
153 153
154 154
155 155 def ValidUserGroup(edit=False, old_data={}):
156 156 class _validator(formencode.validators.FancyValidator):
157 157 messages = {
158 158 'invalid_group': _(u'Invalid user group name'),
159 159 'group_exist': _(u'User group "%(usergroup)s" already exists'),
160 160 'invalid_usergroup_name':
161 161 _(u'user group name may only contain alphanumeric '
162 162 'characters underscores, periods or dashes and must begin '
163 163 'with alphanumeric character')
164 164 }
165 165
166 166 def validate_python(self, value, state):
167 167 if value in ['default']:
168 168 msg = M(self, 'invalid_group', state)
169 169 raise formencode.Invalid(msg, value, state,
170 170 error_dict=dict(users_group_name=msg)
171 171 )
172 172 #check if group is unique
173 173 old_ugname = None
174 174 if edit:
175 175 old_id = old_data.get('users_group_id')
176 176 old_ugname = UserGroup.get(old_id).users_group_name
177 177
178 178 if old_ugname != value or not edit:
179 179 is_existing_group = UserGroup.get_by_group_name(value,
180 180 case_insensitive=True)
181 181 if is_existing_group:
182 182 msg = M(self, 'group_exist', state, usergroup=value)
183 183 raise formencode.Invalid(msg, value, state,
184 184 error_dict=dict(users_group_name=msg)
185 185 )
186 186
187 187 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
188 188 msg = M(self, 'invalid_usergroup_name', state)
189 189 raise formencode.Invalid(msg, value, state,
190 190 error_dict=dict(users_group_name=msg)
191 191 )
192 192
193 193 return _validator
194 194
195 195
196 196 def ValidRepoGroup(edit=False, old_data={}):
197 197 class _validator(formencode.validators.FancyValidator):
198 198 messages = {
199 199 'group_parent_id': _(u'Cannot assign this group as parent'),
200 200 'group_exists': _(u'Group "%(group_name)s" already exists'),
201 201 'repo_exists':
202 202 _(u'Repository with name "%(group_name)s" already exists')
203 203 }
204 204
205 205 def validate_python(self, value, state):
206 206 # TODO WRITE VALIDATIONS
207 207 group_name = value.get('group_name')
208 208 group_parent_id = value.get('group_parent_id')
209 209
210 210 # slugify repo group just in case :)
211 211 slug = repo_name_slug(group_name)
212 212
213 213 # check for parent of self
214 214 parent_of_self = lambda: (
215 215 old_data['group_id'] == int(group_parent_id)
216 216 if group_parent_id else False
217 217 )
218 218 if edit and parent_of_self():
219 219 msg = M(self, 'group_parent_id', state)
220 220 raise formencode.Invalid(msg, value, state,
221 221 error_dict=dict(group_parent_id=msg)
222 222 )
223 223
224 224 old_gname = None
225 225 if edit:
226 226 old_gname = RepoGroup.get(old_data.get('group_id')).group_name
227 227
228 228 if old_gname != group_name or not edit:
229 229
230 230 # check group
231 231 gr = RepoGroup.query()\
232 232 .filter(RepoGroup.group_name == slug)\
233 233 .filter(RepoGroup.group_parent_id == group_parent_id)\
234 234 .scalar()
235 235
236 236 if gr:
237 237 msg = M(self, 'group_exists', state, group_name=slug)
238 238 raise formencode.Invalid(msg, value, state,
239 239 error_dict=dict(group_name=msg)
240 240 )
241 241
242 242 # check for same repo
243 243 repo = Repository.query()\
244 244 .filter(Repository.repo_name == slug)\
245 245 .scalar()
246 246
247 247 if repo:
248 248 msg = M(self, 'repo_exists', state, group_name=slug)
249 249 raise formencode.Invalid(msg, value, state,
250 250 error_dict=dict(group_name=msg)
251 251 )
252 252
253 253 return _validator
254 254
255 255
256 256 def ValidPassword():
257 257 class _validator(formencode.validators.FancyValidator):
258 258 messages = {
259 259 'invalid_password':
260 260 _(u'Invalid characters (non-ascii) in password')
261 261 }
262 262
263 263 def validate_python(self, value, state):
264 264 try:
265 265 (value or '').decode('ascii')
266 266 except UnicodeError:
267 267 msg = M(self, 'invalid_password', state)
268 268 raise formencode.Invalid(msg, value, state,)
269 269 return _validator
270 270
271 271
272 272 def ValidOldPassword(username):
273 273 class _validator(formencode.validators.FancyValidator):
274 274 messages = {
275 275 'invalid_password': _(u'Invalid old password')
276 276 }
277 277
278 278 def validate_python(self, value, state):
279 279 from kallithea.lib import auth_modules
280 280 if not auth_modules.authenticate(username, value, ''):
281 281 msg = M(self, 'invalid_password', state)
282 282 raise formencode.Invalid(msg, value, state,
283 283 error_dict=dict(current_password=msg)
284 284 )
285 285 return _validator
286 286
287 287
288 288 def ValidPasswordsMatch(passwd='new_password', passwd_confirmation='password_confirmation'):
289 289 class _validator(formencode.validators.FancyValidator):
290 290 messages = {
291 291 'password_mismatch': _(u'Passwords do not match'),
292 292 }
293 293
294 294 def validate_python(self, value, state):
295 295
296 296 pass_val = value.get('password') or value.get(passwd)
297 297 if pass_val != value[passwd_confirmation]:
298 298 msg = M(self, 'password_mismatch', state)
299 299 raise formencode.Invalid(msg, value, state,
300 300 error_dict={passwd:msg, passwd_confirmation: msg}
301 301 )
302 302 return _validator
303 303
304 304
305 305 def ValidAuth():
306 306 class _validator(formencode.validators.FancyValidator):
307 307 messages = {
308 308 'invalid_password': _(u'invalid password'),
309 309 'invalid_username': _(u'invalid user name'),
310 310 'disabled_account': _(u'Your account is disabled')
311 311 }
312 312
313 313 def validate_python(self, value, state):
314 314 from kallithea.lib import auth_modules
315 315
316 316 password = value['password']
317 317 username = value['username']
318 318
319 319 if not auth_modules.authenticate(username, password):
320 320 user = User.get_by_username(username)
321 321 if user and not user.active:
322 322 log.warning('user %s is disabled' % username)
323 323 msg = M(self, 'disabled_account', state)
324 324 raise formencode.Invalid(msg, value, state,
325 325 error_dict=dict(username=msg)
326 326 )
327 327 else:
328 328 log.warning('user %s failed to authenticate' % username)
329 329 msg = M(self, 'invalid_username', state)
330 330 msg2 = M(self, 'invalid_password', state)
331 331 raise formencode.Invalid(msg, value, state,
332 332 error_dict=dict(username=msg, password=msg2)
333 333 )
334 334 return _validator
335 335
336 336
337 337 def ValidAuthToken():
338 338 class _validator(formencode.validators.FancyValidator):
339 339 messages = {
340 340 'invalid_token': _(u'Token mismatch')
341 341 }
342 342
343 343 def validate_python(self, value, state):
344 344 if value != authentication_token():
345 345 msg = M(self, 'invalid_token', state)
346 346 raise formencode.Invalid(msg, value, state)
347 347 return _validator
348 348
349 349
350 350 def ValidRepoName(edit=False, old_data={}):
351 351 class _validator(formencode.validators.FancyValidator):
352 352 messages = {
353 353 'invalid_repo_name':
354 354 _(u'Repository name %(repo)s is disallowed'),
355 355 'repository_exists':
356 356 _(u'Repository named %(repo)s already exists'),
357 357 'repository_in_group_exists': _(u'Repository "%(repo)s" already '
358 358 'exists in group "%(group)s"'),
359 359 'same_group_exists': _(u'Repository group with name "%(repo)s" '
360 360 'already exists')
361 361 }
362 362
363 363 def _to_python(self, value, state):
364 364 repo_name = repo_name_slug(value.get('repo_name', ''))
365 365 repo_group = value.get('repo_group')
366 366 if repo_group:
367 367 gr = RepoGroup.get(repo_group)
368 368 group_path = gr.full_path
369 369 group_name = gr.group_name
370 370 # value needs to be aware of group name in order to check
371 371 # db key This is an actual just the name to store in the
372 372 # database
373 373 repo_name_full = group_path + RepoGroup.url_sep() + repo_name
374 374 else:
375 375 group_name = group_path = ''
376 376 repo_name_full = repo_name
377 377
378 378 value['repo_name'] = repo_name
379 379 value['repo_name_full'] = repo_name_full
380 380 value['group_path'] = group_path
381 381 value['group_name'] = group_name
382 382 return value
383 383
384 384 def validate_python(self, value, state):
385 385
386 386 repo_name = value.get('repo_name')
387 387 repo_name_full = value.get('repo_name_full')
388 388 group_path = value.get('group_path')
389 389 group_name = value.get('group_name')
390 390
391 391 if repo_name in [ADMIN_PREFIX, '']:
392 392 msg = M(self, 'invalid_repo_name', state, repo=repo_name)
393 393 raise formencode.Invalid(msg, value, state,
394 394 error_dict=dict(repo_name=msg)
395 395 )
396 396
397 397 rename = old_data.get('repo_name') != repo_name_full
398 398 create = not edit
399 399 if rename or create:
400 400
401 401 if group_path != '':
402 402 if Repository.get_by_repo_name(repo_name_full):
403 403 msg = M(self, 'repository_in_group_exists', state,
404 404 repo=repo_name, group=group_name)
405 405 raise formencode.Invalid(msg, value, state,
406 406 error_dict=dict(repo_name=msg)
407 407 )
408 408 elif RepoGroup.get_by_group_name(repo_name_full):
409 409 msg = M(self, 'same_group_exists', state,
410 410 repo=repo_name)
411 411 raise formencode.Invalid(msg, value, state,
412 412 error_dict=dict(repo_name=msg)
413 413 )
414 414
415 415 elif Repository.get_by_repo_name(repo_name_full):
416 416 msg = M(self, 'repository_exists', state,
417 417 repo=repo_name)
418 418 raise formencode.Invalid(msg, value, state,
419 419 error_dict=dict(repo_name=msg)
420 420 )
421 421 return value
422 422 return _validator
423 423
424 424
425 425 def ValidForkName(*args, **kwargs):
426 426 return ValidRepoName(*args, **kwargs)
427 427
428 428
429 429 def SlugifyName():
430 430 class _validator(formencode.validators.FancyValidator):
431 431
432 432 def _to_python(self, value, state):
433 433 return repo_name_slug(value)
434 434
435 435 def validate_python(self, value, state):
436 436 pass
437 437
438 438 return _validator
439 439
440 440
441 441 def ValidCloneUri():
442 442 from kallithea.lib.utils import make_ui
443 443
444 444 def url_handler(repo_type, url, ui):
445 445 if repo_type == 'hg':
446 446 from kallithea.lib.vcs.backends.hg.repository import MercurialRepository
447 447 if url.startswith('http') or url.startswith('ssh'):
448 448 # initially check if it's at least the proper URL
449 449 # or does it pass basic auth
450 450 MercurialRepository._check_url(url, ui)
451 451 elif url.startswith('svn+http'):
452 452 from hgsubversion.svnrepo import svnremoterepo
453 453 svnremoterepo(ui, url).svn.uuid
454 454 elif url.startswith('git+http'):
455 455 raise NotImplementedError()
456 456 else:
457 457 raise Exception('clone from URI %s not allowed' % (url,))
458 458
459 459 elif repo_type == 'git':
460 460 from kallithea.lib.vcs.backends.git.repository import GitRepository
461 461 if url.startswith('http'):
462 462 # initially check if it's at least the proper URL
463 463 # or does it pass basic auth
464 464 GitRepository._check_url(url)
465 465 elif url.startswith('svn+http'):
466 466 raise NotImplementedError()
467 467 elif url.startswith('hg+http'):
468 468 raise NotImplementedError()
469 469 else:
470 470 raise Exception('clone from URI %s not allowed' % (url))
471 471
472 472 class _validator(formencode.validators.FancyValidator):
473 473 messages = {
474 474 'clone_uri': _(u'invalid clone URL'),
475 475 'invalid_clone_uri': _(u'Invalid clone URL, provide a '
476 476 'valid clone http(s)/svn+http(s)/ssh URL')
477 477 }
478 478
479 479 def validate_python(self, value, state):
480 480 repo_type = value.get('repo_type')
481 481 url = value.get('clone_uri')
482 482
483 483 if not url:
484 484 pass
485 485 else:
486 486 try:
487 487 url_handler(repo_type, url, make_ui('db', clear_session=False))
488 488 except Exception:
489 489 log.exception('URL validation failed')
490 490 msg = M(self, 'clone_uri')
491 491 raise formencode.Invalid(msg, value, state,
492 492 error_dict=dict(clone_uri=msg)
493 493 )
494 494 return _validator
495 495
496 496
497 497 def ValidForkType(old_data={}):
498 498 class _validator(formencode.validators.FancyValidator):
499 499 messages = {
500 500 'invalid_fork_type': _(u'Fork has to be the same type as parent')
501 501 }
502 502
503 503 def validate_python(self, value, state):
504 504 if old_data['repo_type'] != value:
505 505 msg = M(self, 'invalid_fork_type', state)
506 506 raise formencode.Invalid(msg, value, state,
507 507 error_dict=dict(repo_type=msg)
508 508 )
509 509 return _validator
510 510
511 511
512 512 def CanWriteGroup(old_data=None):
513 513 class _validator(formencode.validators.FancyValidator):
514 514 messages = {
515 515 'permission_denied': _(u"You don't have permissions "
516 516 "to create repository in this group"),
517 517 'permission_denied_root': _(u"no permission to create repository "
518 518 "in root location")
519 519 }
520 520
521 521 def _to_python(self, value, state):
522 522 #root location
523 523 if value in [-1, "-1"]:
524 524 return None
525 525 return value
526 526
527 527 def validate_python(self, value, state):
528 528 gr = RepoGroup.get(value)
529 529 gr_name = gr.group_name if gr else None # None means ROOT location
530 530 # create repositories with write permission on group is set to true
531 531 create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
532 532 group_admin = HasRepoGroupPermissionAny('group.admin')(gr_name,
533 533 'can write into group validator')
534 534 group_write = HasRepoGroupPermissionAny('group.write')(gr_name,
535 535 'can write into group validator')
536 536 forbidden = not (group_admin or (group_write and create_on_write))
537 537 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
538 538 gid = (old_data['repo_group'].get('group_id')
539 539 if (old_data and 'repo_group' in old_data) else None)
540 540 value_changed = gid != safe_int(value)
541 541 new = not old_data
542 542 # do check if we changed the value, there's a case that someone got
543 543 # revoked write permissions to a repository, he still created, we
544 544 # don't need to check permission if he didn't change the value of
545 545 # groups in form box
546 546 if value_changed or new:
547 547 #parent group need to be existing
548 548 if gr and forbidden:
549 549 msg = M(self, 'permission_denied', state)
550 550 raise formencode.Invalid(msg, value, state,
551 551 error_dict=dict(repo_type=msg)
552 552 )
553 553 ## check if we can write to root location !
554 554 elif gr is None and not can_create_repos():
555 555 msg = M(self, 'permission_denied_root', state)
556 556 raise formencode.Invalid(msg, value, state,
557 557 error_dict=dict(repo_type=msg)
558 558 )
559 559
560 560 return _validator
561 561
562 562
563 563 def CanCreateGroup(can_create_in_root=False):
564 564 class _validator(formencode.validators.FancyValidator):
565 565 messages = {
566 566 'permission_denied': _(u"You don't have permissions "
567 567 "to create a group in this location")
568 568 }
569 569
570 570 def to_python(self, value, state):
571 571 #root location
572 572 if value in [-1, "-1"]:
573 573 return None
574 574 return value
575 575
576 576 def validate_python(self, value, state):
577 577 gr = RepoGroup.get(value)
578 578 gr_name = gr.group_name if gr else None # None means ROOT location
579 579
580 580 if can_create_in_root and gr is None:
581 581 #we can create in root, we're fine no validations required
582 582 return
583 583
584 584 forbidden_in_root = gr is None and not can_create_in_root
585 585 val = HasRepoGroupPermissionAny('group.admin')
586 586 forbidden = not val(gr_name, 'can create group validator')
587 587 if forbidden_in_root or forbidden:
588 588 msg = M(self, 'permission_denied', state)
589 589 raise formencode.Invalid(msg, value, state,
590 590 error_dict=dict(group_parent_id=msg)
591 591 )
592 592
593 593 return _validator
594 594
595 595
596 596 def ValidPerms(type_='repo'):
597 597 if type_ == 'repo_group':
598 598 EMPTY_PERM = 'group.none'
599 599 elif type_ == 'repo':
600 600 EMPTY_PERM = 'repository.none'
601 601 elif type_ == 'user_group':
602 602 EMPTY_PERM = 'usergroup.none'
603 603
604 604 class _validator(formencode.validators.FancyValidator):
605 605 messages = {
606 606 'perm_new_member_name':
607 607 _(u'This username or user group name is not valid')
608 608 }
609 609
610 610 def to_python(self, value, state):
611 611 perms_update = OrderedSet()
612 612 perms_new = OrderedSet()
613 613 # build a list of permission to update and new permission to create
614 614
615 615 #CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
616 616 new_perms_group = defaultdict(dict)
617 617 for k, v in value.copy().iteritems():
618 618 if k.startswith('perm_new_member'):
619 619 del value[k]
620 620 _type, part = k.split('perm_new_member_')
621 621 args = part.split('_')
622 622 if len(args) == 1:
623 623 new_perms_group[args[0]]['perm'] = v
624 624 elif len(args) == 2:
625 625 _key, pos = args
626 626 new_perms_group[pos][_key] = v
627 627
628 628 # fill new permissions in order of how they were added
629 629 for k in sorted(map(int, new_perms_group.keys())):
630 630 perm_dict = new_perms_group[str(k)]
631 631 new_member = perm_dict.get('name')
632 632 new_perm = perm_dict.get('perm')
633 633 new_type = perm_dict.get('type')
634 634 if new_member and new_perm and new_type:
635 635 perms_new.add((new_member, new_perm, new_type))
636 636
637 637 for k, v in value.iteritems():
638 638 if k.startswith('u_perm_') or k.startswith('g_perm_'):
639 639 member = k[7:]
640 640 t = {'u': 'user',
641 641 'g': 'users_group'
642 642 }[k[0]]
643 643 if member == User.DEFAULT_USER:
644 644 if str2bool(value.get('repo_private')):
645 645 # set none for default when updating to
646 # private repo protects agains form manipulation
646 # private repo protects against form manipulation
647 647 v = EMPTY_PERM
648 648 perms_update.add((member, v, t))
649 649
650 650 value['perms_updates'] = list(perms_update)
651 651 value['perms_new'] = list(perms_new)
652 652
653 653 # update permissions
654 654 for k, v, t in perms_new:
655 655 try:
656 656 if t is 'user':
657 657 self.user_db = User.query()\
658 658 .filter(User.active == True)\
659 659 .filter(User.username == k).one()
660 660 if t is 'users_group':
661 661 self.user_db = UserGroup.query()\
662 662 .filter(UserGroup.users_group_active == True)\
663 663 .filter(UserGroup.users_group_name == k).one()
664 664
665 665 except Exception:
666 666 log.exception('Updated permission failed')
667 667 msg = M(self, 'perm_new_member_type', state)
668 668 raise formencode.Invalid(msg, value, state,
669 669 error_dict=dict(perm_new_member_name=msg)
670 670 )
671 671 return value
672 672 return _validator
673 673
674 674
675 675 def ValidSettings():
676 676 class _validator(formencode.validators.FancyValidator):
677 677 def _to_python(self, value, state):
678 678 # settings form for users that are not admin
679 679 # can't edit certain parameters, it's extra backup if they mangle
680 680 # with forms
681 681
682 682 forbidden_params = [
683 683 'user', 'repo_type', 'repo_enable_locking',
684 684 'repo_enable_downloads', 'repo_enable_statistics'
685 685 ]
686 686
687 687 for param in forbidden_params:
688 688 if param in value:
689 689 del value[param]
690 690 return value
691 691
692 692 def validate_python(self, value, state):
693 693 pass
694 694 return _validator
695 695
696 696
697 697 def ValidPath():
698 698 class _validator(formencode.validators.FancyValidator):
699 699 messages = {
700 700 'invalid_path': _(u'This is not a valid path')
701 701 }
702 702
703 703 def validate_python(self, value, state):
704 704 if not os.path.isdir(value):
705 705 msg = M(self, 'invalid_path', state)
706 706 raise formencode.Invalid(msg, value, state,
707 707 error_dict=dict(paths_root_path=msg)
708 708 )
709 709 return _validator
710 710
711 711
712 712 def UniqSystemEmail(old_data={}):
713 713 class _validator(formencode.validators.FancyValidator):
714 714 messages = {
715 715 'email_taken': _(u'This e-mail address is already taken')
716 716 }
717 717
718 718 def _to_python(self, value, state):
719 719 return value.lower()
720 720
721 721 def validate_python(self, value, state):
722 722 if (old_data.get('email') or '').lower() != value:
723 723 user = User.get_by_email(value, case_insensitive=True)
724 724 if user:
725 725 msg = M(self, 'email_taken', state)
726 726 raise formencode.Invalid(msg, value, state,
727 727 error_dict=dict(email=msg)
728 728 )
729 729 return _validator
730 730
731 731
732 732 def ValidSystemEmail():
733 733 class _validator(formencode.validators.FancyValidator):
734 734 messages = {
735 735 'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
736 736 }
737 737
738 738 def _to_python(self, value, state):
739 739 return value.lower()
740 740
741 741 def validate_python(self, value, state):
742 742 user = User.get_by_email(value, case_insensitive=True)
743 743 if user is None:
744 744 msg = M(self, 'non_existing_email', state, email=value)
745 745 raise formencode.Invalid(msg, value, state,
746 746 error_dict=dict(email=msg)
747 747 )
748 748
749 749 return _validator
750 750
751 751
752 752 def LdapLibValidator():
753 753 class _validator(formencode.validators.FancyValidator):
754 754 messages = {
755 755
756 756 }
757 757
758 758 def validate_python(self, value, state):
759 759 try:
760 760 import ldap
761 761 ldap # pyflakes silence !
762 762 except ImportError:
763 763 raise LdapImportError()
764 764
765 765 return _validator
766 766
767 767
768 768 def AttrLoginValidator():
769 769 class _validator(formencode.validators.UnicodeString):
770 770 messages = {
771 771 'invalid_cn':
772 772 _(u'The LDAP Login attribute of the CN must be specified - '
773 773 'this is the name of the attribute that is equivalent '
774 774 'to "username"')
775 775 }
776 776 messages['empty'] = messages['invalid_cn']
777 777
778 778 return _validator
779 779
780 780
781 781 def NotReviewedRevisions(repo_id):
782 782 class _validator(formencode.validators.FancyValidator):
783 783 messages = {
784 784 'rev_already_reviewed':
785 785 _(u'Revisions %(revs)s are already part of pull request '
786 786 'or have set status')
787 787 }
788 788
789 789 def validate_python(self, value, state):
790 790 # check revisions if they are not reviewed, or a part of another
791 791 # pull request
792 792 statuses = ChangesetStatus.query()\
793 793 .filter(ChangesetStatus.revision.in_(value))\
794 794 .filter(ChangesetStatus.repo_id == repo_id)\
795 795 .all()
796 796
797 797 errors = []
798 798 for cs in statuses:
799 799 if cs.pull_request_id:
800 800 errors.append(['pull_req', cs.revision[:12]])
801 801 elif cs.status:
802 802 errors.append(['status', cs.revision[:12]])
803 803
804 804 if errors:
805 805 revs = ','.join([x[1] for x in errors])
806 806 msg = M(self, 'rev_already_reviewed', state, revs=revs)
807 807 raise formencode.Invalid(msg, value, state,
808 808 error_dict=dict(revisions=revs)
809 809 )
810 810
811 811 return _validator
812 812
813 813
814 814 def ValidIp():
815 815 class _validator(CIDR):
816 816 messages = dict(
817 817 badFormat=_('Please enter a valid IPv4 or IpV6 address'),
818 818 illegalBits=_('The network size (bits) must be within the range'
819 819 ' of 0-32 (not %(bits)r)')
820 820 )
821 821
822 822 def to_python(self, value, state):
823 823 v = super(_validator, self).to_python(value, state)
824 824 v = v.strip()
825 825 net = ipaddr.IPNetwork(address=v)
826 826 if isinstance(net, ipaddr.IPv4Network):
827 827 #if IPv4 doesn't end with a mask, add /32
828 828 if '/' not in value:
829 829 v += '/32'
830 830 if isinstance(net, ipaddr.IPv6Network):
831 831 #if IPv6 doesn't end with a mask, add /128
832 832 if '/' not in value:
833 833 v += '/128'
834 834 return v
835 835
836 836 def validate_python(self, value, state):
837 837 try:
838 838 addr = value.strip()
839 839 #this raises an ValueError if address is not IpV4 or IpV6
840 840 ipaddr.IPNetwork(address=addr)
841 841 except ValueError:
842 842 raise formencode.Invalid(self.message('badFormat', state),
843 843 value, state)
844 844
845 845 return _validator
846 846
847 847
848 848 def FieldKey():
849 849 class _validator(formencode.validators.FancyValidator):
850 850 messages = dict(
851 851 badFormat=_('Key name can only consist of letters, '
852 852 'underscore, dash or numbers')
853 853 )
854 854
855 855 def validate_python(self, value, state):
856 856 if not re.match('[a-zA-Z0-9_-]+$', value):
857 857 raise formencode.Invalid(self.message('badFormat', state),
858 858 value, state)
859 859 return _validator
860 860
861 861
862 862 def BasePath():
863 863 class _validator(formencode.validators.FancyValidator):
864 864 messages = dict(
865 865 badPath=_('Filename cannot be inside a directory')
866 866 )
867 867
868 868 def _to_python(self, value, state):
869 869 return value
870 870
871 871 def validate_python(self, value, state):
872 872 if value != os.path.basename(value):
873 873 raise formencode.Invalid(self.message('badPath', state),
874 874 value, state)
875 875 return _validator
876 876
877 877
878 878 def ValidAuthPlugins():
879 879 class _validator(formencode.validators.FancyValidator):
880 880 messages = dict(
881 881 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
882 882 )
883 883
884 884 def _to_python(self, value, state):
885 885 # filter empty values
886 886 return filter(lambda s: s not in [None, ''], value)
887 887
888 888 def validate_python(self, value, state):
889 889 from kallithea.lib import auth_modules
890 890 module_list = value
891 891 unique_names = {}
892 892 try:
893 893 for module in module_list:
894 894 plugin = auth_modules.loadplugin(module)
895 895 plugin_name = plugin.name
896 896 if plugin_name in unique_names:
897 897 msg = M(self, 'import_duplicate', state,
898 898 loaded=unique_names[plugin_name],
899 899 next_to_load=plugin_name)
900 900 raise formencode.Invalid(msg, value, state)
901 901 unique_names[plugin_name] = plugin
902 902 except (ImportError, AttributeError, TypeError), e:
903 903 raise formencode.Invalid(str(e), value, state)
904 904
905 905 return _validator
General Comments 0
You need to be logged in to leave comments. Login now