models: Extract zerver.models.prereg_users.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2023-12-14 18:46:16 -08:00 committed by Tim Abbott
parent 51f1dc257d
commit 927d7a9a60
6 changed files with 186 additions and 177 deletions

View File

@ -24,15 +24,8 @@ from zerver.lib.queue import queue_json_publish
from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_email from zerver.lib.send_email import FromAddress, clear_scheduled_invitation_emails, send_email
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.types import UnspecifiedValue from zerver.lib.types import UnspecifiedValue
from zerver.models import ( from zerver.models import Message, MultiuseInvite, PreregistrationUser, Realm, Stream, UserProfile
Message, from zerver.models.prereg_users import filter_to_valid_prereg_users
MultiuseInvite,
PreregistrationUser,
Realm,
Stream,
UserProfile,
filter_to_valid_prereg_users,
)
from zerver.tornado.django_api import send_event from zerver.tornado.django_api import send_event

View File

@ -12,7 +12,6 @@ from bitfield import BitField
from bitfield.types import Bit, BitHandler from bitfield.types import Bit, BitHandler
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.search import SearchVectorField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -29,7 +28,6 @@ from django.utils.translation import gettext_lazy
from django_stubs_ext import StrPromise, ValuesQuerySet from django_stubs_ext import StrPromise, ValuesQuerySet
from typing_extensions import override from typing_extensions import override
from confirmation import settings as confirmation_settings
from zerver.lib import cache from zerver.lib import cache
from zerver.lib.cache import ( from zerver.lib.cache import (
cache_delete, cache_delete,
@ -54,7 +52,6 @@ from zerver.lib.types import (
ProfileDataElementBase, ProfileDataElementBase,
ProfileDataElementValue, ProfileDataElementValue,
RealmUserValidator, RealmUserValidator,
UnspecifiedValue,
UserFieldElement, UserFieldElement,
Validator, Validator,
) )
@ -67,12 +64,17 @@ from zerver.lib.validator import (
check_url, check_url,
validate_select_field, validate_select_field,
) )
from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH, MAX_TOPIC_NAME_LENGTH from zerver.models.constants import MAX_TOPIC_NAME_LENGTH
from zerver.models.groups import GroupGroupMembership as GroupGroupMembership from zerver.models.groups import GroupGroupMembership as GroupGroupMembership
from zerver.models.groups import SystemGroups from zerver.models.groups import SystemGroups
from zerver.models.groups import UserGroup as UserGroup from zerver.models.groups import UserGroup as UserGroup
from zerver.models.groups import UserGroupMembership as UserGroupMembership from zerver.models.groups import UserGroupMembership as UserGroupMembership
from zerver.models.linkifiers import RealmFilter as RealmFilter from zerver.models.linkifiers import RealmFilter as RealmFilter
from zerver.models.prereg_users import EmailChangeStatus as EmailChangeStatus
from zerver.models.prereg_users import MultiuseInvite as MultiuseInvite
from zerver.models.prereg_users import PreregistrationRealm as PreregistrationRealm
from zerver.models.prereg_users import PreregistrationUser as PreregistrationUser
from zerver.models.prereg_users import RealmReactivationStatus as RealmReactivationStatus
from zerver.models.realm_emoji import RealmEmoji as RealmEmoji from zerver.models.realm_emoji import RealmEmoji as RealmEmoji
from zerver.models.realm_playgrounds import RealmPlayground as RealmPlayground from zerver.models.realm_playgrounds import RealmPlayground as RealmPlayground
from zerver.models.realms import Realm as Realm from zerver.models.realms import Realm as Realm
@ -139,167 +141,6 @@ def query_for_ids(
return query return query
class PreregistrationRealm(models.Model):
"""Data on a partially created realm entered by a user who has
completed the "new organization" form. Used to transfer the user's
selections from the pre-confirmation "new organization" form to
the post-confirmation user registration form.
Note that the values stored here may not match those of the
created realm (in the event the user creates a realm at all),
because we allow the user to edit these values in the registration
form (and in fact the user will be required to do so if the
`string_id` is claimed by another realm before registraiton is
completed).
"""
name = models.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH)
org_type = models.PositiveSmallIntegerField(
default=Realm.ORG_TYPES["unspecified"]["id"],
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
)
default_language = models.CharField(
default="en",
max_length=MAX_LANGUAGE_ID_LENGTH,
)
string_id = models.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH)
email = models.EmailField()
confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_realm")
status = models.IntegerField(default=0)
# The Realm created upon completion of the registration
# for this PregistrationRealm
created_realm = models.ForeignKey(Realm, null=True, related_name="+", on_delete=models.SET_NULL)
# The UserProfile created upon completion of the registration
# for this PregistrationRealm
created_user = models.ForeignKey(
UserProfile, null=True, related_name="+", on_delete=models.SET_NULL
)
class PreregistrationUser(models.Model):
# Data on a partially created user, before the completion of
# registration. This is used in at least three major code paths:
# * Realm creation, in which case realm is None.
#
# * Invitations, in which case referred_by will always be set.
#
# * Social authentication signup, where it's used to store data
# from the authentication step and pass it to the registration
# form.
email = models.EmailField()
confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_user")
# If the pre-registration process provides a suggested full name for this user,
# store it here to use it to prepopulate the full name field in the registration form:
full_name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH, null=True)
full_name_validated = models.BooleanField(default=False)
referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE)
streams = models.ManyToManyField("zerver.Stream")
invited_at = models.DateTimeField(auto_now=True)
realm_creation = models.BooleanField(default=False)
# Indicates whether the user needs a password. Users who were
# created via SSO style auth (e.g. GitHub/Google) generally do not.
password_required = models.BooleanField(default=True)
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
# The realm should only ever be None for PreregistrationUser
# objects created as part of realm creation.
realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE)
# These values should be consistent with the values
# in settings_config.user_role_values.
INVITE_AS = dict(
REALM_OWNER=100,
REALM_ADMIN=200,
MODERATOR=300,
MEMBER=400,
GUEST_USER=600,
)
invited_as = models.PositiveSmallIntegerField(default=INVITE_AS["MEMBER"])
multiuse_invite = models.ForeignKey("MultiuseInvite", null=True, on_delete=models.SET_NULL)
# The UserProfile created upon completion of the registration
# for this PregistrationUser
created_user = models.ForeignKey(
UserProfile, null=True, related_name="+", on_delete=models.SET_NULL
)
class Meta:
indexes = [
models.Index(Upper("email"), name="upper_preregistration_email_idx"),
]
def filter_to_valid_prereg_users(
query: QuerySet[PreregistrationUser],
invite_expires_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
) -> QuerySet[PreregistrationUser]:
"""
If invite_expires_in_days is specified, we return only those PreregistrationUser
objects that were created at most that many days in the past.
"""
used_value = confirmation_settings.STATUS_USED
revoked_value = confirmation_settings.STATUS_REVOKED
query = query.exclude(status__in=[used_value, revoked_value])
if invite_expires_in_minutes is None:
# Since invite_expires_in_minutes is None, we're invitation will never
# expire, we do not need to check anything else and can simply return
# after excluding objects with active and revoked status.
return query
assert invite_expires_in_minutes is not None
if not isinstance(invite_expires_in_minutes, UnspecifiedValue):
lowest_datetime = timezone_now() - timedelta(minutes=invite_expires_in_minutes)
return query.filter(invited_at__gte=lowest_datetime)
else:
return query.filter(
Q(confirmation__expiry_date=None) | Q(confirmation__expiry_date__gte=timezone_now())
)
class MultiuseInvite(models.Model):
referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE)
streams = models.ManyToManyField("zerver.Stream")
realm = models.ForeignKey(Realm, on_delete=CASCADE)
invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"])
# status for tracking whether the invite has been revoked.
# If revoked, set to confirmation.settings.STATUS_REVOKED.
# STATUS_USED is not supported, because these objects are supposed
# to be usable multiple times.
status = models.IntegerField(default=0)
class EmailChangeStatus(models.Model):
new_email = models.EmailField()
old_email = models.EmailField()
updated_at = models.DateTimeField(auto_now=True)
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE)
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
realm = models.ForeignKey(Realm, on_delete=CASCADE)
class RealmReactivationStatus(models.Model):
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
realm = models.ForeignKey(Realm, on_delete=CASCADE)
class AbstractPushDeviceToken(models.Model): class AbstractPushDeviceToken(models.Model):
APNS = 1 APNS = 1
GCM = 2 GCM = 2

