##// END OF EJS Templates
core: dropped deprecated PartialRenderer that depends on pylons.
marcink -
r2310:d2e9d950 default
parent child Browse files
Show More
@@ -1,980 +1,908 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Utilities library for RhodeCode
23 23 """
24 24
25 25 import datetime
26 26 import decorator
27 27 import json
28 28 import logging
29 29 import os
30 30 import re
31 31 import shutil
32 32 import tempfile
33 33 import traceback
34 34 import tarfile
35 35 import warnings
36 36 import hashlib
37 37 from os.path import join as jn
38 38
39 39 import paste
40 40 import pkg_resources
41 41 from paste.script.command import Command, BadCommand
42 42 from webhelpers.text import collapse, remove_formatting, strip_tags
43 43 from mako import exceptions
44 44 from pyramid.threadlocal import get_current_registry
45 45 from pyramid.request import Request
46 46
47 47 from rhodecode.lib.fakemod import create_module
48 48 from rhodecode.lib.vcs.backends.base import Config
49 49 from rhodecode.lib.vcs.exceptions import VCSError
50 50 from rhodecode.lib.vcs.utils.helpers import get_scm, get_scm_backend
51 51 from rhodecode.lib.utils2 import (
52 52 safe_str, safe_unicode, get_current_rhodecode_user, md5)
53 53 from rhodecode.model import meta
54 54 from rhodecode.model.db import (
55 55 Repository, User, RhodeCodeUi, UserLog, RepoGroup, UserGroup)
56 56 from rhodecode.model.meta import Session
57 57
58 58
59 59 log = logging.getLogger(__name__)
60 60
61 61 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
62 62
63 63 # String which contains characters that are not allowed in slug names for
64 64 # repositories or repository groups. It is properly escaped to use it in
65 65 # regular expressions.
66 66 SLUG_BAD_CHARS = re.escape('`?=[]\;\'"<>,/~!@#$%^&*()+{}|:')
67 67
68 68 # Regex that matches forbidden characters in repo/group slugs.
69 69 SLUG_BAD_CHAR_RE = re.compile('[{}]'.format(SLUG_BAD_CHARS))
70 70
71 71 # Regex that matches allowed characters in repo/group slugs.
72 72 SLUG_GOOD_CHAR_RE = re.compile('[^{}]'.format(SLUG_BAD_CHARS))
73 73
74 74 # Regex that matches whole repo/group slugs.
75 75 SLUG_RE = re.compile('[^{}]+'.format(SLUG_BAD_CHARS))
76 76
77 77 _license_cache = None
78 78
79 79
80 80 def repo_name_slug(value):
81 81 """
82 82 Return slug of name of repository
83 83 This function is called on each creation/modification
84 84 of repository to prevent bad names in repo
85 85 """
86 86 replacement_char = '-'
87 87
88 88 slug = remove_formatting(value)
89 89 slug = SLUG_BAD_CHAR_RE.sub('', slug)
90 90 slug = re.sub('[\s]+', '-', slug)
91 91 slug = collapse(slug, replacement_char)
92 92 return slug
93 93
94 94
95 95 #==============================================================================
96 96 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
97 97 #==============================================================================
98 98 def get_repo_slug(request):
99 99 _repo = ''
100 100 if isinstance(request, Request):
101 101 if hasattr(request, 'db_repo'):
102 102 # if our requests has set db reference use it for name, this
103 103 # translates the example.com/_<id> into proper repo names
104 104 _repo = request.db_repo.repo_name
105 105 elif getattr(request, 'matchdict', None):
106 106 # pyramid
107 107 _repo = request.matchdict.get('repo_name')
108 108
109 109 # TODO(marcink): remove after pylons migration...
110 110 if not _repo:
111 111 _repo = request.environ['pylons.routes_dict'].get('repo_name')
112 112
113 113 if _repo:
114 114 _repo = _repo.rstrip('/')
115 115 return _repo
116 116
117 117
118 118 def get_repo_group_slug(request):
119 119 _group = ''
120 120 if isinstance(request, Request):
121 121 if hasattr(request, 'db_repo_group'):
122 122 # if our requests has set db reference use it for name, this
123 123 # translates the example.com/_<id> into proper repo group names
124 124 _group = request.db_repo_group.group_name
125 125 elif getattr(request, 'matchdict', None):
126 126 # pyramid
127 127 _group = request.matchdict.get('repo_group_name')
128 128
129 129 # TODO(marcink): remove after pylons migration...
130 130 if not _group:
131 131 _group = request.environ['pylons.routes_dict'].get('group_name')
132 132
133 133 if _group:
134 134 _group = _group.rstrip('/')
135 135 return _group
136 136
137 137
138 138 def get_user_group_slug(request):
139 139 _user_group = ''
140 140 if isinstance(request, Request):
141 141
142 142 if hasattr(request, 'db_user_group'):
143 143 _user_group = request.db_user_group.users_group_name
144 144 elif getattr(request, 'matchdict', None):
145 145 # pyramid
146 146 _user_group = request.matchdict.get('user_group_id')
147 147
148 148 try:
149 149 _user_group = UserGroup.get(_user_group)
150 150 if _user_group:
151 151 _user_group = _user_group.users_group_name
152 152 except Exception:
153 153 log.exception('Failed to get user group by id')
154 154 # catch all failures here
155 155 return None
156 156
157 157 # TODO(marcink): remove after pylons migration...
158 158 if not _user_group:
159 159 _user_group = request.environ['pylons.routes_dict'].get('user_group_id')
160 160
161 161 return _user_group
162 162
163 163
164 164 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
165 165 """
166 166 Scans given path for repos and return (name,(type,path)) tuple
167 167
168 168 :param path: path to scan for repositories
169 169 :param recursive: recursive search and return names with subdirs in front
170 170 """
171 171
172 172 # remove ending slash for better results
173 173 path = path.rstrip(os.sep)
174 174 log.debug('now scanning in %s location recursive:%s...', path, recursive)
175 175
176 176 def _get_repos(p):
177 177 dirpaths = _get_dirpaths(p)
178 178 if not _is_dir_writable(p):
179 179 log.warning('repo path without write access: %s', p)
180 180
181 181 for dirpath in dirpaths:
182 182 if os.path.isfile(os.path.join(p, dirpath)):
183 183 continue
184 184 cur_path = os.path.join(p, dirpath)
185 185
186 186 # skip removed repos
187 187 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
188 188 continue
189 189
190 190 #skip .<somethin> dirs
191 191 if dirpath.startswith('.'):
192 192 continue
193 193
194 194 try:
195 195 scm_info = get_scm(cur_path)
196 196 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
197 197 except VCSError:
198 198 if not recursive:
199 199 continue
200 200 #check if this dir containts other repos for recursive scan
201 201 rec_path = os.path.join(p, dirpath)
202 202 if os.path.isdir(rec_path):
203 203 for inner_scm in _get_repos(rec_path):
204 204 yield inner_scm
205 205
206 206 return _get_repos(path)
207 207
208 208
209 209 def _get_dirpaths(p):
210 210 try:
211 211 # OS-independable way of checking if we have at least read-only
212 212 # access or not.
213 213 dirpaths = os.listdir(p)
214 214 except OSError:
215 215 log.warning('ignoring repo path without read access: %s', p)
216 216 return []
217 217
218 218 # os.listpath has a tweak: If a unicode is passed into it, then it tries to
219 219 # decode paths and suddenly returns unicode objects itself. The items it
220 220 # cannot decode are returned as strings and cause issues.
221 221 #
222 222 # Those paths are ignored here until a solid solution for path handling has
223 223 # been built.
224 224 expected_type = type(p)
225 225
226 226 def _has_correct_type(item):
227 227 if type(item) is not expected_type:
228 228 log.error(
229 229 u"Ignoring path %s since it cannot be decoded into unicode.",
230 230 # Using "repr" to make sure that we see the byte value in case
231 231 # of support.
232 232 repr(item))
233 233 return False
234 234 return True
235 235
236 236 dirpaths = [item for item in dirpaths if _has_correct_type(item)]
237 237
238 238 return dirpaths
239 239
240 240
241 241 def _is_dir_writable(path):
242 242 """
243 243 Probe if `path` is writable.
244 244
245 245 Due to trouble on Cygwin / Windows, this is actually probing if it is
246 246 possible to create a file inside of `path`, stat does not produce reliable
247 247 results in this case.
248 248 """
249 249 try:
250 250 with tempfile.TemporaryFile(dir=path):
251 251 pass
252 252 except OSError:
253 253 return False
254 254 return True
255 255
256 256
257 257 def is_valid_repo(repo_name, base_path, expect_scm=None, explicit_scm=None):
258 258 """
259 259 Returns True if given path is a valid repository False otherwise.
260 260 If expect_scm param is given also, compare if given scm is the same
261 261 as expected from scm parameter. If explicit_scm is given don't try to
262 262 detect the scm, just use the given one to check if repo is valid
263 263
264 264 :param repo_name:
265 265 :param base_path:
266 266 :param expect_scm:
267 267 :param explicit_scm:
268 268
269 269 :return True: if given path is a valid repository
270 270 """
271 271 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
272 272 log.debug('Checking if `%s` is a valid path for repository. '
273 273 'Explicit type: %s', repo_name, explicit_scm)
274 274
275 275 try:
276 276 if explicit_scm:
277 277 detected_scms = [get_scm_backend(explicit_scm)]
278 278 else:
279 279 detected_scms = get_scm(full_path)
280 280
281 281 if expect_scm:
282 282 return detected_scms[0] == expect_scm
283 283 log.debug('path: %s is an vcs object:%s', full_path, detected_scms)
284 284 return True
285 285 except VCSError:
286 286 log.debug('path: %s is not a valid repo !', full_path)
287 287 return False
288 288
289 289
290 290 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
291 291 """
292 292 Returns True if given path is a repository group, False otherwise
293 293
294 294 :param repo_name:
295 295 :param base_path:
296 296 """
297 297 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
298 298 log.debug('Checking if `%s` is a valid path for repository group',
299 299 repo_group_name)
300 300
301 301 # check if it's not a repo
302 302 if is_valid_repo(repo_group_name, base_path):
303 303 log.debug('Repo called %s exist, it is not a valid '
304 304 'repo group' % repo_group_name)
305 305 return False
306 306
307 307 try:
308 308 # we need to check bare git repos at higher level
309 309 # since we might match branches/hooks/info/objects or possible
310 310 # other things inside bare git repo
311 311 scm_ = get_scm(os.path.dirname(full_path))
312 312 log.debug('path: %s is a vcs object:%s, not valid '
313 313 'repo group' % (full_path, scm_))
314 314 return False
315 315 except VCSError:
316 316 pass
317 317
318 318 # check if it's a valid path
319 319 if skip_path_check or os.path.isdir(full_path):
320 320 log.debug('path: %s is a valid repo group !', full_path)
321 321 return True
322 322
323 323 log.debug('path: %s is not a valid repo group !', full_path)
324 324 return False
325 325
326 326
327 327 def ask_ok(prompt, retries=4, complaint='[y]es or [n]o please!'):
328 328 while True:
329 329 ok = raw_input(prompt)
330 330 if ok.lower() in ('y', 'ye', 'yes'):
331 331 return True
332 332 if ok.lower() in ('n', 'no', 'nop', 'nope'):
333 333 return False
334 334 retries = retries - 1
335 335 if retries < 0:
336 336 raise IOError
337 337 print(complaint)
338 338
339 339 # propagated from mercurial documentation
340 340 ui_sections = [
341 341 'alias', 'auth',
342 342 'decode/encode', 'defaults',
343 343 'diff', 'email',
344 344 'extensions', 'format',
345 345 'merge-patterns', 'merge-tools',
346 346 'hooks', 'http_proxy',
347 347 'smtp', 'patch',
348 348 'paths', 'profiling',
349 349 'server', 'trusted',
350 350 'ui', 'web', ]
351 351
352 352
353 353 def config_data_from_db(clear_session=True, repo=None):
354 354 """
355 355 Read the configuration data from the database and return configuration
356 356 tuples.
357 357 """
358 358 from rhodecode.model.settings import VcsSettingsModel
359 359
360 360 config = []
361 361
362 362 sa = meta.Session()
363 363 settings_model = VcsSettingsModel(repo=repo, sa=sa)
364 364
365 365 ui_settings = settings_model.get_ui_settings()
366 366
367 367 for setting in ui_settings:
368 368 if setting.active:
369 369 log.debug(
370 370 'settings ui from db: [%s] %s=%s',
371 371 setting.section, setting.key, setting.value)
372 372 config.append((
373 373 safe_str(setting.section), safe_str(setting.key),
374 374 safe_str(setting.value)))
375 375 if setting.key == 'push_ssl':
376 376 # force set push_ssl requirement to False, rhodecode
377 377 # handles that
378 378 config.append((
379 379 safe_str(setting.section), safe_str(setting.key), False))
380 380 if clear_session:
381 381 meta.Session.remove()
382 382
383 383 # TODO: mikhail: probably it makes no sense to re-read hooks information.
384 384 # It's already there and activated/deactivated
385 385 skip_entries = []
386 386 enabled_hook_classes = get_enabled_hook_classes(ui_settings)
387 387 if 'pull' not in enabled_hook_classes:
388 388 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PULL))
389 389 if 'push' not in enabled_hook_classes:
390 390 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH))
391 391 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH))
392 392 skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY))
393 393
394 394 config = [entry for entry in config if entry[:2] not in skip_entries]
395 395
396 396 return config
397 397
398 398
399 399 def make_db_config(clear_session=True, repo=None):
400 400 """
401 401 Create a :class:`Config` instance based on the values in the database.
402 402 """
403 403 config = Config()
404 404 config_data = config_data_from_db(clear_session=clear_session, repo=repo)
405 405 for section, option, value in config_data:
406 406 config.set(section, option, value)
407 407 return config
408 408
409 409
410 410 def get_enabled_hook_classes(ui_settings):
411 411 """
412 412 Return the enabled hook classes.
413 413
414 414 :param ui_settings: List of ui_settings as returned
415 415 by :meth:`VcsSettingsModel.get_ui_settings`
416 416
417 417 :return: a list with the enabled hook classes. The order is not guaranteed.
418 418 :rtype: list
419 419 """
420 420 enabled_hooks = []
421 421 active_hook_keys = [
422 422 key for section, key, value, active in ui_settings
423 423 if section == 'hooks' and active]
424 424
425 425 hook_names = {
426 426 RhodeCodeUi.HOOK_PUSH: 'push',
427 427 RhodeCodeUi.HOOK_PULL: 'pull',
428 428 RhodeCodeUi.HOOK_REPO_SIZE: 'repo_size'
429 429 }
430 430
431 431 for key in active_hook_keys:
432 432 hook = hook_names.get(key)
433 433 if hook:
434 434 enabled_hooks.append(hook)
435 435
436 436 return enabled_hooks
437 437
438 438
439 439 def set_rhodecode_config(config):
440 440 """
441 441 Updates pylons config with new settings from database
442 442
443 443 :param config:
444 444 """
445 445 from rhodecode.model.settings import SettingsModel
446 446 app_settings = SettingsModel().get_all_settings()
447 447
448 448 for k, v in app_settings.items():
449 449 config[k] = v
450 450
451 451
452 452 def get_rhodecode_realm():
453 453 """
454 454 Return the rhodecode realm from database.
455 455 """
456 456 from rhodecode.model.settings import SettingsModel
457 457 realm = SettingsModel().get_setting_by_name('realm')
458 458 return safe_str(realm.app_settings_value)
459 459
460 460
461 461 def get_rhodecode_base_path():
462 462 """
463 463 Returns the base path. The base path is the filesystem path which points
464 464 to the repository store.
465 465 """
466 466 from rhodecode.model.settings import SettingsModel
467 467 paths_ui = SettingsModel().get_ui_by_section_and_key('paths', '/')
468 468 return safe_str(paths_ui.ui_value)
469 469
470 470
471 471 def map_groups(path):
472 472 """
473 473 Given a full path to a repository, create all nested groups that this
474 474 repo is inside. This function creates parent-child relationships between
475 475 groups and creates default perms for all new groups.
476 476
477 477 :param paths: full path to repository
478 478 """
479 479 from rhodecode.model.repo_group import RepoGroupModel
480 480 sa = meta.Session()
481 481 groups = path.split(Repository.NAME_SEP)
482 482 parent = None
483 483 group = None
484 484
485 485 # last element is repo in nested groups structure
486 486 groups = groups[:-1]
487 487 rgm = RepoGroupModel(sa)
488 488 owner = User.get_first_super_admin()
489 489 for lvl, group_name in enumerate(groups):
490 490 group_name = '/'.join(groups[:lvl] + [group_name])
491 491 group = RepoGroup.get_by_group_name(group_name)
492 492 desc = '%s group' % group_name
493 493
494 494 # skip folders that are now removed repos
495 495 if REMOVED_REPO_PAT.match(group_name):
496 496 break
497 497
498 498 if group is None:
499 499 log.debug('creating group level: %s group_name: %s',
500 500 lvl, group_name)
501 501 group = RepoGroup(group_name, parent)
502 502 group.group_description = desc
503 503 group.user = owner
504 504 sa.add(group)
505 505 perm_obj = rgm._create_default_perms(group)
506 506 sa.add(perm_obj)
507 507 sa.flush()
508 508
509 509 parent = group
510 510 return group
511 511
512 512
513 513 def repo2db_mapper(initial_repo_list, remove_obsolete=False):
514 514 """
515 515 maps all repos given in initial_repo_list, non existing repositories
516 516 are created, if remove_obsolete is True it also checks for db entries
517 517 that are not in initial_repo_list and removes them.
518 518
519 519 :param initial_repo_list: list of repositories found by scanning methods
520 520 :param remove_obsolete: check for obsolete entries in database
521 521 """
522 522 from rhodecode.model.repo import RepoModel
523 523 from rhodecode.model.scm import ScmModel
524 524 from rhodecode.model.repo_group import RepoGroupModel
525 525 from rhodecode.model.settings import SettingsModel
526 526
527 527 sa = meta.Session()
528 528 repo_model = RepoModel()
529 529 user = User.get_first_super_admin()
530 530 added = []
531 531
532 532 # creation defaults
533 533 defs = SettingsModel().get_default_repo_settings(strip_prefix=True)
534 534 enable_statistics = defs.get('repo_enable_statistics')
535 535 enable_locking = defs.get('repo_enable_locking')
536 536 enable_downloads = defs.get('repo_enable_downloads')
537 537 private = defs.get('repo_private')
538 538
539 539 for name, repo in initial_repo_list.items():
540 540 group = map_groups(name)
541 541 unicode_name = safe_unicode(name)
542 542 db_repo = repo_model.get_by_repo_name(unicode_name)
543 543 # found repo that is on filesystem not in RhodeCode database
544 544 if not db_repo:
545 545 log.info('repository %s not found, creating now', name)
546 546 added.append(name)
547 547 desc = (repo.description
548 548 if repo.description != 'unknown'
549 549 else '%s repository' % name)
550 550
551 551 db_repo = repo_model._create_repo(
552 552 repo_name=name,
553 553 repo_type=repo.alias,
554 554 description=desc,
555 555 repo_group=getattr(group, 'group_id', None),
556 556 owner=user,
557 557 enable_locking=enable_locking,
558 558 enable_downloads=enable_downloads,
559 559 enable_statistics=enable_statistics,
560 560 private=private,
561 561 state=Repository.STATE_CREATED
562 562 )
563 563 sa.commit()
564 564 # we added that repo just now, and make sure we updated server info
565 565 if db_repo.repo_type == 'git':
566 566 git_repo = db_repo.scm_instance()
567 567 # update repository server-info
568 568 log.debug('Running update server info')
569 569 git_repo._update_server_info()
570 570
571 571 db_repo.update_commit_cache()
572 572
573 573 config = db_repo._config
574 574 config.set('extensions', 'largefiles', '')
575 575 ScmModel().install_hooks(
576 576 db_repo.scm_instance(config=config),
577 577 repo_type=db_repo.repo_type)
578 578
579 579 removed = []
580 580 if remove_obsolete:
581 581 # remove from database those repositories that are not in the filesystem
582 582 for repo in sa.query(Repository).all():
583 583 if repo.repo_name not in initial_repo_list.keys():
584 584 log.debug("Removing non-existing repository found in db `%s`",
585 585 repo.repo_name)
586 586 try:
587 587 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
588 588 sa.commit()
589 589 removed.append(repo.repo_name)
590 590 except Exception:
591 591 # don't hold further removals on error
592 592 log.error(traceback.format_exc())
593 593 sa.rollback()
594 594
595 595 def splitter(full_repo_name):
596 596 _parts = full_repo_name.rsplit(RepoGroup.url_sep(), 1)
597 597 gr_name = None
598 598 if len(_parts) == 2:
599 599 gr_name = _parts[0]
600 600 return gr_name
601 601
602 602 initial_repo_group_list = [splitter(x) for x in
603 603 initial_repo_list.keys() if splitter(x)]
604 604
605 605 # remove from database those repository groups that are not in the
606 606 # filesystem due to parent child relationships we need to delete them
607 607 # in a specific order of most nested first
608 608 all_groups = [x.group_name for x in sa.query(RepoGroup).all()]
609 609 nested_sort = lambda gr: len(gr.split('/'))
610 610 for group_name in sorted(all_groups, key=nested_sort, reverse=True):
611 611 if group_name not in initial_repo_group_list:
612 612 repo_group = RepoGroup.get_by_group_name(group_name)
613 613 if (repo_group.children.all() or
614 614 not RepoGroupModel().check_exist_filesystem(
615 615 group_name=group_name, exc_on_failure=False)):
616 616 continue
617 617
618 618 log.info(
619 619 'Removing non-existing repository group found in db `%s`',
620 620 group_name)
621 621 try:
622 622 RepoGroupModel(sa).delete(group_name, fs_remove=False)
623 623 sa.commit()
624 624 removed.append(group_name)
625 625 except Exception:
626 626 # don't hold further removals on error
627 627 log.exception(
628 628 'Unable to remove repository group `%s`',
629 629 group_name)
630 630 sa.rollback()
631 631 raise
632 632
633 633 return added, removed
634 634
635 635
636 636 def get_default_cache_settings(settings):
637 637 cache_settings = {}
638 638 for key in settings.keys():
639 639 for prefix in ['beaker.cache.', 'cache.']:
640 640 if key.startswith(prefix):
641 641 name = key.split(prefix)[1].strip()
642 642 cache_settings[name] = settings[key].strip()
643 643 return cache_settings
644 644
645 645
646 646 # set cache regions for beaker so celery can utilise it
647 647 def add_cache(settings):
648 648 from rhodecode.lib import caches
649 649 cache_settings = {'regions': None}
650 650 # main cache settings used as default ...
651 651 cache_settings.update(get_default_cache_settings(settings))
652 652
653 653 if cache_settings['regions']:
654 654 for region in cache_settings['regions'].split(','):
655 655 region = region.strip()
656 656 region_settings = {}
657 657 for key, value in cache_settings.items():
658 658 if key.startswith(region):
659 659 region_settings[key.split('.')[1]] = value
660 660
661 661 caches.configure_cache_region(
662 662 region, region_settings, cache_settings)
663 663
664 664
665 665 def load_rcextensions(root_path):
666 666 import rhodecode
667 667 from rhodecode.config import conf
668 668
669 669 path = os.path.join(root_path, 'rcextensions', '__init__.py')
670 670 if os.path.isfile(path):
671 671 rcext = create_module('rc', path)
672 672 EXT = rhodecode.EXTENSIONS = rcext
673 673 log.debug('Found rcextensions now loading %s...', rcext)
674 674
675 675 # Additional mappings that are not present in the pygments lexers
676 676 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
677 677
678 678 # auto check if the module is not missing any data, set to default if is
679 679 # this will help autoupdate new feature of rcext module
680 680 #from rhodecode.config import rcextensions
681 681 #for k in dir(rcextensions):
682 682 # if not k.startswith('_') and not hasattr(EXT, k):
683 683 # setattr(EXT, k, getattr(rcextensions, k))
684 684
685 685
686 686 def get_custom_lexer(extension):
687 687 """
688 688 returns a custom lexer if it is defined in rcextensions module, or None
689 689 if there's no custom lexer defined
690 690 """
691 691 import rhodecode
692 692 from pygments import lexers
693 693
694 694 # custom override made by RhodeCode
695 695 if extension in ['mako']:
696 696 return lexers.get_lexer_by_name('html+mako')
697 697
698 698 # check if we didn't define this extension as other lexer
699 699 extensions = rhodecode.EXTENSIONS and getattr(rhodecode.EXTENSIONS, 'EXTRA_LEXERS', None)
700 700 if extensions and extension in rhodecode.EXTENSIONS.EXTRA_LEXERS:
701 701 _lexer_name = rhodecode.EXTENSIONS.EXTRA_LEXERS[extension]
702 702 return lexers.get_lexer_by_name(_lexer_name)
703 703
704 704
705 705 #==============================================================================
706 706 # TEST FUNCTIONS AND CREATORS
707 707 #==============================================================================
708 708 def create_test_index(repo_location, config):
709 709 """
710 710 Makes default test index.
711 711 """
712 712 import rc_testdata
713 713
714 714 rc_testdata.extract_search_index(
715 715 'vcs_search_index', os.path.dirname(config['search.location']))
716 716
717 717
718 718 def create_test_directory(test_path):
719 719 """
720 720 Create test directory if it doesn't exist.
721 721 """
722 722 if not os.path.isdir(test_path):
723 723 log.debug('Creating testdir %s', test_path)
724 724 os.makedirs(test_path)
725 725
726 726
727 727 def create_test_database(test_path, config):
728 728 """
729 729 Makes a fresh database.
730 730 """
731 731 from rhodecode.lib.db_manage import DbManage
732 732
733 733 # PART ONE create db
734 734 dbconf = config['sqlalchemy.db1.url']
735 735 log.debug('making test db %s', dbconf)
736 736
737 737 dbmanage = DbManage(log_sql=False, dbconf=dbconf, root=config['here'],
738 738 tests=True, cli_args={'force_ask': True})
739 739 dbmanage.create_tables(override=True)
740 740 dbmanage.set_db_version()
741 741 # for tests dynamically set new root paths based on generated content
742 742 dbmanage.create_settings(dbmanage.config_prompt(test_path))
743 743 dbmanage.create_default_user()
744 744 dbmanage.create_test_admin_and_users()
745 745 dbmanage.create_permissions()
746 746 dbmanage.populate_default_permissions()
747 747 Session().commit()
748 748
749 749
750 750 def create_test_repositories(test_path, config):
751 751 """
752 752 Creates test repositories in the temporary directory. Repositories are
753 753 extracted from archives within the rc_testdata package.
754 754 """
755 755 import rc_testdata
756 756 from rhodecode.tests import HG_REPO, GIT_REPO, SVN_REPO
757 757
758 758 log.debug('making test vcs repositories')
759 759
760 760 idx_path = config['search.location']
761 761 data_path = config['cache_dir']
762 762
763 763 # clean index and data
764 764 if idx_path and os.path.exists(idx_path):
765 765 log.debug('remove %s', idx_path)
766 766 shutil.rmtree(idx_path)
767 767
768 768 if data_path and os.path.exists(data_path):
769 769 log.debug('remove %s', data_path)
770 770 shutil.rmtree(data_path)
771 771
772 772 rc_testdata.extract_hg_dump('vcs_test_hg', jn(test_path, HG_REPO))
773 773 rc_testdata.extract_git_dump('vcs_test_git', jn(test_path, GIT_REPO))
774 774
775 775 # Note: Subversion is in the process of being integrated with the system,
776 776 # until we have a properly packed version of the test svn repository, this
777 777 # tries to copy over the repo from a package "rc_testdata"
778 778 svn_repo_path = rc_testdata.get_svn_repo_archive()
779 779 with tarfile.open(svn_repo_path) as tar:
780 780 tar.extractall(jn(test_path, SVN_REPO))
781 781
782 782
783 783 #==============================================================================
784 784 # PASTER COMMANDS
785 785 #==============================================================================
786 786 class BasePasterCommand(Command):
787 787 """
788 788 Abstract Base Class for paster commands.
789 789
790 790 The celery commands are somewhat aggressive about loading
791 791 celery.conf, and since our module sets the `CELERY_LOADER`
792 792 environment variable to our loader, we have to bootstrap a bit and
793 793 make sure we've had a chance to load the pylons config off of the
794 794 command line, otherwise everything fails.
795 795 """
796 796 min_args = 1
797 797 min_args_error = "Please provide a paster config file as an argument."
798 798 takes_config_file = 1
799 799 requires_config_file = True
800 800
801 801 def notify_msg(self, msg, log=False):
802 802 """Make a notification to user, additionally if logger is passed
803 803 it logs this action using given logger
804 804
805 805 :param msg: message that will be printed to user
806 806 :param log: logging instance, to use to additionally log this message
807 807
808 808 """
809 809 if log and isinstance(log, logging):
810 810 log(msg)
811 811
812 812 def run(self, args):
813 813 """
814 814 Overrides Command.run
815 815
816 816 Checks for a config file argument and loads it.
817 817 """
818 818 if len(args) < self.min_args:
819 819 raise BadCommand(
820 820 self.min_args_error % {'min_args': self.min_args,
821 821 'actual_args': len(args)})
822 822
823 823 # Decrement because we're going to lob off the first argument.
824 824 # @@ This is hacky
825 825 self.min_args -= 1
826 826 self.bootstrap_config(args[0])
827 827 self.update_parser()
828 828 return super(BasePasterCommand, self).run(args[1:])
829 829
830 830 def update_parser(self):
831 831 """
832 832 Abstract method. Allows for the class' parser to be updated
833 833 before the superclass' `run` method is called. Necessary to
834 834 allow options/arguments to be passed through to the underlying
835 835 celery command.
836 836 """
837 837 raise NotImplementedError("Abstract Method.")
838 838
839 839 def bootstrap_config(self, conf):
840 840 """
841 841 Loads the pylons configuration.
842 842 """
843 843 from pylons import config as pylonsconfig
844 844
845 845 self.path_to_ini_file = os.path.realpath(conf)
846 846 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
847 847 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
848 848
849 849 def _init_session(self):
850 850 """
851 851 Inits SqlAlchemy Session
852 852 """
853 853 logging.config.fileConfig(self.path_to_ini_file)
854 854 from pylons import config
855 855 from rhodecode.config.utils import initialize_database
856 856
857 857 # get to remove repos !!
858 858 add_cache(config)
859 859 initialize_database(config)
860 860
861 861
862 class PartialRenderer(object):
863 """
864 Partial renderer used to render chunks of html used in datagrids
865 use like::
866
867 _render = PartialRenderer('data_table/_dt_elements.mako')
868 _render('quick_menu', args, kwargs)
869 PartialRenderer.h,
870 c,
871 _,
872 ungettext
873 are the template stuff initialized inside and can be re-used later
874
875 :param tmpl_name: template path relate to /templates/ dir
876 """
877
878 def __init__(self, tmpl_name):
879 import rhodecode
880 from pylons import request, tmpl_context as c
881 from pylons.i18n.translation import _, ungettext
882 from rhodecode.lib import helpers as h
883
884 self.tmpl_name = tmpl_name
885 self.rhodecode = rhodecode
886 self.c = c
887 self._ = _
888 self.ungettext = ungettext
889 self.h = h
890 self.request = request
891
892 def _mako_lookup(self):
893 _tmpl_lookup = self.rhodecode.CONFIG['pylons.app_globals'].mako_lookup
894 return _tmpl_lookup.get_template(self.tmpl_name)
895
896 def _update_kwargs_for_render(self, kwargs):
897 """
898 Inject params required for Mako rendering
899 """
900 _kwargs = {
901 '_': self._,
902 'h': self.h,
903 'c': self.c,
904 'request': self.request,
905 '_ungettext': self.ungettext,
906 }
907 _kwargs.update(kwargs)
908 return _kwargs
909
910 def _render_with_exc(self, render_func, args, kwargs):
911 try:
912 return render_func.render(*args, **kwargs)
913 except:
914 log.error(exceptions.text_error_template().render())
915 raise
916
917 def _get_template(self, template_obj, def_name):
918 if def_name:
919 tmpl = template_obj.get_def(def_name)
920 else:
921 tmpl = template_obj
922 return tmpl
923
924 def render(self, def_name, *args, **kwargs):
925 lookup_obj = self._mako_lookup()
926 tmpl = self._get_template(lookup_obj, def_name=def_name)
927 kwargs = self._update_kwargs_for_render(kwargs)
928 return self._render_with_exc(tmpl, args, kwargs)
929
930 def __call__(self, tmpl, *args, **kwargs):
931 return self.render(tmpl, *args, **kwargs)
932
933
934 862 def password_changed(auth_user, session):
935 863 # Never report password change in case of default user or anonymous user.
936 864 if auth_user.username == User.DEFAULT_USER or auth_user.user_id is None:
937 865 return False
938 866
939 867 password_hash = md5(auth_user.password) if auth_user.password else None
940 868 rhodecode_user = session.get('rhodecode_user', {})
941 869 session_password_hash = rhodecode_user.get('password', '')
942 870 return password_hash != session_password_hash
943 871
944 872
945 873 def read_opensource_licenses():
946 874 global _license_cache
947 875
948 876 if not _license_cache:
949 877 licenses = pkg_resources.resource_string(
950 878 'rhodecode', 'config/licenses.json')
951 879 _license_cache = json.loads(licenses)
952 880
953 881 return _license_cache
954 882
955 883
956 884 def get_registry(request):
957 885 """
958 886 Utility to get the pyramid registry from a request. During migration to
959 887 pyramid we sometimes want to use the pyramid registry from pylons context.
960 888 Therefore this utility returns `request.registry` for pyramid requests and
961 889 uses `get_current_registry()` for pylons requests.
962 890 """
963 891 try:
964 892 return request.registry
965 893 except AttributeError:
966 894 return get_current_registry()
967 895
968 896
969 897 def generate_platform_uuid():
970 898 """
971 899 Generates platform UUID based on it's name
972 900 """
973 901 import platform
974 902
975 903 try:
976 904 uuid_list = [platform.platform()]
977 905 return hashlib.sha256(':'.join(uuid_list)).hexdigest()
978 906 except Exception as e:
979 907 log.error('Failed to generate host uuid: %s' % e)
980 908 return 'UNDEFINED'
@@ -1,377 +1,377 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Model for notifications
24 24 """
25 25
26 26 import logging
27 27 import traceback
28 28
29 from pyramid.threadlocal import get_current_request
29 30 from sqlalchemy.sql.expression import false, true
30 31
31 32 import rhodecode
32 33 from rhodecode.lib import helpers as h
33 from rhodecode.lib.utils import PartialRenderer
34 34 from rhodecode.model import BaseModel
35 35 from rhodecode.model.db import Notification, User, UserNotification
36 36 from rhodecode.model.meta import Session
37 37 from rhodecode.translation import TranslationString
38 38
39 39 log = logging.getLogger(__name__)
40 40
41 41
42 42 class NotificationModel(BaseModel):
43 43
44 44 cls = Notification
45 45
46 46 def __get_notification(self, notification):
47 47 if isinstance(notification, Notification):
48 48 return notification
49 49 elif isinstance(notification, (int, long)):
50 50 return Notification.get(notification)
51 51 else:
52 52 if notification:
53 53 raise Exception('notification must be int, long or Instance'
54 54 ' of Notification got %s' % type(notification))
55 55
56 56 def create(
57 57 self, created_by, notification_subject, notification_body,
58 58 notification_type=Notification.TYPE_MESSAGE, recipients=None,
59 59 mention_recipients=None, with_email=True, email_kwargs=None):
60 60 """
61 61
62 62 Creates notification of given type
63 63
64 64 :param created_by: int, str or User instance. User who created this
65 65 notification
66 66 :param notification_subject: subject of notification itself
67 67 :param notification_body: body of notification text
68 68 :param notification_type: type of notification, based on that we
69 69 pick templates
70 70
71 71 :param recipients: list of int, str or User objects, when None
72 72 is given send to all admins
73 73 :param mention_recipients: list of int, str or User objects,
74 74 that were mentioned
75 75 :param with_email: send email with this notification
76 76 :param email_kwargs: dict with arguments to generate email
77 77 """
78 78
79 79 from rhodecode.lib.celerylib import tasks, run_task
80 80
81 81 if recipients and not getattr(recipients, '__iter__', False):
82 82 raise Exception('recipients must be an iterable object')
83 83
84 84 created_by_obj = self._get_user(created_by)
85 85 # default MAIN body if not given
86 86 email_kwargs = email_kwargs or {'body': notification_body}
87 87 mention_recipients = mention_recipients or set()
88 88
89 89 if not created_by_obj:
90 90 raise Exception('unknown user %s' % created_by)
91 91
92 92 if recipients is None:
93 93 # recipients is None means to all admins
94 94 recipients_objs = User.query().filter(User.admin == true()).all()
95 95 log.debug('sending notifications %s to admins: %s',
96 96 notification_type, recipients_objs)
97 97 else:
98 98 recipients_objs = []
99 99 for u in recipients:
100 100 obj = self._get_user(u)
101 101 if obj:
102 102 recipients_objs.append(obj)
103 103 else: # we didn't find this user, log the error and carry on
104 104 log.error('cannot notify unknown user %r', u)
105 105
106 106 recipients_objs = set(recipients_objs)
107 107 if not recipients_objs:
108 108 raise Exception('no valid recipients specified')
109 109
110 110 log.debug('sending notifications %s to %s',
111 111 notification_type, recipients_objs)
112 112
113 113 # add mentioned users into recipients
114 114 final_recipients = set(recipients_objs).union(mention_recipients)
115 115 notification = Notification.create(
116 116 created_by=created_by_obj, subject=notification_subject,
117 117 body=notification_body, recipients=final_recipients,
118 118 type_=notification_type
119 119 )
120 120
121 121 if not with_email: # skip sending email, and just create notification
122 122 return notification
123 123
124 124 # don't send email to person who created this comment
125 125 rec_objs = set(recipients_objs).difference(set([created_by_obj]))
126 126
127 127 # now notify all recipients in question
128 128
129 129 for recipient in rec_objs.union(mention_recipients):
130 130 # inject current recipient
131 131 email_kwargs['recipient'] = recipient
132 132 email_kwargs['mention'] = recipient in mention_recipients
133 133 (subject, headers, email_body,
134 134 email_body_plaintext) = EmailNotificationModel().render_email(
135 135 notification_type, **email_kwargs)
136 136
137 137 log.debug(
138 138 'Creating notification email task for user:`%s`', recipient)
139 139 task = run_task(
140 140 tasks.send_email, recipient.email, subject,
141 141 email_body_plaintext, email_body)
142 142 log.debug('Created email task: %s', task)
143 143
144 144 return notification
145 145
146 146 def delete(self, user, notification):
147 147 # we don't want to remove actual notification just the assignment
148 148 try:
149 149 notification = self.__get_notification(notification)
150 150 user = self._get_user(user)
151 151 if notification and user:
152 152 obj = UserNotification.query()\
153 153 .filter(UserNotification.user == user)\
154 154 .filter(UserNotification.notification == notification)\
155 155 .one()
156 156 Session().delete(obj)
157 157 return True
158 158 except Exception:
159 159 log.error(traceback.format_exc())
160 160 raise
161 161
162 162 def get_for_user(self, user, filter_=None):
163 163 """
164 164 Get mentions for given user, filter them if filter dict is given
165 165 """
166 166 user = self._get_user(user)
167 167
168 168 q = UserNotification.query()\
169 169 .filter(UserNotification.user == user)\
170 170 .join((
171 171 Notification, UserNotification.notification_id ==
172 172 Notification.notification_id))
173 173 if filter_ == ['all']:
174 174 q = q # no filter
175 175 elif filter_ == ['unread']:
176 176 q = q.filter(UserNotification.read == false())
177 177 elif filter_:
178 178 q = q.filter(Notification.type_.in_(filter_))
179 179
180 180 return q
181 181
182 182 def mark_read(self, user, notification):
183 183 try:
184 184 notification = self.__get_notification(notification)
185 185 user = self._get_user(user)
186 186 if notification and user:
187 187 obj = UserNotification.query()\
188 188 .filter(UserNotification.user == user)\
189 189 .filter(UserNotification.notification == notification)\
190 190 .one()
191 191 obj.read = True
192 192 Session().add(obj)
193 193 return True
194 194 except Exception:
195 195 log.error(traceback.format_exc())
196 196 raise
197 197
198 198 def mark_all_read_for_user(self, user, filter_=None):
199 199 user = self._get_user(user)
200 200 q = UserNotification.query()\
201 201 .filter(UserNotification.user == user)\
202 202 .filter(UserNotification.read == false())\
203 203 .join((
204 204 Notification, UserNotification.notification_id ==
205 205 Notification.notification_id))
206 206 if filter_ == ['unread']:
207 207 q = q.filter(UserNotification.read == false())
208 208 elif filter_:
209 209 q = q.filter(Notification.type_.in_(filter_))
210 210
211 211 # this is a little inefficient but sqlalchemy doesn't support
212 212 # update on joined tables :(
213 213 for obj in q.all():
214 214 obj.read = True
215 215 Session().add(obj)
216 216
217 217 def get_unread_cnt_for_user(self, user):
218 218 user = self._get_user(user)
219 219 return UserNotification.query()\
220 220 .filter(UserNotification.read == false())\
221 221 .filter(UserNotification.user == user).count()
222 222
223 223 def get_unread_for_user(self, user):
224 224 user = self._get_user(user)
225 225 return [x.notification for x in UserNotification.query()
226 226 .filter(UserNotification.read == false())
227 227 .filter(UserNotification.user == user).all()]
228 228
229 229 def get_user_notification(self, user, notification):
230 230 user = self._get_user(user)
231 231 notification = self.__get_notification(notification)
232 232
233 233 return UserNotification.query()\
234 234 .filter(UserNotification.notification == notification)\
235 235 .filter(UserNotification.user == user).scalar()
236 236
237 237 def make_description(self, notification, translate, show_age=True):
238 238 """
239 239 Creates a human readable description based on properties
240 240 of notification object
241 241 """
242 242 _ = translate
243 243 _map = {
244 244 notification.TYPE_CHANGESET_COMMENT: [
245 245 _('%(user)s commented on commit %(date_or_age)s'),
246 246 _('%(user)s commented on commit at %(date_or_age)s'),
247 247 ],
248 248 notification.TYPE_MESSAGE: [
249 249 _('%(user)s sent message %(date_or_age)s'),
250 250 _('%(user)s sent message at %(date_or_age)s'),
251 251 ],
252 252 notification.TYPE_MENTION: [
253 253 _('%(user)s mentioned you %(date_or_age)s'),
254 254 _('%(user)s mentioned you at %(date_or_age)s'),
255 255 ],
256 256 notification.TYPE_REGISTRATION: [
257 257 _('%(user)s registered in RhodeCode %(date_or_age)s'),
258 258 _('%(user)s registered in RhodeCode at %(date_or_age)s'),
259 259 ],
260 260 notification.TYPE_PULL_REQUEST: [
261 261 _('%(user)s opened new pull request %(date_or_age)s'),
262 262 _('%(user)s opened new pull request at %(date_or_age)s'),
263 263 ],
264 264 notification.TYPE_PULL_REQUEST_COMMENT: [
265 265 _('%(user)s commented on pull request %(date_or_age)s'),
266 266 _('%(user)s commented on pull request at %(date_or_age)s'),
267 267 ],
268 268 }
269 269
270 270 templates = _map[notification.type_]
271 271
272 272 if show_age:
273 273 template = templates[0]
274 274 date_or_age = h.age(notification.created_on)
275 275 if translate:
276 276 date_or_age = translate(date_or_age)
277 277
278 278 if isinstance(date_or_age, TranslationString):
279 279 date_or_age = date_or_age.interpolate()
280 280
281 281 else:
282 282 template = templates[1]
283 283 date_or_age = h.format_date(notification.created_on)
284 284
285 285 return template % {
286 286 'user': notification.created_by_user.username,
287 287 'date_or_age': date_or_age,
288 288 }
289 289
290 290
291 291 class EmailNotificationModel(BaseModel):
292 292 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
293 293 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
294 294 TYPE_PULL_REQUEST = Notification.TYPE_PULL_REQUEST
295 295 TYPE_PULL_REQUEST_COMMENT = Notification.TYPE_PULL_REQUEST_COMMENT
296 296 TYPE_MAIN = Notification.TYPE_MESSAGE
297 297
298 298 TYPE_PASSWORD_RESET = 'password_reset'
299 299 TYPE_PASSWORD_RESET_CONFIRMATION = 'password_reset_confirmation'
300 300 TYPE_EMAIL_TEST = 'email_test'
301 301 TYPE_TEST = 'test'
302 302
303 303 email_types = {
304 304 TYPE_MAIN: 'email_templates/main.mako',
305 305 TYPE_TEST: 'email_templates/test.mako',
306 306 TYPE_EMAIL_TEST: 'email_templates/email_test.mako',
307 307 TYPE_REGISTRATION: 'email_templates/user_registration.mako',
308 308 TYPE_PASSWORD_RESET: 'email_templates/password_reset.mako',
309 309 TYPE_PASSWORD_RESET_CONFIRMATION: 'email_templates/password_reset_confirmation.mako',
310 310 TYPE_COMMIT_COMMENT: 'email_templates/commit_comment.mako',
311 311 TYPE_PULL_REQUEST: 'email_templates/pull_request_review.mako',
312 312 TYPE_PULL_REQUEST_COMMENT: 'email_templates/pull_request_comment.mako',
313 313 }
314 314
315 315 def __init__(self):
316 316 """
317 317 Example usage::
318 318
319 319 (subject, headers, email_body,
320 320 email_body_plaintext) = EmailNotificationModel().render_email(
321 321 EmailNotificationModel.TYPE_TEST, **email_kwargs)
322 322
323 323 """
324 324 super(EmailNotificationModel, self).__init__()
325 325 self.rhodecode_instance_name = rhodecode.CONFIG.get('rhodecode_title')
326 326
327 327 def _update_kwargs_for_render(self, kwargs):
328 328 """
329 329 Inject params required for Mako rendering
330 330
331 331 :param kwargs:
332 332 """
333 333
334 334 kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name
335 335 instance_url = h.route_url('home')
336 336 _kwargs = {
337 337 'instance_url': instance_url,
338 338 'whitespace_filter': self.whitespace_filter
339 339 }
340 340 _kwargs.update(kwargs)
341 341 return _kwargs
342 342
343 343 def whitespace_filter(self, text):
344 344 return text.replace('\n', '').replace('\t', '')
345 345
346 def get_renderer(self, type_):
346 def get_renderer(self, type_, request):
347 347 template_name = self.email_types[type_]
348 return PartialRenderer(template_name)
348 return request.get_partial_renderer(template_name)
349 349
350 350 def render_email(self, type_, **kwargs):
351 351 """
352 352 renders template for email, and returns a tuple of
353 353 (subject, email_headers, email_html_body, email_plaintext_body)
354 354 """
355 355 # translator and helpers inject
356 356 _kwargs = self._update_kwargs_for_render(kwargs)
357
358 email_template = self.get_renderer(type_)
357 request = get_current_request()
358 email_template = self.get_renderer(type_, request=request)
359 359
360 360 subject = email_template.render('subject', **_kwargs)
361 361
362 362 try:
363 363 headers = email_template.render('headers', **_kwargs)
364 364 except AttributeError:
365 365 # it's not defined in template, ok we can skip it
366 366 headers = ''
367 367
368 368 try:
369 369 body_plaintext = email_template.render('body_plaintext', **_kwargs)
370 370 except AttributeError:
371 371 # it's not defined in template, ok we can skip it
372 372 body_plaintext = ''
373 373
374 374 # render WHOLE template
375 375 body = email_template.render(None, **_kwargs)
376 376
377 377 return subject, headers, body, body_plaintext
@@ -1,124 +1,143 b''
1 1 import collections
2 # -*- coding: utf-8 -*-
3
4 # Copyright (C) 2010-2017 RhodeCode GmbH
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License, version 3
8 # (only), as published by the Free Software Foundation.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU Affero General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #
18 # This program is dual-licensed. If you wish to learn more about the
19 # RhodeCode Enterprise Edition, including its added features, Support services,
20 # and proprietary license terms, please see https://rhodecode.com/licenses/
2 21
3 22 import pytest
4 23
5 from rhodecode.lib.utils import PartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
6 25 from rhodecode.lib.utils2 import AttributeDict
7 26 from rhodecode.model.notification import EmailNotificationModel
8 27
9 28
10 def test_get_template_obj(app):
29 def test_get_template_obj(app, request_stub):
11 30 template = EmailNotificationModel().get_renderer(
12 EmailNotificationModel.TYPE_TEST)
13 assert isinstance(template, PartialRenderer)
31 EmailNotificationModel.TYPE_TEST, request_stub)
32 assert isinstance(template, PyramidPartialRenderer)
14 33
15 34
16 35 def test_render_email(app, http_host_only_stub):
17 36 kwargs = {}
18 37 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
19 38 EmailNotificationModel.TYPE_TEST, **kwargs)
20 39
21 40 # subject
22 41 assert subject == 'Test "Subject" hello "world"'
23 42
24 43 # headers
25 44 assert headers == 'X=Y'
26 45
27 46 # body plaintext
28 47 assert body_plaintext == 'Email Plaintext Body'
29 48
30 49 # body
31 50 notification_footer = 'This is a notification from RhodeCode. http://%s/' \
32 51 % http_host_only_stub
33 52 assert notification_footer in body
34 53 assert 'Email Body' in body
35 54
36 55
37 56 def test_render_pr_email(app, user_admin):
38 57
39 58 ref = collections.namedtuple('Ref',
40 59 'name, type')(
41 60 'fxies123', 'book'
42 61 )
43 62
44 63 pr = collections.namedtuple('PullRequest',
45 64 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
46 65 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
47 66
48 67 source_repo = target_repo = collections.namedtuple('Repo',
49 68 'type, repo_name')(
50 69 'hg', 'pull_request_1')
51 70
52 71 kwargs = {
53 72 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
54 73 'pull_request': pr,
55 74 'pull_request_commits': [],
56 75
57 76 'pull_request_target_repo': target_repo,
58 77 'pull_request_target_repo_url': 'x',
59 78
60 79 'pull_request_source_repo': source_repo,
61 80 'pull_request_source_repo_url': 'x',
62 81
63 82 'pull_request_url': 'http://localhost/pr1',
64 83 }
65 84
66 85 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
67 86 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
68 87
69 88 # subject
70 89 assert subject == 'Marcin Kuzminski wants you to review pull request #200: "Example Pull Request"'
71 90
72 91
73 92 @pytest.mark.parametrize('mention', [
74 93 True,
75 94 False
76 95 ])
77 96 @pytest.mark.parametrize('email_type', [
78 97 EmailNotificationModel.TYPE_COMMIT_COMMENT,
79 98 EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
80 99 ])
81 100 def test_render_comment_subject_no_newlines(app, mention, email_type):
82 101 ref = collections.namedtuple('Ref',
83 102 'name, type')(
84 103 'fxies123', 'book'
85 104 )
86 105
87 106 pr = collections.namedtuple('PullRequest',
88 107 'pull_request_id, title, description, source_ref_parts, source_ref_name, target_ref_parts, target_ref_name')(
89 108 200, 'Example Pull Request', 'Desc of PR', ref, 'bookmark', ref, 'Branch')
90 109
91 110 source_repo = target_repo = collections.namedtuple('Repo',
92 111 'type, repo_name')(
93 112 'hg', 'pull_request_1')
94 113
95 114 kwargs = {
96 115 'user': '<marcin@rhodecode.com> Marcin Kuzminski',
97 116 'commit': AttributeDict(raw_id='a'*40, message='Commit message'),
98 117 'status_change': 'approved',
99 118 'commit_target_repo': AttributeDict(),
100 119 'repo_name': 'test-repo',
101 120 'comment_file': 'test-file.py',
102 121 'comment_line': 'n100',
103 122 'comment_type': 'note',
104 123 'commit_comment_url': 'http://comment-url',
105 124 'instance_url': 'http://rc-instance',
106 125 'comment_body': 'hello world',
107 126 'mention': mention,
108 127
109 128 'pr_comment_url': 'http://comment-url',
110 129 'pr_source_repo': AttributeDict(repo_name='foobar'),
111 130 'pr_source_repo_url': 'http://soirce-repo/url',
112 131 'pull_request': pr,
113 132 'pull_request_commits': [],
114 133
115 134 'pull_request_target_repo': target_repo,
116 135 'pull_request_target_repo_url': 'x',
117 136
118 137 'pull_request_source_repo': source_repo,
119 138 'pull_request_source_repo_url': 'x',
120 139 }
121 140 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
122 141 email_type, **kwargs)
123 142
124 143 assert '\n' not in subject
General Comments 0
You need to be logged in to leave comments. Login now