From 5da527a1ad711ac3cc6995e1c69108ffd89f4814 Mon Sep 17 00:00:00 2001 From: Sujit <90745363+suzit-10@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:32:49 +0545 Subject: [PATCH] Feat: add regulator emails on projection creation (#345) * feat: add `MultipleEmailInput` component * feat(project-creation): post regulator emails with project details if the project requires regulators approval * feat(task-description): update image uploadBox style * feat: add `BindContentContainer` component * feat: add `countries-list` package for listing all the countries * feat(user-profile): show all the countries name on country dropdown with search functionality * feat(project-approval-page): create UI * feat: add route for project approval page * Feat/project approval from regulator (#352) * feat(task-description): update image uploadBox style * feat: add `BindContentContainer` component * feat: add `countries-list` package for listing all the countries * feat(user-profile): show all the countries name on country dropdown with search functionality * feat(project-approval-page): create UI * feat: add route for project approval page * feat: add regualtor approval and commect section with additon of role regulator * feat: add regualtor approval and commect section with additon of role regulator * feat: add regulator email template * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * feat: add project total area * feat: add project total area * fix: Pydantic class to validate the token as Base64 * fix: using uitls to send email * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Sujit Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * update: email template for local regulation authority committee * commented some statements for project list in sql statements * update: email subject * fix: local regulation committee boolean selection --------- Co-authored-by: Sujit Co-authored-by: Saurav Aryal <64722587+aryalsaurav@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Niraj Adhikari --- src/backend/app/db/db_models.py | 16 ++ .../regulator/approval_request.html | 142 ++++++++++++++++++ ...cc4_add_regulators_approval_and_comment.py | 76 ++++++++++ .../3cd04bfdb1e4_add_user_role_regulator.py | 53 +++++++ src/backend/app/models/enums.py | 9 ++ src/backend/app/projects/project_logic.py | 13 ++ src/backend/app/projects/project_routes.py | 84 ++++++++++- src/backend/app/projects/project_schemas.py | 54 ++++++- src/backend/app/users/user_routes.py | 91 ++++++++++- src/backend/app/users/user_schemas.py | 13 ++ src/backend/app/utils.py | 24 +++ src/frontend/package.json | 1 + .../FormContents/BasicDetails/index.tsx | 15 +- .../CreateprojectLayout/index.tsx | 26 ++++ .../FormContents/Contributions/index.tsx | 44 +++++- .../DescriptionSection/UploadsBox/index.tsx | 2 +- .../DescriptionSection.tsx | 69 +++++++++ .../InstructionSection.tsx | 7 + .../RegulatorsApprovalPage/index.tsx | 39 +++++ .../BasicDetails.tsx/index.tsx | 15 +- .../common/BindContentContainer/index.tsx | 19 +++ .../common/MultipleEmailInput/index.tsx | 99 ++++++++++++ .../components/common/RadioButton/index.tsx | 6 +- src/frontend/src/constants/approvalPage.ts | 5 + src/frontend/src/constants/createProject.tsx | 9 ++ src/frontend/src/routes/appRoutes.ts | 7 + .../src/store/slices/createproject.ts | 4 + .../views/RegulatorsApprovalPage/index.tsx | 60 ++++++++ 28 files changed, 976 insertions(+), 26 deletions(-) create mode 100644 src/backend/app/email_templates/regulator/approval_request.html create mode 100644 src/backend/app/migrations/versions/162edb7e3cc4_add_regulators_approval_and_comment.py create mode 100644 src/backend/app/migrations/versions/3cd04bfdb1e4_add_user_role_regulator.py create mode 100644 src/frontend/src/components/RegulatorsApprovalPage/DescriptionSection.tsx create mode 100644 src/frontend/src/components/RegulatorsApprovalPage/InstructionSection.tsx create mode 100644 src/frontend/src/components/RegulatorsApprovalPage/index.tsx create mode 100644 src/frontend/src/components/common/BindContentContainer/index.tsx create mode 100644 src/frontend/src/components/common/MultipleEmailInput/index.tsx create mode 100644 src/frontend/src/constants/approvalPage.ts create mode 100644 src/frontend/src/views/RegulatorsApprovalPage/index.tsx diff --git a/src/backend/app/db/db_models.py b/src/backend/app/db/db_models.py index 7704ceef..3f6d73ed 100644 --- a/src/backend/app/db/db_models.py +++ b/src/backend/app/db/db_models.py @@ -25,6 +25,7 @@ ProjectVisibility, UserRole, State, + RegulatorApprovalStatus, ) from sqlalchemy.orm import ( object_session, @@ -137,11 +138,26 @@ class DbProject(Base): requires_approval_from_manager_for_locking = cast( bool, Column(Boolean, default=False) ) + requires_approval_from_regulator = cast(bool, Column(Boolean, default=False)) + regulator_emails = cast(list, Column(ARRAY(String), nullable=True)) # PROJECT STATUS status = cast( ProjectStatus, Column(Enum(ProjectStatus), default=ProjectStatus.DRAFT, nullable=False), ) + regulator_approval_status = cast( + RegulatorApprovalStatus, Column(Enum(RegulatorApprovalStatus), nullable=True) + ) + regulator_comment = cast(str, Column(String, nullable=True)) + commenting_regulator_id = cast( + str, + Column( + String, + ForeignKey("users.id", name="fk_projects_commenting_regulator_id"), + nullable=True, + ), + ) + commenting_regulator = relationship(DbUser, uselist=False, backref="regulator") visibility = cast( ProjectVisibility, Column( diff --git a/src/backend/app/email_templates/regulator/approval_request.html b/src/backend/app/email_templates/regulator/approval_request.html new file mode 100644 index 00000000..b620459e --- /dev/null +++ b/src/backend/app/email_templates/regulator/approval_request.html @@ -0,0 +1,142 @@ + + + + + + Drone Tasking Manager Invite + + + + + + + diff --git a/src/backend/app/migrations/versions/162edb7e3cc4_add_regulators_approval_and_comment.py b/src/backend/app/migrations/versions/162edb7e3cc4_add_regulators_approval_and_comment.py new file mode 100644 index 00000000..7606179f --- /dev/null +++ b/src/backend/app/migrations/versions/162edb7e3cc4_add_regulators_approval_and_comment.py @@ -0,0 +1,76 @@ +"""add regulators approval and comment + +Revision ID: 162edb7e3cc4 +Revises: 4ea77c60b715 +Create Date: 2024-11-22 04:21:04.322488 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "162edb7e3cc4" +down_revision: Union[str, None] = "4ea77c60b715" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +regulator_approval_status_enum = sa.Enum( + "PENDING", "APPROVED", "REJECTED", name="regulatorapprovalstatus" +) + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + regulator_approval_status_enum.create(op.get_bind()) + op.add_column( + "projects", + sa.Column("requires_approval_from_regulator", sa.Boolean(), nullable=True), + ) + op.execute(""" + UPDATE projects + SET requires_approval_from_regulator = FALSE + WHERE requires_approval_from_regulator IS NULL + """) + op.add_column( + "projects", sa.Column("regulator_emails", sa.ARRAY(sa.String()), nullable=True) + ) + op.add_column( + "projects", + sa.Column( + "regulator_approval_status", + sa.Enum("PENDING", "APPROVED", "REJECTED", name="regulatorapprovalstatus"), + nullable=True, + ), + ) + op.add_column( + "projects", sa.Column("regulator_comment", sa.String(), nullable=True) + ) + op.add_column( + "projects", sa.Column("commenting_regulator_id", sa.String(), nullable=True) + ) + op.create_foreign_key( + "fk_projects_commenting_regulator_id", + "projects", + "users", + ["commenting_regulator_id"], + ["id"], + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint( + "fk_projects_commenting_regulator_id", "projects", type_="foreignkey" + ) + op.drop_column("projects", "commenting_regulator_id") + op.drop_column("projects", "regulator_comment") + op.drop_column("projects", "regulator_approval_status") + op.drop_column("projects", "regulator_emails") + op.drop_column("projects", "requires_approval_from_regulator") + # ### end Alembic commands ### + regulator_approval_status_enum.drop(op.get_bind()) diff --git a/src/backend/app/migrations/versions/3cd04bfdb1e4_add_user_role_regulator.py b/src/backend/app/migrations/versions/3cd04bfdb1e4_add_user_role_regulator.py new file mode 100644 index 00000000..4fcff8a6 --- /dev/null +++ b/src/backend/app/migrations/versions/3cd04bfdb1e4_add_user_role_regulator.py @@ -0,0 +1,53 @@ +"""add user role regulator + +Revision ID: 3cd04bfdb1e4 +Revises: 162edb7e3cc4 +Create Date: 2024-11-22 04:29:30.071824 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +new_userrole_enum = sa.Enum( + "PROJECT_CREATOR", "DRONE_PILOT", "REGULATOR", name="userrole" +) + +old_userrole_enum = sa.Enum("PROJECT_CREATOR", "DRONE_PILOT", name="userrole") + +# revision identifiers, used by Alembic. +revision: str = "3cd04bfdb1e4" +down_revision: Union[str, None] = "162edb7e3cc4" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute("ALTER TYPE userrole ADD VALUE 'REGULATOR'") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute("ALTER TYPE userrole RENAME TO userrole_old") + op.execute("CREATE TYPE userrole AS ENUM('PROJECT_CREATOR', 'DRONE_PILOT')") + ## remove row containing regulator + op.execute(""" + UPDATE user_profile + SET role = array_remove(role, 'REGULATOR') + WHERE 'REGULATOR' = ANY(role) + """) + + op.execute(""" + ALTER TABLE user_profile + ALTER COLUMN role + TYPE userrole[] + USING role::text[]::userrole[] + """) + + op.execute("DROP TYPE userrole_old") + # ### end Alembic commands ### diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index efa7e5cc..d15f5b94 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -66,6 +66,14 @@ class ProjectStatus(IntEnum, Enum): DRAFT = 2 +class RegulatorApprovalStatus(IntEnum, Enum): + """Enum to describe all possible state of a Project from Regulator""" + + PENDING = 0 + APPROVED = 1 + REJECTED = 2 + + class ProjectVisibility(IntEnum, Enum): """Enum describing task splitting type.""" @@ -120,6 +128,7 @@ class DroneType(IntEnum): class UserRole(IntEnum, Enum): PROJECT_CREATOR = 1 DRONE_PILOT = 2 + REGULATOR = 3 class State(int, Enum): diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index f6be2532..8badd923 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -268,3 +268,16 @@ def get_project_info_from_s3(project_id: uuid.UUID, task_id: uuid.UUID): except Exception as e: log.exception(f"An error occurred while retrieving assets info: {e}") raise HTTPException(status_code=500, detail=str(e)) + + +async def check_regulator_project(db: Connection, project_id: str, email: str): + sql = """ + SELECT id FROM projects WHERE + id = %(project_id)s + AND %(email)s = ANY(regulator_emails) + AND regulator_comment IS NULL + """ + async with db.cursor() as cur: + await cur.execute(sql, {"project_id": project_id, "email": email}) + project = await cur.fetchone() + return bool(project) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index a9ab36d5..f92ddd37 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -32,7 +32,11 @@ from app.users.user_deps import login_required from app.users.user_schemas import AuthUser from app.tasks import task_schemas -from app.utils import geojson_to_kml, timestamp +from app.utils import ( + geojson_to_kml, + timestamp, + send_project_approval_email_to_regulator, +) from app.users import user_schemas from minio.deleteobjects import DeleteObject @@ -193,6 +197,7 @@ async def delete_project_by_id( async def create_project( project_info: project_schemas.ProjectIn, db: Annotated[Connection, Depends(database.get_db)], + background_tasks: BackgroundTasks, user_data: Annotated[AuthUser, Depends(login_required)], dem: UploadFile = File(None), image: UploadFile = File(None), @@ -220,6 +225,16 @@ async def create_project( status_code=HTTPStatus.BAD_REQUEST, detail="Project creation failed" ) + if project_info.requires_approval_from_regulator: + regulator_emails = project_info.regulator_emails + background_tasks.add_task( + send_project_approval_email_to_regulator, + regulator_emails, + project_id, + user_data.name, + project_info.name, + ) + return {"message": "Project successfully created", "project_id": project_id} @@ -582,3 +597,70 @@ async def odm_webhook( log.info(f"Task ID: {task_id}, Status: Webhook received") return {"message": "Webhook received", "task_id": task_id} + + +@router.post("/regulator/comment/{project_id}", tags=["Regulator Approval"]) +async def regulator_approval( + project_id: str, + data: dict, + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], + response: Response, +): + """ + Endpoint to allow a regulator to add comments and approve or reject to a project. + + Args: + project_id (str): The unique identifier of the project. + data (dict): A dictionary containing the regulator's comment. + Expected key: 'regulator_comment' and 'regulator_approval_status. + db (Connection): Database connection instance, provided via dependency injection. + user_data (AuthUser): Authenticated user data, provided via dependency injection. + response (Response): FastAPI Response object to set custom status codes. + + Returns: + dict: A response message indicating success or failure. + + Raises: + HTTPException: Raised with status code 400 if any error occurs during execution. + + Notes: + - Requires the user to be logged in and to have the "REGULATOR" role. + - Ensures that the user is authorized to comment on the specified project. + """ + + try: + if ( + user_data.role != "REGULATOR" + or not await project_logic.check_regulator_project( + db, project_id, user_data.email + ) + ): + response.status_code = 403 + return {"details": "You are not authorized to perform the action"} + + sql = """ + UPDATE projects SET + regulator_comment = %(comment)s, + commenting_regulator_id = %(user_id)s, + regulator_approval_status = %(regulator_approval_status)s + WHERE id = %(project_id)s + """ + + async with db.cursor() as cur: + await cur.execute( + sql, + { + "comment": data["regulator_comment"], + "regulator_approval_status": data["regulator_approval_status"], + "user_id": user_data.id, + "project_id": project_id, + }, + ) + + return {"message": "Commend Added successfully !!!"} + except Exception as e: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"An error occurred: {str(e)}", + ) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 33e7309f..e9bc01d5 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -4,7 +4,14 @@ from datetime import datetime, date import geojson from loguru import logger as log -from pydantic import BaseModel, computed_field, Field, model_validator, root_validator +from pydantic import ( + BaseModel, + computed_field, + Field, + model_validator, + root_validator, + EmailStr, +) from pydantic.functional_validators import AfterValidator from pydantic.functional_serializers import PlainSerializer from geojson_pydantic import Feature, FeatureCollection, Polygon, Point, MultiPolygon @@ -13,11 +20,7 @@ from psycopg.rows import class_row from slugify import slugify from app.models.enums import FinalOutput, ProjectVisibility, UserRole -from app.models.enums import ( - IntEnum, - ProjectStatus, - HTTPStatus, -) +from app.models.enums import IntEnum, ProjectStatus, HTTPStatus, RegulatorApprovalStatus from app.utils import ( merge_multipolygon, ) @@ -115,6 +118,8 @@ class ProjectIn(BaseModel): ], ) requires_approval_from_manager_for_locking: Optional[bool] = False + requires_approval_from_regulator: Optional[bool] = False + regulator_emails: Optional[List[EmailStr]] = None front_overlap: Optional[float] = None side_overlap: Optional[float] = None @@ -191,7 +196,14 @@ class DbProject(BaseModel): task_count: int = 0 tasks: Optional[list[TaskOut]] = [] requires_approval_from_manager_for_locking: Optional[bool] = None + requires_approval_from_regulator: Optional[bool] = False + regulator_emails: Optional[List[EmailStr]] = None + regulator_approval_status: Optional[str] = None + regulator_comment: Optional[str] = None + commenting_regulator_id: Optional[str] = None author_id: Optional[str] = None + author_name: Optional[str] = None + project_area: Optional[float] = None front_overlap: Optional[float] = None side_overlap: Optional[float] = None gsd_cm_px: Optional[float] = None @@ -236,12 +248,20 @@ async def one(db: Connection, project_id: uuid.UUID): ), 'id', projects.id ) AS no_fly_zones, - ST_AsGeoJSON(projects.centroid)::jsonb AS centroid + ST_AsGeoJSON(projects.centroid)::jsonb AS centroid, + users.name as author_name, + COALESCE(SUM(ST_Area(tasks.outline::geography)) / 1000000, 0) AS project_area FROM projects + JOIN + users ON projects.author_id = users.id + LEFT JOIN + tasks ON projects.id = tasks.project_id WHERE projects.id = %(project_id)s + GROUP BY + projects.id, users.name LIMIT 1; """, {"project_id": project_id}, @@ -365,6 +385,14 @@ async def all( WHERE (p.author_id = COALESCE(%(user_id)s, p.author_id)) AND p.name ILIKE %(search)s + + -- Uncomment this if we want to restrict projects before local regulation accepts it + -- AND ( + -- %(user_id)s IS NOT NULL + -- OR p.requires_approval_from_regulator = 'f' + -- OR p.regulator_approval_status = 'APPROVED' + -- ) + GROUP BY p.id ORDER BY p.created_at DESC OFFSET %(skip)s @@ -415,6 +443,11 @@ async def create(db: Connection, project: ProjectIn, user_id: str) -> uuid.UUID: model_dump = project.model_dump( exclude_none=True, exclude=["outline", "centroid"] ) + # NOTE to change the approach here to pass the value + if "regulator_emails" in model_dump.keys(): + model_dump["regulator_approval_status"] = ( + RegulatorApprovalStatus.PENDING.name + ) columns = ", ".join(model_dump.keys()) value_placeholders = ", ".join(f"%({key})s" for key in model_dump.keys()) sql = f""" @@ -530,6 +563,13 @@ class ProjectInfo(BaseModel): outline: Optional[Polygon | Feature | FeatureCollection] no_fly_zones: Optional[Polygon | Feature | FeatureCollection | MultiPolygon] = None requires_approval_from_manager_for_locking: bool + requires_approval_from_regulator: Optional[bool] = False + regulator_emails: Optional[List[EmailStr]] = None + regulator_approval_status: Optional[str] = None + regulator_comment: Optional[str] = None + commenting_regulator_id: Optional[str] = None + author_name: Optional[str] = None + project_area: Optional[float] = None total_task_count: int = 0 tasks: Optional[list[TaskOut]] = [] image_url: Optional[str] = None diff --git a/src/backend/app/users/user_routes.py b/src/backend/app/users/user_routes.py index baa7dcc7..a3d25410 100644 --- a/src/backend/app/users/user_routes.py +++ b/src/backend/app/users/user_routes.py @@ -1,4 +1,6 @@ import os +import base64 +import uuid import jwt from app.users import user_schemas from app.users import user_deps @@ -6,17 +8,13 @@ from fastapi import APIRouter, HTTPException, Depends, Request, BackgroundTasks, Form from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm -from app.users.user_schemas import ( - DbUser, - Token, - UserProfileIn, - AuthUser, -) +from app.users.user_schemas import DbUser, Token, UserProfileIn, AuthUser, Base64Request from app.users.user_deps import login_required, init_google_auth from app.config import settings from app.db import database from app.models.enums import HTTPStatus from psycopg import Connection +from psycopg.rows import class_row from fastapi.responses import JSONResponse from loguru import logger as log from pydantic import EmailStr @@ -262,3 +260,84 @@ async def reset_password( content={"detail": "Your password has been successfully reset!"}, status_code=200, ) + + +@router.post("/regulator/", tags=["Auto Regulator Account Creation"]) +async def regulator_create( + db: Annotated[Connection, Depends(database.get_db)], data: Base64Request +): + """ + Automatically create a regulator account with email and password same as email and with some dummy data + for required fields with role as REGULATOR + """ + try: + token = data.token + email = base64.urlsafe_b64decode(token.encode()).decode() + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + """ + SELECT * FROM users WHERE email_address = %(email)s; + """, + {"email": email}, + ) + user_data = await cur.fetchone() + if not user_data: ## if user is not already present return user data token + sql = """ + INSERT INTO users ( + id, name, email_address, password, is_active, is_superuser, profile_img,date_registered + ) + VALUES ( + %(user_id)s, %(name)s, %(email_address)s, %(password)s, True, False, now(), %(profile_img)s + ) + RETURNING * + """ + async with db.cursor(row_factory=class_row(DbUser)) as cur: + await cur.execute( + sql, + { + "user_id": uuid.uuid4().int, + "name": email, + "email_address": email, + "password": user_logic.get_password_hash(email), + "profile_img": None, + }, + ) + user_data = await cur.fetchone() + + user_profile_sql = """ + INSERT INTO user_profile ( + user_id, role, phone_number, country, city + ) + VALUES ( + %(user_id)s, %(role)s, %(phone_number)s, %(country)s, %(city)s + ) + """ + + async with db.cursor() as cur: + await cur.execute( + user_profile_sql, + { + "user_id": user_data.id, + "role": ["REGULATOR"], + "phone_number": "9866666666", + "country": "Nepal", + "city": "Kathmandu", + }, + ) + user_info = { + "id": user_data.id, + "email": user_data.email_address, + "name": user_data.name, + "profile_img": user_data.profile_img, + "role": "REGULATOR", + } + access_token, refresh_token = await user_logic.create_access_token(user_info) + + return Token( + access_token=access_token, refresh_token=refresh_token, role="REGULATOR" + ) + except Exception as e: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"An error occurred: {str(e)}", + ) diff --git a/src/backend/app/users/user_schemas.py b/src/backend/app/users/user_schemas.py index e2ab854d..20ab4342 100644 --- a/src/backend/app/users/user_schemas.py +++ b/src/backend/app/users/user_schemas.py @@ -1,4 +1,5 @@ import uuid +import base64 from app.models.enums import HTTPStatus, State, UserRole from pydantic import BaseModel, EmailStr, ValidationInfo, Field from pydantic.functional_validators import field_validator @@ -342,3 +343,15 @@ async def get_requested_user_id( if result is None: raise ValueError("No user requested for mapping") return result["user_id"] + + +class Base64Request(BaseModel): + token: str + + @field_validator("token") + def validate_base64(cls, value: str) -> str: + try: + base64.b64decode(value, validate=True) + return value + except Exception: + raise ValueError("Invalid Base64 string") diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py index cc347631..bcc54154 100644 --- a/src/backend/app/utils.py +++ b/src/backend/app/utils.py @@ -3,6 +3,7 @@ import requests import shapely import json +import base64 from datetime import datetime, timezone from typing import Optional, Union, Any from geojson_pydantic import Feature, MultiPolygon, Polygon @@ -527,3 +528,26 @@ def geojson_to_kml(geojson_data: dict) -> str: kml_output.append("") return "\n".join(kml_output) + + +async def send_project_approval_email_to_regulator( + emails: list, project_id: str, creator_name: str, project_name: str +): + for email in emails: + encoded_email = base64.urlsafe_b64encode(email.encode()).decode() + project_link = f"{settings.FRONTEND_URL}/projects/{project_id}/approval/?token={encoded_email}" + context = { + "project_link": project_link, + "project_name": project_name, + "creator_name": creator_name, + } + + html_content = render_email_template( + "regulator", "approval_request.html", context + ) + + await send_notification_email( + email_to=email, + subject="Project Review Request for Drone Operations Approval", + html_content=html_content, + ) diff --git a/src/frontend/package.json b/src/frontend/package.json index 81213b8c..5ee58cd9 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -23,6 +23,7 @@ "axios": "^1.3.4", "class-variance-authority": "^0.6.1", "clsx": "^2.0.0", + "countries-list": "^3.1.1", "date-fns": "^2.30.0", "dom-to-code": "^1.5.4", "dotenv": "^16.0.3", diff --git a/src/frontend/src/components/CompleteUserProfile/FormContents/BasicDetails/index.tsx b/src/frontend/src/components/CompleteUserProfile/FormContents/BasicDetails/index.tsx index 1975f21c..95bf70ae 100644 --- a/src/frontend/src/components/CompleteUserProfile/FormContents/BasicDetails/index.tsx +++ b/src/frontend/src/components/CompleteUserProfile/FormContents/BasicDetails/index.tsx @@ -1,15 +1,21 @@ import { Flex, FlexColumn } from '@Components/common/Layouts'; import { FormControl, Select, Input, Label } from '@Components/common/FormUI'; import ErrorMessage from '@Components/common/ErrorMessage'; -import { countriesWithPhoneCodes } from '@Constants/countryCode'; import { Controller } from 'react-hook-form'; import { getLocalStorageValue } from '@Utils/getLocalStorageValue'; +import { countries } from 'countries-list'; export default function BasicDetails({ formProps }: { formProps: any }) { const { register, formState, control } = formProps; const userProfile = getLocalStorageValue('userprofile'); + // eslint-disable-next-line no-unused-vars + const countryList = Object.entries(countries).map(([_, value]) => ({ + name: value?.name, + phone: value?.phone?.[0], + })); + return (
@@ -42,10 +48,11 @@ export default function BasicDetails({ formProps }: { formProps: any }) { }} render={({ field: { value, onChange } }) => (