diff --git a/app/api/bit_extension.py b/app/api/bit_extension.py index 2ef2304..9b24759 100644 --- a/app/api/bit_extension.py +++ b/app/api/bit_extension.py @@ -14,6 +14,9 @@ api.namespaces.clear() # Adding namespaces +from app.api.resources.mentorship_relation import mentorship_relation_ns as mentorship_relation_namespace +api.add_namespace(mentorship_relation_namespace, path="/") + from app.api.resources.users import users_ns as user_namespace api.add_namespace(user_namespace, path="/") diff --git a/app/api/dao/mentorship_relation.py b/app/api/dao/mentorship_relation.py new file mode 100644 index 0000000..c206f38 --- /dev/null +++ b/app/api/dao/mentorship_relation.py @@ -0,0 +1,121 @@ +from datetime import datetime, timedelta +from typing import Dict +from http import HTTPStatus +from app import messages +from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel +from app.database.models.bit_schema.user_extension import UserExtensionModel +from app.utils.decorator_utils import email_verification_required +from app.utils.enum_utils import MentorshipRelationState + + +class MentorshipRelationDAO: + """Data Access Object for mentorship relation functionalities. + + Provides various functions pertaining to mentorship. + + Attributes: + MAXIMUM_MENTORSHIP_DURATION + MINIMUM_MENTORSHIP_DURATION + """ + + MAXIMUM_MENTORSHIP_DURATION = timedelta(weeks=24) # 6 months = approximately 6*4 + MINIMUM_MENTORSHIP_DURATION = timedelta(weeks=4) + + def create_mentorship_relation(self, user_id: int, data: Dict[str, str]): + """Creates a relationship between two users. + + Establishes the mentor-mentee relationship. + + Args: + user_id: ID of the user initiating this request. Has to be either the mentor or the mentee. + data: List containing the mentor_id, mentee_id, end_date_timestamp and notes. + + Returns: + message: A message corresponding to the completed action; success if mentorship relationship is established, failure if otherwise. + """ + action_user_id = user_id + mentor_id = data["mentor_id"] + mentee_id = data["mentee_id"] + end_date_timestamp = data["end_date"] + notes = data["notes"] + + # user_id has to match either mentee_id or mentor_id + is_valid_user_ids = action_user_id == mentor_id or action_user_id == mentee_id + if not is_valid_user_ids: + return messages.MATCH_EITHER_MENTOR_OR_MENTEE, HTTPStatus.BAD_REQUEST + + # mentor_id has to be different from mentee_id + if mentor_id == mentee_id: + return messages.MENTOR_ID_SAME_AS_MENTEE_ID, HTTPStatus.BAD_REQUEST + + try: + end_date_datetime = datetime.fromtimestamp(end_date_timestamp) + except ValueError: + return messages.INVALID_END_DATE, HTTPStatus.BAD_REQUEST + + now_datetime = datetime.now() + if end_date_datetime < now_datetime: + return messages.END_TIME_BEFORE_PRESENT, HTTPStatus.BAD_REQUEST + + # business logic constraints + + max_relation_duration = end_date_datetime - now_datetime + if max_relation_duration > self.MAXIMUM_MENTORSHIP_DURATION: + return messages.MENTOR_TIME_GREATER_THAN_MAX_TIME, HTTPStatus.BAD_REQUEST + + if max_relation_duration < self.MINIMUM_MENTORSHIP_DURATION: + return messages.MENTOR_TIME_LESS_THAN_MIN_TIME, HTTPStatus.BAD_REQUEST + + # validate if mentor user exists + mentor_user = UserModel.find_by_id(mentor_id) + if mentor_user is None: + return messages.MENTOR_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND + + # validate if mentor is available to mentor + if not mentor_user.available_to_mentor: + return messages.MENTOR_NOT_AVAILABLE_TO_MENTOR, HTTPStatus.BAD_REQUEST + + # validate if mentee user exists + mentee_user = UserModel.find_by_id(mentee_id) + if mentee_user is None: + return messages.MENTEE_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND + + # validate if mentee is wants to be mentored + if not mentee_user.need_mentoring: + return messages.MENTEE_NOT_AVAIL_TO_BE_MENTORED, HTTPStatus.BAD_REQUEST + + # TODO add tests for this portion + + all_mentor_relations = ( + mentor_user.mentor_relations + mentor_user.mentee_relations + ) + for relation in all_mentor_relations: + if relation.state == MentorshipRelationState.ACCEPTED: + return messages.MENTOR_ALREADY_IN_A_RELATION, HTTPStatus.BAD_REQUEST + + all_mentee_relations = ( + mentee_user.mentor_relations + mentee_user.mentee_relations + ) + for relation in all_mentee_relations: + if relation.state == MentorshipRelationState.ACCEPTED: + return messages.MENTEE_ALREADY_IN_A_RELATION, HTTPStatus.BAD_REQUEST + + # All validations were checked + + tasks_list = TasksListModel() + tasks_list.save_to_db() + + mentorship_relation = MentorshipRelationModel( + action_user_id=action_user_id, + mentor_user=mentor_user, + mentee_user=mentee_user, + creation_date=datetime.now().timestamp(), + end_date=end_date_timestamp, + state=MentorshipRelationState.PENDING, + notes=notes, + tasks_list=tasks_list, + ) + + mentorship_relation.save_to_db() + + return messages.MENTORSHIP_RELATION_WAS_SENT_SUCCESSFULLY, HTTPStatus.CREATED diff --git a/app/api/models/mentorship_relation.py b/app/api/models/mentorship_relation.py new file mode 100644 index 0000000..2c96f08 --- /dev/null +++ b/app/api/models/mentorship_relation.py @@ -0,0 +1,56 @@ +from flask_restx import fields, Model + +from app.utils.enum_utils import MentorshipRelationState + + +def add_models_to_namespace(api_namespace): + api_namespace.models[ + mentorship_request_response_body.name + ] = mentorship_request_response_body + api_namespace.models[relation_user_response_body.name] = relation_user_response_body + +relation_user_response_body = Model( + "User", + { + "id": fields.Integer(required=True, description="User ID"), + "name": fields.String(required=True, description="User name"), + }, +) + +mentorship_request_response_body = Model( + "List mentorship relation request model", + { + "id": fields.Integer(required=True, description="Mentorship relation ID"), + "action_user_id": fields.Integer( + required=True, description="Mentorship relation requester user ID" + ), + "sent_by_me": fields.Boolean( + required=True, + description="Mentorship relation sent by current user indication", + ), + "mentor": fields.Nested(relation_user_response_body), + "mentee": fields.Nested(relation_user_response_body), + "creation_date": fields.Float( + required=True, + description="Mentorship relation creation date in UNIX timestamp format", + ), + "accept_date": fields.Float( + required=True, + description="Mentorship relation acceptance date in UNIX timestamp format", + ), + "start_date": fields.Float( + required=True, + description="Mentorship relation start date in UNIX timestamp format", + ), + "end_date": fields.Float( + required=True, + description="Mentorship relation end date in UNIX timestamp format", + ), + "state": fields.Integer( + required=True, + enum=MentorshipRelationState.values, + description="Mentorship relation state", + ), + "notes": fields.String(required=True, description="Mentorship relation notes"), + }, +) diff --git a/app/api/resources/mentorship_relation.py b/app/api/resources/mentorship_relation.py new file mode 100644 index 0000000..a335fef --- /dev/null +++ b/app/api/resources/mentorship_relation.py @@ -0,0 +1,78 @@ +import ast +import json +from flask import request +from flask_restx import Resource, Namespace, marshal +from flask_jwt_extended import jwt_required, get_jwt_identity +from http import HTTPStatus +from app import messages +from app.api.request_api_utils import ( + post_request, + get_request, + put_request, + http_response_checker, + AUTH_COOKIE, + validate_token) +from app.api.resources.common import auth_header_parser +from app.api.dao.mentorship_relation import MentorshipRelationDAO +from app.api.dao.user_extension import UserExtensionDAO +from app.api.models.mentorship_relation import * +from app.utils.validation_utils import get_length_validation_error_message +from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel +from app.utils.ms_constants import DEFAULT_PAGE, DEFAULT_USERS_PER_PAGE + +mentorship_relation_ns = Namespace( + "Mentorship Relation", + description="Operations related to " "mentorship relations " "between users", +) +add_models_to_namespace(mentorship_relation_ns) + +DAO = MentorshipRelationDAO() +UserExtensionDAO = UserExtensionDAO() + + +@mentorship_relation_ns.route("mentorship_relations") +class GetAllMyMentorshipRelation(Resource): + @classmethod + @jwt_required + @mentorship_relation_ns.doc("get_all_user_mentorship_relations") + @mentorship_relation_ns.expect(auth_header_parser) + @mentorship_relation_ns.param( + name="relation_state", + description="Mentorship relation state filter.", + _in="query", + ) + @mentorship_relation_ns.response( + HTTPStatus.OK, + "Return all user's mentorship relations, filtered by the relation state, was successfully.", + model=mentorship_request_response_body, + ) + @mentorship_relation_ns.response( + HTTPStatus.UNAUTHORIZED, + f"{messages.TOKEN_HAS_EXPIRED}\n" + f"{messages.TOKEN_IS_INVALID}\n" + f"{messages.AUTHORISATION_TOKEN_IS_MISSING}" + ) + @mentorship_relation_ns.marshal_list_with(mentorship_request_response_body) + def get(cls): + """ + Lists all mentorship relations of current user. + + Input: + 1. Header: valid access token + + Returns: + JSON array containing user's relations as objects. + """ + + user_id = get_jwt_identity() + rel_state_param = request.args + rel_state_filter = None + + if rel_state_param: + rel_state_filter = rel_state_param["relation_state"].upper() + + response = DAO.list_mentorship_relations( + user_id=user_id, state=rel_state_filter + ) + + return response