diff --git a/backend/api/comments/resources.py b/backend/api/comments/resources.py index dcc40b9944..0d05f6727d 100644 --- a/backend/api/comments/resources.py +++ b/backend/api/comments/resources.py @@ -83,7 +83,6 @@ async def post( message=message, user_id=user.id, project_id=project_id, - # timestamp=datetime.now(), timestamp=datetime.utcnow(), username=user.username, ) diff --git a/backend/api/organisations/resources.py b/backend/api/organisations/resources.py index cfac010ea1..8748e52cc8 100644 --- a/backend/api/organisations/resources.py +++ b/backend/api/organisations/resources.py @@ -1,5 +1,5 @@ from databases import Database -from fastapi import APIRouter, Depends, Query, Request +from fastapi import APIRouter, Depends, Query, Request, Path from fastapi.responses import JSONResponse, Response from loguru import logger @@ -28,89 +28,52 @@ ) -@router.get("/{organisation_id}/", response_model=OrganisationDTO) -async def retrieve_organisation( - request: Request, - organisation_id: int, +async def get_organisation_by_identifier( + identifier: str = Path(..., description="Either organisation ID or slug"), db: Database = Depends(get_db), omit_managers: bool = Query( False, alias="omitManagerList", description="Omit organization managers list from the response.", ), -): - """ - Retrieves an organisation - --- - tags: - - organisations - produces: - - application/json - parameters: - - in: header - name: Authorization - description: Base64 encoded session token - type: string - default: Token sessionTokenHere== - - name: organisation_id - in: path - description: The unique organisation ID - required: true - type: integer - default: 1 - - in: query - name: omitManagerList - type: boolean - description: Set it to true if you don't want the managers list on the response. - default: False - responses: - 200: - description: Organisation found - 401: - description: Unauthorized - Invalid credentials - 404: - description: Organisation not found - 500: - description: Internal Server Error - """ + request: Request = None, +) -> OrganisationDTO: authenticated_user_id = request.user.display_name if request.user else None - if authenticated_user_id is None: - user_id = 0 - else: - user_id = authenticated_user_id - organisation_dto = await OrganisationService.get_organisation_by_id_as_dto( - organisation_id, user_id, omit_managers, db - ) + user_id = authenticated_user_id or 0 + + try: + organisation_id = int(identifier) + organisation_dto = await OrganisationService.get_organisation_by_id_as_dto( + organisation_id, user_id, omit_managers, db + ) + except ValueError: + organisation_dto = await OrganisationService.get_organisation_by_slug_as_dto( + identifier, user_id, omit_managers, db + ) + + if not organisation_dto: + return JSONResponse( + content={"Error": "Organisation not found."}, status_code=404 + ) + return organisation_dto -@router.get("/{slug}/", response_model=OrganisationDTO) -async def retrieve_organisation_by_slug( - request: Request, - slug: str, - db: Database = Depends(get_db), - omit_managers: bool = Query( - True, - alias="omitManagerList", - description="Omit organization managers list from the response.", - ), +@router.get("/{identifier}/", response_model=OrganisationDTO) +async def retrieve_organisation( + organisation: OrganisationDTO = Depends(get_organisation_by_identifier), ): """ - Retrieves an organisation + Retrieve an organisation by either ID or slug. --- tags: - organisations produces: - application/json parameters: - - in: header - name: Authorization - description: Base64 encoded session token - type: string - default: Token sessionTokenHere== - - name: slug - in: path - description: The unique organisation slug + - in: path + name: identifier + description: The organisation ID or slug required: true type: string default: hot @@ -127,15 +90,7 @@ async def retrieve_organisation_by_slug( 500: description: Internal Server Error """ - authenticated_user_id = request.user.display_name if request.user else None - if authenticated_user_id is None: - user_id = 0 - else: - user_id = authenticated_user_id - organisation_dto = await OrganisationService.get_organisation_by_slug_as_dto( - slug, user_id, omit_managers, db - ) - return organisation_dto + return organisation @router.post("/") diff --git a/backend/models/dtos/interests_dto.py b/backend/models/dtos/interests_dto.py index c2ea00dcae..b0d900b953 100644 --- a/backend/models/dtos/interests_dto.py +++ b/backend/models/dtos/interests_dto.py @@ -30,11 +30,6 @@ class Config: populate_by_name = True -class ListInterestDTO(BaseModel): - id: Optional[int] = None - name: Optional[str] = Field(default=None, min_length=1) - - class InterestsListDTO(BaseModel): """DTO for a list of interests.""" diff --git a/backend/models/dtos/message_dto.py b/backend/models/dtos/message_dto.py index 715661938e..b17675515e 100644 --- a/backend/models/dtos/message_dto.py +++ b/backend/models/dtos/message_dto.py @@ -42,10 +42,10 @@ class Config: class ChatMessageDTO(BaseModel): """DTO describing an individual project chat message""" - id: Optional[int] = Field(None, alias="id", serialize_when_none=False) + id: Optional[int] = Field(None, alias="id") message: str = Field(required=True) - user_id: int = Field(required=True, serialize_when_none=False) - project_id: int = Field(required=True, serialize_when_none=False) + user_id: int = Field(required=True) + project_id: int = Field(required=True) picture_url: str = Field(default=None, alias="pictureUrl") timestamp: datetime username: str diff --git a/backend/models/postgis/project.py b/backend/models/postgis/project.py index 2adfb59c5c..156ea40898 100644 --- a/backend/models/postgis/project.py +++ b/backend/models/postgis/project.py @@ -36,7 +36,7 @@ from backend.db import Base from backend.exceptions import NotFound from backend.models.dtos.campaign_dto import CampaignDTO, ListCampaignDTO -from backend.models.dtos.interests_dto import ListInterestDTO +from backend.models.dtos.interests_dto import InterestDTO from backend.models.dtos.project_dto import ( CustomEditorDTO, DraftProjectDTO, @@ -1786,7 +1786,7 @@ async def get_project_and_base_dto(project_id: int, db: Database) -> ProjectDTO: WHERE pi.project_id = :project_id """ interests = await db.fetch_all(interests_query, {"project_id": project_id}) - project_dto.interests = [ListInterestDTO(**i) for i in interests] + project_dto.interests = [InterestDTO(**i) for i in interests] return project_dto @staticmethod diff --git a/backend/models/postgis/utils.py b/backend/models/postgis/utils.py index c0a12b180d..21fe2e5a56 100644 --- a/backend/models/postgis/utils.py +++ b/backend/models/postgis/utils.py @@ -135,6 +135,7 @@ class ST_Y(GenericFunction): def timestamp(): """Used in SQL Alchemy models to ensure we refresh timestamp when new models initialised""" return datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + # return datetime.datetime.now(datetime.timezone.utc) # Based on https://stackoverflow.com/a/51916936 diff --git a/backend/services/interests_service.py b/backend/services/interests_service.py index 84d0ec0e7e..0f1cb6945f 100644 --- a/backend/services/interests_service.py +++ b/backend/services/interests_service.py @@ -4,7 +4,6 @@ InterestRateListDTO, InterestsListDTO, InterestDTO, - ListInterestDTO, ) from backend.models.postgis.interests import ( Interest, @@ -142,11 +141,8 @@ async def get_user_interests(user_id, db: Database) -> InterestsListDTO: WHERE ui.user_id = :user_id """ rows = await db.fetch_all(query, {"user_id": user_id}) - dto = InterestsListDTO() - dto.interests = [ - ListInterestDTO(id=row["id"], name=row["name"]) for row in rows - ] + dto.interests = [InterestDTO(id=row["id"], name=row["name"]) for row in rows] return dto @staticmethod diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index 6d22a824ff..5c57c1ce07 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -46,7 +46,7 @@ def __init__(self, message): class MessageService: @staticmethod - def send_welcome_message(user: User): + async def send_welcome_message(user: User, db: Database): """Sends welcome message to new user at Sign up""" org_code = settings.ORG_CODE text_template = get_txt_template("welcome_message_en.txt") @@ -65,9 +65,7 @@ def send_welcome_message(user: User): welcome_message.to_user_id = user.id welcome_message.subject = "Welcome to the {} Tasking Manager".format(org_code) welcome_message.message = text_template - welcome_message.save() - - return welcome_message.id + await Message.save(welcome_message, db) @staticmethod async def send_message_after_validation( diff --git a/backend/services/messaging/smtp_service.py b/backend/services/messaging/smtp_service.py index c703491694..e7bdbe5c0a 100644 --- a/backend/services/messaging/smtp_service.py +++ b/backend/services/messaging/smtp_service.py @@ -27,7 +27,6 @@ async def send_verification_email(to_address: str, username: str): "VERIFICATION_LINK": verification_url, } html_template = get_template("email_verification_en.html", values) - subject = "Confirm your email address" await SMTPService._send_message(to_address, subject, html_template) return True @@ -178,7 +177,7 @@ def send_email_alert( return True @staticmethod - def _send_message( + async def _send_message( to_address: str, subject: str, html_message: str, text_message: str = None ): """Helper sends SMTP message""" @@ -194,9 +193,10 @@ def _send_message( logger.debug(f"Sending email via SMTP {to_address}") if settings.LOG_LEVEL == "DEBUG": logger.debug(msg.as_string()) + else: try: - mail.send_message(msg) + await mail.send_message(msg) logger.debug(f"Email sent {to_address}") except Exception as e: # ERROR level logs are automatically captured by sentry so that admins are notified diff --git a/backend/services/users/authentication_service.py b/backend/services/users/authentication_service.py index 7697efa38b..d89dc82109 100644 --- a/backend/services/users/authentication_service.py +++ b/backend/services/users/authentication_service.py @@ -140,10 +140,11 @@ async def login_user(osm_user_details, email, db, user_element="user") -> dict: # User not found, so must be new user changesets = osm_user.get("changesets") changeset_count = int(changesets.get("count")) - new_user = UserService.register_user( - osm_id, username, changeset_count, user_picture, email - ) - MessageService.send_welcome_message(new_user) + async with db.transaction(): + new_user = await UserService.register_user( + osm_id, username, changeset_count, user_picture, email, db + ) + await MessageService.send_welcome_message(new_user, db) session_token = AuthenticationService.generate_session_token_for_user(osm_id) return { diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py index 991c91308e..95465a7a39 100644 --- a/backend/services/users/user_service.py +++ b/backend/services/users/user_service.py @@ -3,16 +3,7 @@ import datetime from loguru import logger from sqlalchemy.sql import outerjoin -from sqlalchemy import ( - func, - or_, - desc, - and_, - distinct, - cast, - Time, - select, -) +from sqlalchemy import func, or_, desc, and_, distinct, cast, Time, select, insert from databases import Database from backend.exceptions import NotFound @@ -32,7 +23,6 @@ from backend.models.dtos.interests_dto import ( InterestsListDTO, InterestDTO, - ListInterestDTO, ) from backend.models.postgis.interests import Interest, project_interests from backend.models.postgis.message import MessageType @@ -175,33 +165,58 @@ async def get_projects_mapped(user_id: int, db: Database): return projects_mapped @staticmethod - def register_user(osm_id, username, changeset_count, picture_url, email): + async def register_user(osm_id, username, changeset_count, picture_url, email, db): """ Creates user in DB :param osm_id: Unique OSM user id :param username: OSM Username :param changeset_count: OSM changeset count """ - new_user = User() - new_user.id = osm_id - new_user.username = username - if picture_url is not None: - new_user.picture_url = picture_url - + """ + Creates user in DB + :param osm_id: Unique OSM user id + :param username: OSM Username + :param changeset_count: OSM changeset count + """ + # Determine mapping level based on changeset count intermediate_level = settings.MAPPER_LEVEL_INTERMEDIATE advanced_level = settings.MAPPER_LEVEL_ADVANCED if changeset_count > advanced_level: - new_user.mapping_level = MappingLevel.ADVANCED.value - elif intermediate_level < changeset_count < advanced_level: - new_user.mapping_level = MappingLevel.INTERMEDIATE.value + mapping_level = MappingLevel.ADVANCED.value + elif intermediate_level < changeset_count <= advanced_level: + mapping_level = MappingLevel.INTERMEDIATE.value else: - new_user.mapping_level = MappingLevel.BEGINNER.value - - if email is not None: - new_user.email_address = email - - new_user.create() + mapping_level = MappingLevel.BEGINNER.value + + values = { + "id": osm_id, + "username": username, + "role": 0, + "mapping_level": mapping_level, + "tasks_mapped": 0, + "tasks_validated": 0, + "tasks_invalidated": 0, + "projects_mapped": [], + "email_address": email, + "is_email_verified": False, + "is_expert": False, + "picture_url": picture_url, + "default_editor": "ID", + "mentions_notifications": True, + "projects_comments_notifications": False, + "projects_notifications": True, + "tasks_notifications": True, + "tasks_comments_notifications": False, + "teams_announcement_notifications": True, + "date_registered": datetime.datetime.utcnow(), + } + + query = insert(User).values(values) + await db.execute(query) + + user_query = select(User).where(User.id == osm_id) + new_user = await db.fetch_one(user_query) return new_user @staticmethod @@ -542,7 +557,6 @@ async def update_user_details( """Update user with info supplied by user, if they add or change their email address a verification mail will be sent""" user = await UserService.get_user_by_id(user_id, db) - verification_email_sent = False if ( user_dto.email_address @@ -1000,12 +1014,9 @@ async def get_interests(user: User, db: Database) -> InterestsListDTO: """ interests = await db.fetch_all(query) interest_list_dto = InterestsListDTO() - for interest in interests: - int_dto = ListInterestDTO(**interest) - - if interest in user.interests: + int_dto = InterestDTO(**interest) + if interest.name in user.interests: int_dto.user_selected = True interest_list_dto.interests.append(int_dto) - return interest_list_dto