Show More
@@ -39,7 +39,8 b' from rhodecode.model.db import true, Use' | |||
|
39 | 39 | from rhodecode.lib import audit_logger, rc_cache |
|
40 | 40 | from rhodecode.lib.exceptions import ( |
|
41 | 41 | UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException, |
|
42 |
UserOwnsUserGroupsException, |
|
|
42 | UserOwnsUserGroupsException, UserOwnsPullRequestsException, | |
|
43 | UserOwnsArtifactsException, DefaultUserException) | |
|
43 | 44 | from rhodecode.lib.ext_json import json |
|
44 | 45 | from rhodecode.lib.auth import ( |
|
45 | 46 | LoginRequired, HasPermissionAllDecorator, CSRFRequired) |
@@ -377,11 +378,13 b' class UsersView(UserAppView):' | |||
|
377 | 378 | _repos = c.user.repositories |
|
378 | 379 | _repo_groups = c.user.repository_groups |
|
379 | 380 | _user_groups = c.user.user_groups |
|
381 | _pull_requests = c.user.user_pull_requests | |
|
380 | 382 | _artifacts = c.user.artifacts |
|
381 | 383 | |
|
382 | 384 | handle_repos = None |
|
383 | 385 | handle_repo_groups = None |
|
384 | 386 | handle_user_groups = None |
|
387 | handle_pull_requests = None | |
|
385 | 388 | handle_artifacts = None |
|
386 | 389 | |
|
387 | 390 | # calls for flash of handle based on handle case detach or delete |
@@ -412,6 +415,15 b' class UsersView(UserAppView):' | |||
|
412 | 415 | h.flash(_('Deleted %s user groups') % len(_user_groups), |
|
413 | 416 | category='success') |
|
414 | 417 | |
|
418 | def set_handle_flash_pull_requests(): | |
|
419 | handle = handle_pull_requests | |
|
420 | if handle == 'detach': | |
|
421 | h.flash(_('Detached %s pull requests') % len(_pull_requests), | |
|
422 | category='success') | |
|
423 | elif handle == 'delete': | |
|
424 | h.flash(_('Deleted %s pull requests') % len(_pull_requests), | |
|
425 | category='success') | |
|
426 | ||
|
415 | 427 | def set_handle_flash_artifacts(): |
|
416 | 428 | handle = handle_artifacts |
|
417 | 429 | if handle == 'detach': |
@@ -421,6 +433,12 b' class UsersView(UserAppView):' | |||
|
421 | 433 | h.flash(_('Deleted %s artifacts') % len(_artifacts), |
|
422 | 434 | category='success') |
|
423 | 435 | |
|
436 | handle_user = User.get_first_super_admin() | |
|
437 | handle_user_id = safe_int(self.request.POST.get('detach_user_id')) | |
|
438 | if handle_user_id: | |
|
439 | # NOTE(marcink): we get new owner for objects... | |
|
440 | handle_user = User.get_or_404(handle_user_id) | |
|
441 | ||
|
424 | 442 | if _repos and self.request.POST.get('user_repos'): |
|
425 | 443 | handle_repos = self.request.POST['user_repos'] |
|
426 | 444 | |
@@ -430,16 +448,25 b' class UsersView(UserAppView):' | |||
|
430 | 448 | if _user_groups and self.request.POST.get('user_user_groups'): |
|
431 | 449 | handle_user_groups = self.request.POST['user_user_groups'] |
|
432 | 450 | |
|
451 | if _pull_requests and self.request.POST.get('user_pull_requests'): | |
|
452 | handle_pull_requests = self.request.POST['user_pull_requests'] | |
|
453 | ||
|
433 | 454 | if _artifacts and self.request.POST.get('user_artifacts'): |
|
434 | 455 | handle_artifacts = self.request.POST['user_artifacts'] |
|
435 | 456 | |
|
436 | 457 | old_values = c.user.get_api_data() |
|
437 | 458 | |
|
438 | 459 | try: |
|
439 | UserModel().delete(c.user, handle_repos=handle_repos, | |
|
440 | handle_repo_groups=handle_repo_groups, | |
|
441 | handle_user_groups=handle_user_groups, | |
|
442 |
|
|
|
460 | ||
|
461 | UserModel().delete( | |
|
462 | c.user, | |
|
463 | handle_repos=handle_repos, | |
|
464 | handle_repo_groups=handle_repo_groups, | |
|
465 | handle_user_groups=handle_user_groups, | |
|
466 | handle_pull_requests=handle_pull_requests, | |
|
467 | handle_artifacts=handle_artifacts, | |
|
468 | handle_new_owner=handle_user | |
|
469 | ) | |
|
443 | 470 | |
|
444 | 471 | audit_logger.store_web( |
|
445 | 472 | 'user.delete', action_data={'old_data': old_values}, |
@@ -449,11 +476,13 b' class UsersView(UserAppView):' | |||
|
449 | 476 | set_handle_flash_repos() |
|
450 | 477 | set_handle_flash_repo_groups() |
|
451 | 478 | set_handle_flash_user_groups() |
|
479 | set_handle_flash_pull_requests() | |
|
452 | 480 | set_handle_flash_artifacts() |
|
453 | 481 | username = h.escape(old_values['username']) |
|
454 | 482 | h.flash(_('Successfully deleted user `{}`').format(username), category='success') |
|
455 | 483 | except (UserOwnsReposException, UserOwnsRepoGroupsException, |
|
456 |
UserOwnsUserGroupsException, |
|
|
484 | UserOwnsUserGroupsException, UserOwnsPullRequestsException, | |
|
485 | UserOwnsArtifactsException, DefaultUserException) as e: | |
|
457 | 486 | h.flash(e, category='warning') |
|
458 | 487 | except Exception: |
|
459 | 488 | log.exception("Exception during deletion of user") |
@@ -502,6 +531,11 b' class UsersView(UserAppView):' | |||
|
502 | 531 | user_id = self.db_user_id |
|
503 | 532 | c.user = self.db_user |
|
504 | 533 | |
|
534 | c.detach_user = User.get_first_super_admin() | |
|
535 | detach_user_id = safe_int(self.request.GET.get('detach_user_id')) | |
|
536 | if detach_user_id: | |
|
537 | c.detach_user = User.get_or_404(detach_user_id) | |
|
538 | ||
|
505 | 539 | c.active = 'advanced' |
|
506 | 540 | c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id) |
|
507 | 541 | c.personal_repo_group_name = RepoGroupModel()\ |
@@ -511,7 +545,6 b' class UsersView(UserAppView):' | |||
|
511 | 545 | (x.user for x in c.user.user_review_rules), |
|
512 | 546 | key=lambda u: u.username.lower()) |
|
513 | 547 | |
|
514 | c.first_admin = User.get_first_super_admin() | |
|
515 | 548 | defaults = c.user.get_dict() |
|
516 | 549 | |
|
517 | 550 | # Interim workaround if the user participated on any pull requests as a |
@@ -58,6 +58,10 b' class UserOwnsUserGroupsException(Except' | |||
|
58 | 58 | pass |
|
59 | 59 | |
|
60 | 60 | |
|
61 | class UserOwnsPullRequestsException(Exception): | |
|
62 | pass | |
|
63 | ||
|
64 | ||
|
61 | 65 | class UserOwnsArtifactsException(Exception): |
|
62 | 66 | pass |
|
63 | 67 |
@@ -617,6 +617,7 b' class User(Base, BaseModel):' | |||
|
617 | 617 | user_gists = relationship('Gist', cascade='all') |
|
618 | 618 | # user pull requests |
|
619 | 619 | user_pull_requests = relationship('PullRequest', cascade='all') |
|
620 | ||
|
620 | 621 | # external identities |
|
621 | 622 | external_identities = relationship( |
|
622 | 623 | 'ExternalIdentity', |
@@ -43,7 +43,9 b' from rhodecode.lib.compat import Ordered' | |||
|
43 | 43 | from rhodecode.lib.hooks_daemon import prepare_callback_daemon |
|
44 | 44 | from rhodecode.lib.markup_renderer import ( |
|
45 | 45 | DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer) |
|
46 | from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe, AttributeDict, safe_int | |
|
46 | from rhodecode.lib.utils2 import ( | |
|
47 | safe_unicode, safe_str, md5_safe, AttributeDict, safe_int, | |
|
48 | get_current_rhodecode_user) | |
|
47 | 49 | from rhodecode.lib.vcs.backends.base import ( |
|
48 | 50 | Reference, MergeResponse, MergeFailureReason, UpdateFailureReason, |
|
49 | 51 | TargetRefMissing, SourceRefMissing) |
@@ -1427,7 +1429,10 b' class PullRequestModel(BaseModel):' | |||
|
1427 | 1429 | email_kwargs=email_kwargs, |
|
1428 | 1430 | ) |
|
1429 | 1431 | |
|
1430 | def delete(self, pull_request, user): | |
|
1432 | def delete(self, pull_request, user=None): | |
|
1433 | if not user: | |
|
1434 | user = getattr(get_current_rhodecode_user(), 'username', None) | |
|
1435 | ||
|
1431 | 1436 | pull_request = self.__get_pull_request(pull_request) |
|
1432 | 1437 | old_data = pull_request.get_api_data(with_merge_state=False) |
|
1433 | 1438 | self._cleanup_merge_workspace(pull_request) |
@@ -37,17 +37,17 b' from rhodecode.lib.utils2 import (' | |||
|
37 | 37 | AttributeDict, str2bool) |
|
38 | 38 | from rhodecode.lib.exceptions import ( |
|
39 | 39 | DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, |
|
40 |
UserOwnsUserGroupsException, NotAllowedToCreateUserError, |
|
|
40 | UserOwnsUserGroupsException, NotAllowedToCreateUserError, | |
|
41 | UserOwnsPullRequestsException, UserOwnsArtifactsException) | |
|
41 | 42 | from rhodecode.lib.caching_query import FromCache |
|
42 | 43 | from rhodecode.model import BaseModel |
|
43 | from rhodecode.model.auth_token import AuthTokenModel | |
|
44 | 44 | from rhodecode.model.db import ( |
|
45 | 45 | _hash_key, true, false, or_, joinedload, User, UserToPerm, |
|
46 | 46 | UserEmailMap, UserIpMap, UserLog) |
|
47 | 47 | from rhodecode.model.meta import Session |
|
48 | from rhodecode.model.auth_token import AuthTokenModel | |
|
48 | 49 | from rhodecode.model.repo_group import RepoGroupModel |
|
49 | 50 | |
|
50 | ||
|
51 | 51 | log = logging.getLogger(__name__) |
|
52 | 52 | |
|
53 | 53 | |
@@ -261,7 +261,7 b' class UserModel(BaseModel):' | |||
|
261 | 261 | cur_user = getattr(get_current_rhodecode_user(), 'username', None) |
|
262 | 262 | |
|
263 | 263 | from rhodecode.lib.auth import ( |
|
264 |
get_crypt_password, check_password |
|
|
264 | get_crypt_password, check_password) | |
|
265 | 265 | from rhodecode.lib.hooks_base import ( |
|
266 | 266 | log_create_user, check_allowed_create_user) |
|
267 | 267 | |
@@ -443,15 +443,16 b' class UserModel(BaseModel):' | |||
|
443 | 443 | log.error(traceback.format_exc()) |
|
444 | 444 | raise |
|
445 | 445 | |
|
446 |
def _handle_user_repos(self, username, repositories, handle_ |
|
|
447 | _superadmin = self.cls.get_first_super_admin() | |
|
446 | def _handle_user_repos(self, username, repositories, handle_user, | |
|
447 | handle_mode=None): | |
|
448 | ||
|
448 | 449 | left_overs = True |
|
449 | 450 | |
|
450 | 451 | from rhodecode.model.repo import RepoModel |
|
451 | 452 | |
|
452 | 453 | if handle_mode == 'detach': |
|
453 | 454 | for obj in repositories: |
|
454 |
obj.user = |
|
|
455 | obj.user = handle_user | |
|
455 | 456 | # set description we know why we super admin now owns |
|
456 | 457 | # additional repositories that were orphaned ! |
|
457 | 458 | obj.description += ' \n::detached repository from deleted user: %s' % (username,) |
@@ -465,16 +466,16 b' class UserModel(BaseModel):' | |||
|
465 | 466 | # if nothing is done we have left overs left |
|
466 | 467 | return left_overs |
|
467 | 468 | |
|
468 | def _handle_user_repo_groups(self, username, repository_groups, | |
|
469 | def _handle_user_repo_groups(self, username, repository_groups, handle_user, | |
|
469 | 470 | handle_mode=None): |
|
470 | _superadmin = self.cls.get_first_super_admin() | |
|
471 | ||
|
471 | 472 | left_overs = True |
|
472 | 473 | |
|
473 | 474 | from rhodecode.model.repo_group import RepoGroupModel |
|
474 | 475 | |
|
475 | 476 | if handle_mode == 'detach': |
|
476 | 477 | for r in repository_groups: |
|
477 |
r.user = |
|
|
478 | r.user = handle_user | |
|
478 | 479 | # set description we know why we super admin now owns |
|
479 | 480 | # additional repositories that were orphaned ! |
|
480 | 481 | r.group_description += ' \n::detached repository group from deleted user: %s' % (username,) |
@@ -489,8 +490,9 b' class UserModel(BaseModel):' | |||
|
489 | 490 | # if nothing is done we have left overs left |
|
490 | 491 | return left_overs |
|
491 | 492 | |
|
492 |
def _handle_user_user_groups(self, username, user_groups, handle_ |
|
|
493 | _superadmin = self.cls.get_first_super_admin() | |
|
493 | def _handle_user_user_groups(self, username, user_groups, handle_user, | |
|
494 | handle_mode=None): | |
|
495 | ||
|
494 | 496 | left_overs = True |
|
495 | 497 | |
|
496 | 498 | from rhodecode.model.user_group import UserGroupModel |
@@ -499,8 +501,8 b' class UserModel(BaseModel):' | |||
|
499 | 501 | for r in user_groups: |
|
500 | 502 | for user_user_group_to_perm in r.user_user_group_to_perm: |
|
501 | 503 | if user_user_group_to_perm.user.username == username: |
|
502 |
user_user_group_to_perm.user = |
|
|
503 |
r.user = |
|
|
504 | user_user_group_to_perm.user = handle_user | |
|
505 | r.user = handle_user | |
|
504 | 506 | # set description we know why we super admin now owns |
|
505 | 507 | # additional repositories that were orphaned ! |
|
506 | 508 | r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,) |
@@ -514,13 +516,37 b' class UserModel(BaseModel):' | |||
|
514 | 516 | # if nothing is done we have left overs left |
|
515 | 517 | return left_overs |
|
516 | 518 | |
|
517 |
def _handle_user_ |
|
|
518 | _superadmin = self.cls.get_first_super_admin() | |
|
519 | def _handle_user_pull_requests(self, username, pull_requests, handle_user, | |
|
520 | handle_mode=None): | |
|
521 | left_overs = True | |
|
522 | ||
|
523 | from rhodecode.model.pull_request import PullRequestModel | |
|
524 | ||
|
525 | if handle_mode == 'detach': | |
|
526 | for pr in pull_requests: | |
|
527 | pr.user_id = handle_user.user_id | |
|
528 | # set description we know why we super admin now owns | |
|
529 | # additional repositories that were orphaned ! | |
|
530 | pr.description += ' \n::detached pull requests from deleted user: %s' % (username,) | |
|
531 | self.sa.add(pr) | |
|
532 | left_overs = False | |
|
533 | elif handle_mode == 'delete': | |
|
534 | for pr in pull_requests: | |
|
535 | PullRequestModel().delete(pr) | |
|
536 | ||
|
537 | left_overs = False | |
|
538 | ||
|
539 | # if nothing is done we have left overs left | |
|
540 | return left_overs | |
|
541 | ||
|
542 | def _handle_user_artifacts(self, username, artifacts, handle_user, | |
|
543 | handle_mode=None): | |
|
544 | ||
|
519 | 545 | left_overs = True |
|
520 | 546 | |
|
521 | 547 | if handle_mode == 'detach': |
|
522 | 548 | for a in artifacts: |
|
523 |
a.upload_user = |
|
|
549 | a.upload_user = handle_user | |
|
524 | 550 | # set description we know why we super admin now owns |
|
525 | 551 | # additional artifacts that were orphaned ! |
|
526 | 552 | a.file_description += ' \n::detached artifact from deleted user: %s' % (username,) |
@@ -528,7 +554,8 b' class UserModel(BaseModel):' | |||
|
528 | 554 | left_overs = False |
|
529 | 555 | elif handle_mode == 'delete': |
|
530 | 556 | from rhodecode.apps.file_store import utils as store_utils |
|
531 | storage = store_utils.get_file_storage(self.request.registry.settings) | |
|
557 | request = get_current_request() | |
|
558 | storage = store_utils.get_file_storage(request.registry.settings) | |
|
532 | 559 | for a in artifacts: |
|
533 | 560 | file_uid = a.file_uid |
|
534 | 561 | storage.delete(file_uid) |
@@ -540,11 +567,13 b' class UserModel(BaseModel):' | |||
|
540 | 567 | return left_overs |
|
541 | 568 | |
|
542 | 569 | def delete(self, user, cur_user=None, handle_repos=None, |
|
543 |
handle_repo_groups=None, handle_user_groups=None, |
|
|
570 | handle_repo_groups=None, handle_user_groups=None, | |
|
571 | handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None): | |
|
544 | 572 | from rhodecode.lib.hooks_base import log_delete_user |
|
545 | 573 | |
|
546 | 574 | if not cur_user: |
|
547 | 575 | cur_user = getattr(get_current_rhodecode_user(), 'username', None) |
|
576 | ||
|
548 | 577 | user = self._get_user(user) |
|
549 | 578 | |
|
550 | 579 | try: |
@@ -552,9 +581,11 b' class UserModel(BaseModel):' | |||
|
552 | 581 | raise DefaultUserException( |
|
553 | 582 | u"You can't remove this user since it's" |
|
554 | 583 | u" crucial for entire application") |
|
584 | handle_user = handle_new_owner or self.cls.get_first_super_admin() | |
|
585 | log.debug('New detached objects owner %s', handle_user) | |
|
555 | 586 | |
|
556 | 587 | left_overs = self._handle_user_repos( |
|
557 | user.username, user.repositories, handle_repos) | |
|
588 | user.username, user.repositories, handle_user, handle_repos) | |
|
558 | 589 | if left_overs and user.repositories: |
|
559 | 590 | repos = [x.repo_name for x in user.repositories] |
|
560 | 591 | raise UserOwnsReposException( |
@@ -564,7 +595,7 b' class UserModel(BaseModel):' | |||
|
564 | 595 | 'list_repos': ', '.join(repos)}) |
|
565 | 596 | |
|
566 | 597 | left_overs = self._handle_user_repo_groups( |
|
567 | user.username, user.repository_groups, handle_repo_groups) | |
|
598 | user.username, user.repository_groups, handle_user, handle_repo_groups) | |
|
568 | 599 | if left_overs and user.repository_groups: |
|
569 | 600 | repo_groups = [x.group_name for x in user.repository_groups] |
|
570 | 601 | raise UserOwnsRepoGroupsException( |
@@ -574,7 +605,7 b' class UserModel(BaseModel):' | |||
|
574 | 605 | 'list_repo_groups': ', '.join(repo_groups)}) |
|
575 | 606 | |
|
576 | 607 | left_overs = self._handle_user_user_groups( |
|
577 | user.username, user.user_groups, handle_user_groups) | |
|
608 | user.username, user.user_groups, handle_user, handle_user_groups) | |
|
578 | 609 | if left_overs and user.user_groups: |
|
579 | 610 | user_groups = [x.users_group_name for x in user.user_groups] |
|
580 | 611 | raise UserOwnsUserGroupsException( |
@@ -582,8 +613,17 b' class UserModel(BaseModel):' | |||
|
582 | 613 | u'removed. Switch owners or remove those user groups:%s' |
|
583 | 614 | % (user.username, len(user_groups), ', '.join(user_groups))) |
|
584 | 615 | |
|
616 | left_overs = self._handle_user_pull_requests( | |
|
617 | user.username, user.user_pull_requests, handle_user, handle_pull_requests) | |
|
618 | if left_overs and user.user_pull_requests: | |
|
619 | pull_requests = ['!{}'.format(x.pull_request_id) for x in user.user_pull_requests] | |
|
620 | raise UserOwnsPullRequestsException( | |
|
621 | u'user "%s" still owns %s pull requests and cannot be ' | |
|
622 | u'removed. Switch owners or remove those pull requests:%s' | |
|
623 | % (user.username, len(pull_requests), ', '.join(pull_requests))) | |
|
624 | ||
|
585 | 625 | left_overs = self._handle_user_artifacts( |
|
586 | user.username, user.artifacts, handle_artifacts) | |
|
626 | user.username, user.artifacts, handle_user, handle_artifacts) | |
|
587 | 627 | if left_overs and user.artifacts: |
|
588 | 628 | artifacts = [x.file_uid for x in user.artifacts] |
|
589 | 629 | raise UserOwnsArtifactsException( |
@@ -878,7 +918,7 b' class UserModel(BaseModel):' | |||
|
878 | 918 | end_ip = ipaddress.ip_address(safe_unicode(end_ip.strip())) |
|
879 | 919 | parsed_ip_range = [] |
|
880 | 920 | |
|
881 |
for index in |
|
|
921 | for index in range(int(start_ip), int(end_ip) + 1): | |
|
882 | 922 | new_ip = ipaddress.ip_address(index) |
|
883 | 923 | parsed_ip_range.append(str(new_ip)) |
|
884 | 924 | ip_list.extend(parsed_ip_range) |
@@ -149,6 +149,18 b'' | |||
|
149 | 149 | |
|
150 | 150 | <tr> |
|
151 | 151 | <td> |
|
152 | ${_ungettext('This user owns %s pull request.', 'This user owns %s pull requests.', len(c.user.user_pull_requests)) % len(c.user.user_pull_requests)} | |
|
153 | </td> | |
|
154 | <td> | |
|
155 | <input type="radio" id="user_pull_requests_1" name="user_pull_requests" value="detach" checked="checked" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_1">${_('Detach pull requests')}</label> | |
|
156 | </td> | |
|
157 | <td> | |
|
158 | <input type="radio" id="user_pull_requests_2" name="user_pull_requests" value="delete" ${'disabled=1' if len(c.user.user_pull_requests) == 0 else ''}/> <label for="user_pull_requests_2">${_('Delete pull requests')}</label> | |
|
159 | </td> | |
|
160 | </tr> | |
|
161 | ||
|
162 | <tr> | |
|
163 | <td> | |
|
152 | 164 | ${_ungettext('This user owns %s artifact.', 'This user owns %s artifacts.', len(c.user.artifacts)) % len(c.user.artifacts)} |
|
153 | 165 | </td> |
|
154 | 166 | <td> |
@@ -166,7 +178,8 b'' | |||
|
166 | 178 | % endif |
|
167 | 179 | |
|
168 | 180 | <span style="padding: 0 5px 0 0">${_('New owner for detached objects')}:</span> |
|
169 |
<div class="pull-right">${base.gravatar_with_user(c. |
|
|
181 | <div class="pull-right">${base.gravatar_with_user(c.detach_user.email, 16, tooltip=True)}</div> | |
|
182 | <input type="hidden" name="detach_user_id" value="${c.detach_user.user_id}"> | |
|
170 | 183 | </div> |
|
171 | 184 | <div style="clear: both"> |
|
172 | 185 | |
@@ -186,11 +199,11 b'' | |||
|
186 | 199 | <div style="margin: 0 0 20px 0" class="fake-space"></div> |
|
187 | 200 | |
|
188 | 201 | <div class="field"> |
|
189 |
< |
|
|
190 |
|
|
|
191 |
|
|
|
192 | ${_('Delete this user')} | |
|
193 |
|
|
|
202 | <input class="btn btn-small btn-danger" id="remove_user" name="remove_user" | |
|
203 | onclick="submitConfirm(event, this, _gettext('Confirm to delete this user'), _gettext('Confirm Delete'), '${c.user.username}')" | |
|
204 | ${("disabled=1" if not c.can_delete_user else "")} | |
|
205 | type="submit" value="${_('Delete this user')}" | |
|
206 | > | |
|
194 | 207 | </div> |
|
195 | 208 | |
|
196 | 209 | ${h.end_form()} |
General Comments 0
You need to be logged in to leave comments.
Login now