|
3 | 3 | from collections.abc import Mapping
|
4 | 4 | from typing import Any
|
5 | 5 |
|
6 |
| -from django.db import IntegrityError, models, router, transaction |
| 6 | +from django.db import models, router, transaction |
7 | 7 | from django.db.models.expressions import CombinedExpression, F
|
8 | 8 | from django.dispatch import Signal
|
9 | 9 |
|
10 | 10 | from sentry import roles
|
11 | 11 | from sentry.api.serializers import serialize
|
| 12 | +from sentry.backup.dependencies import merge_users_for_model_in_org |
12 | 13 | from sentry.db.postgres.transactions import enforce_constraints
|
| 14 | +from sentry.incidents.models.alert_rule import AlertRule, AlertRuleActivity |
| 15 | +from sentry.incidents.models.incident import IncidentActivity, IncidentSubscription |
13 | 16 | from sentry.models.activity import Activity
|
| 17 | +from sentry.models.dashboard import Dashboard |
| 18 | +from sentry.models.dynamicsampling import CustomDynamicSamplingRule |
14 | 19 | from sentry.models.groupassignee import GroupAssignee
|
15 | 20 | from sentry.models.groupbookmark import GroupBookmark
|
| 21 | +from sentry.models.groupsearchview import GroupSearchView |
16 | 22 | from sentry.models.groupseen import GroupSeen
|
17 | 23 | from sentry.models.groupshare import GroupShare
|
18 | 24 | from sentry.models.groupsubscription import GroupSubscription
|
19 | 25 | from sentry.models.organization import Organization, OrganizationStatus
|
| 26 | +from sentry.models.organizationaccessrequest import OrganizationAccessRequest |
20 | 27 | from sentry.models.organizationmapping import OrganizationMapping
|
21 | 28 | from sentry.models.organizationmember import InviteStatus, OrganizationMember
|
22 | 29 | from sentry.models.organizationmemberteam import OrganizationMemberTeam
|
23 | 30 | from sentry.models.outbox import ControlOutbox, OutboxCategory, OutboxScope, outbox_context
|
| 31 | +from sentry.models.projectbookmark import ProjectBookmark |
| 32 | +from sentry.models.recentsearch import RecentSearch |
| 33 | +from sentry.models.rule import Rule, RuleActivity |
| 34 | +from sentry.models.rulesnooze import RuleSnooze |
| 35 | +from sentry.models.savedsearch import SavedSearch |
24 | 36 | from sentry.models.scheduledeletion import RegionScheduledDeletion
|
25 | 37 | from sentry.models.team import Team, TeamStatus
|
| 38 | +from sentry.monitors.models import Monitor |
26 | 39 | from sentry.services.hybrid_cloud import OptionValue, logger
|
27 | 40 | from sentry.services.hybrid_cloud.app import app_service
|
28 | 41 | from sentry.services.hybrid_cloud.organization import (
|
@@ -515,33 +528,75 @@ def merge_users(self, *, organization_id: int, from_user_id: int, to_user_id: in
|
515 | 528 |
|
516 | 529 | assert to_member
|
517 | 530 |
|
518 |
| - for team in from_member.teams.all(): |
519 |
| - try: |
520 |
| - with enforce_constraints( |
521 |
| - transaction.atomic(router.db_for_write(OrganizationMemberTeam)) |
522 |
| - ): |
| 531 | + with enforce_constraints(transaction.atomic(using=router.db_for_write(OrganizationMember))): |
| 532 | + # Delete all org access requests between the two now-merged users. |
| 533 | + OrganizationAccessRequest.objects.filter( |
| 534 | + member=from_member, requester_id=to_user_id |
| 535 | + ).delete() |
| 536 | + OrganizationAccessRequest.objects.filter( |
| 537 | + member=to_member, requester_id=from_user_id |
| 538 | + ).delete() |
| 539 | + |
| 540 | + # All other org access requests should be pointed from the old member to the new |
| 541 | + # one. |
| 542 | + reqs = OrganizationAccessRequest.objects.filter(member=from_member) |
| 543 | + for req in reqs: |
| 544 | + req.member = to_member |
| 545 | + req.save() |
| 546 | + |
| 547 | + # Move all old team memberships to the newly merged `OrganizationMember`. |
| 548 | + for team in from_member.teams.all(): |
| 549 | + OrganizationMemberTeam.objects.filter( |
| 550 | + organizationmember=from_member, team=team |
| 551 | + ).delete() |
| 552 | + to_member_team = OrganizationMemberTeam.objects.filter( |
| 553 | + organizationmember=to_member, team=team |
| 554 | + ).first() |
| 555 | + if to_member_team is None: |
523 | 556 | OrganizationMemberTeam.objects.create(organizationmember=to_member, team=team)
|
524 |
| - except IntegrityError: |
525 |
| - pass |
526 |
| - |
527 |
| - model_list = ( |
528 |
| - GroupAssignee, |
529 |
| - GroupBookmark, |
530 |
| - GroupSeen, |
531 |
| - GroupShare, |
532 |
| - GroupSubscription, |
533 |
| - Activity, |
534 |
| - ) |
535 | 557 |
|
536 |
| - for model in model_list: |
537 |
| - for obj in model.objects.filter( |
538 |
| - user_id=from_user_id, project__organization_id=organization_id |
539 |
| - ): |
540 |
| - try: |
541 |
| - with enforce_constraints(transaction.atomic(router.db_for_write(model))): |
542 |
| - obj.update(user_id=to_user_id) |
543 |
| - except IntegrityError: |
544 |
| - pass |
| 558 | + # Update all organization region models to only use the new user id. |
| 559 | + model_list = [ |
| 560 | + Activity, |
| 561 | + AlertRule, |
| 562 | + AlertRuleActivity, |
| 563 | + CustomDynamicSamplingRule, |
| 564 | + Dashboard, |
| 565 | + GroupAssignee, |
| 566 | + GroupBookmark, |
| 567 | + GroupSeen, |
| 568 | + GroupShare, |
| 569 | + GroupSearchView, |
| 570 | + GroupSubscription, |
| 571 | + IncidentActivity, |
| 572 | + IncidentSubscription, |
| 573 | + OrganizationAccessRequest, |
| 574 | + ProjectBookmark, |
| 575 | + RecentSearch, |
| 576 | + Rule, |
| 577 | + RuleActivity, |
| 578 | + RuleSnooze, |
| 579 | + SavedSearch, |
| 580 | + ] |
| 581 | + for model in model_list: |
| 582 | + merge_users_for_model_in_org( |
| 583 | + model, |
| 584 | + organization_id=organization_id, |
| 585 | + from_user_id=from_user_id, |
| 586 | + to_user_id=to_user_id, |
| 587 | + ) |
| 588 | + |
| 589 | + # Finally, delete the old member. |
| 590 | + from_member.delete() |
| 591 | + |
| 592 | + # TODO: for some reason, `Monitor` insists on being updated outside of the transaction, even |
| 593 | + # though it's also not region siloed? |
| 594 | + merge_users_for_model_in_org( |
| 595 | + Monitor, |
| 596 | + organization_id=organization_id, |
| 597 | + from_user_id=from_user_id, |
| 598 | + to_user_id=to_user_id, |
| 599 | + ) |
545 | 600 |
|
546 | 601 | def reset_idp_flags(self, *, organization_id: int) -> None:
|
547 | 602 | with unguarded_write(using=router.db_for_write(OrganizationMember)):
|
|
0 commit comments