Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/common-library/src/common_library/users_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ class UserStatus(str, Enum):
BANNED = "BANNED"
# This user is inactive because it was marked for deletion
DELETED = "DELETED"


class AccountRequestStatus(str, Enum):
"""Status of the request for an account"""

PENDING = "PENDING" # Pending PO review to approve/reject the request
APPROVED = "APPROVED" # PO approved the request
REJECTED = "REJECTED" # PO rejected the request
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""new pre-registration columns

Revision ID: ba9c4816a31b
Revises: b39f2dc87ccd
Create Date: 2025-05-19 15:21:40.182354+00:00

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "ba9c4816a31b"
down_revision = "b39f2dc87ccd"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
# Create the enum type first before using it
account_request_status = sa.Enum(
"PENDING", "APPROVED", "REJECTED", name="accountrequeststatus"
)
account_request_status.create(op.get_bind(), checkfirst=True)

op.add_column(
"users_pre_registration_details",
sa.Column(
"id",
sa.BigInteger(),
sa.Identity(always=False, start=1, cycle=False),
nullable=False,
),
)
op.add_column(
"users_pre_registration_details",
sa.Column(
"account_request_status",
account_request_status, # Use the created enum type
server_default="PENDING", # Simply use the string value as default
nullable=False,
),
)
op.add_column(
"users_pre_registration_details",
sa.Column(
"account_request_reviewed_by",
sa.Integer(),
nullable=True,
),
)
op.add_column(
"users_pre_registration_details",
sa.Column(
"account_request_reviewed_at",
sa.DateTime(timezone=True),
nullable=True,
),
)
op.add_column(
"users_pre_registration_details",
sa.Column("product_name", sa.String(), nullable=True),
)
op.drop_constraint(
"users_pre_registration_details_pre_email_key",
"users_pre_registration_details",
type_="unique",
)
op.create_foreign_key(
"fk_users_pre_registration_details_product_name",
"users_pre_registration_details",
"products",
["product_name"],
["name"],
onupdate="CASCADE",
ondelete="SET NULL",
)
# Add foreign key for account_request_reviewed_by
op.create_foreign_key(
"fk_users_pre_registration_reviewed_by_user_id",
"users_pre_registration_details",
"users",
["account_request_reviewed_by"],
["id"],
onupdate="CASCADE",
ondelete="SET NULL",
)
# Set primary key on id column
op.create_primary_key(
"users_pre_registration_details_pk",
"users_pre_registration_details",
["id"],
)
# Add composite unique constraint on pre_email and product_name
op.create_unique_constraint(
"users_pre_registration_details_pre_email_product_name_key",
"users_pre_registration_details",
["pre_email", "product_name"],
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# Drop the composite unique constraint
op.drop_constraint(
"users_pre_registration_details_pre_email_product_name_key",
"users_pre_registration_details",
type_="unique",
)
op.drop_constraint(
"users_pre_registration_details_pk",
"users_pre_registration_details",
type_="primary",
)
op.drop_constraint(
"fk_users_pre_registration_reviewed_by_user_id",
"users_pre_registration_details",
type_="foreignkey",
)
op.drop_constraint(
"fk_users_pre_registration_details_product_name",
"users_pre_registration_details",
type_="foreignkey",
)
op.create_unique_constraint(
"users_pre_registration_details_pre_email_key",
"users_pre_registration_details",
["pre_email"],
)
op.drop_column("users_pre_registration_details", "product_name")
op.drop_column("users_pre_registration_details", "account_request_reviewed_at")
op.drop_column("users_pre_registration_details", "account_request_reviewed_by")
op.drop_column("users_pre_registration_details", "account_request_status")
op.drop_column("users_pre_registration_details", "id")

# Drop the enum type in downgrade
sa.Enum(name="accountrequeststatus").drop(op.get_bind(), checkfirst=True)
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import sqlalchemy as sa
from common_library.users_enums import AccountRequestStatus
from sqlalchemy.dialects import postgresql

from ._common import (
RefActions,
column_created_by_user,
column_created_datetime,
column_modified_datetime,
register_modified_datetime_auto_update_trigger,
)
from .base import metadata
from .products import products # Import the products table
from .users import users

users_pre_registration_details = sa.Table(
Expand All @@ -18,6 +21,13 @@
# a row can be added in this table during pre-registration i.e. even before the `users` row exists.
#
metadata,
sa.Column(
"id",
sa.BigInteger,
sa.Identity(start=1, cycle=False),
primary_key=True,
doc="Primary key for the pre-registration entry",
),
sa.Column(
"user_id",
sa.Integer,
Expand All @@ -34,7 +44,6 @@
"pre_email",
sa.String(),
nullable=False,
unique=True,
doc="Email of the user on pre-registration (copied to users.email upon registration)",
),
sa.Column(
Expand All @@ -53,6 +62,45 @@
doc="Phone provided on pre-registration"
"NOTE: this is not copied upon registration since it needs to be confirmed",
),
# Account Request
sa.Column(
"account_request_status",
sa.Enum(AccountRequestStatus),
nullable=False,
server_default=AccountRequestStatus.PENDING.value,
doc="Status of review for the account request",
),
sa.Column(
"account_request_reviewed_by",
sa.Integer,
sa.ForeignKey(
users.c.id,
onupdate=RefActions.CASCADE,
ondelete=RefActions.SET_NULL,
name="fk_users_pre_registration_reviewed_by_user_id",
),
nullable=True,
doc="Tracks who approved or rejected the account request",
),
sa.Column(
"account_request_reviewed_at",
sa.DateTime(timezone=True),
nullable=True,
doc="Timestamp when the account request was reviewed",
),
# Product the user is requesting access to
sa.Column(
"product_name",
sa.String,
sa.ForeignKey(
products.c.name,
onupdate=RefActions.CASCADE,
ondelete=RefActions.SET_NULL,
name="fk_users_pre_registration_details_product_name",
),
nullable=True,
doc="Product that the user is requesting an account for",
),
# Billable address columns:
sa.Column("institution", sa.String(), doc="the name of a company or university"),
sa.Column("address", sa.String()),
Expand All @@ -67,19 +115,16 @@
doc="Extra information provided in the form but still not defined as a column.",
),
# Other related users
sa.Column(
"created_by",
sa.Integer,
sa.ForeignKey(
users.c.id,
onupdate=RefActions.CASCADE,
ondelete=RefActions.SET_NULL,
),
nullable=True,
doc="PO user that issued this pre-registration",
),
column_created_by_user(users_table=users, required=False),
column_created_datetime(timezone=False),
column_modified_datetime(timezone=False),
# CONSTRAINTS:
# Composite unique constraint to ensure a user can only have one pre-registration per product
sa.UniqueConstraint(
"pre_email",
"product_name",
name="users_pre_registration_details_pre_email_product_name_key",
),
)

