##// END OF EJS Templates
implements #193 journal stores information about deleting of repos...
marcink -
r1747:88047154 beta
parent child Browse files
Show More
@@ -1,407 +1,410
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.lib.celerylib.tasks
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 RhodeCode task modules, containing all task that suppose to be run
7 7 by celery daemon
8 8
9 9 :created_on: Oct 6, 2010
10 10 :author: marcink
11 11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
15 15 # it under the terms of the GNU General Public License as published by
16 16 # the Free Software Foundation, either version 3 of the License, or
17 17 # (at your option) any later version.
18 18 #
19 19 # This program is distributed in the hope that it will be useful,
20 20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 22 # GNU General Public License for more details.
23 23 #
24 24 # You should have received a copy of the GNU General Public License
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 from celery.decorators import task
27 27
28 28 import os
29 29 import traceback
30 30 import logging
31 31 from os.path import join as jn
32 32
33 33 from time import mktime
34 34 from operator import itemgetter
35 35 from string import lower
36 36
37 37 from pylons import config, url
38 38 from pylons.i18n.translation import _
39 39
40 40 from vcs import get_backend
41 41
42 42 from rhodecode import CELERY_ON
43 43 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
44 44 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
45 45 __get_lockkey, LockHeld, DaemonLock
46 46 from rhodecode.lib.helpers import person
47 47 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
48 48 from rhodecode.lib.utils import add_cache, action_logger
49 49 from rhodecode.lib.compat import json, OrderedDict
50 50
51 51 from rhodecode.model import init_model
52 52 from rhodecode.model import meta
53 53 from rhodecode.model.db import Statistics, Repository, User
54 54
55 55 from sqlalchemy import engine_from_config
56 56
57 57 add_cache(config)
58 58
59 59 __all__ = ['whoosh_index', 'get_commits_stats',
60 60 'reset_user_password', 'send_email']
61 61
62 62
63 63 def get_session():
64 64 if CELERY_ON:
65 65 engine = engine_from_config(config, 'sqlalchemy.db1.')
66 66 init_model(engine)
67 67 sa = meta.Session()
68 68 return sa
69 69
70 70 def get_logger(cls):
71 71 if CELERY_ON:
72 72 try:
73 73 log = cls.get_logger()
74 74 except:
75 75 log = logging.getLogger(__name__)
76 76 else:
77 77 log = logging.getLogger(__name__)
78 78
79 79 return log
80 80
81 81 @task(ignore_result=True)
82 82 @locked_task
83 83 def whoosh_index(repo_location, full_index):
84 84 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
85 85
86 86 #log = whoosh_index.get_logger()
87 87
88 88 index_location = config['index_dir']
89 89 WhooshIndexingDaemon(index_location=index_location,
90 90 repo_location=repo_location, sa=get_session())\
91 91 .run(full_index=full_index)
92 92
93 93
94 94 @task(ignore_result=True)
95 95 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
96 96 log = get_logger(get_commits_stats)
97 97
98 98 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
99 99 ts_max_y)
100 100 lockkey_path = config['here']
101 101
102 102 log.info('running task with lockkey %s', lockkey)
103 103 try:
104 104 sa = get_session()
105 105 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
106 106
107 107 # for js data compatibilty cleans the key for person from '
108 108 akc = lambda k: person(k).replace('"', "")
109 109
110 110 co_day_auth_aggr = {}
111 111 commits_by_day_aggregate = {}
112 112 repo = Repository.get_by_repo_name(repo_name).scm_instance
113 113 repo_size = len(repo.revisions)
114 114 #return if repo have no revisions
115 115 if repo_size < 1:
116 116 lock.release()
117 117 return True
118 118
119 119 skip_date_limit = True
120 120 parse_limit = int(config['app_conf'].get('commit_parse_limit'))
121 121 last_rev = 0
122 122 last_cs = None
123 123 timegetter = itemgetter('time')
124 124
125 125 dbrepo = sa.query(Repository)\
126 126 .filter(Repository.repo_name == repo_name).scalar()
127 127 cur_stats = sa.query(Statistics)\
128 128 .filter(Statistics.repository == dbrepo).scalar()
129 129
130 130 if cur_stats is not None:
131 131 last_rev = cur_stats.stat_on_revision
132 132
133 133 if last_rev == repo.get_changeset().revision and repo_size > 1:
134 134 # pass silently without any work if we're not on first revision or
135 135 # current state of parsing revision(from db marker) is the
136 136 # last revision
137 137 lock.release()
138 138 return True
139 139
140 140 if cur_stats:
141 141 commits_by_day_aggregate = OrderedDict(json.loads(
142 142 cur_stats.commit_activity_combined))
143 143 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
144 144
145 145 log.debug('starting parsing %s', parse_limit)
146 146 lmktime = mktime
147 147
148 148 last_rev = last_rev + 1 if last_rev > 0 else last_rev
149 149
150 150 for cs in repo[last_rev:last_rev + parse_limit]:
151 151 last_cs = cs # remember last parsed changeset
152 152 k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1],
153 153 cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0])
154 154
155 155 if akc(cs.author) in co_day_auth_aggr:
156 156 try:
157 157 l = [timegetter(x) for x in
158 158 co_day_auth_aggr[akc(cs.author)]['data']]
159 159 time_pos = l.index(k)
160 160 except ValueError:
161 161 time_pos = False
162 162
163 163 if time_pos >= 0 and time_pos is not False:
164 164
165 165 datadict = \
166 166 co_day_auth_aggr[akc(cs.author)]['data'][time_pos]
167 167
168 168 datadict["commits"] += 1
169 169 datadict["added"] += len(cs.added)
170 170 datadict["changed"] += len(cs.changed)
171 171 datadict["removed"] += len(cs.removed)
172 172
173 173 else:
174 174 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
175 175
176 176 datadict = {"time": k,
177 177 "commits": 1,
178 178 "added": len(cs.added),
179 179 "changed": len(cs.changed),
180 180 "removed": len(cs.removed),
181 181 }
182 182 co_day_auth_aggr[akc(cs.author)]['data']\
183 183 .append(datadict)
184 184
185 185 else:
186 186 if k >= ts_min_y and k <= ts_max_y or skip_date_limit:
187 187 co_day_auth_aggr[akc(cs.author)] = {
188 188 "label": akc(cs.author),
189 189 "data": [{"time":k,
190 190 "commits":1,
191 191 "added":len(cs.added),
192 192 "changed":len(cs.changed),
193 193 "removed":len(cs.removed),
194 194 }],
195 195 "schema": ["commits"],
196 196 }
197 197
198 198 #gather all data by day
199 199 if k in commits_by_day_aggregate:
200 200 commits_by_day_aggregate[k] += 1
201 201 else:
202 202 commits_by_day_aggregate[k] = 1
203 203
204 204 overview_data = sorted(commits_by_day_aggregate.items(),
205 205 key=itemgetter(0))
206 206
207 207 if not co_day_auth_aggr:
208 208 co_day_auth_aggr[akc(repo.contact)] = {
209 209 "label": akc(repo.contact),
210 210 "data": [0, 1],
211 211 "schema": ["commits"],
212 212 }
213 213
214 214 stats = cur_stats if cur_stats else Statistics()
215 215 stats.commit_activity = json.dumps(co_day_auth_aggr)
216 216 stats.commit_activity_combined = json.dumps(overview_data)
217 217
218 218 log.debug('last revison %s', last_rev)
219 219 leftovers = len(repo.revisions[last_rev:])
220 220 log.debug('revisions to parse %s', leftovers)
221 221
222 222 if last_rev == 0 or leftovers < parse_limit:
223 223 log.debug('getting code trending stats')
224 224 stats.languages = json.dumps(__get_codes_stats(repo_name))
225 225
226 226 try:
227 227 stats.repository = dbrepo
228 228 stats.stat_on_revision = last_cs.revision if last_cs else 0
229 229 sa.add(stats)
230 230 sa.commit()
231 231 except:
232 232 log.error(traceback.format_exc())
233 233 sa.rollback()
234 234 lock.release()
235 235 return False
236 236
237 237 #final release
238 238 lock.release()
239 239
240 240 #execute another task if celery is enabled
241 241 if len(repo.revisions) > 1 and CELERY_ON:
242 242 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
243 243 return True
244 244 except LockHeld:
245 245 log.info('LockHeld')
246 246 return 'Task with key %s already running' % lockkey
247 247
248 248 @task(ignore_result=True)
249 249 def send_password_link(user_email):
250 250 from rhodecode.model.notification import EmailNotificationModel
251 251
252 252 log = get_logger(send_password_link)
253 253
254 254 try:
255 255 sa = get_session()
256 256 user = User.get_by_email(user_email)
257 257 if user:
258 258 log.debug('password reset user found %s' % user)
259 259 link = url('reset_password_confirmation', key=user.api_key,
260 260 qualified=True)
261 261 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
262 262 body = EmailNotificationModel().get_email_tmpl(reg_type,
263 263 **{'user':user.short_contact,
264 264 'reset_url':link})
265 265 log.debug('sending email')
266 266 run_task(send_email, user_email,
267 267 _("password reset link"), body)
268 268 log.info('send new password mail to %s', user_email)
269 269 else:
270 270 log.debug("password reset email %s not found" % user_email)
271 271 except:
272 272 log.error(traceback.format_exc())
273 273 return False
274 274
275 275 return True
276 276
277 277 @task(ignore_result=True)
278 278 def reset_user_password(user_email):
279 279 from rhodecode.lib import auth
280 280
281 281 log = get_logger(reset_user_password)
282 282
283 283 try:
284 284 try:
285 285 sa = get_session()
286 286 user = User.get_by_email(user_email)
287 287 new_passwd = auth.PasswordGenerator().gen_password(8,
288 288 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
289 289 if user:
290 290 user.password = auth.get_crypt_password(new_passwd)
291 291 user.api_key = auth.generate_api_key(user.username)
292 292 sa.add(user)
293 293 sa.commit()
294 294 log.info('change password for %s', user_email)
295 295 if new_passwd is None:
296 296 raise Exception('unable to generate new password')
297 297 except:
298 298 log.error(traceback.format_exc())
299 299 sa.rollback()
300 300
301 301 run_task(send_email, user_email,
302 302 'Your new password',
303 303 'Your new RhodeCode password:%s' % (new_passwd))
304 304 log.info('send new password mail to %s', user_email)
305 305
306 306 except:
307 307 log.error('Failed to update user password')
308 308 log.error(traceback.format_exc())
309 309
310 310 return True
311 311
312 312
313 313 @task(ignore_result=True)
314 314 def send_email(recipients, subject, body, html_body=''):
315 315 """
316 316 Sends an email with defined parameters from the .ini files.
317 317
318 318 :param recipients: list of recipients, it this is empty the defined email
319 319 address from field 'email_to' is used instead
320 320 :param subject: subject of the mail
321 321 :param body: body of the mail
322 322 :param html_body: html version of body
323 323 """
324 324 log = get_logger(send_email)
325 325 sa = get_session()
326 326 email_config = config
327 327 subject = "%s %s" % (email_config.get('email_prefix'), subject)
328 328 if not recipients:
329 329 # if recipients are not defined we send to email_config + all admins
330 330 admins = [u.email for u in User.query()
331 331 .filter(User.admin == True).all()]
332 332 recipients = [email_config.get('email_to')] + admins
333 333
334 334 mail_from = email_config.get('app_email_from', 'RhodeCode')
335 335 user = email_config.get('smtp_username')
336 336 passwd = email_config.get('smtp_password')
337 337 mail_server = email_config.get('smtp_server')
338 338 mail_port = email_config.get('smtp_port')
339 339 tls = str2bool(email_config.get('smtp_use_tls'))
340 340 ssl = str2bool(email_config.get('smtp_use_ssl'))
341 341 debug = str2bool(config.get('debug'))
342 342 smtp_auth = email_config.get('smtp_auth')
343 343
344 344 try:
345 345 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
346 346 mail_port, ssl, tls, debug=debug)
347 347 m.send(recipients, subject, body, html_body)
348 348 except:
349 349 log.error('Mail sending failed')
350 350 log.error(traceback.format_exc())
351 351 return False
352 352 return True
353 353
354 354
355 355 @task(ignore_result=True)
356 356 def create_repo_fork(form_data, cur_user):
357 357 """
358 358 Creates a fork of repository using interval VCS methods
359 359
360 360 :param form_data:
361 361 :param cur_user:
362 362 """
363 363 from rhodecode.model.repo import RepoModel
364 364
365 365 log = get_logger(create_repo_fork)
366 366
367 367 Session = get_session()
368 368 base_path = Repository.base_path()
369 369
370 370 RepoModel(Session).create(form_data, cur_user, just_db=True, fork=True)
371 371
372 372 alias = form_data['repo_type']
373 373 org_repo_name = form_data['org_path']
374 374 fork_name = form_data['repo_name_full']
375 375 update_after_clone = form_data['update_after_clone']
376 376 source_repo_path = os.path.join(base_path, org_repo_name)
377 377 destination_fork_path = os.path.join(base_path, fork_name)
378 378
379 379 log.info('creating fork of %s as %s', source_repo_path,
380 380 destination_fork_path)
381 381 backend = get_backend(alias)
382 382 backend(safe_str(destination_fork_path), create=True,
383 383 src_url=safe_str(source_repo_path),
384 384 update_after_clone=update_after_clone)
385 385 action_logger(cur_user, 'user_forked_repo:%s' % fork_name,
386 386 org_repo_name, '', Session)
387
388 action_logger(cur_user, 'user_created_fork:%s' % fork_name,
389 fork_name, '', Session)
387 390 # finally commit at latest possible stage
388 391 Session.commit()
389 392
390 393 def __get_codes_stats(repo_name):
391 394 repo = Repository.get_by_repo_name(repo_name).scm_instance
392 395
393 396 tip = repo.get_changeset()
394 397 code_stats = {}
395 398
396 399 def aggregate(cs):
397 400 for f in cs[2]:
398 401 ext = lower(f.extension)
399 402 if ext in LANGUAGES_EXTENSIONS_MAP.keys() and not f.is_binary:
400 403 if ext in code_stats:
401 404 code_stats[ext] += 1
402 405 else:
403 406 code_stats[ext] = 1
404 407
405 408 map(aggregate, tip.walk('/'))
406 409
407 410 return code_stats or {}
@@ -1,675 +1,677
1 1 """Helper functions
2 2
3 3 Consists of functions to typically be used within templates, but also
4 4 available to Controllers. This module is available to both as 'h'.
5 5 """
6 6 import random
7 7 import hashlib
8 8 import StringIO
9 9 import urllib
10 10 import math
11 11
12 12 from datetime import datetime
13 13 from pygments.formatters.html import HtmlFormatter
14 14 from pygments import highlight as code_highlight
15 15 from pylons import url, request, config
16 16 from pylons.i18n.translation import _, ungettext
17 17
18 18 from webhelpers.html import literal, HTML, escape
19 19 from webhelpers.html.tools import *
20 20 from webhelpers.html.builder import make_tag
21 21 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 22 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
23 23 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
24 24 password, textarea, title, ul, xml_declaration, radio
25 25 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
26 26 mail_to, strip_links, strip_tags, tag_re
27 27 from webhelpers.number import format_byte_size, format_bit_size
28 28 from webhelpers.pylonslib import Flash as _Flash
29 29 from webhelpers.pylonslib.secure_form import secure_form
30 30 from webhelpers.text import chop_at, collapse, convert_accented_entities, \
31 31 convert_misc_entities, lchop, plural, rchop, remove_formatting, \
32 32 replace_whitespace, urlify, truncate, wrap_paragraphs
33 33 from webhelpers.date import time_ago_in_words
34 34 from webhelpers.paginate import Page
35 35 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 36 convert_boolean_attrs, NotGiven, _make_safe_id_component
37 37
38 38 from vcs.utils.annotate import annotate_highlight
39 39 from rhodecode.lib.utils import repo_name_slug
40 40 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
41 41
42 42 from rhodecode.lib.markup_renderer import MarkupRenderer
43 43
44 44 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
45 45 """
46 46 Reset button
47 47 """
48 48 _set_input_attrs(attrs, type, name, value)
49 49 _set_id_attr(attrs, id, name)
50 50 convert_boolean_attrs(attrs, ["disabled"])
51 51 return HTML.input(**attrs)
52 52
53 53 reset = _reset
54 54 safeid = _make_safe_id_component
55 55
56 56 def get_token():
57 57 """Return the current authentication token, creating one if one doesn't
58 58 already exist.
59 59 """
60 60 token_key = "_authentication_token"
61 61 from pylons import session
62 62 if not token_key in session:
63 63 try:
64 64 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
65 65 except AttributeError: # Python < 2.4
66 66 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest()
67 67 session[token_key] = token
68 68 if hasattr(session, 'save'):
69 69 session.save()
70 70 return session[token_key]
71 71
72 72 class _GetError(object):
73 73 """Get error from form_errors, and represent it as span wrapped error
74 74 message
75 75
76 76 :param field_name: field to fetch errors for
77 77 :param form_errors: form errors dict
78 78 """
79 79
80 80 def __call__(self, field_name, form_errors):
81 81 tmpl = """<span class="error_msg">%s</span>"""
82 82 if form_errors and form_errors.has_key(field_name):
83 83 return literal(tmpl % form_errors.get(field_name))
84 84
85 85 get_error = _GetError()
86 86
87 87 class _ToolTip(object):
88 88
89 89 def __call__(self, tooltip_title, trim_at=50):
90 90 """Special function just to wrap our text into nice formatted
91 91 autowrapped text
92 92
93 93 :param tooltip_title:
94 94 """
95 95 return escape(tooltip_title)
96 96 tooltip = _ToolTip()
97 97
98 98 class _FilesBreadCrumbs(object):
99 99
100 100 def __call__(self, repo_name, rev, paths):
101 101 if isinstance(paths, str):
102 102 paths = safe_unicode(paths)
103 103 url_l = [link_to(repo_name, url('files_home',
104 104 repo_name=repo_name,
105 105 revision=rev, f_path=''))]
106 106 paths_l = paths.split('/')
107 107 for cnt, p in enumerate(paths_l):
108 108 if p != '':
109 109 url_l.append(link_to(p, url('files_home',
110 110 repo_name=repo_name,
111 111 revision=rev,
112 112 f_path='/'.join(paths_l[:cnt + 1]))))
113 113
114 114 return literal('/'.join(url_l))
115 115
116 116 files_breadcrumbs = _FilesBreadCrumbs()
117 117
118 118 class CodeHtmlFormatter(HtmlFormatter):
119 119 """My code Html Formatter for source codes
120 120 """
121 121
122 122 def wrap(self, source, outfile):
123 123 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
124 124
125 125 def _wrap_code(self, source):
126 126 for cnt, it in enumerate(source):
127 127 i, t = it
128 128 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
129 129 yield i, t
130 130
131 131 def _wrap_tablelinenos(self, inner):
132 132 dummyoutfile = StringIO.StringIO()
133 133 lncount = 0
134 134 for t, line in inner:
135 135 if t:
136 136 lncount += 1
137 137 dummyoutfile.write(line)
138 138
139 139 fl = self.linenostart
140 140 mw = len(str(lncount + fl - 1))
141 141 sp = self.linenospecial
142 142 st = self.linenostep
143 143 la = self.lineanchors
144 144 aln = self.anchorlinenos
145 145 nocls = self.noclasses
146 146 if sp:
147 147 lines = []
148 148
149 149 for i in range(fl, fl + lncount):
150 150 if i % st == 0:
151 151 if i % sp == 0:
152 152 if aln:
153 153 lines.append('<a href="#%s%d" class="special">%*d</a>' %
154 154 (la, i, mw, i))
155 155 else:
156 156 lines.append('<span class="special">%*d</span>' % (mw, i))
157 157 else:
158 158 if aln:
159 159 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
160 160 else:
161 161 lines.append('%*d' % (mw, i))
162 162 else:
163 163 lines.append('')
164 164 ls = '\n'.join(lines)
165 165 else:
166 166 lines = []
167 167 for i in range(fl, fl + lncount):
168 168 if i % st == 0:
169 169 if aln:
170 170 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
171 171 else:
172 172 lines.append('%*d' % (mw, i))
173 173 else:
174 174 lines.append('')
175 175 ls = '\n'.join(lines)
176 176
177 177 # in case you wonder about the seemingly redundant <div> here: since the
178 178 # content in the other cell also is wrapped in a div, some browsers in
179 179 # some configurations seem to mess up the formatting...
180 180 if nocls:
181 181 yield 0, ('<table class="%stable">' % self.cssclass +
182 182 '<tr><td><div class="linenodiv" '
183 183 'style="background-color: #f0f0f0; padding-right: 10px">'
184 184 '<pre style="line-height: 125%">' +
185 185 ls + '</pre></div></td><td id="hlcode" class="code">')
186 186 else:
187 187 yield 0, ('<table class="%stable">' % self.cssclass +
188 188 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
189 189 ls + '</pre></div></td><td id="hlcode" class="code">')
190 190 yield 0, dummyoutfile.getvalue()
191 191 yield 0, '</td></tr></table>'
192 192
193 193
194 194 def pygmentize(filenode, **kwargs):
195 195 """pygmentize function using pygments
196 196
197 197 :param filenode:
198 198 """
199 199
200 200 return literal(code_highlight(filenode.content,
201 201 filenode.lexer, CodeHtmlFormatter(**kwargs)))
202 202
203 203 def pygmentize_annotation(repo_name, filenode, **kwargs):
204 204 """pygmentize function for annotation
205 205
206 206 :param filenode:
207 207 """
208 208
209 209 color_dict = {}
210 210 def gen_color(n=10000):
211 211 """generator for getting n of evenly distributed colors using
212 212 hsv color and golden ratio. It always return same order of colors
213 213
214 214 :returns: RGB tuple
215 215 """
216 216
217 217 def hsv_to_rgb(h, s, v):
218 218 if s == 0.0: return v, v, v
219 219 i = int(h * 6.0) # XXX assume int() truncates!
220 220 f = (h * 6.0) - i
221 221 p = v * (1.0 - s)
222 222 q = v * (1.0 - s * f)
223 223 t = v * (1.0 - s * (1.0 - f))
224 224 i = i % 6
225 225 if i == 0: return v, t, p
226 226 if i == 1: return q, v, p
227 227 if i == 2: return p, v, t
228 228 if i == 3: return p, q, v
229 229 if i == 4: return t, p, v
230 230 if i == 5: return v, p, q
231 231
232 232 golden_ratio = 0.618033988749895
233 233 h = 0.22717784590367374
234 234
235 235 for _ in xrange(n):
236 236 h += golden_ratio
237 237 h %= 1
238 238 HSV_tuple = [h, 0.95, 0.95]
239 239 RGB_tuple = hsv_to_rgb(*HSV_tuple)
240 240 yield map(lambda x:str(int(x * 256)), RGB_tuple)
241 241
242 242 cgenerator = gen_color()
243 243
244 244 def get_color_string(cs):
245 245 if color_dict.has_key(cs):
246 246 col = color_dict[cs]
247 247 else:
248 248 col = color_dict[cs] = cgenerator.next()
249 249 return "color: rgb(%s)! important;" % (', '.join(col))
250 250
251 251 def url_func(repo_name):
252 252
253 253 def _url_func(changeset):
254 254 author = changeset.author
255 255 date = changeset.date
256 256 message = tooltip(changeset.message)
257 257
258 258 tooltip_html = ("<div style='font-size:0.8em'><b>Author:</b>"
259 259 " %s<br/><b>Date:</b> %s</b><br/><b>Message:"
260 260 "</b> %s<br/></div>")
261 261
262 262 tooltip_html = tooltip_html % (author, date, message)
263 263 lnk_format = '%5s:%s' % ('r%s' % changeset.revision,
264 264 short_id(changeset.raw_id))
265 265 uri = link_to(
266 266 lnk_format,
267 267 url('changeset_home', repo_name=repo_name,
268 268 revision=changeset.raw_id),
269 269 style=get_color_string(changeset.raw_id),
270 270 class_='tooltip',
271 271 title=tooltip_html
272 272 )
273 273
274 274 uri += '\n'
275 275 return uri
276 276 return _url_func
277 277
278 278 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
279 279
280 280 def is_following_repo(repo_name, user_id):
281 281 from rhodecode.model.scm import ScmModel
282 282 return ScmModel().is_following_repo(repo_name, user_id)
283 283
284 284 flash = _Flash()
285 285
286 286 #==============================================================================
287 287 # SCM FILTERS available via h.
288 288 #==============================================================================
289 289 from vcs.utils import author_name, author_email
290 290 from rhodecode.lib import credentials_filter, age as _age
291 291
292 292 age = lambda x:_age(x)
293 293 capitalize = lambda x: x.capitalize()
294 294 email = author_email
295 295 email_or_none = lambda x: email(x) if email(x) != x else None
296 296 person = lambda x: author_name(x)
297 297 short_id = lambda x: x[:12]
298 298 hide_credentials = lambda x: ''.join(credentials_filter(x))
299 299
300 300 def bool2icon(value):
301 301 """Returns True/False values represented as small html image of true/false
302 302 icons
303 303
304 304 :param value: bool value
305 305 """
306 306
307 307 if value is True:
308 308 return HTML.tag('img', src=url("/images/icons/accept.png"),
309 309 alt=_('True'))
310 310
311 311 if value is False:
312 312 return HTML.tag('img', src=url("/images/icons/cancel.png"),
313 313 alt=_('False'))
314 314
315 315 return value
316 316
317 317
318 318 def action_parser(user_log, feed=False):
319 319 """This helper will action_map the specified string action into translated
320 320 fancy names with icons and links
321 321
322 322 :param user_log: user log instance
323 323 :param feed: use output for feeds (no html and fancy icons)
324 324 """
325 325
326 326 action = user_log.action
327 327 action_params = ' '
328 328
329 329 x = action.split(':')
330 330
331 331 if len(x) > 1:
332 332 action, action_params = x
333 333
334 334 def get_cs_links():
335 335 revs_limit = 3 #display this amount always
336 336 revs_top_limit = 50 #show upto this amount of changesets hidden
337 337 revs = action_params.split(',')
338 338 repo_name = user_log.repository.repo_name
339 339
340 340 from rhodecode.model.scm import ScmModel
341 341 repo = user_log.repository.scm_instance
342 342
343 343 message = lambda rev: get_changeset_safe(repo, rev).message
344 344 cs_links = []
345 345 cs_links.append(" " + ', '.join ([link_to(rev,
346 346 url('changeset_home',
347 347 repo_name=repo_name,
348 348 revision=rev), title=tooltip(message(rev)),
349 349 class_='tooltip') for rev in revs[:revs_limit] ]))
350 350
351 351 compare_view = (' <div class="compare_view tooltip" title="%s">'
352 352 '<a href="%s">%s</a> '
353 353 '</div>' % (_('Show all combined changesets %s->%s' \
354 354 % (revs[0], revs[-1])),
355 355 url('changeset_home', repo_name=repo_name,
356 356 revision='%s...%s' % (revs[0], revs[-1])
357 357 ),
358 358 _('compare view'))
359 359 )
360 360
361 361 if len(revs) > revs_limit:
362 362 uniq_id = revs[0]
363 363 html_tmpl = ('<span> %s '
364 364 '<a class="show_more" id="_%s" href="#more">%s</a> '
365 365 '%s</span>')
366 366 if not feed:
367 367 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
368 368 % (len(revs) - revs_limit),
369 369 _('revisions')))
370 370
371 371 if not feed:
372 372 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
373 373 else:
374 374 html_tmpl = '<span id="%s"> %s </span>'
375 375
376 376 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
377 377 url('changeset_home',
378 378 repo_name=repo_name, revision=rev),
379 379 title=message(rev), class_='tooltip')
380 380 for rev in revs[revs_limit:revs_top_limit]])))
381 381 if len(revs) > 1:
382 382 cs_links.append(compare_view)
383 383 return ''.join(cs_links)
384 384
385 385 def get_fork_name():
386 386 repo_name = action_params
387 387 return _('fork name ') + str(link_to(action_params, url('summary_home',
388 388 repo_name=repo_name,)))
389 389
390 390 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
391 391 'user_created_repo':(_('[created] repository'), None),
392 'user_created_fork':(_('[created] repository as fork'), None),
392 393 'user_forked_repo':(_('[forked] repository'), get_fork_name),
393 394 'user_updated_repo':(_('[updated] repository'), None),
394 395 'admin_deleted_repo':(_('[delete] repository'), None),
395 396 'admin_created_repo':(_('[created] repository'), None),
396 397 'admin_forked_repo':(_('[forked] repository'), None),
397 398 'admin_updated_repo':(_('[updated] repository'), None),
398 399 'push':(_('[pushed] into'), get_cs_links),
399 400 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
400 401 'push_remote':(_('[pulled from remote] into'), get_cs_links),
401 402 'pull':(_('[pulled] from'), None),
402 403 'started_following_repo':(_('[started following] repository'), None),
403 404 'stopped_following_repo':(_('[stopped following] repository'), None),
404 405 }
405 406
406 407 action_str = action_map.get(action, action)
407 408 if feed:
408 409 action = action_str[0].replace('[', '').replace(']', '')
409 410 else:
410 411 action = action_str[0].replace('[', '<span class="journal_highlight">')\
411 412 .replace(']', '</span>')
412 413
413 414 action_params_func = lambda :""
414 415
415 416 if callable(action_str[1]):
416 417 action_params_func = action_str[1]
417 418
418 419 return [literal(action), action_params_func]
419 420
420 421 def action_parser_icon(user_log):
421 422 action = user_log.action
422 423 action_params = None
423 424 x = action.split(':')
424 425
425 426 if len(x) > 1:
426 427 action, action_params = x
427 428
428 429 tmpl = """<img src="%s%s" alt="%s"/>"""
429 430 map = {'user_deleted_repo':'database_delete.png',
430 431 'user_created_repo':'database_add.png',
432 'user_created_fork':'arrow_divide.png',
431 433 'user_forked_repo':'arrow_divide.png',
432 434 'user_updated_repo':'database_edit.png',
433 435 'admin_deleted_repo':'database_delete.png',
434 436 'admin_created_repo':'database_add.png',
435 437 'admin_forked_repo':'arrow_divide.png',
436 438 'admin_updated_repo':'database_edit.png',
437 439 'push':'script_add.png',
438 440 'push_local':'script_edit.png',
439 441 'push_remote':'connect.png',
440 442 'pull':'down_16.png',
441 443 'started_following_repo':'heart_add.png',
442 444 'stopped_following_repo':'heart_delete.png',
443 445 }
444 446 return literal(tmpl % ((url('/images/icons/')),
445 447 map.get(action, action), action))
446 448
447 449
448 450 #==============================================================================
449 451 # PERMS
450 452 #==============================================================================
451 453 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
452 454 HasRepoPermissionAny, HasRepoPermissionAll
453 455
454 456 #==============================================================================
455 457 # GRAVATAR URL
456 458 #==============================================================================
457 459
458 460 def gravatar_url(email_address, size=30):
459 461 if (not str2bool(config['app_conf'].get('use_gravatar')) or
460 462 not email_address or email_address == 'anonymous@rhodecode.org'):
461 463 return url("/images/user%s.png" % size)
462 464
463 465 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
464 466 default = 'identicon'
465 467 baseurl_nossl = "http://www.gravatar.com/avatar/"
466 468 baseurl_ssl = "https://secure.gravatar.com/avatar/"
467 469 baseurl = baseurl_ssl if ssl_enabled else baseurl_nossl
468 470
469 471 if isinstance(email_address, unicode):
470 472 #hashlib crashes on unicode items
471 473 email_address = safe_str(email_address)
472 474 # construct the url
473 475 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
474 476 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
475 477
476 478 return gravatar_url
477 479
478 480
479 481 #==============================================================================
480 482 # REPO PAGER, PAGER FOR REPOSITORY
481 483 #==============================================================================
482 484 class RepoPage(Page):
483 485
484 486 def __init__(self, collection, page=1, items_per_page=20,
485 487 item_count=None, url=None, **kwargs):
486 488
487 489 """Create a "RepoPage" instance. special pager for paging
488 490 repository
489 491 """
490 492 self._url_generator = url
491 493
492 494 # Safe the kwargs class-wide so they can be used in the pager() method
493 495 self.kwargs = kwargs
494 496
495 497 # Save a reference to the collection
496 498 self.original_collection = collection
497 499
498 500 self.collection = collection
499 501
500 502 # The self.page is the number of the current page.
501 503 # The first page has the number 1!
502 504 try:
503 505 self.page = int(page) # make it int() if we get it as a string
504 506 except (ValueError, TypeError):
505 507 self.page = 1
506 508
507 509 self.items_per_page = items_per_page
508 510
509 511 # Unless the user tells us how many items the collections has
510 512 # we calculate that ourselves.
511 513 if item_count is not None:
512 514 self.item_count = item_count
513 515 else:
514 516 self.item_count = len(self.collection)
515 517
516 518 # Compute the number of the first and last available page
517 519 if self.item_count > 0:
518 520 self.first_page = 1
519 521 self.page_count = int(math.ceil(float(self.item_count) /
520 522 self.items_per_page))
521 523 self.last_page = self.first_page + self.page_count - 1
522 524
523 525 # Make sure that the requested page number is the range of valid pages
524 526 if self.page > self.last_page:
525 527 self.page = self.last_page
526 528 elif self.page < self.first_page:
527 529 self.page = self.first_page
528 530
529 531 # Note: the number of items on this page can be less than
530 532 # items_per_page if the last page is not full
531 533 self.first_item = max(0, (self.item_count) - (self.page *
532 534 items_per_page))
533 535 self.last_item = ((self.item_count - 1) - items_per_page *
534 536 (self.page - 1))
535 537
536 538 self.items = list(self.collection[self.first_item:self.last_item + 1])
537 539
538 540
539 541 # Links to previous and next page
540 542 if self.page > self.first_page:
541 543 self.previous_page = self.page - 1
542 544 else:
543 545 self.previous_page = None
544 546
545 547 if self.page < self.last_page:
546 548 self.next_page = self.page + 1
547 549 else:
548 550 self.next_page = None
549 551
550 552 # No items available
551 553 else:
552 554 self.first_page = None
553 555 self.page_count = 0
554 556 self.last_page = None
555 557 self.first_item = None
556 558 self.last_item = None
557 559 self.previous_page = None
558 560 self.next_page = None
559 561 self.items = []
560 562
561 563 # This is a subclass of the 'list' type. Initialise the list now.
562 564 list.__init__(self, reversed(self.items))
563 565
564 566
565 567 def changed_tooltip(nodes):
566 568 """
567 569 Generates a html string for changed nodes in changeset page.
568 570 It limits the output to 30 entries
569 571
570 572 :param nodes: LazyNodesGenerator
571 573 """
572 574 if nodes:
573 575 pref = ': <br/> '
574 576 suf = ''
575 577 if len(nodes) > 30:
576 578 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
577 579 return literal(pref + '<br/> '.join([safe_unicode(x.path)
578 580 for x in nodes[:30]]) + suf)
579 581 else:
580 582 return ': ' + _('No Files')
581 583
582 584
583 585
584 586 def repo_link(groups_and_repos):
585 587 """
586 588 Makes a breadcrumbs link to repo within a group
587 589 joins &raquo; on each group to create a fancy link
588 590
589 591 ex::
590 592 group >> subgroup >> repo
591 593
592 594 :param groups_and_repos:
593 595 """
594 596 groups, repo_name = groups_and_repos
595 597
596 598 if not groups:
597 599 return repo_name
598 600 else:
599 601 def make_link(group):
600 602 return link_to(group.name, url('repos_group_home',
601 603 group_name=group.group_name))
602 604 return literal(' &raquo; '.join(map(make_link, groups)) + \
603 605 " &raquo; " + repo_name)
604 606
605 607 def fancy_file_stats(stats):
606 608 """
607 609 Displays a fancy two colored bar for number of added/deleted
608 610 lines of code on file
609 611
610 612 :param stats: two element list of added/deleted lines of code
611 613 """
612 614
613 615 a, d, t = stats[0], stats[1], stats[0] + stats[1]
614 616 width = 100
615 617 unit = float(width) / (t or 1)
616 618
617 619 # needs > 9% of width to be visible or 0 to be hidden
618 620 a_p = max(9, unit * a) if a > 0 else 0
619 621 d_p = max(9, unit * d) if d > 0 else 0
620 622 p_sum = a_p + d_p
621 623
622 624 if p_sum > width:
623 625 #adjust the percentage to be == 100% since we adjusted to 9
624 626 if a_p > d_p:
625 627 a_p = a_p - (p_sum - width)
626 628 else:
627 629 d_p = d_p - (p_sum - width)
628 630
629 631 a_v = a if a > 0 else ''
630 632 d_v = d if d > 0 else ''
631 633
632 634
633 635 def cgen(l_type):
634 636 mapping = {'tr':'top-right-rounded-corner',
635 637 'tl':'top-left-rounded-corner',
636 638 'br':'bottom-right-rounded-corner',
637 639 'bl':'bottom-left-rounded-corner'}
638 640 map_getter = lambda x:mapping[x]
639 641
640 642 if l_type == 'a' and d_v:
641 643 #case when added and deleted are present
642 644 return ' '.join(map(map_getter, ['tl', 'bl']))
643 645
644 646 if l_type == 'a' and not d_v:
645 647 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
646 648
647 649 if l_type == 'd' and a_v:
648 650 return ' '.join(map(map_getter, ['tr', 'br']))
649 651
650 652 if l_type == 'd' and not a_v:
651 653 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
652 654
653 655
654 656
655 657 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
656 658 a_p, a_v)
657 659 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
658 660 d_p, d_v)
659 661 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
660 662
661 663
662 664 def urlify_text(text):
663 665 import re
664 666
665 667 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
666 668
667 669 def url_func(match_obj):
668 670 url_full = match_obj.groups()[0]
669 671 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
670 672
671 673 return literal(url_pat.sub(url_func, text))
672 674
673 675
674 676 def rst(source):
675 677 return literal('<div class="rst-block">%s</div>' % MarkupRenderer.rst(source))
@@ -1,1237 +1,1237
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) 2009-2011 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
31 31 from sqlalchemy import *
32 32 from sqlalchemy.ext.hybrid import hybrid_property
33 33 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
34 34 from beaker.cache import cache_region, region_invalidate
35 35
36 36 from vcs import get_backend
37 37 from vcs.utils.helpers import get_scm
38 38 from vcs.exceptions import VCSError
39 39 from vcs.utils.lazy import LazyProperty
40 40
41 41 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode
42 42 from rhodecode.lib.exceptions import UsersGroupsAssignedException
43 43 from rhodecode.lib.compat import json
44 44 from rhodecode.lib.caching_query import FromCache
45 45
46 46 from rhodecode.model.meta import Base, Session
47 47
48 48 log = logging.getLogger(__name__)
49 49
50 50 #==============================================================================
51 51 # BASE CLASSES
52 52 #==============================================================================
53 53
54 54 class ModelSerializer(json.JSONEncoder):
55 55 """
56 56 Simple Serializer for JSON,
57 57
58 58 usage::
59 59
60 60 to make object customized for serialization implement a __json__
61 61 method that will return a dict for serialization into json
62 62
63 63 example::
64 64
65 65 class Task(object):
66 66
67 67 def __init__(self, name, value):
68 68 self.name = name
69 69 self.value = value
70 70
71 71 def __json__(self):
72 72 return dict(name=self.name,
73 73 value=self.value)
74 74
75 75 """
76 76
77 77 def default(self, obj):
78 78
79 79 if hasattr(obj, '__json__'):
80 80 return obj.__json__()
81 81 else:
82 82 return json.JSONEncoder.default(self, obj)
83 83
84 84 class BaseModel(object):
85 85 """Base Model for all classess
86 86
87 87 """
88 88
89 89 @classmethod
90 90 def _get_keys(cls):
91 91 """return column names for this model """
92 92 return class_mapper(cls).c.keys()
93 93
94 94 def get_dict(self):
95 95 """return dict with keys and values corresponding
96 96 to this model data """
97 97
98 98 d = {}
99 99 for k in self._get_keys():
100 100 d[k] = getattr(self, k)
101 101 return d
102 102
103 103 def get_appstruct(self):
104 104 """return list with keys and values tupples corresponding
105 105 to this model data """
106 106
107 107 l = []
108 108 for k in self._get_keys():
109 109 l.append((k, getattr(self, k),))
110 110 return l
111 111
112 112 def populate_obj(self, populate_dict):
113 113 """populate model with data from given populate_dict"""
114 114
115 115 for k in self._get_keys():
116 116 if k in populate_dict:
117 117 setattr(self, k, populate_dict[k])
118 118
119 119 @classmethod
120 120 def query(cls):
121 121 return Session().query(cls)
122 122
123 123 @classmethod
124 124 def get(cls, id_):
125 125 if id_:
126 126 return cls.query().get(id_)
127 127
128 128 @classmethod
129 129 def getAll(cls):
130 130 return cls.query().all()
131 131
132 132 @classmethod
133 133 def delete(cls, id_):
134 134 obj = cls.query().get(id_)
135 135 Session().delete(obj)
136 136
137 137
138 138 class RhodeCodeSetting(Base, BaseModel):
139 139 __tablename__ = 'rhodecode_settings'
140 140 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
141 141 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
142 142 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
143 143 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
144 144
145 145 def __init__(self, k='', v=''):
146 146 self.app_settings_name = k
147 147 self.app_settings_value = v
148 148
149 149
150 150 @validates('_app_settings_value')
151 151 def validate_settings_value(self, key, val):
152 152 assert type(val) == unicode
153 153 return val
154 154
155 155 @hybrid_property
156 156 def app_settings_value(self):
157 157 v = self._app_settings_value
158 158 if v == 'ldap_active':
159 159 v = str2bool(v)
160 160 return v
161 161
162 162 @app_settings_value.setter
163 163 def app_settings_value(self, val):
164 164 """
165 165 Setter that will always make sure we use unicode in app_settings_value
166 166
167 167 :param val:
168 168 """
169 169 self._app_settings_value = safe_unicode(val)
170 170
171 171 def __repr__(self):
172 172 return "<%s('%s:%s')>" % (self.__class__.__name__,
173 173 self.app_settings_name, self.app_settings_value)
174 174
175 175
176 176 @classmethod
177 177 def get_by_name(cls, ldap_key):
178 178 return cls.query()\
179 179 .filter(cls.app_settings_name == ldap_key).scalar()
180 180
181 181 @classmethod
182 182 def get_app_settings(cls, cache=False):
183 183
184 184 ret = cls.query()
185 185
186 186 if cache:
187 187 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
188 188
189 189 if not ret:
190 190 raise Exception('Could not get application settings !')
191 191 settings = {}
192 192 for each in ret:
193 193 settings['rhodecode_' + each.app_settings_name] = \
194 194 each.app_settings_value
195 195
196 196 return settings
197 197
198 198 @classmethod
199 199 def get_ldap_settings(cls, cache=False):
200 200 ret = cls.query()\
201 201 .filter(cls.app_settings_name.startswith('ldap_')).all()
202 202 fd = {}
203 203 for row in ret:
204 204 fd.update({row.app_settings_name:row.app_settings_value})
205 205
206 206 return fd
207 207
208 208
209 209 class RhodeCodeUi(Base, BaseModel):
210 210 __tablename__ = 'rhodecode_ui'
211 211 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
212 212
213 213 HOOK_UPDATE = 'changegroup.update'
214 214 HOOK_REPO_SIZE = 'changegroup.repo_size'
215 215 HOOK_PUSH = 'pretxnchangegroup.push_logger'
216 216 HOOK_PULL = 'preoutgoing.pull_logger'
217 217
218 218 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
219 219 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
220 220 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
221 221 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
222 222 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
223 223
224 224
225 225 @classmethod
226 226 def get_by_key(cls, key):
227 227 return cls.query().filter(cls.ui_key == key)
228 228
229 229
230 230 @classmethod
231 231 def get_builtin_hooks(cls):
232 232 q = cls.query()
233 233 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
234 234 cls.HOOK_REPO_SIZE,
235 235 cls.HOOK_PUSH, cls.HOOK_PULL]))
236 236 return q.all()
237 237
238 238 @classmethod
239 239 def get_custom_hooks(cls):
240 240 q = cls.query()
241 241 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
242 242 cls.HOOK_REPO_SIZE,
243 243 cls.HOOK_PUSH, cls.HOOK_PULL]))
244 244 q = q.filter(cls.ui_section == 'hooks')
245 245 return q.all()
246 246
247 247 @classmethod
248 248 def create_or_update_hook(cls, key, val):
249 249 new_ui = cls.get_by_key(key).scalar() or cls()
250 250 new_ui.ui_section = 'hooks'
251 251 new_ui.ui_active = True
252 252 new_ui.ui_key = key
253 253 new_ui.ui_value = val
254 254
255 255 Session().add(new_ui)
256 256 Session().commit()
257 257
258 258
259 259 class User(Base, BaseModel):
260 260 __tablename__ = 'users'
261 261 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
262 262 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
263 263 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
264 264 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
265 265 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
266 266 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
267 267 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
268 268 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 269 email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
270 270 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
271 271 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
272 272 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
273 273
274 274 user_log = relationship('UserLog', cascade='all')
275 275 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
276 276
277 277 repositories = relationship('Repository')
278 278 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
279 279 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
280 280
281 281 group_member = relationship('UsersGroupMember', cascade='all')
282 282
283 283 notifications = relationship('UserNotification',)
284 284
285 285 @property
286 286 def full_name(self):
287 287 return '%s %s' % (self.name, self.lastname)
288 288
289 289 @property
290 290 def full_contact(self):
291 291 return '%s %s <%s>' % (self.name, self.lastname, self.email)
292 292
293 293 @property
294 294 def short_contact(self):
295 295 return '%s %s' % (self.name, self.lastname)
296 296
297 297 @property
298 298 def is_admin(self):
299 299 return self.admin
300 300
301 301 def __repr__(self):
302 302 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
303 303 self.user_id, self.username)
304 304
305 305
306 306 @classmethod
307 307 def get_by_username(cls, username, case_insensitive=False, cache=False):
308 308 if case_insensitive:
309 309 q = cls.query().filter(cls.username.ilike(username))
310 310 else:
311 311 q = cls.query().filter(cls.username == username)
312 312
313 313 if cache:
314 314 q = q.options(FromCache("sql_cache_short",
315 315 "get_user_%s" % username))
316 316 return q.scalar()
317 317
318 318 @classmethod
319 319 def get_by_api_key(cls, api_key, cache=False):
320 320 q = cls.query().filter(cls.api_key == api_key)
321 321
322 322 if cache:
323 323 q = q.options(FromCache("sql_cache_short",
324 324 "get_api_key_%s" % api_key))
325 325 return q.scalar()
326 326
327 327 @classmethod
328 328 def get_by_email(cls, email, cache=False):
329 329 q = cls.query().filter(cls.email == email)
330 330
331 331 if cache:
332 332 q = q.options(FromCache("sql_cache_short",
333 333 "get_api_key_%s" % email))
334 334 return q.scalar()
335 335
336 336 def update_lastlogin(self):
337 337 """Update user lastlogin"""
338 338
339 339 self.last_login = datetime.datetime.now()
340 340 Session().add(self)
341 341 Session().commit()
342 342 log.debug('updated user %s lastlogin', self.username)
343 343
344 344
345 345 class UserLog(Base, BaseModel):
346 346 __tablename__ = 'user_logs'
347 347 __table_args__ = {'extend_existing':True}
348 348 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
349 349 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
350 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
350 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
351 351 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
352 352 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
353 353 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
354 354 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
355 355
356 356 @property
357 357 def action_as_day(self):
358 358 return datetime.date(*self.action_date.timetuple()[:3])
359 359
360 360 user = relationship('User')
361 repository = relationship('Repository')
361 repository = relationship('Repository',cascade='')
362 362
363 363
364 364 class UsersGroup(Base, BaseModel):
365 365 __tablename__ = 'users_groups'
366 366 __table_args__ = {'extend_existing':True}
367 367
368 368 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
369 369 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
370 370 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
371 371
372 372 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
373 373
374 374 def __repr__(self):
375 375 return '<userGroup(%s)>' % (self.users_group_name)
376 376
377 377 @classmethod
378 378 def get_by_group_name(cls, group_name, cache=False,
379 379 case_insensitive=False):
380 380 if case_insensitive:
381 381 q = cls.query().filter(cls.users_group_name.ilike(group_name))
382 382 else:
383 383 q = cls.query().filter(cls.users_group_name == group_name)
384 384 if cache:
385 385 q = q.options(FromCache("sql_cache_short",
386 386 "get_user_%s" % group_name))
387 387 return q.scalar()
388 388
389 389
390 390 @classmethod
391 391 def get(cls, users_group_id, cache=False):
392 392 users_group = cls.query()
393 393 if cache:
394 394 users_group = users_group.options(FromCache("sql_cache_short",
395 395 "get_users_group_%s" % users_group_id))
396 396 return users_group.get(users_group_id)
397 397
398 398 @classmethod
399 399 def create(cls, form_data):
400 400 try:
401 401 new_users_group = cls()
402 402 for k, v in form_data.items():
403 403 setattr(new_users_group, k, v)
404 404
405 405 Session().add(new_users_group)
406 406 Session().commit()
407 407 return new_users_group
408 408 except:
409 409 log.error(traceback.format_exc())
410 410 Session().rollback()
411 411 raise
412 412
413 413 @classmethod
414 414 def update(cls, users_group_id, form_data):
415 415
416 416 try:
417 417 users_group = cls.get(users_group_id, cache=False)
418 418
419 419 for k, v in form_data.items():
420 420 if k == 'users_group_members':
421 421 users_group.members = []
422 422 Session().flush()
423 423 members_list = []
424 424 if v:
425 425 v = [v] if isinstance(v, basestring) else v
426 426 for u_id in set(v):
427 427 member = UsersGroupMember(users_group_id, u_id)
428 428 members_list.append(member)
429 429 setattr(users_group, 'members', members_list)
430 430 setattr(users_group, k, v)
431 431
432 432 Session().add(users_group)
433 433 Session().commit()
434 434 except:
435 435 log.error(traceback.format_exc())
436 436 Session().rollback()
437 437 raise
438 438
439 439 @classmethod
440 440 def delete(cls, users_group_id):
441 441 try:
442 442
443 443 # check if this group is not assigned to repo
444 444 assigned_groups = UsersGroupRepoToPerm.query()\
445 445 .filter(UsersGroupRepoToPerm.users_group_id ==
446 446 users_group_id).all()
447 447
448 448 if assigned_groups:
449 449 raise UsersGroupsAssignedException('RepoGroup assigned to %s' %
450 450 assigned_groups)
451 451
452 452 users_group = cls.get(users_group_id, cache=False)
453 453 Session().delete(users_group)
454 454 Session().commit()
455 455 except:
456 456 log.error(traceback.format_exc())
457 457 Session().rollback()
458 458 raise
459 459
460 460 class UsersGroupMember(Base, BaseModel):
461 461 __tablename__ = 'users_groups_members'
462 462 __table_args__ = {'extend_existing':True}
463 463
464 464 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
465 465 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
466 466 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
467 467
468 468 user = relationship('User', lazy='joined')
469 469 users_group = relationship('UsersGroup')
470 470
471 471 def __init__(self, gr_id='', u_id=''):
472 472 self.users_group_id = gr_id
473 473 self.user_id = u_id
474 474
475 475 @staticmethod
476 476 def add_user_to_group(group, user):
477 477 ugm = UsersGroupMember()
478 478 ugm.users_group = group
479 479 ugm.user = user
480 480 Session().add(ugm)
481 481 Session().commit()
482 482 return ugm
483 483
484 484 class Repository(Base, BaseModel):
485 485 __tablename__ = 'repositories'
486 486 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
487 487
488 488 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
489 489 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
490 490 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
491 491 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
492 492 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
493 493 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
494 494 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
495 495 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
496 496 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
497 497 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
498 498
499 499 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
500 500 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
501 501
502 502
503 503 user = relationship('User')
504 504 fork = relationship('Repository', remote_side=repo_id)
505 505 group = relationship('RepoGroup')
506 506 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
507 507 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
508 508 stats = relationship('Statistics', cascade='all', uselist=False)
509 509
510 510 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
511 511
512 logs = relationship('UserLog', cascade='all')
512 logs = relationship('UserLog')
513 513
514 514 def __repr__(self):
515 515 return "<%s('%s:%s')>" % (self.__class__.__name__,
516 516 self.repo_id, self.repo_name)
517 517
518 518 @classmethod
519 519 def url_sep(cls):
520 520 return '/'
521 521
522 522 @classmethod
523 523 def get_by_repo_name(cls, repo_name):
524 524 q = Session().query(cls).filter(cls.repo_name == repo_name)
525 525 q = q.options(joinedload(Repository.fork))\
526 526 .options(joinedload(Repository.user))\
527 527 .options(joinedload(Repository.group))
528 528 return q.one()
529 529
530 530 @classmethod
531 531 def get_repo_forks(cls, repo_id):
532 532 return cls.query().filter(Repository.fork_id == repo_id)
533 533
534 534 @classmethod
535 535 def base_path(cls):
536 536 """
537 537 Returns base path when all repos are stored
538 538
539 539 :param cls:
540 540 """
541 541 q = Session().query(RhodeCodeUi)\
542 542 .filter(RhodeCodeUi.ui_key == cls.url_sep())
543 543 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
544 544 return q.one().ui_value
545 545
546 546 @property
547 547 def just_name(self):
548 548 return self.repo_name.split(Repository.url_sep())[-1]
549 549
550 550 @property
551 551 def groups_with_parents(self):
552 552 groups = []
553 553 if self.group is None:
554 554 return groups
555 555
556 556 cur_gr = self.group
557 557 groups.insert(0, cur_gr)
558 558 while 1:
559 559 gr = getattr(cur_gr, 'parent_group', None)
560 560 cur_gr = cur_gr.parent_group
561 561 if gr is None:
562 562 break
563 563 groups.insert(0, gr)
564 564
565 565 return groups
566 566
567 567 @property
568 568 def groups_and_repo(self):
569 569 return self.groups_with_parents, self.just_name
570 570
571 571 @LazyProperty
572 572 def repo_path(self):
573 573 """
574 574 Returns base full path for that repository means where it actually
575 575 exists on a filesystem
576 576 """
577 577 q = Session().query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
578 578 Repository.url_sep())
579 579 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
580 580 return q.one().ui_value
581 581
582 582 @property
583 583 def repo_full_path(self):
584 584 p = [self.repo_path]
585 585 # we need to split the name by / since this is how we store the
586 586 # names in the database, but that eventually needs to be converted
587 587 # into a valid system path
588 588 p += self.repo_name.split(Repository.url_sep())
589 589 return os.path.join(*p)
590 590
591 591 def get_new_name(self, repo_name):
592 592 """
593 593 returns new full repository name based on assigned group and new new
594 594
595 595 :param group_name:
596 596 """
597 597 path_prefix = self.group.full_path_splitted if self.group else []
598 598 return Repository.url_sep().join(path_prefix + [repo_name])
599 599
600 600 @property
601 601 def _ui(self):
602 602 """
603 603 Creates an db based ui object for this repository
604 604 """
605 605 from mercurial import ui
606 606 from mercurial import config
607 607 baseui = ui.ui()
608 608
609 609 #clean the baseui object
610 610 baseui._ocfg = config.config()
611 611 baseui._ucfg = config.config()
612 612 baseui._tcfg = config.config()
613 613
614 614
615 615 ret = RhodeCodeUi.query()\
616 616 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
617 617
618 618 hg_ui = ret
619 619 for ui_ in hg_ui:
620 620 if ui_.ui_active:
621 621 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
622 622 ui_.ui_key, ui_.ui_value)
623 623 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
624 624
625 625 return baseui
626 626
627 627 @classmethod
628 628 def is_valid(cls, repo_name):
629 629 """
630 630 returns True if given repo name is a valid filesystem repository
631 631
632 632 @param cls:
633 633 @param repo_name:
634 634 """
635 635 from rhodecode.lib.utils import is_valid_repo
636 636
637 637 return is_valid_repo(repo_name, cls.base_path())
638 638
639 639
640 640 #==========================================================================
641 641 # SCM PROPERTIES
642 642 #==========================================================================
643 643
644 644 def get_changeset(self, rev):
645 645 return get_changeset_safe(self.scm_instance, rev)
646 646
647 647 @property
648 648 def tip(self):
649 649 return self.get_changeset('tip')
650 650
651 651 @property
652 652 def author(self):
653 653 return self.tip.author
654 654
655 655 @property
656 656 def last_change(self):
657 657 return self.scm_instance.last_change
658 658
659 659 #==========================================================================
660 660 # SCM CACHE INSTANCE
661 661 #==========================================================================
662 662
663 663 @property
664 664 def invalidate(self):
665 665 return CacheInvalidation.invalidate(self.repo_name)
666 666
667 667 def set_invalidate(self):
668 668 """
669 669 set a cache for invalidation for this instance
670 670 """
671 671 CacheInvalidation.set_invalidate(self.repo_name)
672 672
673 673 @LazyProperty
674 674 def scm_instance(self):
675 675 return self.__get_instance()
676 676
677 677 @property
678 678 def scm_instance_cached(self):
679 679 @cache_region('long_term')
680 680 def _c(repo_name):
681 681 return self.__get_instance()
682 682 rn = self.repo_name
683 683 log.debug('Getting cached instance of repo')
684 684 inv = self.invalidate
685 685 if inv is not None:
686 686 region_invalidate(_c, None, rn)
687 687 # update our cache
688 688 CacheInvalidation.set_valid(inv.cache_key)
689 689 return _c(rn)
690 690
691 691 def __get_instance(self):
692 692 repo_full_path = self.repo_full_path
693 693 try:
694 694 alias = get_scm(repo_full_path)[0]
695 695 log.debug('Creating instance of %s repository', alias)
696 696 backend = get_backend(alias)
697 697 except VCSError:
698 698 log.error(traceback.format_exc())
699 699 log.error('Perhaps this repository is in db and not in '
700 700 'filesystem run rescan repositories with '
701 701 '"destroy old data " option from admin panel')
702 702 return
703 703
704 704 if alias == 'hg':
705 705 repo = backend(safe_str(repo_full_path), create=False,
706 706 baseui=self._ui)
707 707 # skip hidden web repository
708 708 if repo._get_hidden():
709 709 return
710 710 else:
711 711 repo = backend(repo_full_path, create=False)
712 712
713 713 return repo
714 714
715 715
716 716 class RepoGroup(Base, BaseModel):
717 717 __tablename__ = 'groups'
718 718 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
719 719 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
720 720 __mapper_args__ = {'order_by':'group_name'}
721 721
722 722 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
723 723 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
724 724 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
725 725 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
726 726
727 727 parent_group = relationship('RepoGroup', remote_side=group_id)
728 728
729 729
730 730 def __init__(self, group_name='', parent_group=None):
731 731 self.group_name = group_name
732 732 self.parent_group = parent_group
733 733
734 734 def __repr__(self):
735 735 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
736 736 self.group_name)
737 737
738 738 @classmethod
739 739 def groups_choices(cls):
740 740 from webhelpers.html import literal as _literal
741 741 repo_groups = [('', '')]
742 742 sep = ' &raquo; '
743 743 _name = lambda k: _literal(sep.join(k))
744 744
745 745 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
746 746 for x in cls.query().all()])
747 747
748 748 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
749 749 return repo_groups
750 750
751 751 @classmethod
752 752 def url_sep(cls):
753 753 return '/'
754 754
755 755 @classmethod
756 756 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
757 757 if case_insensitive:
758 758 gr = cls.query()\
759 759 .filter(cls.group_name.ilike(group_name))
760 760 else:
761 761 gr = cls.query()\
762 762 .filter(cls.group_name == group_name)
763 763 if cache:
764 764 gr = gr.options(FromCache("sql_cache_short",
765 765 "get_group_%s" % group_name))
766 766 return gr.scalar()
767 767
768 768 @property
769 769 def parents(self):
770 770 parents_recursion_limit = 5
771 771 groups = []
772 772 if self.parent_group is None:
773 773 return groups
774 774 cur_gr = self.parent_group
775 775 groups.insert(0, cur_gr)
776 776 cnt = 0
777 777 while 1:
778 778 cnt += 1
779 779 gr = getattr(cur_gr, 'parent_group', None)
780 780 cur_gr = cur_gr.parent_group
781 781 if gr is None:
782 782 break
783 783 if cnt == parents_recursion_limit:
784 784 # this will prevent accidental infinit loops
785 785 log.error('group nested more than %s' %
786 786 parents_recursion_limit)
787 787 break
788 788
789 789 groups.insert(0, gr)
790 790 return groups
791 791
792 792 @property
793 793 def children(self):
794 794 return RepoGroup.query().filter(RepoGroup.parent_group == self)
795 795
796 796 @property
797 797 def name(self):
798 798 return self.group_name.split(RepoGroup.url_sep())[-1]
799 799
800 800 @property
801 801 def full_path(self):
802 802 return self.group_name
803 803
804 804 @property
805 805 def full_path_splitted(self):
806 806 return self.group_name.split(RepoGroup.url_sep())
807 807
808 808 @property
809 809 def repositories(self):
810 810 return Repository.query().filter(Repository.group == self)
811 811
812 812 @property
813 813 def repositories_recursive_count(self):
814 814 cnt = self.repositories.count()
815 815
816 816 def children_count(group):
817 817 cnt = 0
818 818 for child in group.children:
819 819 cnt += child.repositories.count()
820 820 cnt += children_count(child)
821 821 return cnt
822 822
823 823 return cnt + children_count(self)
824 824
825 825
826 826 def get_new_name(self, group_name):
827 827 """
828 828 returns new full group name based on parent and new name
829 829
830 830 :param group_name:
831 831 """
832 832 path_prefix = (self.parent_group.full_path_splitted if
833 833 self.parent_group else [])
834 834 return RepoGroup.url_sep().join(path_prefix + [group_name])
835 835
836 836
837 837 class Permission(Base, BaseModel):
838 838 __tablename__ = 'permissions'
839 839 __table_args__ = {'extend_existing':True}
840 840 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
841 841 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
842 842 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
843 843
844 844 def __repr__(self):
845 845 return "<%s('%s:%s')>" % (self.__class__.__name__,
846 846 self.permission_id, self.permission_name)
847 847
848 848 @classmethod
849 849 def get_by_key(cls, key):
850 850 return cls.query().filter(cls.permission_name == key).scalar()
851 851
852 852 @classmethod
853 853 def get_default_perms(cls, default_user_id):
854 854 q = Session().query(UserRepoToPerm, Repository, cls)\
855 855 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
856 856 .join((cls, UserRepoToPerm.permission_id == cls.permission_id))\
857 857 .filter(UserRepoToPerm.user_id == default_user_id)
858 858
859 859 return q.all()
860 860
861 861
862 862 class UserRepoToPerm(Base, BaseModel):
863 863 __tablename__ = 'repo_to_perm'
864 864 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
865 865 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
866 866 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
867 867 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
868 868 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
869 869
870 870 user = relationship('User')
871 871 permission = relationship('Permission')
872 872 repository = relationship('Repository')
873 873
874 874 @classmethod
875 875 def create(cls, user, repository, permission):
876 876 n = cls()
877 877 n.user = user
878 878 n.repository = repository
879 879 n.permission = permission
880 880 Session().add(n)
881 881 return n
882 882
883 883 def __repr__(self):
884 884 return '<user:%s => %s >' % (self.user, self.repository)
885 885
886 886 class UserToPerm(Base, BaseModel):
887 887 __tablename__ = 'user_to_perm'
888 888 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
889 889 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
890 890 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
891 891 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
892 892
893 893 user = relationship('User')
894 894 permission = relationship('Permission', lazy='joined')
895 895
896 896 @classmethod
897 897 def has_perm(cls, user_id, perm):
898 898 if not isinstance(perm, Permission):
899 899 raise Exception('perm needs to be an instance of Permission class')
900 900
901 901 return cls.query().filter(cls.user_id == user_id)\
902 902 .filter(cls.permission == perm).scalar() is not None
903 903
904 904 @classmethod
905 905 def grant_perm(cls, user_id, perm):
906 906 if not isinstance(perm, Permission):
907 907 raise Exception('perm needs to be an instance of Permission class')
908 908
909 909 new = cls()
910 910 new.user_id = user_id
911 911 new.permission = perm
912 912 try:
913 913 Session().add(new)
914 914 Session().commit()
915 915 except:
916 916 Session().rollback()
917 917
918 918
919 919 @classmethod
920 920 def revoke_perm(cls, user_id, perm):
921 921 if not isinstance(perm, Permission):
922 922 raise Exception('perm needs to be an instance of Permission class')
923 923
924 924 try:
925 925 obj = cls.query().filter(cls.user_id == user_id)\
926 926 .filter(cls.permission == perm).one()
927 927 Session().delete(obj)
928 928 Session().commit()
929 929 except:
930 930 Session().rollback()
931 931
932 932 class UsersGroupRepoToPerm(Base, BaseModel):
933 933 __tablename__ = 'users_group_repo_to_perm'
934 934 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
935 935 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
936 936 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
937 937 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
938 938 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
939 939
940 940 users_group = relationship('UsersGroup')
941 941 permission = relationship('Permission')
942 942 repository = relationship('Repository')
943 943
944 944 @classmethod
945 945 def create(cls, users_group, repository, permission):
946 946 n = cls()
947 947 n.users_group = users_group
948 948 n.repository = repository
949 949 n.permission = permission
950 950 Session().add(n)
951 951 return n
952 952
953 953 def __repr__(self):
954 954 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
955 955
956 956 class UsersGroupToPerm(Base, BaseModel):
957 957 __tablename__ = 'users_group_to_perm'
958 958 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
959 959 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
960 960 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
961 961
962 962 users_group = relationship('UsersGroup')
963 963 permission = relationship('Permission')
964 964
965 965
966 966 @classmethod
967 967 def has_perm(cls, users_group_id, perm):
968 968 if not isinstance(perm, Permission):
969 969 raise Exception('perm needs to be an instance of Permission class')
970 970
971 971 return cls.query().filter(cls.users_group_id ==
972 972 users_group_id)\
973 973 .filter(cls.permission == perm)\
974 974 .scalar() is not None
975 975
976 976 @classmethod
977 977 def grant_perm(cls, users_group_id, perm):
978 978 if not isinstance(perm, Permission):
979 979 raise Exception('perm needs to be an instance of Permission class')
980 980
981 981 new = cls()
982 982 new.users_group_id = users_group_id
983 983 new.permission = perm
984 984 try:
985 985 Session().add(new)
986 986 Session().commit()
987 987 except:
988 988 Session().rollback()
989 989
990 990
991 991 @classmethod
992 992 def revoke_perm(cls, users_group_id, perm):
993 993 if not isinstance(perm, Permission):
994 994 raise Exception('perm needs to be an instance of Permission class')
995 995
996 996 try:
997 997 obj = cls.query().filter(cls.users_group_id == users_group_id)\
998 998 .filter(cls.permission == perm).one()
999 999 Session().delete(obj)
1000 1000 Session().commit()
1001 1001 except:
1002 1002 Session().rollback()
1003 1003
1004 1004
1005 1005 class UserRepoGroupToPerm(Base, BaseModel):
1006 1006 __tablename__ = 'group_to_perm'
1007 1007 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
1008 1008
1009 1009 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1010 1010 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1011 1011 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1012 1012 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1013 1013
1014 1014 user = relationship('User')
1015 1015 permission = relationship('Permission')
1016 1016 group = relationship('RepoGroup')
1017 1017
1018 1018 class UsersGroupRepoGroupToPerm(Base, BaseModel):
1019 1019 __tablename__ = 'users_group_repo_group_to_perm'
1020 1020 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
1021 1021
1022 1022 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)
1023 1023 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1024 1024 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
1025 1025 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
1026 1026
1027 1027 users_group = relationship('UsersGroup')
1028 1028 permission = relationship('Permission')
1029 1029 group = relationship('RepoGroup')
1030 1030
1031 1031 class Statistics(Base, BaseModel):
1032 1032 __tablename__ = 'statistics'
1033 1033 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
1034 1034 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1035 1035 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
1036 1036 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
1037 1037 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
1038 1038 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
1039 1039 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
1040 1040
1041 1041 repository = relationship('Repository', single_parent=True)
1042 1042
1043 1043 class UserFollowing(Base, BaseModel):
1044 1044 __tablename__ = 'user_followings'
1045 1045 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
1046 1046 UniqueConstraint('user_id', 'follows_user_id')
1047 1047 , {'extend_existing':True})
1048 1048
1049 1049 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1050 1050 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1051 1051 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1052 1052 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1053 1053 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1054 1054
1055 1055 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1056 1056
1057 1057 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1058 1058 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1059 1059
1060 1060
1061 1061 @classmethod
1062 1062 def get_repo_followers(cls, repo_id):
1063 1063 return cls.query().filter(cls.follows_repo_id == repo_id)
1064 1064
1065 1065 class CacheInvalidation(Base, BaseModel):
1066 1066 __tablename__ = 'cache_invalidation'
1067 1067 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
1068 1068 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1069 1069 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1070 1070 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1071 1071 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1072 1072
1073 1073
1074 1074 def __init__(self, cache_key, cache_args=''):
1075 1075 self.cache_key = cache_key
1076 1076 self.cache_args = cache_args
1077 1077 self.cache_active = False
1078 1078
1079 1079 def __repr__(self):
1080 1080 return "<%s('%s:%s')>" % (self.__class__.__name__,
1081 1081 self.cache_id, self.cache_key)
1082 1082
1083 1083 @classmethod
1084 1084 def invalidate(cls, key):
1085 1085 """
1086 1086 Returns Invalidation object if this given key should be invalidated
1087 1087 None otherwise. `cache_active = False` means that this cache
1088 1088 state is not valid and needs to be invalidated
1089 1089
1090 1090 :param key:
1091 1091 """
1092 1092 return cls.query()\
1093 1093 .filter(CacheInvalidation.cache_key == key)\
1094 1094 .filter(CacheInvalidation.cache_active == False)\
1095 1095 .scalar()
1096 1096
1097 1097 @classmethod
1098 1098 def set_invalidate(cls, key):
1099 1099 """
1100 1100 Mark this Cache key for invalidation
1101 1101
1102 1102 :param key:
1103 1103 """
1104 1104
1105 1105 log.debug('marking %s for invalidation' % key)
1106 1106 inv_obj = Session().query(cls)\
1107 1107 .filter(cls.cache_key == key).scalar()
1108 1108 if inv_obj:
1109 1109 inv_obj.cache_active = False
1110 1110 else:
1111 1111 log.debug('cache key not found in invalidation db -> creating one')
1112 1112 inv_obj = CacheInvalidation(key)
1113 1113
1114 1114 try:
1115 1115 Session().add(inv_obj)
1116 1116 Session().commit()
1117 1117 except Exception:
1118 1118 log.error(traceback.format_exc())
1119 1119 Session().rollback()
1120 1120
1121 1121 @classmethod
1122 1122 def set_valid(cls, key):
1123 1123 """
1124 1124 Mark this cache key as active and currently cached
1125 1125
1126 1126 :param key:
1127 1127 """
1128 1128 inv_obj = CacheInvalidation.query()\
1129 1129 .filter(CacheInvalidation.cache_key == key).scalar()
1130 1130 inv_obj.cache_active = True
1131 1131 Session().add(inv_obj)
1132 1132 Session().commit()
1133 1133
1134 1134
1135 1135 class ChangesetComment(Base, BaseModel):
1136 1136 __tablename__ = 'changeset_comments'
1137 1137 __table_args__ = ({'extend_existing':True},)
1138 1138 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
1139 1139 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1140 1140 revision = Column('revision', String(40), nullable=False)
1141 1141 line_no = Column('line_no', Unicode(10), nullable=True)
1142 1142 f_path = Column('f_path', Unicode(1000), nullable=True)
1143 1143 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1144 1144 text = Column('text', Unicode(25000), nullable=False)
1145 1145 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
1146 1146
1147 1147 author = relationship('User', lazy='joined')
1148 1148 repo = relationship('Repository')
1149 1149
1150 1150
1151 1151 @classmethod
1152 1152 def get_users(cls, revision):
1153 1153 """
1154 1154 Returns user associated with this changesetComment. ie those
1155 1155 who actually commented
1156 1156
1157 1157 :param cls:
1158 1158 :param revision:
1159 1159 """
1160 1160 return Session().query(User)\
1161 1161 .filter(cls.revision == revision)\
1162 1162 .join(ChangesetComment.author).all()
1163 1163
1164 1164
1165 1165 class Notification(Base, BaseModel):
1166 1166 __tablename__ = 'notifications'
1167 1167 __table_args__ = ({'extend_existing':True})
1168 1168
1169 1169 TYPE_CHANGESET_COMMENT = u'cs_comment'
1170 1170 TYPE_MESSAGE = u'message'
1171 1171 TYPE_MENTION = u'mention'
1172 1172 TYPE_REGISTRATION = u'registration'
1173 1173
1174 1174 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
1175 1175 subject = Column('subject', Unicode(512), nullable=True)
1176 1176 body = Column('body', Unicode(50000), nullable=True)
1177 1177 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
1178 1178 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1179 1179 type_ = Column('type', Unicode(256))
1180 1180
1181 1181 created_by_user = relationship('User')
1182 1182 notifications_to_users = relationship('UserNotification', lazy='joined',
1183 1183 cascade="all, delete, delete-orphan")
1184 1184
1185 1185 @property
1186 1186 def recipients(self):
1187 1187 return [x.user for x in UserNotification.query()\
1188 1188 .filter(UserNotification.notification == self).all()]
1189 1189
1190 1190 @classmethod
1191 1191 def create(cls, created_by, subject, body, recipients, type_=None):
1192 1192 if type_ is None:
1193 1193 type_ = Notification.TYPE_MESSAGE
1194 1194
1195 1195 notification = cls()
1196 1196 notification.created_by_user = created_by
1197 1197 notification.subject = subject
1198 1198 notification.body = body
1199 1199 notification.type_ = type_
1200 1200 notification.created_on = datetime.datetime.now()
1201 1201
1202 1202 for u in recipients:
1203 1203 assoc = UserNotification()
1204 1204 assoc.notification = notification
1205 1205 u.notifications.append(assoc)
1206 1206 Session().add(notification)
1207 1207 return notification
1208 1208
1209 1209 @property
1210 1210 def description(self):
1211 1211 from rhodecode.model.notification import NotificationModel
1212 1212 return NotificationModel().make_description(self)
1213 1213
1214 1214 class UserNotification(Base, BaseModel):
1215 1215 __tablename__ = 'user_to_notification'
1216 1216 __table_args__ = (UniqueConstraint('user_id', 'notification_id'),
1217 1217 {'extend_existing':True})
1218 1218 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), primary_key=True)
1219 1219 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
1220 1220 read = Column('read', Boolean, default=False)
1221 1221 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
1222 1222
1223 1223 user = relationship('User', lazy="joined")
1224 1224 notification = relationship('Notification', lazy="joined",
1225 1225 order_by=lambda:Notification.created_on.desc(),)
1226 1226
1227 1227 def mark_as_read(self):
1228 1228 self.read = True
1229 1229 Session().add(self)
1230 1230
1231 1231 class DbMigrateVersion(Base, BaseModel):
1232 1232 __tablename__ = 'db_migrate_version'
1233 1233 __table_args__ = {'extend_existing':True}
1234 1234 repository_id = Column('repository_id', String(250), primary_key=True)
1235 1235 repository_path = Column('repository_path', Text)
1236 1236 version = Column('version', Integer)
1237 1237
General Comments 0
You need to be logged in to leave comments. Login now