Skip to content

Commit

Permalink
(PC-32565)[BO] feat: add page for ds account update requests
Browse files Browse the repository at this point in the history
  • Loading branch information
vroullier-pass committed Nov 22, 2024
1 parent 53a6e25 commit 6e7df43
Show file tree
Hide file tree
Showing 19 changed files with 574 additions and 3 deletions.
2 changes: 1 addition & 1 deletion api/.env.testauto
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ DATABASE_LOCK_TIMEOUT=500
DEMARCHES_SIMPLIFIEES_BANK_ACCOUNT_PROCEDURE_ID=80876
DEMARCHES_SIMPLIFIEES_RIB_VENUE_PROCEDURE_ID_V4=100000
DEMARCHES_SIMPLIFIEES_TOKEN="1"
DEMARCHES_SIMPLIFIEES_WEBHOOK_TOKEN=good_token
DEMARCHES_SIMPLIFIEES_USER_ACCOUNT_UPDATE_PROCEDURE_ID=104118
DEMARCHES_SIMPLIFIEES_WEBHOOK_TOKEN=good_token
DEV_EMAIL_ADDRESS=dev@example.com
DS_MARK_WITHOUT_CONTINUATION_ANNOTATION_DEADLINE=180
DS_MARK_WITHOUT_CONTINUATION_APPLICATION_DEADLINE=90
Expand Down
4 changes: 2 additions & 2 deletions api/alembic_version_conflict_detection.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
025aaed1c957 (pre) (head)
9bca134a0476 (post) (head)
cb98ffb241da (pre) (head)
ddb791b7e221 (post) (head)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Create table UserAccountUpdateRequest
"""

from alembic import op
import sqlalchemy as sa

from pcapi.connectors.dms.models import GraphQLApplicationStates
from pcapi.utils.db import MagicEnum


# pre/post deployment: pre
# revision identifiers, used by Alembic.
revision = "cb98ffb241da"
down_revision = "025aaed1c957"
branch_labels: tuple[str] | None = None
depends_on: list[str] | None = None


def upgrade() -> None:
op.create_table(
"user_account_update_request",
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("dsApplicationId", sa.BigInteger(), nullable=False),
sa.Column("status", MagicEnum(GraphQLApplicationStates), nullable=False),
sa.Column("dateCreated", sa.DateTime(), server_default=sa.text("now()"), nullable=False),
sa.Column("dateLastStatusUpdate", sa.DateTime(), server_default=sa.text("now()"), nullable=True),
sa.Column("firstName", sa.Text(), nullable=True),
sa.Column("lastName", sa.Text(), nullable=True),
sa.Column("email", sa.Text(), nullable=False),
sa.Column("birthDate", sa.Date(), nullable=True),
sa.Column("userId", sa.BigInteger(), nullable=True),
sa.Column("newEmail", sa.Text(), nullable=True),
sa.Column("newPhoneNumber", sa.Text(), nullable=True),
sa.Column("newFirstName", sa.Text(), nullable=True),
sa.Column("newLastName", sa.Text(), nullable=True),
sa.Column("allConditionsChecked", sa.Boolean(), nullable=False),
sa.Column("lastInstructorId", sa.BigInteger(), nullable=True),
sa.Column("dateLastUserMessage", sa.DateTime(), nullable=True),
sa.Column("dateLastInstructorMessage", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(
["lastInstructorId"],
["user.id"],
postgresql_not_valid=True,
),
sa.ForeignKeyConstraint(
["userId"],
["user.id"],
postgresql_not_valid=True,
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_user_account_update_request_dsApplicationId"),
"user_account_update_request",
["dsApplicationId"],
unique=True,
)
op.create_index(
op.f("ix_user_account_update_request_userId"),
"user_account_update_request",
["userId"],
)
op.create_index(
op.f("ix_user_account_update_request_lastInstructorId"),
"user_account_update_request",
["lastInstructorId"],
)


def downgrade() -> None:
op.drop_table("user_account_update_request")
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Validate constraint on user_account_update_request.userId and user_account_update_request.lastInstructorId
"""

from alembic import op

from pcapi import settings


# pre/post deployment: post
# revision identifiers, used by Alembic.
revision = "ddb791b7e221"
down_revision = "9bca134a0476"
branch_labels: tuple[str] | None = None
depends_on: list[str] | None = None


def upgrade() -> None:
op.execute("SET SESSION statement_timeout = '300s'")
op.execute(
"""ALTER TABLE user_account_update_request VALIDATE CONSTRAINT "user_account_update_request_userId_fkey" """
)
op.execute(
"""ALTER TABLE user_account_update_request VALIDATE CONSTRAINT "user_account_update_request_lastInstructorId_fkey" """
)
op.execute(f"SET SESSION statement_timeout={settings.DATABASE_STATEMENT_TIMEOUT}")


def downgrade() -> None:
pass
2 changes: 2 additions & 0 deletions api/src/pcapi/core/permissions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ class Permissions(enum.Enum):

MANAGE_SPECIAL_EVENTS = "gérer les opérations spéciales"

