Skip to content

Commit 02abec0

Browse files
ref(control_silo): Move UserEmail model to users module (#76134)
Part of moving control silo user related resources into the users module. Includes adding any missing types for the model. Apart of (#73856)
1 parent 0dda960 commit 02abec0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+214
-209
lines changed

src/sentry/api/endpoints/oauth_userinfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sentry.api.base import Endpoint, control_silo_endpoint
99
from sentry.api.exceptions import ParameterValidationError, ResourceDoesNotExist, SentryAPIException
1010
from sentry.models.apitoken import ApiToken
11-
from sentry.models.useremail import UserEmail
11+
from sentry.users.models.useremail import UserEmail
1212

1313

1414
class InsufficientScopesError(SentryAPIException):

src/sentry/api/endpoints/organization_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
from sentry.models.options.organization_option import OrganizationOption
7878
from sentry.models.organization import Organization, OrganizationStatus
7979
from sentry.models.scheduledeletion import RegionScheduledDeletion
80-
from sentry.models.useremail import UserEmail
8180
from sentry.organizations.services.organization import organization_service
8281
from sentry.organizations.services.organization.model import (
8382
RpcOrganization,
@@ -88,6 +87,7 @@
8887
OrganizationSlugCollisionException,
8988
organization_provisioning_service,
9089
)
90+
from sentry.users.models.useremail import UserEmail
9191
from sentry.users.services.user.serial import serialize_generic_user
9292
from sentry.utils.audit import create_audit_entry
9393

src/sentry/api/endpoints/user_details.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
from sentry.models.organization import OrganizationStatus
2626
from sentry.models.organizationmapping import OrganizationMapping
2727
from sentry.models.organizationmembermapping import OrganizationMemberMapping
28-
from sentry.models.useremail import UserEmail
2928
from sentry.organizations.services.organization import organization_service
3029
from sentry.organizations.services.organization.model import RpcOrganizationDeleteState
3130
from sentry.users.models.user import User
31+
from sentry.users.models.useremail import UserEmail
3232
from sentry.users.services.user.serial import serialize_generic_user
3333
from sentry.utils.dates import AVAILABLE_TIMEZONES
3434

src/sentry/api/endpoints/user_emails.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from sentry.api.serializers import serialize
1212
from sentry.api.validators import AllowedEmailField
1313
from sentry.models.options.user_option import UserOption
14-
from sentry.models.useremail import UserEmail
1514
from sentry.users.models.user import User
15+
from sentry.users.models.useremail import UserEmail
1616

1717
logger = logging.getLogger("sentry.accounts")
1818

src/sentry/api/endpoints/user_emails_confirm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from sentry.api.base import control_silo_endpoint
99
from sentry.api.bases.user import UserEndpoint
1010
from sentry.api.validators import AllowedEmailField
11-
from sentry.models.useremail import UserEmail
1211
from sentry.types.ratelimit import RateLimit, RateLimitCategory
12+
from sentry.users.models.useremail import UserEmail
1313

1414
logger = logging.getLogger("sentry.accounts")
1515

src/sentry/api/endpoints/user_notification_email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from sentry.api.base import control_silo_endpoint
99
from sentry.api.bases.user import UserEndpoint
1010
from sentry.models.options.user_option import UserOption
11-
from sentry.models.useremail import UserEmail
11+
from sentry.users.models.useremail import UserEmail
1212

1313
INVALID_EMAIL_MSG = (
1414
"Invalid email value(s) provided. Email values must be verified emails for the given user."

src/sentry/api/endpoints/user_subscriptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from sentry.api.api_publish_status import ApiPublishStatus
88
from sentry.api.base import control_silo_endpoint
99
from sentry.api.bases.user import UserEndpoint
10-
from sentry.models.useremail import UserEmail
1110
from sentry.users.models.user import User
11+
from sentry.users.models.useremail import UserEmail
1212

1313

1414
class DefaultNewsletterValidator(serializers.Serializer):

src/sentry/api/invite_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@
99
from sentry import audit_log, features
1010
from sentry.models.authidentity import AuthIdentity
1111
from sentry.models.authprovider import AuthProvider
12-
from sentry.models.useremail import UserEmail
1312
from sentry.organizations.services.organization import (
1413
RpcOrganizationMember,
1514
RpcUserInviteContext,
1615
organization_service,
1716
)
1817
from sentry.signals import member_joined
1918
from sentry.users.models.user import User
19+
from sentry.users.models.useremail import UserEmail
2020
from sentry.utils import metrics
2121
from sentry.utils.audit import create_audit_entry
2222

src/sentry/api/serializers/models/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323
from sentry.models.organization import OrganizationStatus
2424
from sentry.models.organizationmapping import OrganizationMapping
2525
from sentry.models.organizationmembermapping import OrganizationMemberMapping
26-
from sentry.models.useremail import UserEmail
2726
from sentry.models.userpermission import UserPermission
2827
from sentry.organizations.services.organization import RpcOrganizationSummary
2928
from sentry.users.models.authenticator import Authenticator
3029
from sentry.users.models.user import User
30+
from sentry.users.models.useremail import UserEmail
3131
from sentry.users.models.userrole import UserRoleUser
3232
from sentry.users.services.user import RpcUser
3333
from sentry.utils.avatar import get_gravatar_url

src/sentry/api/serializers/models/useremail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from sentry.api.serializers import Serializer, register
2-
from sentry.models.useremail import UserEmail
2+
from sentry.users.models.useremail import UserEmail
33

44

55
@register(UserEmail)

src/sentry/auth/email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
from dataclasses import dataclass
66

77
from sentry.models.organization import Organization
8-
from sentry.models.useremail import UserEmail
98
from sentry.organizations.services.organization import organization_service
109
from sentry.users.models.user import User
10+
from sentry.users.models.useremail import UserEmail
1111
from sentry.utils import metrics
1212

1313

src/sentry/integrations/slack/tasks/link_slack_user_identities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from sentry.integrations.slack.utils.users import SlackUserData, get_slack_data_by_user
1010
from sentry.integrations.utils import get_identities_by_user
1111
from sentry.models.identity import Identity, IdentityProvider, IdentityStatus
12-
from sentry.models.useremail import UserEmail
1312
from sentry.organizations.services.organization import organization_service
1413
from sentry.silo.base import SiloMode
1514
from sentry.tasks.base import instrumented_task
1615
from sentry.users.models.user import User
16+
from sentry.users.models.useremail import UserEmail
1717

1818
logger = logging.getLogger("sentry.integrations.slack.tasks")
1919

src/sentry/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from sentry.users.models.email import * # NOQA
33
from sentry.users.models.lostpasswordhash import * # NOQA
44
from sentry.users.models.user import * # NOQA
5+
from sentry.users.models.useremail import * # NOQA
56
from sentry.users.models.userrole import * # NOQA
67

78
from .activity import * # NOQA
@@ -121,7 +122,6 @@
121122
from .teamreplica import * # NOQA
122123
from .tombstone import * # NOQA
123124
from .transaction_threshold import * # NOQA
124-
from .useremail import * # NOQA
125125
from .userip import * # NOQA
126126
from .userpermission import * # NOQA
127127
from .userreport import * # NOQA

src/sentry/models/useremail.py

Lines changed: 2 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,164 +1,3 @@
1-
from __future__ import annotations
1+
from sentry.users.models.useremail import UserEmail
22

3-
from collections import defaultdict
4-
from collections.abc import Iterable, Mapping
5-
from datetime import timedelta
6-
from typing import TYPE_CHECKING, Any, ClassVar
7-
8-
from django.conf import settings
9-
from django.db import models
10-
from django.utils import timezone
11-
from django.utils.translation import gettext_lazy as _
12-
13-
from sentry.backup.dependencies import (
14-
ImportKind,
15-
NormalizedModelName,
16-
PrimaryKeyMap,
17-
get_model_name,
18-
)
19-
from sentry.backup.helpers import ImportFlags
20-
from sentry.backup.sanitize import SanitizableField, Sanitizer
21-
from sentry.backup.scopes import ImportScope, RelocationScope
22-
from sentry.db.models import FlexibleForeignKey, control_silo_model, sane_repr
23-
from sentry.db.models.manager.base import BaseManager
24-
from sentry.hybridcloud.models.outbox import ControlOutboxBase
25-
from sentry.hybridcloud.outbox.base import ControlOutboxProducingModel
26-
from sentry.hybridcloud.outbox.category import OutboxCategory
27-
from sentry.organizations.services.organization.model import RpcOrganization
28-
from sentry.types.region import find_regions_for_user
29-
from sentry.users.services.user.model import RpcUser
30-
from sentry.utils.security import get_secure_token
31-
32-
if TYPE_CHECKING:
33-
from sentry.users.models.user import User
34-
35-
36-
class UserEmailManager(BaseManager["UserEmail"]):
37-
def get_emails_by_user(self, organization: RpcOrganization) -> Mapping[User, Iterable[str]]:
38-
from sentry.models.organizationmembermapping import OrganizationMemberMapping
39-
40-
emails_by_user = defaultdict(set)
41-
user_emails = self.filter(
42-
user_id__in=OrganizationMemberMapping.objects.filter(
43-
organization_id=organization.id
44-
).values_list("user_id", flat=True)
45-
).select_related("user")
46-
for entry in user_emails:
47-
emails_by_user[entry.user].add(entry.email)
48-
return emails_by_user
49-
50-
def get_primary_email(self, user: RpcUser | User) -> UserEmail:
51-
user_email, _ = self.get_or_create(user_id=user.id, email=user.email)
52-
return user_email
53-
54-
55-
@control_silo_model
56-
class UserEmail(ControlOutboxProducingModel):
57-
__relocation_scope__ = RelocationScope.User
58-
__relocation_dependencies__ = {"sentry.Email"}
59-
__relocation_custom_ordinal__ = ["user", "email"]
60-
61-
user = FlexibleForeignKey(settings.AUTH_USER_MODEL, related_name="emails")
62-
email = models.EmailField(_("email address"), max_length=75)
63-
validation_hash = models.CharField(max_length=32, default=get_secure_token)
64-
date_hash_added = models.DateTimeField(default=timezone.now)
65-
is_verified = models.BooleanField(
66-
_("verified"),
67-
default=False,
68-
help_text=_("Designates whether this user has confirmed their email."),
69-
)
70-
71-
objects: ClassVar[UserEmailManager] = UserEmailManager()
72-
73-
class Meta:
74-
app_label = "sentry"
75-
db_table = "sentry_useremail"
76-
unique_together = (("user", "email"),)
77-
78-
__repr__ = sane_repr("user_id", "email")
79-
80-
def outboxes_for_update(self, shard_identifier: int | None = None) -> list[ControlOutboxBase]:
81-
regions = find_regions_for_user(self.user_id)
82-
return [
83-
outbox
84-
for outbox in OutboxCategory.USER_UPDATE.as_control_outboxes(
85-
region_names=regions,
86-
shard_identifier=self.user_id,
87-
object_identifier=self.user_id,
88-
)
89-
]
90-
91-
def set_hash(self):
92-
self.date_hash_added = timezone.now()
93-
self.validation_hash = get_secure_token()
94-
95-
def hash_is_valid(self):
96-
return self.validation_hash and self.date_hash_added > timezone.now() - timedelta(hours=48)
97-
98-
def is_primary(self):
99-
return self.user.email == self.email
100-
101-
@classmethod
102-
def get_primary_email(cls, user: User) -> UserEmail:
103-
"""@deprecated"""
104-
return cls.objects.get_primary_email(user)
105-
106-
def normalize_before_relocation_import(
107-
self, pk_map: PrimaryKeyMap, scope: ImportScope, flags: ImportFlags
108-
) -> int | None:
109-
from sentry.users.models.user import User
110-
111-
old_user_id = self.user_id
112-
old_pk = super().normalize_before_relocation_import(pk_map, scope, flags)
113-
if old_pk is None:
114-
return None
115-
116-
# If we are merging users, ignore the imported email and use the existing user's email
117-
# instead.
118-
if pk_map.get_kind(get_model_name(User), old_user_id) == ImportKind.Existing:
119-
return None
120-
121-
# Only preserve validation hashes in the backup/restore scope - in all others, have the user
122-
# verify their email again.
123-
if scope != ImportScope.Global:
124-
self.is_verified = False
125-
self.validation_hash = get_secure_token()
126-
self.date_hash_added = timezone.now()
127-
128-
return old_pk
129-
130-
def write_relocation_import(
131-
self, _s: ImportScope, _f: ImportFlags
132-
) -> tuple[int, ImportKind] | None:
133-
# The `UserEmail` was automatically generated `post_save()`, but only if it was the user's
134-
# primary email. We just need to update it with the data being imported. Note that if we've
135-
# reached this point, we cannot be merging into an existing user, and are instead modifying
136-
# the just-created `UserEmail` for a new one.
137-
try:
138-
useremail = self.__class__.objects.get(user=self.user, email=self.email)
139-
for f in self._meta.fields:
140-
if f.name not in ["id", "pk"]:
141-
setattr(useremail, f.name, getattr(self, f.name))
142-
except self.__class__.DoesNotExist:
143-
# This is a non-primary email, so was not auto-created - go ahead and add it in.
144-
useremail = self
145-
146-
useremail.save()
147-
148-
# If we've entered this method at all, we can be sure that the `UserEmail` was created as
149-
# part of the import, since this is a new `User` (the "existing" `User` due to
150-
# `--merge_users=true` case is handled in the `normalize_before_relocation_import()` method
151-
# above).
152-
return (useremail.pk, ImportKind.Inserted)
153-
154-
@classmethod
155-
def sanitize_relocation_json(
156-
cls, json: Any, sanitizer: Sanitizer, model_name: NormalizedModelName | None = None
157-
) -> None:
158-
model_name = get_model_name(cls) if model_name is None else model_name
159-
super().sanitize_relocation_json(json, sanitizer, model_name)
160-
161-
validation_hash = get_secure_token()
162-
sanitizer.set_string(
163-
json, SanitizableField(model_name, "validation_hash"), lambda _: validation_hash
164-
)
3+
__all__ = ("UserEmail",)

src/sentry/newsletter/dummy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(
2626
unsubscribed_date=None,
2727
**kwargs,
2828
):
29-
from sentry.models.useremail import UserEmail
29+
from sentry.users.models.useremail import UserEmail
3030

3131
self.email = user.email or email
3232
self.list_id = list_id

src/sentry/receivers/email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from django.db import IntegrityError, router, transaction
22
from django.db.models.signals import post_delete, post_save
33

4-
from sentry.models.useremail import UserEmail
54
from sentry.users.models.email import Email
5+
from sentry.users.models.useremail import UserEmail
66

77

88
def create_email(instance, created, **kwargs):

src/sentry/receivers/useremail.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from django.db import IntegrityError
22
from django.db.models.signals import post_save
33

4-
from sentry.models.useremail import UserEmail
54
from sentry.users.models.user import User
5+
from sentry.users.models.useremail import UserEmail
66

77

88
def create_user_email(instance, created, **kwargs):

src/sentry/tasks/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
from sentry.auth.exceptions import ProviderNotRegistered
1313
from sentry.models.organization import Organization
1414
from sentry.models.organizationmember import OrganizationMember
15-
from sentry.models.useremail import UserEmail
1615
from sentry.organizations.services.organization.service import organization_service
1716
from sentry.silo.base import SiloMode
1817
from sentry.silo.safety import unguarded_write
1918
from sentry.tasks.base import instrumented_task, retry
2019
from sentry.types.region import RegionMappingNotFound
2120
from sentry.users.models.user import User
21+
from sentry.users.models.useremail import UserEmail
2222
from sentry.users.services.user import RpcUser
2323
from sentry.users.services.user.service import user_service
2424
from sentry.utils.audit import create_audit_entry_from_user

src/sentry/testutils/cases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@
111111
from sentry.models.releasecommit import ReleaseCommit
112112
from sentry.models.repository import Repository
113113
from sentry.models.rule import RuleSource
114-
from sentry.models.useremail import UserEmail
115114
from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorType, ScheduleType
116115
from sentry.notifications.notifications.base import alert_page_needs_org_id
117116
from sentry.notifications.types import FineTuningAPIKey
@@ -146,6 +145,7 @@
146145
from sentry.testutils.pytest.selenium import Browser
147146
from sentry.types.condition_activity import ConditionActivity, ConditionActivityType
148147
from sentry.users.models.user import User
148+
from sentry.users.models.useremail import UserEmail
149149
from sentry.utils import json
150150
from sentry.utils.auth import SsoSession
151151
from sentry.utils.json import dumps_htmlsafe

src/sentry/testutils/factories.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,6 @@
138138
from sentry.models.savedsearch import SavedSearch
139139
from sentry.models.servicehook import ServiceHook
140140
from sentry.models.team import Team
141-
from sentry.models.useremail import UserEmail
142141
from sentry.models.userpermission import UserPermission
143142
from sentry.models.userreport import UserReport
144143
from sentry.organizations.services.organization import RpcOrganization, RpcUserOrganizationContext
@@ -167,6 +166,7 @@
167166
UptimeSubscription,
168167
)
169168
from sentry.users.models.user import User
169+
from sentry.users.models.useremail import UserEmail
170170
from sentry.users.models.userrole import UserRole
171171
from sentry.users.services.user import RpcUser
172172
from sentry.utils import loremipsum

0 commit comments

Comments
 (0)