View File

@ -0,0 +1,175 @@
from datetime import timedelta
from typing import Optional, Union
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import CASCADE, Q, QuerySet
from django.db.models.functions import Upper
from django.utils.timezone import now as timezone_now
from confirmation import settings as confirmation_settings
from zerver.lib.types import UnspecifiedValue
from zerver.models.constants import MAX_LANGUAGE_ID_LENGTH
from zerver.models.realms import Realm
from zerver.models.users import UserProfile
class PreregistrationRealm(models.Model):
"""Data on a partially created realm entered by a user who has
completed the "new organization" form. Used to transfer the user's
selections from the pre-confirmation "new organization" form to
the post-confirmation user registration form.
Note that the values stored here may not match those of the
created realm (in the event the user creates a realm at all),
because we allow the user to edit these values in the registration
form (and in fact the user will be required to do so if the
`string_id` is claimed by another realm before registraiton is
completed).
"""
name = models.CharField(max_length=Realm.MAX_REALM_NAME_LENGTH)
org_type = models.PositiveSmallIntegerField(
default=Realm.ORG_TYPES["unspecified"]["id"],
choices=[(t["id"], t["name"]) for t in Realm.ORG_TYPES.values()],
)
default_language = models.CharField(
default="en",
max_length=MAX_LANGUAGE_ID_LENGTH,
)
string_id = models.CharField(max_length=Realm.MAX_REALM_SUBDOMAIN_LENGTH)
email = models.EmailField()
confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_realm")
status = models.IntegerField(default=0)
# The Realm created upon completion of the registration
# for this PregistrationRealm
created_realm = models.ForeignKey(Realm, null=True, related_name="+", on_delete=models.SET_NULL)
# The UserProfile created upon completion of the registration
# for this PregistrationRealm
created_user = models.ForeignKey(
UserProfile, null=True, related_name="+", on_delete=models.SET_NULL
)
class PreregistrationUser(models.Model):
# Data on a partially created user, before the completion of
# registration. This is used in at least three major code paths:
# * Realm creation, in which case realm is None.
#
# * Invitations, in which case referred_by will always be set.
#
# * Social authentication signup, where it's used to store data
# from the authentication step and pass it to the registration
# form.
email = models.EmailField()
confirmation = GenericRelation("confirmation.Confirmation", related_query_name="prereg_user")
# If the pre-registration process provides a suggested full name for this user,
# store it here to use it to prepopulate the full name field in the registration form:
full_name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH, null=True)
full_name_validated = models.BooleanField(default=False)
referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE)
streams = models.ManyToManyField("zerver.Stream")
invited_at = models.DateTimeField(auto_now=True)
realm_creation = models.BooleanField(default=False)
# Indicates whether the user needs a password. Users who were
# created via SSO style auth (e.g. GitHub/Google) generally do not.
password_required = models.BooleanField(default=True)
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
# The realm should only ever be None for PreregistrationUser
# objects created as part of realm creation.
realm = models.ForeignKey(Realm, null=True, on_delete=CASCADE)
# These values should be consistent with the values
# in settings_config.user_role_values.
INVITE_AS = dict(
REALM_OWNER=100,
REALM_ADMIN=200,
MODERATOR=300,
MEMBER=400,
GUEST_USER=600,
)
invited_as = models.PositiveSmallIntegerField(default=INVITE_AS["MEMBER"])
multiuse_invite = models.ForeignKey("MultiuseInvite", null=True, on_delete=models.SET_NULL)
# The UserProfile created upon completion of the registration
# for this PregistrationUser
created_user = models.ForeignKey(
UserProfile, null=True, related_name="+", on_delete=models.SET_NULL
)
class Meta:
indexes = [
models.Index(Upper("email"), name="upper_preregistration_email_idx"),
]
def filter_to_valid_prereg_users(
query: QuerySet[PreregistrationUser],
invite_expires_in_minutes: Union[Optional[int], UnspecifiedValue] = UnspecifiedValue(),
) -> QuerySet[PreregistrationUser]:
"""
If invite_expires_in_days is specified, we return only those PreregistrationUser
objects that were created at most that many days in the past.
"""
used_value = confirmation_settings.STATUS_USED
revoked_value = confirmation_settings.STATUS_REVOKED
query = query.exclude(status__in=[used_value, revoked_value])
if invite_expires_in_minutes is None:
# Since invite_expires_in_minutes is None, we're invitation will never
# expire, we do not need to check anything else and can simply return
# after excluding objects with active and revoked status.
return query
assert invite_expires_in_minutes is not None
if not isinstance(invite_expires_in_minutes, UnspecifiedValue):
lowest_datetime = timezone_now() - timedelta(minutes=invite_expires_in_minutes)
return query.filter(invited_at__gte=lowest_datetime)
else:
return query.filter(
Q(confirmation__expiry_date=None) | Q(confirmation__expiry_date__gte=timezone_now())
)
class MultiuseInvite(models.Model):
referred_by = models.ForeignKey(UserProfile, on_delete=CASCADE)
streams = models.ManyToManyField("zerver.Stream")
realm = models.ForeignKey(Realm, on_delete=CASCADE)
invited_as = models.PositiveSmallIntegerField(default=PreregistrationUser.INVITE_AS["MEMBER"])
# status for tracking whether the invite has been revoked.
# If revoked, set to confirmation.settings.STATUS_REVOKED.
# STATUS_USED is not supported, because these objects are supposed
# to be usable multiple times.
status = models.IntegerField(default=0)
class EmailChangeStatus(models.Model):
new_email = models.EmailField()
old_email = models.EmailField()
updated_at = models.DateTimeField(auto_now=True)
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE)
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
realm = models.ForeignKey(Realm, on_delete=CASCADE)
class RealmReactivationStatus(models.Model):
# status: whether an object has been confirmed.
# if confirmed, set to confirmation.settings.STATUS_USED
status = models.IntegerField(default=0)
realm = models.ForeignKey(Realm, on_delete=CASCADE)

