##// END OF EJS Templates
added caching layer into RSS/ATOM feeds...
marcink -
r3018:023f7873 beta
parent child Browse files
Show More
@@ -1,147 +1,175 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.feed
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Feed controller for rhodecode
7 7
8 8 :created_on: Apr 23, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 27
28 28 from pylons import url, response, tmpl_context as c
29 29 from pylons.i18n.translation import _
30 30
31 from beaker.cache import cache_region, region_invalidate
31 32 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
32 33
33 34 from rhodecode.lib import helpers as h
34 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 36 from rhodecode.lib.base import BaseRepoController
36 from rhodecode.lib.diffs import DiffProcessor
37 from rhodecode.lib.diffs import DiffProcessor, LimitedDiffContainer
38 from rhodecode.model.db import CacheInvalidation
37 39
38 40 log = logging.getLogger(__name__)
39 41
40 42
41 43 class FeedController(BaseRepoController):
42 44
43 45 @LoginRequired(api_access=True)
44 46 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
45 47 'repository.admin')
46 48 def __before__(self):
47 49 super(FeedController, self).__before__()
48 50 #common values for feeds
49 51 self.description = _('Changes on %s repository')
50 52 self.title = self.title = _('%s %s feed') % (c.rhodecode_name, '%s')
51 53 self.language = 'en-us'
52 54 self.ttl = "5"
53 55 self.feed_nr = 20
56 # we need to protect from parsing huge diffs here other way
57 # we can kill the server, 32*1024 chars is a reasonable limit
58 self.feed_diff_limit = 32 * 1024
54 59
55 60 def _get_title(self, cs):
56 61 return "%s" % (
57 62 h.shorter(cs.message, 160)
58 63 )
59 64
60 65 def __changes(self, cs):
61 66 changes = []
62 _diff = cs.diff()
63 # we need to protect from parsing huge diffs here other way
64 # we can kill the server, 32*1024 chars is a reasonable limit
65 HUGE_DIFF = 32 * 1024
66 if len(_diff) > HUGE_DIFF:
67 changes = ['\n ' + _('Changeset was too big and was cut off...')]
68 return changes
69 diffprocessor = DiffProcessor(_diff)
70 stats = diffprocessor.prepare(inline_diff=False)
71 for st in stats:
67 diff_processor = DiffProcessor(cs.diff(),
68 diff_limit=self.feed_diff_limit)
69 _parsed = diff_processor.prepare(inline_diff=False)
70 limited_diff = False
71 if isinstance(_parsed, LimitedDiffContainer):
72 limited_diff = True
73
74 for st in _parsed:
72 75 st.update({'added': st['stats'][0],
73 76 'removed': st['stats'][1]})
74 77 changes.append('\n %(operation)s %(filename)s '
75 78 '(%(added)s lines added, %(removed)s lines removed)'
76 79 % st)
80 if limited_diff:
81 changes = changes + ['\n ' +
82 _('Changeset was too big and was cut off...')]
77 83 return changes
78 84
79 85 def __get_desc(self, cs):
80 86 desc_msg = []
81 desc_msg.append('%s %s %s:<br/>' % (cs.author, _('commited on'),
87 desc_msg.append('%s %s %s<br/>' % (h.person(cs.author),
88 _('commited on'),
82 89 h.fmt_date(cs.date)))
83 90 #branches, tags, bookmarks
84 91 if cs.branch:
85 92 desc_msg.append('branch: %s<br/>' % cs.branch)
86 93 if h.is_hg(c.rhodecode_repo):
87 94 for book in cs.bookmarks:
88 95 desc_msg.append('bookmark: %s<br/>' % book)
89 96 for tag in cs.tags:
90 97 desc_msg.append('tag: %s<br/>' % tag)
91 98 # rev link
92 99 _url = url('changeset_home', repo_name=cs.repository.name,
93 100 revision=cs.raw_id, qualified=True)
94 101 desc_msg.append('changesest: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
95 102
96 103 desc_msg.append('<pre>')
97 104 desc_msg.append(cs.message)
98 105 desc_msg.append('\n')
99 106 desc_msg.extend(self.__changes(cs))
100 107 desc_msg.append('</pre>')
101 108 return desc_msg
102 109
103 110 def atom(self, repo_name):
104 111 """Produce an atom-1.0 feed via feedgenerator module"""
105 feed = Atom1Feed(
106 title=self.title % repo_name,
107 link=url('summary_home', repo_name=repo_name,
108 qualified=True),
109 description=self.description % repo_name,
110 language=self.language,
111 ttl=self.ttl
112 )
112
113 @cache_region('long_term')
114 def _get_feed_from_cache(key):
115 feed = Atom1Feed(
116 title=self.title % repo_name,
117 link=url('summary_home', repo_name=repo_name,
118 qualified=True),
119 description=self.description % repo_name,
120 language=self.language,
121 ttl=self.ttl
122 )
113 123
114 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 feed.add_item(title=self._get_title(cs),
116 link=url('changeset_home', repo_name=repo_name,
117 revision=cs.raw_id, qualified=True),
118 author_name=cs.author,
119 description=''.join(self.__get_desc(cs)),
120 pubdate=cs.date,
121 )
124 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
125 feed.add_item(title=self._get_title(cs),
126 link=url('changeset_home', repo_name=repo_name,
127 revision=cs.raw_id, qualified=True),
128 author_name=cs.author,
129 description=''.join(self.__get_desc(cs)),
130 pubdate=cs.date,
131 )
122 132
123 response.content_type = feed.mime_type
124 return feed.writeString('utf-8')
133 response.content_type = feed.mime_type
134 return feed.writeString('utf-8')
135
136 key = repo_name + '_ATOM'
137 inv = CacheInvalidation.invalidate(key)
138 if inv is not None:
139 region_invalidate(_get_feed_from_cache, None, key)
140 CacheInvalidation.set_valid(inv.cache_key)
141 return _get_feed_from_cache(key)
125 142
126 143 def rss(self, repo_name):
127 144 """Produce an rss2 feed via feedgenerator module"""
128 feed = Rss201rev2Feed(
129 title=self.title % repo_name,
130 link=url('summary_home', repo_name=repo_name,
131 qualified=True),
132 description=self.description % repo_name,
133 language=self.language,
134 ttl=self.ttl
135 )
145
146 @cache_region('long_term')
147 def _get_feed_from_cache(key):
148 feed = Rss201rev2Feed(
149 title=self.title % repo_name,
150 link=url('summary_home', repo_name=repo_name,
151 qualified=True),
152 description=self.description % repo_name,
153 language=self.language,
154 ttl=self.ttl
155 )
136 156
137 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
138 feed.add_item(title=self._get_title(cs),
139 link=url('changeset_home', repo_name=repo_name,
140 revision=cs.raw_id, qualified=True),
141 author_name=cs.author,
142 description=''.join(self.__get_desc(cs)),
143 pubdate=cs.date,
144 )
157 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
158 feed.add_item(title=self._get_title(cs),
159 link=url('changeset_home', repo_name=repo_name,
160 revision=cs.raw_id, qualified=True),
161 author_name=cs.author,
162 description=''.join(self.__get_desc(cs)),
163 pubdate=cs.date,
164 )
145 165
146 response.content_type = feed.mime_type
147 return feed.writeString('utf-8')
166 response.content_type = feed.mime_type
167 return feed.writeString('utf-8')
168
169 key = repo_name + '_RSS'
170 inv = CacheInvalidation.invalidate(key)
171 if inv is not None:
172 region_invalidate(_get_feed_from_cache, None, key)
173 CacheInvalidation.set_valid(inv.cache_key)
174 return _get_feed_from_cache(key)
175
@@ -1,557 +1,569 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.utils
4 4 ~~~~~~~~~~~~~~~~~~~
5 5
6 6 Some simple helper functions
7 7
8 8 :created_on: Jan 5, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import re
27 27 import time
28 28 import datetime
29 29 import webob
30 30
31 31 from pylons.i18n.translation import _, ungettext
32 32 from rhodecode.lib.vcs.utils.lazy import LazyProperty
33 33
34 34
35 35 def __get_lem():
36 36 """
37 37 Get language extension map based on what's inside pygments lexers
38 38 """
39 39 from pygments import lexers
40 40 from string import lower
41 41 from collections import defaultdict
42 42
43 43 d = defaultdict(lambda: [])
44 44
45 45 def __clean(s):
46 46 s = s.lstrip('*')
47 47 s = s.lstrip('.')
48 48
49 49 if s.find('[') != -1:
50 50 exts = []
51 51 start, stop = s.find('['), s.find(']')
52 52
53 53 for suffix in s[start + 1:stop]:
54 54 exts.append(s[:s.find('[')] + suffix)
55 55 return map(lower, exts)
56 56 else:
57 57 return map(lower, [s])
58 58
59 59 for lx, t in sorted(lexers.LEXERS.items()):
60 60 m = map(__clean, t[-2])
61 61 if m:
62 62 m = reduce(lambda x, y: x + y, m)
63 63 for ext in m:
64 64 desc = lx.replace('Lexer', '')
65 65 d[ext].append(desc)
66 66
67 67 return dict(d)
68 68
69 69
70 70 def str2bool(_str):
71 71 """
72 72 returs True/False value from given string, it tries to translate the
73 73 string into boolean
74 74
75 75 :param _str: string value to translate into boolean
76 76 :rtype: boolean
77 77 :returns: boolean from given string
78 78 """
79 79 if _str is None:
80 80 return False
81 81 if _str in (True, False):
82 82 return _str
83 83 _str = str(_str).strip().lower()
84 84 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
85 85
86 86
87 87 def aslist(obj, sep=None, strip=True):
88 88 """
89 89 Returns given string separated by sep as list
90 90
91 91 :param obj:
92 92 :param sep:
93 93 :param strip:
94 94 """
95 95 if isinstance(obj, (basestring)):
96 96 lst = obj.split(sep)
97 97 if strip:
98 98 lst = [v.strip() for v in lst]
99 99 return lst
100 100 elif isinstance(obj, (list, tuple)):
101 101 return obj
102 102 elif obj is None:
103 103 return []
104 104 else:
105 105 return [obj]
106 106
107 107
108 108 def convert_line_endings(line, mode):
109 109 """
110 110 Converts a given line "line end" accordingly to given mode
111 111
112 112 Available modes are::
113 113 0 - Unix
114 114 1 - Mac
115 115 2 - DOS
116 116
117 117 :param line: given line to convert
118 118 :param mode: mode to convert to
119 119 :rtype: str
120 120 :return: converted line according to mode
121 121 """
122 122 from string import replace
123 123
124 124 if mode == 0:
125 125 line = replace(line, '\r\n', '\n')
126 126 line = replace(line, '\r', '\n')
127 127 elif mode == 1:
128 128 line = replace(line, '\r\n', '\r')
129 129 line = replace(line, '\n', '\r')
130 130 elif mode == 2:
131 131 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
132 132 return line
133 133
134 134
135 135 def detect_mode(line, default):
136 136 """
137 137 Detects line break for given line, if line break couldn't be found
138 138 given default value is returned
139 139
140 140 :param line: str line
141 141 :param default: default
142 142 :rtype: int
143 143 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
144 144 """
145 145 if line.endswith('\r\n'):
146 146 return 2
147 147 elif line.endswith('\n'):
148 148 return 0
149 149 elif line.endswith('\r'):
150 150 return 1
151 151 else:
152 152 return default
153 153
154 154
155 155 def generate_api_key(username, salt=None):
156 156 """
157 157 Generates unique API key for given username, if salt is not given
158 158 it'll be generated from some random string
159 159
160 160 :param username: username as string
161 161 :param salt: salt to hash generate KEY
162 162 :rtype: str
163 163 :returns: sha1 hash from username+salt
164 164 """
165 165 from tempfile import _RandomNameSequence
166 166 import hashlib
167 167
168 168 if salt is None:
169 169 salt = _RandomNameSequence().next()
170 170
171 171 return hashlib.sha1(username + salt).hexdigest()
172 172
173 173
174 174 def safe_int(val, default=None):
175 175 """
176 176 Returns int() of val if val is not convertable to int use default
177 177 instead
178 178
179 179 :param val:
180 180 :param default:
181 181 """
182 182
183 183 try:
184 184 val = int(val)
185 185 except ValueError:
186 186 val = default
187 187
188 188 return val
189 189
190 190
191 191 def safe_unicode(str_, from_encoding=None):
192 192 """
193 193 safe unicode function. Does few trick to turn str_ into unicode
194 194
195 195 In case of UnicodeDecode error we try to return it with encoding detected
196 196 by chardet library if it fails fallback to unicode with errors replaced
197 197
198 198 :param str_: string to decode
199 199 :rtype: unicode
200 200 :returns: unicode object
201 201 """
202 202 if isinstance(str_, unicode):
203 203 return str_
204 204
205 205 if not from_encoding:
206 206 import rhodecode
207 207 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
208 208 'utf8'), sep=',')
209 209 from_encoding = DEFAULT_ENCODINGS
210 210
211 211 if not isinstance(from_encoding, (list, tuple)):
212 212 from_encoding = [from_encoding]
213 213
214 214 try:
215 215 return unicode(str_)
216 216 except UnicodeDecodeError:
217 217 pass
218 218
219 219 for enc in from_encoding:
220 220 try:
221 221 return unicode(str_, enc)
222 222 except UnicodeDecodeError:
223 223 pass
224 224
225 225 try:
226 226 import chardet
227 227 encoding = chardet.detect(str_)['encoding']
228 228 if encoding is None:
229 229 raise Exception()
230 230 return str_.decode(encoding)
231 231 except (ImportError, UnicodeDecodeError, Exception):
232 232 return unicode(str_, from_encoding[0], 'replace')
233 233
234 234
235 235 def safe_str(unicode_, to_encoding=None):
236 236 """
237 237 safe str function. Does few trick to turn unicode_ into string
238 238
239 239 In case of UnicodeEncodeError we try to return it with encoding detected
240 240 by chardet library if it fails fallback to string with errors replaced
241 241
242 242 :param unicode_: unicode to encode
243 243 :rtype: str
244 244 :returns: str object
245 245 """
246 246
247 247 # if it's not basestr cast to str
248 248 if not isinstance(unicode_, basestring):
249 249 return str(unicode_)
250 250
251 251 if isinstance(unicode_, str):
252 252 return unicode_
253 253
254 254 if not to_encoding:
255 255 import rhodecode
256 256 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
257 257 'utf8'), sep=',')
258 258 to_encoding = DEFAULT_ENCODINGS
259 259
260 260 if not isinstance(to_encoding, (list, tuple)):
261 261 to_encoding = [to_encoding]
262 262
263 263 for enc in to_encoding:
264 264 try:
265 265 return unicode_.encode(enc)
266 266 except UnicodeEncodeError:
267 267 pass
268 268
269 269 try:
270 270 import chardet
271 271 encoding = chardet.detect(unicode_)['encoding']
272 272 if encoding is None:
273 273 raise UnicodeEncodeError()
274 274
275 275 return unicode_.encode(encoding)
276 276 except (ImportError, UnicodeEncodeError):
277 277 return unicode_.encode(to_encoding[0], 'replace')
278 278
279 279 return safe_str
280 280
281 281
282 def remove_suffix(s, suffix):
283 if s.endswith(suffix):
284 s = s[:-1 * len(suffix)]
285 return s
286
287
288 def remove_prefix(s, prefix):
289 if s.startswith(prefix):
290 s = s[:-1 * len(prefix)]
291 return s
292
293
282 294 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
283 295 """
284 296 Custom engine_from_config functions that makes sure we use NullPool for
285 297 file based sqlite databases. This prevents errors on sqlite. This only
286 298 applies to sqlalchemy versions < 0.7.0
287 299
288 300 """
289 301 import sqlalchemy
290 302 from sqlalchemy import engine_from_config as efc
291 303 import logging
292 304
293 305 if int(sqlalchemy.__version__.split('.')[1]) < 7:
294 306
295 307 # This solution should work for sqlalchemy < 0.7.0, and should use
296 308 # proxy=TimerProxy() for execution time profiling
297 309
298 310 from sqlalchemy.pool import NullPool
299 311 url = configuration[prefix + 'url']
300 312
301 313 if url.startswith('sqlite'):
302 314 kwargs.update({'poolclass': NullPool})
303 315 return efc(configuration, prefix, **kwargs)
304 316 else:
305 317 import time
306 318 from sqlalchemy import event
307 319 from sqlalchemy.engine import Engine
308 320
309 321 log = logging.getLogger('sqlalchemy.engine')
310 322 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
311 323 engine = efc(configuration, prefix, **kwargs)
312 324
313 325 def color_sql(sql):
314 326 COLOR_SEQ = "\033[1;%dm"
315 327 COLOR_SQL = YELLOW
316 328 normal = '\x1b[0m'
317 329 return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
318 330
319 331 if configuration['debug']:
320 332 #attach events only for debug configuration
321 333
322 334 def before_cursor_execute(conn, cursor, statement,
323 335 parameters, context, executemany):
324 336 context._query_start_time = time.time()
325 337 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
326 338
327 339 def after_cursor_execute(conn, cursor, statement,
328 340 parameters, context, executemany):
329 341 total = time.time() - context._query_start_time
330 342 log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
331 343
332 344 event.listen(engine, "before_cursor_execute",
333 345 before_cursor_execute)
334 346 event.listen(engine, "after_cursor_execute",
335 347 after_cursor_execute)
336 348
337 349 return engine
338 350
339 351
340 352 def age(prevdate):
341 353 """
342 354 turns a datetime into an age string.
343 355
344 356 :param prevdate: datetime object
345 357 :rtype: unicode
346 358 :returns: unicode words describing age
347 359 """
348 360
349 361 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
350 362 deltas = {}
351 363 future = False
352 364
353 365 # Get date parts deltas
354 366 now = datetime.datetime.now()
355 367 if prevdate > now:
356 368 now, prevdate = prevdate, now
357 369 future = True
358 370
359 371 for part in order:
360 372 deltas[part] = getattr(now, part) - getattr(prevdate, part)
361 373
362 374 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
363 375 # not 1 hour, -59 minutes and -59 seconds)
364 376
365 377 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
366 378 part = order[num]
367 379 carry_part = order[num - 1]
368 380
369 381 if deltas[part] < 0:
370 382 deltas[part] += length
371 383 deltas[carry_part] -= 1
372 384
373 385 # Same thing for days except that the increment depends on the (variable)
374 386 # number of days in the month
375 387 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
376 388 if deltas['day'] < 0:
377 389 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
378 390 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
379 391 deltas['day'] += 29
380 392 else:
381 393 deltas['day'] += month_lengths[prevdate.month - 1]
382 394
383 395 deltas['month'] -= 1
384 396
385 397 if deltas['month'] < 0:
386 398 deltas['month'] += 12
387 399 deltas['year'] -= 1
388 400
389 401 # Format the result
390 402 fmt_funcs = {
391 403 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
392 404 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
393 405 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
394 406 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
395 407 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
396 408 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
397 409 }
398 410
399 411 for i, part in enumerate(order):
400 412 value = deltas[part]
401 413 if value == 0:
402 414 continue
403 415
404 416 if i < 5:
405 417 sub_part = order[i + 1]
406 418 sub_value = deltas[sub_part]
407 419 else:
408 420 sub_value = 0
409 421
410 422 if sub_value == 0:
411 423 if future:
412 424 return _(u'in %s') % fmt_funcs[part](value)
413 425 else:
414 426 return _(u'%s ago') % fmt_funcs[part](value)
415 427 if future:
416 428 return _(u'in %s and %s') % (fmt_funcs[part](value),
417 429 fmt_funcs[sub_part](sub_value))
418 430 else:
419 431 return _(u'%s and %s ago') % (fmt_funcs[part](value),
420 432 fmt_funcs[sub_part](sub_value))
421 433
422 434 return _(u'just now')
423 435
424 436
425 437 def uri_filter(uri):
426 438 """
427 439 Removes user:password from given url string
428 440
429 441 :param uri:
430 442 :rtype: unicode
431 443 :returns: filtered list of strings
432 444 """
433 445 if not uri:
434 446 return ''
435 447
436 448 proto = ''
437 449
438 450 for pat in ('https://', 'http://'):
439 451 if uri.startswith(pat):
440 452 uri = uri[len(pat):]
441 453 proto = pat
442 454 break
443 455
444 456 # remove passwords and username
445 457 uri = uri[uri.find('@') + 1:]
446 458
447 459 # get the port
448 460 cred_pos = uri.find(':')
449 461 if cred_pos == -1:
450 462 host, port = uri, None
451 463 else:
452 464 host, port = uri[:cred_pos], uri[cred_pos + 1:]
453 465
454 466 return filter(None, [proto, host, port])
455 467
456 468
457 469 def credentials_filter(uri):
458 470 """
459 471 Returns a url with removed credentials
460 472
461 473 :param uri:
462 474 """
463 475
464 476 uri = uri_filter(uri)
465 477 #check if we have port
466 478 if len(uri) > 2 and uri[2]:
467 479 uri[2] = ':' + uri[2]
468 480
469 481 return ''.join(uri)
470 482
471 483
472 484 def get_changeset_safe(repo, rev):
473 485 """
474 486 Safe version of get_changeset if this changeset doesn't exists for a
475 487 repo it returns a Dummy one instead
476 488
477 489 :param repo:
478 490 :param rev:
479 491 """
480 492 from rhodecode.lib.vcs.backends.base import BaseRepository
481 493 from rhodecode.lib.vcs.exceptions import RepositoryError
482 494 from rhodecode.lib.vcs.backends.base import EmptyChangeset
483 495 if not isinstance(repo, BaseRepository):
484 496 raise Exception('You must pass an Repository '
485 497 'object as first argument got %s', type(repo))
486 498
487 499 try:
488 500 cs = repo.get_changeset(rev)
489 501 except RepositoryError:
490 502 cs = EmptyChangeset(requested_revision=rev)
491 503 return cs
492 504
493 505
494 506 def datetime_to_time(dt):
495 507 if dt:
496 508 return time.mktime(dt.timetuple())
497 509
498 510
499 511 def time_to_datetime(tm):
500 512 if tm:
501 513 if isinstance(tm, basestring):
502 514 try:
503 515 tm = float(tm)
504 516 except ValueError:
505 517 return
506 518 return datetime.datetime.fromtimestamp(tm)
507 519
508 520 MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
509 521
510 522
511 523 def extract_mentioned_users(s):
512 524 """
513 525 Returns unique usernames from given string s that have @mention
514 526
515 527 :param s: string to get mentions
516 528 """
517 529 usrs = set()
518 530 for username in re.findall(MENTIONS_REGEX, s):
519 531 usrs.add(username)
520 532
521 533 return sorted(list(usrs), key=lambda k: k.lower())
522 534
523 535
524 536 class AttributeDict(dict):
525 537 def __getattr__(self, attr):
526 538 return self.get(attr, None)
527 539 __setattr__ = dict.__setitem__
528 540 __delattr__ = dict.__delitem__
529 541
530 542
531 543 def fix_PATH(os_=None):
532 544 """
533 545 Get current active python path, and append it to PATH variable to fix issues
534 546 of subprocess calls and different python versions
535 547 """
536 548 import sys
537 549 if os_ is None:
538 550 import os
539 551 else:
540 552 os = os_
541 553
542 554 cur_path = os.path.split(sys.executable)[0]
543 555 if not os.environ['PATH'].startswith(cur_path):
544 556 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
545 557
546 558
547 559 def obfuscate_url_pw(engine):
548 560 from sqlalchemy.engine import url
549 561 url = url.make_url(engine)
550 562 if url.password:
551 563 url.password = 'XXXXX'
552 564 return str(url)
553 565
554 566
555 567 def get_server_url(environ):
556 568 req = webob.Request(environ)
557 569 return req.host_url + req.script_name
@@ -1,1797 +1,1802 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.model.db
4 4 ~~~~~~~~~~~~~~~~~~
5 5
6 6 Database Models for RhodeCode
7 7
8 8 :created_on: Apr 08, 2010
9 9 :author: marcink
10 10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
14 14 # it under the terms of the GNU General Public License as published by
15 15 # the Free Software Foundation, either version 3 of the License, or
16 16 # (at your option) any later version.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 27 import logging
28 28 import datetime
29 29 import traceback
30 30 import hashlib
31 31 import time
32 32 from collections import defaultdict
33 33
34 34 from sqlalchemy import *
35 35 from sqlalchemy.ext.hybrid import hybrid_property
36 36 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
37 37 from sqlalchemy.exc import DatabaseError
38 38 from beaker.cache import cache_region, region_invalidate
39 39 from webob.exc import HTTPNotFound
40 40
41 41 from pylons.i18n.translation import lazy_ugettext as _
42 42
43 43 from rhodecode.lib.vcs import get_backend
44 44 from rhodecode.lib.vcs.utils.helpers import get_scm
45 45 from rhodecode.lib.vcs.exceptions import VCSError
46 46 from rhodecode.lib.vcs.utils.lazy import LazyProperty
47 47
48 48 from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \
49 safe_unicode
49 safe_unicode, remove_suffix
50 50 from rhodecode.lib.compat import json
51 51 from rhodecode.lib.caching_query import FromCache
52 52
53 53 from rhodecode.model.meta import Base, Session
54 54
55 55 URL_SEP = '/'
56 56 log = logging.getLogger(__name__)
57 57
58 58 #==============================================================================
59 59 # BASE CLASSES
60 60 #==============================================================================
61 61
62 62 _hash_key = lambda k: hashlib.md5(safe_str(k)).hexdigest()
63 63
64 64
65 65 class BaseModel(object):
66 66 """
67 67 Base Model for all classess
68 68 """
69 69
70 70 @classmethod
71 71 def _get_keys(cls):
72 72 """return column names for this model """
73 73 return class_mapper(cls).c.keys()
74 74
75 75 def get_dict(self):
76 76 """
77 77 return dict with keys and values corresponding
78 78 to this model data """
79 79
80 80 d = {}
81 81 for k in self._get_keys():
82 82 d[k] = getattr(self, k)
83 83
84 84 # also use __json__() if present to get additional fields
85 85 _json_attr = getattr(self, '__json__', None)
86 86 if _json_attr:
87 87 # update with attributes from __json__
88 88 if callable(_json_attr):
89 89 _json_attr = _json_attr()
90 90 for k, val in _json_attr.iteritems():
91 91 d[k] = val
92 92 return d
93 93
94 94 def get_appstruct(self):
95 95 """return list with keys and values tupples corresponding
96 96 to this model data """
97 97
98 98 l = []
99 99 for k in self._get_keys():
100 100 l.append((k, getattr(self, k),))
101 101 return l
102 102
103 103 def populate_obj(self, populate_dict):
104 104 """populate model with data from given populate_dict"""
105 105
106 106 for k in self._get_keys():
107 107 if k in populate_dict:
108 108 setattr(self, k, populate_dict[k])
109 109
110 110 @classmethod
111 111 def query(cls):
112 112 return Session().query(cls)
113 113
114 114 @classmethod
115 115 def get(cls, id_):
116 116 if id_:
117 117 return cls.query().get(id_)
118 118
119 119 @classmethod
120 120 def get_or_404(cls, id_):
121 121 try:
122 122 id_ = int(id_)
123 123 except (TypeError, ValueError):
124 124 raise HTTPNotFound
125 125
126 126 res = cls.query().get(id_)
127 127 if not res:
128 128 raise HTTPNotFound
129 129 return res
130 130
131 131 @classmethod
132 132 def getAll(cls):
133 133 return cls.query().all()
134 134
135 135 @classmethod
136 136 def delete(cls, id_):
137 137 obj = cls.query().get(id_)
138 138 Session().delete(obj)
139 139
140 140 def __repr__(self):
141 141 if hasattr(self, '__unicode__'):
142 142 # python repr needs to return str
143 143 return safe_str(self.__unicode__())
144 144 return '<DB:%s>' % (self.__class__.__name__)
145 145
146 146
147 147 class RhodeCodeSetting(Base, BaseModel):
148 148 __tablename__ = 'rhodecode_settings'
149 149 __table_args__ = (
150 150 UniqueConstraint('app_settings_name'),
151 151 {'extend_existing': True, 'mysql_engine': 'InnoDB',
152 152 'mysql_charset': 'utf8'}
153 153 )
154 154 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
155 155 app_settings_name = Column("app_settings_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
156 156 _app_settings_value = Column("app_settings_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
157 157
158 158 def __init__(self, k='', v=''):
159 159 self.app_settings_name = k
160 160 self.app_settings_value = v
161 161
162 162 @validates('_app_settings_value')
163 163 def validate_settings_value(self, key, val):
164 164 assert type(val) == unicode
165 165 return val
166 166
167 167 @hybrid_property
168 168 def app_settings_value(self):
169 169 v = self._app_settings_value
170 170 if self.app_settings_name == 'ldap_active':
171 171 v = str2bool(v)
172 172 return v
173 173
174 174 @app_settings_value.setter
175 175 def app_settings_value(self, val):
176 176 """
177 177 Setter that will always make sure we use unicode in app_settings_value
178 178
179 179 :param val:
180 180 """
181 181 self._app_settings_value = safe_unicode(val)
182 182
183 183 def __unicode__(self):
184 184 return u"<%s('%s:%s')>" % (
185 185 self.__class__.__name__,
186 186 self.app_settings_name, self.app_settings_value
187 187 )
188 188
189 189 @classmethod
190 190 def get_by_name(cls, key):
191 191 return cls.query()\
192 192 .filter(cls.app_settings_name == key).scalar()
193 193
194 194 @classmethod
195 195 def get_by_name_or_create(cls, key):
196 196 res = cls.get_by_name(key)
197 197 if not res:
198 198 res = cls(key)
199 199 return res
200 200
201 201 @classmethod
202 202 def get_app_settings(cls, cache=False):
203 203
204 204 ret = cls.query()
205 205
206 206 if cache:
207 207 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
208 208
209 209 if not ret:
210 210 raise Exception('Could not get application settings !')
211 211 settings = {}
212 212 for each in ret:
213 213 settings['rhodecode_' + each.app_settings_name] = \
214 214 each.app_settings_value
215 215
216 216 return settings
217 217
218 218 @classmethod
219 219 def get_ldap_settings(cls, cache=False):
220 220 ret = cls.query()\
221 221 .filter(cls.app_settings_name.startswith('ldap_')).all()
222 222 fd = {}
223 223 for row in ret:
224 224 fd.update({row.app_settings_name: row.app_settings_value})
225 225
226 226 return fd
227 227
228 228
229 229 class RhodeCodeUi(Base, BaseModel):
230 230 __tablename__ = 'rhodecode_ui'
231 231 __table_args__ = (
232 232 UniqueConstraint('ui_key'),
233 233 {'extend_existing': True, 'mysql_engine': 'InnoDB',
234 234 'mysql_charset': 'utf8'}
235 235 )
236 236
237 237 HOOK_UPDATE = 'changegroup.update'
238 238 HOOK_REPO_SIZE = 'changegroup.repo_size'
239 239 HOOK_PUSH = 'changegroup.push_logger'
240 240 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
241 241 HOOK_PULL = 'outgoing.pull_logger'
242 242 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
243 243
244 244 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
245 245 ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
246 246 ui_key = Column("ui_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
247 247 ui_value = Column("ui_value", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
248 248 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
249 249
250 250 @classmethod
251 251 def get_by_key(cls, key):
252 252 return cls.query().filter(cls.ui_key == key).scalar()
253 253
254 254 @classmethod
255 255 def get_builtin_hooks(cls):
256 256 q = cls.query()
257 257 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
258 258 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
259 259 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
260 260 return q.all()
261 261
262 262 @classmethod
263 263 def get_custom_hooks(cls):
264 264 q = cls.query()
265 265 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE,
266 266 cls.HOOK_PUSH, cls.HOOK_PRE_PUSH,
267 267 cls.HOOK_PULL, cls.HOOK_PRE_PULL]))
268 268 q = q.filter(cls.ui_section == 'hooks')
269 269 return q.all()
270 270
271 271 @classmethod
272 272 def get_repos_location(cls):
273 273 return cls.get_by_key('/').ui_value
274 274
275 275 @classmethod
276 276 def create_or_update_hook(cls, key, val):
277 277 new_ui = cls.get_by_key(key) or cls()
278 278 new_ui.ui_section = 'hooks'
279 279 new_ui.ui_active = True
280 280 new_ui.ui_key = key
281 281 new_ui.ui_value = val
282 282
283 283 Session().add(new_ui)
284 284
285 285 def __repr__(self):
286 286 return '<DB:%s[%s:%s]>' % (self.__class__.__name__, self.ui_key,
287 287 self.ui_value)
288 288
289 289
290 290 class User(Base, BaseModel):
291 291 __tablename__ = 'users'
292 292 __table_args__ = (
293 293 UniqueConstraint('username'), UniqueConstraint('email'),
294 294 Index('u_username_idx', 'username'),
295 295 Index('u_email_idx', 'email'),
296 296 {'extend_existing': True, 'mysql_engine': 'InnoDB',
297 297 'mysql_charset': 'utf8'}
298 298 )
299 299 DEFAULT_USER = 'default'
300 300 DEFAULT_PERMISSIONS = [
301 301 'hg.register.manual_activate', 'hg.create.repository',
302 302 'hg.fork.repository', 'repository.read'
303 303 ]
304 304 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
305 305 username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
306 306 password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
307 307 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
308 308 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
309 309 name = Column("firstname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
310 310 lastname = Column("lastname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
311 311 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
312 312 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
313 313 ldap_dn = Column("ldap_dn", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
314 314 api_key = Column("api_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
315 315 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
316 316
317 317 user_log = relationship('UserLog', cascade='all')
318 318 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
319 319
320 320 repositories = relationship('Repository')
321 321 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
322 322 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
323 323 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
324 324
325 325 group_member = relationship('UsersGroupMember', cascade='all')
326 326
327 327 notifications = relationship('UserNotification', cascade='all')
328 328 # notifications assigned to this user
329 329 user_created_notifications = relationship('Notification', cascade='all')
330 330 # comments created by this user
331 331 user_comments = relationship('ChangesetComment', cascade='all')
332 332 #extra emails for this user
333 333 user_emails = relationship('UserEmailMap', cascade='all')
334 334
335 335 @hybrid_property
336 336 def email(self):
337 337 return self._email
338 338
339 339 @email.setter
340 340 def email(self, val):
341 341 self._email = val.lower() if val else None
342 342
343 343 @property
344 344 def firstname(self):
345 345 # alias for future
346 346 return self.name
347 347
348 348 @property
349 349 def emails(self):
350 350 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
351 351 return [self.email] + [x.email for x in other]
352 352
353 353 @property
354 354 def username_and_name(self):
355 355 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
356 356
357 357 @property
358 358 def full_name(self):
359 359 return '%s %s' % (self.firstname, self.lastname)
360 360
361 361 @property
362 362 def full_name_or_username(self):
363 363 return ('%s %s' % (self.firstname, self.lastname)
364 364 if (self.firstname and self.lastname) else self.username)
365 365
366 366 @property
367 367 def full_contact(self):
368 368 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
369 369
370 370 @property
371 371 def short_contact(self):
372 372 return '%s %s' % (self.firstname, self.lastname)
373 373
374 374 @property
375 375 def is_admin(self):
376 376 return self.admin
377 377
378 378 def __unicode__(self):
379 379 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
380 380 self.user_id, self.username)
381 381
382 382 @classmethod
383 383 def get_by_username(cls, username, case_insensitive=False, cache=False):
384 384 if case_insensitive:
385 385 q = cls.query().filter(cls.username.ilike(username))
386 386 else:
387 387 q = cls.query().filter(cls.username == username)
388 388
389 389 if cache:
390 390 q = q.options(FromCache(
391 391 "sql_cache_short",
392 392 "get_user_%s" % _hash_key(username)
393 393 )
394 394 )
395 395 return q.scalar()
396 396
397 397 @classmethod
398 398 def get_by_api_key(cls, api_key, cache=False):
399 399 q = cls.query().filter(cls.api_key == api_key)
400 400
401 401 if cache:
402 402 q = q.options(FromCache("sql_cache_short",
403 403 "get_api_key_%s" % api_key))
404 404 return q.scalar()
405 405
406 406 @classmethod
407 407 def get_by_email(cls, email, case_insensitive=False, cache=False):
408 408 if case_insensitive:
409 409 q = cls.query().filter(cls.email.ilike(email))
410 410 else:
411 411 q = cls.query().filter(cls.email == email)
412 412
413 413 if cache:
414 414 q = q.options(FromCache("sql_cache_short",
415 415 "get_email_key_%s" % email))
416 416
417 417 ret = q.scalar()
418 418 if ret is None:
419 419 q = UserEmailMap.query()
420 420 # try fetching in alternate email map
421 421 if case_insensitive:
422 422 q = q.filter(UserEmailMap.email.ilike(email))
423 423 else:
424 424 q = q.filter(UserEmailMap.email == email)
425 425 q = q.options(joinedload(UserEmailMap.user))
426 426 if cache:
427 427 q = q.options(FromCache("sql_cache_short",
428 428 "get_email_map_key_%s" % email))
429 429 ret = getattr(q.scalar(), 'user', None)
430 430
431 431 return ret
432 432
433 433 def update_lastlogin(self):
434 434 """Update user lastlogin"""
435 435 self.last_login = datetime.datetime.now()
436 436 Session().add(self)
437 437 log.debug('updated user %s lastlogin' % self.username)
438 438
439 439 def get_api_data(self):
440 440 """
441 441 Common function for generating user related data for API
442 442 """
443 443 user = self
444 444 data = dict(
445 445 user_id=user.user_id,
446 446 username=user.username,
447 447 firstname=user.name,
448 448 lastname=user.lastname,
449 449 email=user.email,
450 450 emails=user.emails,
451 451 api_key=user.api_key,
452 452 active=user.active,
453 453 admin=user.admin,
454 454 ldap_dn=user.ldap_dn,
455 455 last_login=user.last_login,
456 456 )
457 457 return data
458 458
459 459 def __json__(self):
460 460 data = dict(
461 461 full_name=self.full_name,
462 462 full_name_or_username=self.full_name_or_username,
463 463 short_contact=self.short_contact,
464 464 full_contact=self.full_contact
465 465 )
466 466 data.update(self.get_api_data())
467 467 return data
468 468
469 469
470 470 class UserEmailMap(Base, BaseModel):
471 471 __tablename__ = 'user_email_map'
472 472 __table_args__ = (
473 473 Index('uem_email_idx', 'email'),
474 474 UniqueConstraint('email'),
475 475 {'extend_existing': True, 'mysql_engine': 'InnoDB',
476 476 'mysql_charset': 'utf8'}
477 477 )
478 478 __mapper_args__ = {}
479 479
480 480 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
481 481 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
482 482 _email = Column("email", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
483 483 user = relationship('User', lazy='joined')
484 484
485 485 @validates('_email')
486 486 def validate_email(self, key, email):
487 487 # check if this email is not main one
488 488 main_email = Session().query(User).filter(User.email == email).scalar()
489 489 if main_email is not None:
490 490 raise AttributeError('email %s is present is user table' % email)
491 491 return email
492 492
493 493 @hybrid_property
494 494 def email(self):
495 495 return self._email
496 496
497 497 @email.setter
498 498 def email(self, val):
499 499 self._email = val.lower() if val else None
500 500
501 501
502 502 class UserLog(Base, BaseModel):
503 503 __tablename__ = 'user_logs'
504 504 __table_args__ = (
505 505 {'extend_existing': True, 'mysql_engine': 'InnoDB',
506 506 'mysql_charset': 'utf8'},
507 507 )
508 508 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
509 509 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
510 510 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
511 511 repository_name = Column("repository_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
512 512 user_ip = Column("user_ip", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
513 513 action = Column("action", UnicodeText(1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
514 514 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 515
516 516 @property
517 517 def action_as_day(self):
518 518 return datetime.date(*self.action_date.timetuple()[:3])
519 519
520 520 user = relationship('User')
521 521 repository = relationship('Repository', cascade='')
522 522
523 523
524 524 class UsersGroup(Base, BaseModel):
525 525 __tablename__ = 'users_groups'
526 526 __table_args__ = (
527 527 {'extend_existing': True, 'mysql_engine': 'InnoDB',
528 528 'mysql_charset': 'utf8'},
529 529 )
530 530
531 531 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
532 532 users_group_name = Column("users_group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
533 533 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
534 534 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
535 535
536 536 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
537 537 users_group_to_perm = relationship('UsersGroupToPerm', cascade='all')
538 538 users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
539 539
540 540 def __unicode__(self):
541 541 return u'<userGroup(%s)>' % (self.users_group_name)
542 542
543 543 @classmethod
544 544 def get_by_group_name(cls, group_name, cache=False,
545 545 case_insensitive=False):
546 546 if case_insensitive:
547 547 q = cls.query().filter(cls.users_group_name.ilike(group_name))
548 548 else:
549 549 q = cls.query().filter(cls.users_group_name == group_name)
550 550 if cache:
551 551 q = q.options(FromCache(
552 552 "sql_cache_short",
553 553 "get_user_%s" % _hash_key(group_name)
554 554 )
555 555 )
556 556 return q.scalar()
557 557
558 558 @classmethod
559 559 def get(cls, users_group_id, cache=False):
560 560 users_group = cls.query()
561 561 if cache:
562 562 users_group = users_group.options(FromCache("sql_cache_short",
563 563 "get_users_group_%s" % users_group_id))
564 564 return users_group.get(users_group_id)
565 565
566 566 def get_api_data(self):
567 567 users_group = self
568 568
569 569 data = dict(
570 570 users_group_id=users_group.users_group_id,
571 571 group_name=users_group.users_group_name,
572 572 active=users_group.users_group_active,
573 573 )
574 574
575 575 return data
576 576
577 577
578 578 class UsersGroupMember(Base, BaseModel):
579 579 __tablename__ = 'users_groups_members'
580 580 __table_args__ = (
581 581 {'extend_existing': True, 'mysql_engine': 'InnoDB',
582 582 'mysql_charset': 'utf8'},
583 583 )
584 584
585 585 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
586 586 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
587 587 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
588 588
589 589 user = relationship('User', lazy='joined')
590 590 users_group = relationship('UsersGroup')
591 591
592 592 def __init__(self, gr_id='', u_id=''):
593 593 self.users_group_id = gr_id
594 594 self.user_id = u_id
595 595
596 596
597 597 class Repository(Base, BaseModel):
598 598 __tablename__ = 'repositories'
599 599 __table_args__ = (
600 600 UniqueConstraint('repo_name'),
601 601 Index('r_repo_name_idx', 'repo_name'),
602 602 {'extend_existing': True, 'mysql_engine': 'InnoDB',
603 603 'mysql_charset': 'utf8'},
604 604 )
605 605
606 606 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
607 607 repo_name = Column("repo_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
608 608 clone_uri = Column("clone_uri", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
609 609 repo_type = Column("repo_type", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
610 610 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
611 611 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
612 612 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
613 613 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
614 614 description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
615 615 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
616 616 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
617 617 landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None)
618 618 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
619 619 _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
620 620
621 621 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
622 622 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
623 623
624 624 user = relationship('User')
625 625 fork = relationship('Repository', remote_side=repo_id)
626 626 group = relationship('RepoGroup')
627 627 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
628 628 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
629 629 stats = relationship('Statistics', cascade='all', uselist=False)
630 630
631 631 followers = relationship('UserFollowing',
632 632 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
633 633 cascade='all')
634 634
635 635 logs = relationship('UserLog')
636 636 comments = relationship('ChangesetComment', cascade="all, delete, delete-orphan")
637 637
638 638 pull_requests_org = relationship('PullRequest',
639 639 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
640 640 cascade="all, delete, delete-orphan")
641 641
642 642 pull_requests_other = relationship('PullRequest',
643 643 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
644 644 cascade="all, delete, delete-orphan")
645 645
646 646 def __unicode__(self):
647 647 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
648 648 self.repo_name)
649 649
650 650 @hybrid_property
651 651 def locked(self):
652 652 # always should return [user_id, timelocked]
653 653 if self._locked:
654 654 _lock_info = self._locked.split(':')
655 655 return int(_lock_info[0]), _lock_info[1]
656 656 return [None, None]
657 657
658 658 @locked.setter
659 659 def locked(self, val):
660 660 if val and isinstance(val, (list, tuple)):
661 661 self._locked = ':'.join(map(str, val))
662 662 else:
663 663 self._locked = None
664 664
665 665 @classmethod
666 666 def url_sep(cls):
667 667 return URL_SEP
668 668
669 669 @classmethod
670 670 def get_by_repo_name(cls, repo_name):
671 671 q = Session().query(cls).filter(cls.repo_name == repo_name)
672 672 q = q.options(joinedload(Repository.fork))\
673 673 .options(joinedload(Repository.user))\
674 674 .options(joinedload(Repository.group))
675 675 return q.scalar()
676 676
677 677 @classmethod
678 678 def get_by_full_path(cls, repo_full_path):
679 679 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
680 680 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
681 681
682 682 @classmethod
683 683 def get_repo_forks(cls, repo_id):
684 684 return cls.query().filter(Repository.fork_id == repo_id)
685 685
686 686 @classmethod
687 687 def base_path(cls):
688 688 """
689 689 Returns base path when all repos are stored
690 690
691 691 :param cls:
692 692 """
693 693 q = Session().query(RhodeCodeUi)\
694 694 .filter(RhodeCodeUi.ui_key == cls.url_sep())
695 695 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
696 696 return q.one().ui_value
697 697
698 698 @property
699 699 def forks(self):
700 700 """
701 701 Return forks of this repo
702 702 """
703 703 return Repository.get_repo_forks(self.repo_id)
704 704
705 705 @property
706 706 def parent(self):
707 707 """
708 708 Returns fork parent
709 709 """
710 710 return self.fork
711 711
712 712 @property
713 713 def just_name(self):
714 714 return self.repo_name.split(Repository.url_sep())[-1]
715 715
716 716 @property
717 717 def groups_with_parents(self):
718 718 groups = []
719 719 if self.group is None:
720 720 return groups
721 721
722 722 cur_gr = self.group
723 723 groups.insert(0, cur_gr)
724 724 while 1:
725 725 gr = getattr(cur_gr, 'parent_group', None)
726 726 cur_gr = cur_gr.parent_group
727 727 if gr is None:
728 728 break
729 729 groups.insert(0, gr)
730 730
731 731 return groups
732 732
733 733 @property
734 734 def groups_and_repo(self):
735 735 return self.groups_with_parents, self.just_name
736 736
737 737 @LazyProperty
738 738 def repo_path(self):
739 739 """
740 740 Returns base full path for that repository means where it actually
741 741 exists on a filesystem
742 742 """
743 743 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
744 744 Repository.url_sep())
745 745 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
746 746 return q.one().ui_value
747 747
748 748 @property
749 749 def repo_full_path(self):
750 750 p = [self.repo_path]
751 751 # we need to split the name by / since this is how we store the
752 752 # names in the database, but that eventually needs to be converted
753 753 # into a valid system path
754 754 p += self.repo_name.split(Repository.url_sep())
755 755 return os.path.join(*p)
756 756
757 757 @property
758 758 def cache_keys(self):
759 759 """
760 760 Returns associated cache keys for that repo
761 761 """
762 762 return CacheInvalidation.query()\
763 763 .filter(CacheInvalidation.cache_args == self.repo_name)\
764 764 .order_by(CacheInvalidation.cache_key)\
765 765 .all()
766 766
767 767 def get_new_name(self, repo_name):
768 768 """
769 769 returns new full repository name based on assigned group and new new
770 770
771 771 :param group_name:
772 772 """
773 773 path_prefix = self.group.full_path_splitted if self.group else []
774 774 return Repository.url_sep().join(path_prefix + [repo_name])
775 775
776 776 @property
777 777 def _ui(self):
778 778 """
779 779 Creates an db based ui object for this repository
780 780 """
781 781 from rhodecode.lib.utils import make_ui
782 782 return make_ui('db', clear_session=False)
783 783
784 784 @classmethod
785 785 def inject_ui(cls, repo, extras={}):
786 786 from rhodecode.lib.vcs.backends.hg import MercurialRepository
787 787 from rhodecode.lib.vcs.backends.git import GitRepository
788 788 required = (MercurialRepository, GitRepository)
789 789 if not isinstance(repo, required):
790 790 raise Exception('repo must be instance of %s' % required)
791 791
792 792 # inject ui extra param to log this action via push logger
793 793 for k, v in extras.items():
794 794 repo._repo.ui.setconfig('rhodecode_extras', k, v)
795 795
796 796 @classmethod
797 797 def is_valid(cls, repo_name):
798 798 """
799 799 returns True if given repo name is a valid filesystem repository
800 800
801 801 :param cls:
802 802 :param repo_name:
803 803 """
804 804 from rhodecode.lib.utils import is_valid_repo
805 805
806 806 return is_valid_repo(repo_name, cls.base_path())
807 807
808 808 def get_api_data(self):
809 809 """
810 810 Common function for generating repo api data
811 811
812 812 """
813 813 repo = self
814 814 data = dict(
815 815 repo_id=repo.repo_id,
816 816 repo_name=repo.repo_name,
817 817 repo_type=repo.repo_type,
818 818 clone_uri=repo.clone_uri,
819 819 private=repo.private,
820 820 created_on=repo.created_on,
821 821 description=repo.description,
822 822 landing_rev=repo.landing_rev,
823 823 owner=repo.user.username,
824 824 fork_of=repo.fork.repo_name if repo.fork else None
825 825 )
826 826
827 827 return data
828 828
829 829 @classmethod
830 830 def lock(cls, repo, user_id):
831 831 repo.locked = [user_id, time.time()]
832 832 Session().add(repo)
833 833 Session().commit()
834 834
835 835 @classmethod
836 836 def unlock(cls, repo):
837 837 repo.locked = None
838 838 Session().add(repo)
839 839 Session().commit()
840 840
841 841 @property
842 842 def last_db_change(self):
843 843 return self.updated_on
844 844
845 845 #==========================================================================
846 846 # SCM PROPERTIES
847 847 #==========================================================================
848 848
849 849 def get_changeset(self, rev=None):
850 850 return get_changeset_safe(self.scm_instance, rev)
851 851
852 852 def get_landing_changeset(self):
853 853 """
854 854 Returns landing changeset, or if that doesn't exist returns the tip
855 855 """
856 856 cs = self.get_changeset(self.landing_rev) or self.get_changeset()
857 857 return cs
858 858
859 859 def update_last_change(self, last_change=None):
860 860 if last_change is None:
861 861 last_change = datetime.datetime.now()
862 862 if self.updated_on is None or self.updated_on != last_change:
863 863 log.debug('updated repo %s with new date %s' % (self, last_change))
864 864 self.updated_on = last_change
865 865 Session().add(self)
866 866 Session().commit()
867 867
868 868 @property
869 869 def tip(self):
870 870 return self.get_changeset('tip')
871 871
872 872 @property
873 873 def author(self):
874 874 return self.tip.author
875 875
876 876 @property
877 877 def last_change(self):
878 878 return self.scm_instance.last_change
879 879
880 880 def get_comments(self, revisions=None):
881 881 """
882 882 Returns comments for this repository grouped by revisions
883 883
884 884 :param revisions: filter query by revisions only
885 885 """
886 886 cmts = ChangesetComment.query()\
887 887 .filter(ChangesetComment.repo == self)
888 888 if revisions:
889 889 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
890 890 grouped = defaultdict(list)
891 891 for cmt in cmts.all():
892 892 grouped[cmt.revision].append(cmt)
893 893 return grouped
894 894
895 895 def statuses(self, revisions=None):
896 896 """
897 897 Returns statuses for this repository
898 898
899 899 :param revisions: list of revisions to get statuses for
900 900 :type revisions: list
901 901 """
902 902
903 903 statuses = ChangesetStatus.query()\
904 904 .filter(ChangesetStatus.repo == self)\
905 905 .filter(ChangesetStatus.version == 0)
906 906 if revisions:
907 907 statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
908 908 grouped = {}
909 909
910 910 #maybe we have open new pullrequest without a status ?
911 911 stat = ChangesetStatus.STATUS_UNDER_REVIEW
912 912 status_lbl = ChangesetStatus.get_status_lbl(stat)
913 913 for pr in PullRequest.query().filter(PullRequest.org_repo == self).all():
914 914 for rev in pr.revisions:
915 915 pr_id = pr.pull_request_id
916 916 pr_repo = pr.other_repo.repo_name
917 917 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
918 918
919 919 for stat in statuses.all():
920 920 pr_id = pr_repo = None
921 921 if stat.pull_request:
922 922 pr_id = stat.pull_request.pull_request_id
923 923 pr_repo = stat.pull_request.other_repo.repo_name
924 924 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
925 925 pr_id, pr_repo]
926 926 return grouped
927 927
928 928 #==========================================================================
929 929 # SCM CACHE INSTANCE
930 930 #==========================================================================
931 931
932 932 @property
933 933 def invalidate(self):
934 934 return CacheInvalidation.invalidate(self.repo_name)
935 935
936 936 def set_invalidate(self):
937 937 """
938 938 set a cache for invalidation for this instance
939 939 """
940 940 CacheInvalidation.set_invalidate(self.repo_name)
941 941
942 942 @LazyProperty
943 943 def scm_instance(self):
944 return self.scm_instance_cached()
944 945 return self.__get_instance()
945 946
946 947 def scm_instance_cached(self, cache_map=None):
947 948 @cache_region('long_term')
948 949 def _c(repo_name):
949 950 return self.__get_instance()
950 951 rn = self.repo_name
951 952 log.debug('Getting cached instance of repo')
952 953
953 954 if cache_map:
954 955 # get using prefilled cache_map
955 956 invalidate_repo = cache_map[self.repo_name]
956 957 if invalidate_repo:
957 958 invalidate_repo = (None if invalidate_repo.cache_active
958 959 else invalidate_repo)
959 960 else:
960 961 # get from invalidate
961 962 invalidate_repo = self.invalidate
962 963
963 964 if invalidate_repo is not None:
964 965 region_invalidate(_c, None, rn)
965 966 # update our cache
966 967 CacheInvalidation.set_valid(invalidate_repo.cache_key)
967 968 return _c(rn)
968 969
969 970 def __get_instance(self):
970 971 repo_full_path = self.repo_full_path
971 972 try:
972 973 alias = get_scm(repo_full_path)[0]
973 974 log.debug('Creating instance of %s repository' % alias)
974 975 backend = get_backend(alias)
975 976 except VCSError:
976 977 log.error(traceback.format_exc())
977 978 log.error('Perhaps this repository is in db and not in '
978 979 'filesystem run rescan repositories with '
979 980 '"destroy old data " option from admin panel')
980 981 return
981 982
982 983 if alias == 'hg':
983 984
984 985 repo = backend(safe_str(repo_full_path), create=False,
985 986 baseui=self._ui)
986 987 # skip hidden web repository
987 988 if repo._get_hidden():
988 989 return
989 990 else:
990 991 repo = backend(repo_full_path, create=False)
991 992
992 993 return repo
993 994
994 995
995 996 class RepoGroup(Base, BaseModel):
996 997 __tablename__ = 'groups'
997 998 __table_args__ = (
998 999 UniqueConstraint('group_name', 'group_parent_id'),
999 1000 CheckConstraint('group_id != group_parent_id'),
1000 1001 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1001 1002 'mysql_charset': 'utf8'},
1002 1003 )
1003 1004 __mapper_args__ = {'order_by': 'group_name'}
1004 1005
1005 1006 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1006 1007 group_name = Column("group_name", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
1007 1008 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
1008 1009 group_description = Column("group_description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1009 1010 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
1010 1011
1011 1012 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1012 1013 users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
1013 1014
1014 1015 parent_group = relationship('RepoGroup', remote_side=group_id)
1015 1016
1016 1017 def __init__(self, group_name='', parent_group=None):
1017 1018 self.group_name = group_name
1018 1019 self.parent_group = parent_group
1019 1020
1020 1021 def __unicode__(self):
1021 1022 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
1022 1023 self.group_name)
1023 1024
1024 1025 @classmethod
1025 1026 def groups_choices(cls, check_perms=False):
1026 1027 from webhelpers.html import literal as _literal
1027 1028 from rhodecode.model.scm import ScmModel
1028 1029 groups = cls.query().all()
1029 1030 if check_perms:
1030 1031 #filter group user have access to, it's done
1031 1032 #magically inside ScmModel based on current user
1032 1033 groups = ScmModel().get_repos_groups(groups)
1033 1034 repo_groups = [('', '')]
1034 1035 sep = ' &raquo; '
1035 1036 _name = lambda k: _literal(sep.join(k))
1036 1037
1037 1038 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
1038 1039 for x in groups])
1039 1040
1040 1041 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
1041 1042 return repo_groups
1042 1043
1043 1044 @classmethod
1044 1045 def url_sep(cls):
1045 1046 return URL_SEP
1046 1047
1047 1048 @classmethod
1048 1049 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
1049 1050 if case_insensitive:
1050 1051 gr = cls.query()\
1051 1052 .filter(cls.group_name.ilike(group_name))
1052 1053 else:
1053 1054 gr = cls.query()\
1054 1055 .filter(cls.group_name == group_name)
1055 1056 if cache:
1056 1057 gr = gr.options(FromCache(
1057 1058 "sql_cache_short",
1058 1059 "get_group_%s" % _hash_key(group_name)
1059 1060 )
1060 1061 )
1061 1062 return gr.scalar()
1062 1063
1063 1064 @property
1064 1065 def parents(self):
1065 1066 parents_recursion_limit = 5
1066 1067 groups = []
1067 1068 if self.parent_group is None:
1068 1069 return groups
1069 1070 cur_gr = self.parent_group
1070 1071 groups.insert(0, cur_gr)
1071 1072 cnt = 0
1072 1073 while 1:
1073 1074 cnt += 1
1074 1075 gr = getattr(cur_gr, 'parent_group', None)
1075 1076 cur_gr = cur_gr.parent_group
1076 1077 if gr is None:
1077 1078 break
1078 1079 if cnt == parents_recursion_limit:
1079 1080 # this will prevent accidental infinit loops
1080 1081 log.error('group nested more than %s' %
1081 1082 parents_recursion_limit)
1082 1083 break
1083 1084
1084 1085 groups.insert(0, gr)
1085 1086 return groups
1086 1087
1087 1088 @property
1088 1089 def children(self):
1089 1090 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1090 1091
1091 1092 @property
1092 1093 def name(self):
1093 1094 return self.group_name.split(RepoGroup.url_sep())[-1]
1094 1095
1095 1096 @property
1096 1097 def full_path(self):
1097 1098 return self.group_name
1098 1099
1099 1100 @property
1100 1101 def full_path_splitted(self):
1101 1102 return self.group_name.split(RepoGroup.url_sep())
1102 1103
1103 1104 @property
1104 1105 def repositories(self):
1105 1106 return Repository.query()\
1106 1107 .filter(Repository.group == self)\
1107 1108 .order_by(Repository.repo_name)
1108 1109
1109 1110 @property
1110 1111 def repositories_recursive_count(self):
1111 1112 cnt = self.repositories.count()
1112 1113
1113 1114 def children_count(group):
1114 1115 cnt = 0
1115 1116 for child in group.children:
1116 1117 cnt += child.repositories.count()
1117 1118 cnt += children_count(child)
1118 1119 return cnt
1119 1120
1120 1121 return cnt + children_count(self)
1121 1122
1122 1123 def recursive_groups_and_repos(self):
1123 1124 """
1124 1125 Recursive return all groups, with repositories in those groups
1125 1126 """
1126 1127 all_ = []
1127 1128
1128 1129 def _get_members(root_gr):
1129 1130 for r in root_gr.repositories:
1130 1131 all_.append(r)
1131 1132 childs = root_gr.children.all()
1132 1133 if childs:
1133 1134 for gr in childs:
1134 1135 all_.append(gr)
1135 1136 _get_members(gr)
1136 1137
1137 1138 _get_members(self)
1138 1139 return [self] + all_
1139 1140
1140 1141 def get_new_name(self, group_name):
1141 1142 """
1142 1143 returns new full group name based on parent and new name
1143 1144
1144 1145 :param group_name:
1145 1146 """
1146 1147 path_prefix = (self.parent_group.full_path_splitted if
1147 1148 self.parent_group else [])
1148 1149 return RepoGroup.url_sep().join(path_prefix + [group_name])
1149 1150
1150 1151
1151 1152 class Permission(Base, BaseModel):
1152 1153 __tablename__ = 'permissions'
1153 1154 __table_args__ = (
1154 1155 Index('p_perm_name_idx', 'permission_name'),
1155 1156 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1156 1157 'mysql_charset': 'utf8'},
1157 1158 )
1158 1159 PERMS = [
1159 1160 ('repository.none', _('Repository no access')),
1160 1161 ('repository.read', _('Repository read access')),
1161 1162 ('repository.write', _('Repository write access')),
1162 1163 ('repository.admin', _('Repository admin access')),
1163 1164
1164 1165 ('group.none', _('Repositories Group no access')),
1165 1166 ('group.read', _('Repositories Group read access')),
1166 1167 ('group.write', _('Repositories Group write access')),
1167 1168 ('group.admin', _('Repositories Group admin access')),
1168 1169
1169 1170 ('hg.admin', _('RhodeCode Administrator')),
1170 1171 ('hg.create.none', _('Repository creation disabled')),
1171 1172 ('hg.create.repository', _('Repository creation enabled')),
1172 1173 ('hg.fork.none', _('Repository forking disabled')),
1173 1174 ('hg.fork.repository', _('Repository forking enabled')),
1174 1175 ('hg.register.none', _('Register disabled')),
1175 1176 ('hg.register.manual_activate', _('Register new user with RhodeCode '
1176 1177 'with manual activation')),
1177 1178
1178 1179 ('hg.register.auto_activate', _('Register new user with RhodeCode '
1179 1180 'with auto activation')),
1180 1181 ]
1181 1182
1182 1183 # defines which permissions are more important higher the more important
1183 1184 PERM_WEIGHTS = {
1184 1185 'repository.none': 0,
1185 1186 'repository.read': 1,
1186 1187 'repository.write': 3,
1187 1188 'repository.admin': 4,
1188 1189
1189 1190 'group.none': 0,
1190 1191 'group.read': 1,
1191 1192 'group.write': 3,
1192 1193 'group.admin': 4,
1193 1194
1194 1195 'hg.fork.none': 0,
1195 1196 'hg.fork.repository': 1,
1196 1197 'hg.create.none': 0,
1197 1198 'hg.create.repository':1
1198 1199 }
1199 1200
1200 1201 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1201 1202 permission_name = Column("permission_name", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1202 1203 permission_longname = Column("permission_longname", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1203 1204
1204 1205 def __unicode__(self):
1205 1206 return u"<%s('%s:%s')>" % (
1206 1207 self.__class__.__name__, self.permission_id, self.permission_name
1207 1208 )
1208 1209
1209 1210 @classmethod
1210 1211 def get_by_key(cls, key):
1211 1212 return cls.query().filter(cls.permission_name == key).scalar()
1212 1213
1213 1214 @classmethod
1214 1215 def get_default_perms(cls, default_user_id):
1215 1216 q = Session().query(UserRepoToPerm, Repository, cls)\
1216 1217 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
1217 1218 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
1218 1219 .filter(UserRepoToPerm.user_id == default_user_id)
1219 1220
1220 1221 return q.all()
1221 1222
1222 1223 @classmethod
1223 1224 def get_default_group_perms(cls, default_user_id):
1224 1225 q = Session().query(UserRepoGroupToPerm, RepoGroup, cls)\
1225 1226 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
1226 1227 .join((cls, UserRepoGroupToPerm.permission_id == cls.permission_id))\
1227 1228 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1228 1229
1229 1230 return q.all()
1230 1231
1231 1232
1232 1233 class UserRepoToPerm(Base, BaseModel):
1233 1234 __tablename__ = 'repo_to_perm'
1234 1235 __table_args__ = (
1235 1236 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1236 1237 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1237 1238 'mysql_charset': 'utf8'}
1238 1239 )
1239 1240 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1240 1241 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1241 1242 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1242 1243 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1243 1244
1244 1245 user = relationship('User')
1245 1246 repository = relationship('Repository')
1246 1247 permission = relationship('Permission')
1247 1248
1248 1249 @classmethod
1249 1250 def create(cls, user, repository, permission):
1250 1251 n = cls()
1251 1252 n.user = user
1252 1253 n.repository = repository
1253 1254 n.permission = permission
1254 1255 Session().add(n)
1255 1256 return n
1256 1257
1257 1258 def __unicode__(self):
1258 1259 return u'<user:%s => %s >' % (self.user, self.repository)
1259 1260
1260 1261
1261 1262 class UserToPerm(Base, BaseModel):
1262 1263 __tablename__ = 'user_to_perm'
1263 1264 __table_args__ = (
1264 1265 UniqueConstraint('user_id', 'permission_id'),
1265 1266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1266 1267 'mysql_charset': 'utf8'}
1267 1268 )
1268 1269 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1269 1270 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1270 1271 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1271 1272
1272 1273 user = relationship('User')
1273 1274 permission = relationship('Permission', lazy='joined')
1274 1275
1275 1276
1276 1277 class UsersGroupRepoToPerm(Base, BaseModel):
1277 1278 __tablename__ = 'users_group_repo_to_perm'
1278 1279 __table_args__ = (
1279 1280 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1280 1281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1281 1282 'mysql_charset': 'utf8'}
1282 1283 )
1283 1284 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1284 1285 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1285 1286 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1286 1287 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1287 1288
1288 1289 users_group = relationship('UsersGroup')
1289 1290 permission = relationship('Permission')
1290 1291 repository = relationship('Repository')
1291 1292
1292 1293 @classmethod
1293 1294 def create(cls, users_group, repository, permission):
1294 1295 n = cls()
1295 1296 n.users_group = users_group
1296 1297 n.repository = repository
1297 1298 n.permission = permission
1298 1299 Session().add(n)
1299 1300 return n
1300 1301
1301 1302 def __unicode__(self):
1302 1303 return u'<userGroup:%s => %s >' % (self.users_group, self.repository)
1303 1304
1304 1305
1305 1306 class UsersGroupToPerm(Base, BaseModel):
1306 1307 __tablename__ = 'users_group_to_perm'
1307 1308 __table_args__ = (
1308 1309 UniqueConstraint('users_group_id', 'permission_id',),
1309 1310 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1310 1311 'mysql_charset': 'utf8'}
1311 1312 )
1312 1313 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1313 1314 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1314 1315 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1315 1316
1316 1317 users_group = relationship('UsersGroup')
1317 1318 permission = relationship('Permission')
1318 1319
1319 1320
1320 1321 class UserRepoGroupToPerm(Base, BaseModel):
1321 1322 __tablename__ = 'user_repo_group_to_perm'
1322 1323 __table_args__ = (
1323 1324 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1324 1325 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1325 1326 'mysql_charset': 'utf8'}
1326 1327 )
1327 1328
1328 1329 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1329 1330 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1330 1331 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1331 1332 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1332 1333
1333 1334 user = relationship('User')
1334 1335 group = relationship('RepoGroup')
1335 1336 permission = relationship('Permission')
1336 1337
1337 1338
1338 1339 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1339 1340 __tablename__ = 'users_group_repo_group_to_perm'
1340 1341 __table_args__ = (
1341 1342 UniqueConstraint('users_group_id', 'group_id'),
1342 1343 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1343 1344 'mysql_charset': 'utf8'}
1344 1345 )
1345 1346
1346 1347 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)
1347 1348 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1348 1349 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1349 1350 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1350 1351
1351 1352 users_group = relationship('UsersGroup')
1352 1353 permission = relationship('Permission')
1353 1354 group = relationship('RepoGroup')
1354 1355
1355 1356
1356 1357 class Statistics(Base, BaseModel):
1357 1358 __tablename__ = 'statistics'
1358 1359 __table_args__ = (
1359 1360 UniqueConstraint('repository_id'),
1360 1361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1361 1362 'mysql_charset': 'utf8'}
1362 1363 )
1363 1364 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1364 1365 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1365 1366 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1366 1367 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1367 1368 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1368 1369 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1369 1370
1370 1371 repository = relationship('Repository', single_parent=True)
1371 1372
1372 1373
1373 1374 class UserFollowing(Base, BaseModel):
1374 1375 __tablename__ = 'user_followings'
1375 1376 __table_args__ = (
1376 1377 UniqueConstraint('user_id', 'follows_repository_id'),
1377 1378 UniqueConstraint('user_id', 'follows_user_id'),
1378 1379 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1379 1380 'mysql_charset': 'utf8'}
1380 1381 )
1381 1382
1382 1383 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1383 1384 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1384 1385 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1385 1386 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1386 1387 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1387 1388
1388 1389 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1389 1390
1390 1391 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1391 1392 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1392 1393
1393 1394 @classmethod
1394 1395 def get_repo_followers(cls, repo_id):
1395 1396 return cls.query().filter(cls.follows_repo_id == repo_id)
1396 1397
1397 1398
1398 1399 class CacheInvalidation(Base, BaseModel):
1399 1400 __tablename__ = 'cache_invalidation'
1400 1401 __table_args__ = (
1401 1402 UniqueConstraint('cache_key'),
1402 1403 Index('key_idx', 'cache_key'),
1403 1404 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1404 1405 'mysql_charset': 'utf8'},
1405 1406 )
1406 1407 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1407 1408 cache_key = Column("cache_key", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1408 1409 cache_args = Column("cache_args", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1409 1410 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1410 1411
1411 1412 def __init__(self, cache_key, cache_args=''):
1412 1413 self.cache_key = cache_key
1413 1414 self.cache_args = cache_args
1414 1415 self.cache_active = False
1415 1416
1416 1417 def __unicode__(self):
1417 1418 return u"<%s('%s:%s')>" % (self.__class__.__name__,
1418 1419 self.cache_id, self.cache_key)
1419 1420
1420 1421 @property
1421 1422 def prefix(self):
1422 1423 _split = self.cache_key.split(self.cache_args, 1)
1423 1424 if _split and len(_split) == 2:
1424 1425 return _split[0]
1425 1426 return ''
1426 1427
1427 1428 @classmethod
1428 1429 def clear_cache(cls):
1429 1430 cls.query().delete()
1430 1431
1431 1432 @classmethod
1432 1433 def _get_key(cls, key):
1433 1434 """
1434 1435 Wrapper for generating a key, together with a prefix
1435 1436
1436 1437 :param key:
1437 1438 """
1438 1439 import rhodecode
1439 1440 prefix = ''
1440 1441 iid = rhodecode.CONFIG.get('instance_id')
1441 1442 if iid:
1442 1443 prefix = iid
1443 return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
1444 #remove specific suffixes like _README or _RSS
1445 key = remove_suffix(key, '_README')
1446 key = remove_suffix(key, '_RSS')
1447 key = remove_suffix(key, '_ATOM')
1448 return "%s%s" % (prefix, key), prefix, key
1444 1449
1445 1450 @classmethod
1446 1451 def get_by_key(cls, key):
1447 1452 return cls.query().filter(cls.cache_key == key).scalar()
1448 1453
1449 1454 @classmethod
1450 1455 def _get_or_create_key(cls, key, prefix, org_key, commit=True):
1451 1456 inv_obj = Session().query(cls).filter(cls.cache_key == key).scalar()
1452 1457 if not inv_obj:
1453 1458 try:
1454 1459 inv_obj = CacheInvalidation(key, org_key)
1455 1460 Session().add(inv_obj)
1456 1461 if commit:
1457 1462 Session().commit()
1458 1463 except Exception:
1459 1464 log.error(traceback.format_exc())
1460 1465 Session().rollback()
1461 1466 return inv_obj
1462 1467
1463 1468 @classmethod
1464 1469 def invalidate(cls, key):
1465 1470 """
1466 1471 Returns Invalidation object if this given key should be invalidated
1467 1472 None otherwise. `cache_active = False` means that this cache
1468 1473 state is not valid and needs to be invalidated
1469 1474
1470 1475 :param key:
1471 1476 """
1472 1477
1473 1478 key, _prefix, _org_key = cls._get_key(key)
1474 1479 inv = cls._get_or_create_key(key, _prefix, _org_key)
1475 1480
1476 1481 if inv and inv.cache_active is False:
1477 1482 return inv
1478 1483
1479 1484 @classmethod
1480 1485 def set_invalidate(cls, key):
1481 1486 """
1482 1487 Mark this Cache key for invalidation
1483 1488
1484 1489 :param key:
1485 1490 """
1486 1491
1487 1492 key, _prefix, _org_key = cls._get_key(key)
1488 1493 inv_objs = Session().query(cls).filter(cls.cache_args == _org_key).all()
1489 1494 log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
1490 1495 _org_key))
1491 1496 try:
1492 1497 for inv_obj in inv_objs:
1493 1498 if inv_obj:
1494 1499 inv_obj.cache_active = False
1495 1500
1496 1501 Session().add(inv_obj)
1497 1502 Session().commit()
1498 1503 except Exception:
1499 1504 log.error(traceback.format_exc())
1500 1505 Session().rollback()
1501 1506
1502 1507 @classmethod
1503 1508 def set_valid(cls, key):
1504 1509 """
1505 1510 Mark this cache key as active and currently cached
1506 1511
1507 1512 :param key:
1508 1513 """
1509 1514 inv_obj = cls.get_by_key(key)
1510 1515 inv_obj.cache_active = True
1511 1516 Session().add(inv_obj)
1512 1517 Session().commit()
1513 1518
1514 1519 @classmethod
1515 1520 def get_cache_map(cls):
1516 1521
1517 1522 class cachemapdict(dict):
1518 1523
1519 1524 def __init__(self, *args, **kwargs):
1520 1525 fixkey = kwargs.get('fixkey')
1521 1526 if fixkey:
1522 1527 del kwargs['fixkey']
1523 1528 self.fixkey = fixkey
1524 1529 super(cachemapdict, self).__init__(*args, **kwargs)
1525 1530
1526 1531 def __getattr__(self, name):
1527 1532 key = name
1528 1533 if self.fixkey:
1529 1534 key, _prefix, _org_key = cls._get_key(key)
1530 1535 if key in self.__dict__:
1531 1536 return self.__dict__[key]
1532 1537 else:
1533 1538 return self[key]
1534 1539
1535 1540 def __getitem__(self, key):
1536 1541 if self.fixkey:
1537 1542 key, _prefix, _org_key = cls._get_key(key)
1538 1543 try:
1539 1544 return super(cachemapdict, self).__getitem__(key)
1540 1545 except KeyError:
1541 1546 return
1542 1547
1543 1548 cache_map = cachemapdict(fixkey=True)
1544 1549 for obj in cls.query().all():
1545 1550 cache_map[obj.cache_key] = cachemapdict(obj.get_dict())
1546 1551 return cache_map
1547 1552
1548 1553
1549 1554 class ChangesetComment(Base, BaseModel):
1550 1555 __tablename__ = 'changeset_comments'
1551 1556 __table_args__ = (
1552 1557 Index('cc_revision_idx', 'revision'),
1553 1558 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1554 1559 'mysql_charset': 'utf8'},
1555 1560 )
1556 1561 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1557 1562 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1558 1563 revision = Column('revision', String(40), nullable=True)
1559 1564 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1560 1565 line_no = Column('line_no', Unicode(10), nullable=True)
1561 1566 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
1562 1567 f_path = Column('f_path', Unicode(1000), nullable=True)
1563 1568 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1564 1569 text = Column('text', UnicodeText(25000), nullable=False)
1565 1570 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1566 1571 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1567 1572
1568 1573 author = relationship('User', lazy='joined')
1569 1574 repo = relationship('Repository')
1570 1575 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
1571 1576 pull_request = relationship('PullRequest', lazy='joined')
1572 1577
1573 1578 @classmethod
1574 1579 def get_users(cls, revision=None, pull_request_id=None):
1575 1580 """
1576 1581 Returns user associated with this ChangesetComment. ie those
1577 1582 who actually commented
1578 1583
1579 1584 :param cls:
1580 1585 :param revision:
1581 1586 """
1582 1587 q = Session().query(User)\
1583 1588 .join(ChangesetComment.author)
1584 1589 if revision:
1585 1590 q = q.filter(cls.revision == revision)
1586 1591 elif pull_request_id:
1587 1592 q = q.filter(cls.pull_request_id == pull_request_id)
1588 1593 return q.all()
1589 1594
1590 1595
1591 1596 class ChangesetStatus(Base, BaseModel):
1592 1597 __tablename__ = 'changeset_statuses'
1593 1598 __table_args__ = (
1594 1599 Index('cs_revision_idx', 'revision'),
1595 1600 Index('cs_version_idx', 'version'),
1596 1601 UniqueConstraint('repo_id', 'revision', 'version'),
1597 1602 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1598 1603 'mysql_charset': 'utf8'}
1599 1604 )
1600 1605 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1601 1606 STATUS_APPROVED = 'approved'
1602 1607 STATUS_REJECTED = 'rejected'
1603 1608 STATUS_UNDER_REVIEW = 'under_review'
1604 1609
1605 1610 STATUSES = [
1606 1611 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
1607 1612 (STATUS_APPROVED, _("Approved")),
1608 1613 (STATUS_REJECTED, _("Rejected")),
1609 1614 (STATUS_UNDER_REVIEW, _("Under Review")),
1610 1615 ]
1611 1616
1612 1617 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
1613 1618 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1614 1619 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1615 1620 revision = Column('revision', String(40), nullable=False)
1616 1621 status = Column('status', String(128), nullable=False, default=DEFAULT)
1617 1622 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
1618 1623 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1619 1624 version = Column('version', Integer(), nullable=False, default=0)
1620 1625 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1621 1626
1622 1627 author = relationship('User', lazy='joined')
1623 1628 repo = relationship('Repository')
1624 1629 comment = relationship('ChangesetComment', lazy='joined')
1625 1630 pull_request = relationship('PullRequest', lazy='joined')
1626 1631
1627 1632 def __unicode__(self):
1628 1633 return u"<%s('%s:%s')>" % (
1629 1634 self.__class__.__name__,
1630 1635 self.status, self.author
1631 1636 )
1632 1637
1633 1638 @classmethod
1634 1639 def get_status_lbl(cls, value):
1635 1640 return dict(cls.STATUSES).get(value)
1636 1641
1637 1642 @property
1638 1643 def status_lbl(self):
1639 1644 return ChangesetStatus.get_status_lbl(self.status)
1640 1645
1641 1646
1642 1647 class PullRequest(Base, BaseModel):
1643 1648 __tablename__ = 'pull_requests'
1644 1649 __table_args__ = (
1645 1650 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1646 1651 'mysql_charset': 'utf8'},
1647 1652 )
1648 1653
1649 1654 STATUS_NEW = u'new'
1650 1655 STATUS_OPEN = u'open'
1651 1656 STATUS_CLOSED = u'closed'
1652 1657
1653 1658 pull_request_id = Column('pull_request_id', Integer(), nullable=False, primary_key=True)
1654 1659 title = Column('title', Unicode(256), nullable=True)
1655 1660 description = Column('description', UnicodeText(10240), nullable=True)
1656 1661 status = Column('status', Unicode(256), nullable=False, default=STATUS_NEW)
1657 1662 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1658 1663 updated_on = Column('updated_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1659 1664 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
1660 1665 _revisions = Column('revisions', UnicodeText(20500)) # 500 revisions max
1661 1666 org_repo_id = Column('org_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1662 1667 org_ref = Column('org_ref', Unicode(256), nullable=False)
1663 1668 other_repo_id = Column('other_repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1664 1669 other_ref = Column('other_ref', Unicode(256), nullable=False)
1665 1670
1666 1671 @hybrid_property
1667 1672 def revisions(self):
1668 1673 return self._revisions.split(':')
1669 1674
1670 1675 @revisions.setter
1671 1676 def revisions(self, val):
1672 1677 self._revisions = ':'.join(val)
1673 1678
1674 1679 author = relationship('User', lazy='joined')
1675 1680 reviewers = relationship('PullRequestReviewers',
1676 1681 cascade="all, delete, delete-orphan")
1677 1682 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
1678 1683 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
1679 1684 statuses = relationship('ChangesetStatus')
1680 1685 comments = relationship('ChangesetComment',
1681 1686 cascade="all, delete, delete-orphan")
1682 1687
1683 1688 def is_closed(self):
1684 1689 return self.status == self.STATUS_CLOSED
1685 1690
1686 1691 def __json__(self):
1687 1692 return dict(
1688 1693 revisions=self.revisions
1689 1694 )
1690 1695
1691 1696
1692 1697 class PullRequestReviewers(Base, BaseModel):
1693 1698 __tablename__ = 'pull_request_reviewers'
1694 1699 __table_args__ = (
1695 1700 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1696 1701 'mysql_charset': 'utf8'},
1697 1702 )
1698 1703
1699 1704 def __init__(self, user=None, pull_request=None):
1700 1705 self.user = user
1701 1706 self.pull_request = pull_request
1702 1707
1703 1708 pull_requests_reviewers_id = Column('pull_requests_reviewers_id', Integer(), nullable=False, primary_key=True)
1704 1709 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
1705 1710 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
1706 1711
1707 1712 user = relationship('User')
1708 1713 pull_request = relationship('PullRequest')
1709 1714
1710 1715
1711 1716 class Notification(Base, BaseModel):
1712 1717 __tablename__ = 'notifications'
1713 1718 __table_args__ = (
1714 1719 Index('notification_type_idx', 'type'),
1715 1720 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1716 1721 'mysql_charset': 'utf8'},
1717 1722 )
1718 1723
1719 1724 TYPE_CHANGESET_COMMENT = u'cs_comment'
1720 1725 TYPE_MESSAGE = u'message'
1721 1726 TYPE_MENTION = u'mention'
1722 1727 TYPE_REGISTRATION = u'registration'
1723 1728 TYPE_PULL_REQUEST = u'pull_request'
1724 1729 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
1725 1730
1726 1731 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1727 1732 subject = Column('subject', Unicode(512), nullable=True)
1728 1733 body = Column('body', UnicodeText(50000), nullable=True)
1729 1734 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1730 1735 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1731 1736 type_ = Column('type', Unicode(256))
1732 1737
1733 1738 created_by_user = relationship('User')
1734 1739 notifications_to_users = relationship('UserNotification', lazy='joined',
1735 1740 cascade="all, delete, delete-orphan")
1736 1741
1737 1742 @property
1738 1743 def recipients(self):
1739 1744 return [x.user for x in UserNotification.query()\
1740 1745 .filter(UserNotification.notification == self)\
1741 1746 .order_by(UserNotification.user_id.asc()).all()]
1742 1747
1743 1748 @classmethod
1744 1749 def create(cls, created_by, subject, body, recipients, type_=None):
1745 1750 if type_ is None:
1746 1751 type_ = Notification.TYPE_MESSAGE
1747 1752
1748 1753 notification = cls()
1749 1754 notification.created_by_user = created_by
1750 1755 notification.subject = subject
1751 1756 notification.body = body
1752 1757 notification.type_ = type_
1753 1758 notification.created_on = datetime.datetime.now()
1754 1759
1755 1760 for u in recipients:
1756 1761 assoc = UserNotification()
1757 1762 assoc.notification = notification
1758 1763 u.notifications.append(assoc)
1759 1764 Session().add(notification)
1760 1765 return notification
1761 1766
1762 1767 @property
1763 1768 def description(self):
1764 1769 from rhodecode.model.notification import NotificationModel
1765 1770 return NotificationModel().make_description(self)
1766 1771
1767 1772
1768 1773 class UserNotification(Base, BaseModel):
1769 1774 __tablename__ = 'user_to_notification'
1770 1775 __table_args__ = (
1771 1776 UniqueConstraint('user_id', 'notification_id'),
1772 1777 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1773 1778 'mysql_charset': 'utf8'}
1774 1779 )
1775 1780 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
1776 1781 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1777 1782 read = Column('read', Boolean, default=False)
1778 1783 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1779 1784
1780 1785 user = relationship('User', lazy="joined")
1781 1786 notification = relationship('Notification', lazy="joined",
1782 1787 order_by=lambda: Notification.created_on.desc(),)
1783 1788
1784 1789 def mark_as_read(self):
1785 1790 self.read = True
1786 1791 Session().add(self)
1787 1792
1788 1793
1789 1794 class DbMigrateVersion(Base, BaseModel):
1790 1795 __tablename__ = 'db_migrate_version'
1791 1796 __table_args__ = (
1792 1797 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1793 1798 'mysql_charset': 'utf8'},
1794 1799 )
1795 1800 repository_id = Column('repository_id', String(250), primary_key=True)
1796 1801 repository_path = Column('repository_path', Text)
1797 1802 version = Column('version', Integer)
General Comments 0
You need to be logged in to leave comments. Login now