register_modified_datetime_auto_update_trigger(users_pre_registration_details)
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ async def new_user(
users.c.status,
).where(users.c.id == user_id)
)
row = await maybe_await(result.first())
from aiopg.sa.result import RowProxy

assert isinstance(row, RowProxy) # nosec
return row
return await maybe_await(result.first())

@staticmethod
async def join_and_update_from_pre_registration_details(
conn: DBConnection, new_user_id: int, new_user_email: str
async def link_and_update_user_from_pre_registration(
conn: DBConnection,
*,
new_user_id: int,
new_user_email: str,
update_user: bool = True,
) -> None:
"""After a user is created, it can be associated with information provided during invitation

Expand All @@ -113,11 +113,8 @@ async def join_and_update_from_pre_registration_details(
.values(user_id=new_user_id)
)

from aiopg.sa.result import ResultProxy

assert isinstance(result, ResultProxy) # nosec

if result.rowcount:
if update_user:
# COPIES some pre-registration details to the users table
pre_columns = (
users_pre_registration_details.c.pre_first_name,
users_pre_registration_details.c.pre_last_name,
Expand All @@ -141,13 +138,14 @@ async def join_and_update_from_pre_registration_details(
users_pre_registration_details.c.pre_email == new_user_email
)
)
if details := await maybe_await(result.fetchone()):
if pre_registration_details_data := result.first():
# NOTE: could have many products! which to use?
await conn.execute(
users.update()
.where(users.c.id == new_user_id)
.values(
first_name=details.pre_first_name, # type: ignore[union-attr]
last_name=details.pre_last_name, # type: ignore[union-attr]
first_name=pre_registration_details_data.pre_first_name, # type: ignore[union-attr]
last_name=pre_registration_details_data.pre_last_name, # type: ignore[union-attr]
)
)

Expand Down
Loading
Loading