View File

@ -73,11 +73,11 @@ from zerver.models import (
UserProfile, UserProfile,
UserTopic, UserTopic,
check_valid_user_ids, check_valid_user_ids,
filter_to_valid_prereg_users,
get_client, get_client,
get_stream, get_stream,
) )
from zerver.models.groups import SystemGroups from zerver.models.groups import SystemGroups
from zerver.models.prereg_users import filter_to_valid_prereg_users
from zerver.models.realms import InvalidFakeEmailDomainError, get_fake_email_domain, get_realm from zerver.models.realms import InvalidFakeEmailDomainError, get_fake_email_domain, get_realm
from zerver.models.users import ( from zerver.models.users import (
get_source_profile, get_source_profile,

View File

@ -74,8 +74,8 @@ from zerver.models import (
PreregistrationUser, PreregistrationUser,
Realm, Realm,
UserProfile, UserProfile,
filter_to_valid_prereg_users,
) )
from zerver.models.prereg_users import filter_to_valid_prereg_users
from zerver.models.realms import get_realm from zerver.models.realms import get_realm
from zerver.models.users import remote_user_to_email from zerver.models.users import remote_user_to_email
from zerver.signals import email_on_new_login from zerver.signals import email_on_new_login

View File

@ -104,10 +104,10 @@ from zerver.models import (
Stream, Stream,
UserMessage, UserMessage,
UserProfile, UserProfile,
filter_to_valid_prereg_users,
get_bot_services, get_bot_services,
get_client, get_client,
) )
from zerver.models.prereg_users import filter_to_valid_prereg_users
from zerver.models.users import get_system_bot, get_user_profile_by_id from zerver.models.users import get_system_bot, get_user_profile_by_id
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)