MANAGE_ACCOUNT_UPDATE_REQUEST = "instruire les demandes de modification de compte (DS)"

@classmethod
def exists(cls, name: str) -> bool:
try:
Expand Down
32 changes: 32 additions & 0 deletions api/src/pcapi/core/users/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from pcapi import settings
from pcapi.connectors.beneficiaries.educonnect import models as educonnect_models
from pcapi.connectors.dms import models as dms_models
from pcapi.connectors.serialization import ubble_serializers
from pcapi.core.factories import BaseFactory
import pcapi.core.finance.api as finance_api
Expand Down Expand Up @@ -1066,6 +1067,37 @@ class EmailAdminUpdateEntryFactory(UserEmailHistoryFactory):
eventType = models.EmailHistoryEventTypeEnum.ADMIN_UPDATE


class UserAccountUpdateRequestFactory(BaseFactory):
class Meta:
model = models.UserAccountUpdateRequest

dsApplicationId = factory.Sequence(lambda n: 1230000 + n + 1)
status = dms_models.GraphQLApplicationStates.on_going
firstName = "Jeune"
lastName = "Changeant d'Email"
email = factory.Sequence(lambda n: f"ancien_email_{n+1}@example.com")
birthDate = factory.Sequence(lambda n: date.today() - timedelta(days=18 * 366 + 10 * n))
user = factory.SubFactory(
BeneficiaryGrant18Factory,
firstName=factory.SelfAttribute("..firstName"),
lastName=factory.SelfAttribute("..lastName"),
email=factory.SelfAttribute("..email"),
dateOfBirth=factory.SelfAttribute("..birthDate"),
)
newEmail = factory.Sequence(lambda n: f"nouvel_email_{n+1}@example.com")
newPhoneNumber = None
newFirstName = None
newLastName = None
allConditionsChecked = True
lastInstructor = factory.SubFactory(
AdminFactory,
firstName="Instructeur",
lastName="du Backoffice",
)
dateLastUserMessage = LazyAttribute(lambda _: datetime.utcnow() - relativedelta(days=1))
dateLastInstructorMessage = factory.LazyAttribute(lambda _: datetime.utcnow() - relativedelta(days=3))


class UserProFlagsFactory(BaseFactory):
class Meta:
model = models.UserProFlags
Expand Down
39 changes: 39 additions & 0 deletions api/src/pcapi/core/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from sqlalchemy.sql.elements import BinaryExpression
from sqlalchemy.sql.elements import BooleanClauseList

from pcapi.connectors.dms import models as dms_models
from pcapi.core.finance.enum import DepositType
from pcapi.core.geography.models import IrisFrance
from pcapi.core.users import constants
Expand All @@ -35,6 +36,7 @@
from pcapi.models.validation_status_mixin import ValidationStatus
from pcapi.utils import crypto
from pcapi.utils import regions as regions_utils
from pcapi.utils.db import MagicEnum
from pcapi.utils.phone_number import ParsedPhoneNumber


Expand Down Expand Up @@ -923,6 +925,43 @@ def newEmail(cls): # pylint: disable=no-self-argument
)


class UserAccountUpdateRequest(PcObject, Base, Model):
__tablename__ = "user_account_update_request"
dsApplicationId: int = sa.Column(sa.BigInteger, nullable=False, index=True, unique=True)
status: dms_models.GraphQLApplicationStates = sa.Column(
MagicEnum(dms_models.GraphQLApplicationStates), nullable=False
)
dateCreated: datetime = sa.Column(
sa.DateTime, nullable=False, default=datetime.utcnow, server_default=sa.func.now()
)
dateLastStatusUpdate: datetime = sa.Column(
sa.DateTime, nullable=True, default=datetime.utcnow, server_default=sa.func.now()
)
# Information about applicant, used to match with a single user
firstName: str = sa.Column(sa.Text, nullable=True)
lastName: str = sa.Column(sa.Text, nullable=True)
email: str = sa.Column(sa.Text, nullable=False)
birthDate = sa.Column(sa.Date, nullable=True)
# User found from his/her email - may be null in case of wrong email
userId: int = sa.Column(sa.BigInteger, sa.ForeignKey("user.id"), index=True, nullable=True)
user: orm.Mapped[User] = orm.relationship(User, foreign_keys=[userId], backref="accountUpdateRequests")
# One or several changes may be requested
newEmail: str = sa.Column(sa.Text, nullable=True)
newPhoneNumber: str = sa.Column(sa.Text, nullable=True)
newFirstName: str = sa.Column(sa.Text, nullable=True)
newLastName: str = sa.Column(sa.Text, nullable=True)
# Ensures that all checkboxes are checked (GCU, sworn statement)
allConditionsChecked: bool = sa.Column(sa.Boolean, nullable=False, default=False)
lastInstructorId: int = sa.Column(sa.BigInteger, sa.ForeignKey("user.id"), index=True, nullable=True)
lastInstructor: orm.Mapped[User] = orm.relationship(User, foreign_keys=[lastInstructorId])
dateLastUserMessage: datetime = sa.Column(sa.DateTime, nullable=True)
dateLastInstructorMessage: datetime = sa.Column(sa.DateTime, nullable=True)

@property
def applicant_age(self) -> int | None:
return users_utils.get_age_from_birth_date(self.birthDate) if self.birthDate else None


class UserSession(PcObject, Base, Model):
userId: int = sa.Column(sa.BigInteger, nullable=False)
uuid: UUID = sa.Column(postgresql.UUID(as_uuid=True), unique=True, nullable=False)
Expand Down
1 change: 1 addition & 0 deletions api/src/pcapi/repository/clean_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
users_models.GdprUserAnonymization,
users_models.GdprUserDataExtract,
users_models.SingleSignOn,
users_models.UserAccountUpdateRequest,
users_models.User,
users_models.UserSession,
geography_models.IrisFrance,
Expand Down
1 change: 1 addition & 0 deletions api/src/pcapi/routes/backoffice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def install_routes(app: Flask) -> None:
from . import pro
from . import redirect
from .accounts import blueprint as accounts_blueprint
from .accounts import update_request_blueprint
from .admin import blueprint as admin_blueprint
from .admin import bo_users_blueprint
from .bank_account import blueprint as bank_account_blueprint
Expand Down
14 changes: 14 additions & 0 deletions api/src/pcapi/routes/backoffice/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,17 @@ class ManualReviewForm(FlaskForm):

class CommentForm(FlaskForm):
comment = fields.PCCommentField("Commentaire interne pour le compte jeune")


class AccountUpdateRequestSearchForm(utils.PCForm):
class Meta:
csrf = False

q = fields.PCOptSearchField("Numéro de dossier")
page = wtforms.HiddenField("page", default="1", validators=(wtforms.validators.Optional(),))
per_page = fields.PCSelectField(
"Par page",
choices=(("10", "10"), ("25", "25"), ("50", "50"), ("100", "100")),
default="100",
validators=(wtforms.validators.Optional(),),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from functools import partial

from flask import render_template
from flask import url_for
import sqlalchemy as sa

from pcapi.core.permissions import models as perm_models
from pcapi.core.users import models as users_models
from pcapi.repository import atomic
from pcapi.routes.backoffice import search_utils
from pcapi.routes.backoffice import utils

from . import forms as account_forms


account_update_blueprint = utils.child_backoffice_blueprint(
"account_update",
__name__,
url_prefix="/account-update-requests",
permission=perm_models.Permissions.MANAGE_ACCOUNT_UPDATE_REQUEST,
)


@account_update_blueprint.route("", methods=["GET"])
@atomic()
def list_account_update_requests() -> utils.BackofficeResponse:
form = account_forms.AccountUpdateRequestSearchForm(formdata=utils.get_query_params())
if not form.validate():
return render_template("accounts/update_requests_list.html", rows=[], form=form), 400

query = users_models.UserAccountUpdateRequest.query.options(
sa.orm.joinedload(users_models.UserAccountUpdateRequest.user).load_only(
users_models.User.id,
users_models.User.email,
users_models.User.firstName,
users_models.User.lastName,
users_models.User.civility,
users_models.User.phoneNumber,
users_models.User.dateOfBirth,
users_models.User.validatedBirthDate,
),
sa.orm.joinedload(users_models.UserAccountUpdateRequest.lastInstructor).load_only(
users_models.User.id,
users_models.User.email,
users_models.User.firstName,
users_models.User.lastName,
),
).order_by(users_models.UserAccountUpdateRequest.id.desc())

paginated_rows = query.paginate(page=int(form.page.data), per_page=int(form.per_page.data))
next_page = partial(url_for, ".list_account_update_requests", **form.raw_data)
next_pages_urls = search_utils.pagination_links(next_page, int(form.page.data), paginated_rows.pages)
form.page.data = 1 # Reset to first page when form is submitted ("Chercher" clicked)

return render_template(
"accounts/update_requests_list.html",
rows=paginated_rows,
form=form,
next_pages_urls=next_pages_urls,
)
9 changes: 9 additions & 0 deletions api/src/pcapi/routes/backoffice/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ def pluralize(count: int, singular: str = "", plural: str = "s") -> str:
return plural if count > 1 else singular


def genderize(text: str, civility: str | None) -> str:
if civility == users_models.GenderEnum.M.value:
return text
if civility == users_models.GenderEnum.F.value:
return text + "e"
return text + "(e)"


def format_reason_label(reason: str | None) -> str:
if reason:
return users_constants.SUSPENSION_REASON_CHOICES.get(
Expand Down Expand Up @@ -1387,6 +1395,7 @@ def install_template_filters(app: Flask) -> None:
app.jinja_env.filters["format_rate_multiply_by_100"] = format_rate_multiply_by_100
app.jinja_env.filters["format_string_list"] = format_string_list
app.jinja_env.filters["pluralize"] = pluralize
app.jinja_env.filters["genderize"] = genderize
app.jinja_env.filters["format_date"] = format_date
app.jinja_env.filters["format_date_time"] = format_date_time
app.jinja_env.filters["format_string_to_date_time"] = format_string_to_date_time
Expand Down
Loading

0 comments on commit 6e7df43

Please sign in to comment.