diff --git a/app/api/bit_extension.py b/app/api/bit_extension.py index 74d0fe7..21d1951 100644 --- a/app/api/bit_extension.py +++ b/app/api/bit_extension.py @@ -19,3 +19,6 @@ from app.api.resources.organizations import organizations_ns as organization_namespace api.add_namespace(organization_namespace, path="/") + +from app.api.resources.mentorship_relation import mentorship_relation_ns as mentorship_relation_namespace +api.add_namespace(mentorship_relation_namespace, path="/") diff --git a/app/api/dao/mentorship_relation_extension.py b/app/api/dao/mentorship_relation_extension.py new file mode 100644 index 0000000..e9b821d --- /dev/null +++ b/app/api/dao/mentorship_relation_extension.py @@ -0,0 +1,37 @@ +import ast +from http import HTTPStatus +from flask import json +from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel +from app import messages +from app.api.request_api_utils import AUTH_COOKIE +from app.utils.bitschema_utils import Timezone + + +class MentorshipRelationExtensionDAO: + + """Data Access Object for Users_Extension functionalities""" + + @staticmethod + def createMentorshipRelationExtension(program_id, mentorship_relation_id , mentee_request_date): + """Creates the extending mentorship relation between organization's program and the user which is logged in. + + Arguments: + organization_id: The ID organization + program_id: The ID of program + + Returns: + A dictionary containing "message" which indicates whether or not + the relation was created successfully and "code" for the HTTP response code. + """ + + try: + mentorship_relation_extension_object = MentorshipRelationExtensionModel(program_id,mentorship_relation_id) + mentorship_relation_extension_object.mentee_request_date = mentee_request_date + mentorship_relation_extension_object.save_to_db() + return messages.MENTORSHIP_RELATION_WAS_SENT_SUCCESSFULLY, HTTPStatus.CREATED + except: + return messages.INTERNAL_SERVER_ERROR, HTTPStatus.BAD_REQUEST + + + + \ No newline at end of file diff --git a/app/api/models/mentorship_relation.py b/app/api/models/mentorship_relation.py new file mode 100644 index 0000000..7fc0459 --- /dev/null +++ b/app/api/models/mentorship_relation.py @@ -0,0 +1,26 @@ +from flask_restx import fields, Model + +from app.utils.enum_utils import MentorshipRelationState + + +def add_models_to_namespace(api_namespace): + api_namespace.models[send_mentorship_extension_request_body.name] = send_mentorship_extension_request_body + + +send_mentorship_extension_request_body = Model( + "Send mentorship relation request to organziation model", + { + "mentee_id": fields.Integer( + required=True, description="Mentorship relation mentee ID" + ), + "mentee_request_date": fields.Float( + required=True, description="Mentorship relation mentee_request_date in UNIX timestamp format" + ), + "end_date": fields.Float( + required=True, + description="Mentorship relation end date in UNIX timestamp format", + ), + "notes": fields.String(required=True, description="Mentorship relation notes"), + + }, +) diff --git a/app/api/request_api_utils.py b/app/api/request_api_utils.py index 5b798ef..d0782ea 100644 --- a/app/api/request_api_utils.py +++ b/app/api/request_api_utils.py @@ -47,6 +47,37 @@ def post_request(request_string, data): return response_message, response_code +def post_request_with_token(request_string, token, data): + request_url = f"{BASE_MS_API_URL}{request_string}" + is_wrong_token = validate_token(token) + + if not is_wrong_token: + try: + response = requests.post( + request_url, + json=data, + headers={"Authorization": AUTH_COOKIE["Authorization"].value, "Accept": "application/json"}, + ) + response.raise_for_status() + response_message = response.json() + response_code = response.status_code + except requests.exceptions.ConnectionError as e: + response_message = messages.INTERNAL_SERVER_ERROR + response_code = json.dumps(HTTPStatus.INTERNAL_SERVER_ERROR) + logging.fatal(f"{e}") + except requests.exceptions.HTTPError as e: + response_message = e.response.json() + response_code = e.response.status_code + except Exception as e: + response_message = messages.INTERNAL_SERVER_ERROR + response_code = json.dumps(HTTPStatus.INTERNAL_SERVER_ERROR) + logging.fatal(f"{e}") + finally: + logging.fatal(f"{response_message}") + return response_message, response_code + return is_wrong_token + + def get_user(token): request_url = "/user" return get_request(request_url, token, params=None) diff --git a/app/api/resources/mentorship_relation.py b/app/api/resources/mentorship_relation.py new file mode 100644 index 0000000..10bfa21 --- /dev/null +++ b/app/api/resources/mentorship_relation.py @@ -0,0 +1,154 @@ +import ast +import json +from http import HTTPStatus, cookies +from datetime import datetime, timedelta +from flask import request +from flask_restx import Resource, marshal, Namespace +from app import messages +from app.api.request_api_utils import ( + post_request, + post_request_with_token, + get_request, + put_request, + http_response_checker, + AUTH_COOKIE, + validate_token) +# Common Resources +from app.api.resources.common import auth_header_parser +# Validations +from app.api.validations.user import * +from app.api.validations.task_comment import ( + validate_task_comment_request_data, + COMMENT_MAX_LENGTH, +) +from app.api.validations.mentorship_relation import org_mentee_req_body_is_valid_data +from app.utils.validation_utils import get_length_validation_error_message,expected_fields_validator +from app.utils.ms_constants import DEFAULT_PAGE, DEFAULT_USERS_PER_PAGE +# Namespace Models +from app.api.models.mentorship_relation import * +# Databases Models +from app.database.models.bit_schema.user_extension import UserExtensionModel +from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel +from app.database.models.bit_schema.organization import OrganizationModel +from app.database.models.bit_schema.program import ProgramModel +# DAOs +from app.api.dao.user_extension import UserExtensionDAO +from app.api.dao.personal_background import PersonalBackgroundDAO +from app.api.dao.mentorship_relation_extension import MentorshipRelationExtensionDAO +from app.api.dao.organization import OrganizationDAO +from app.api.dao.program import ProgramDAO + +mentorship_relation_ns = Namespace( + "Mentorship Relation", + description="Operations related to " "mentorship relations " "between users", +) +add_models_to_namespace(mentorship_relation_ns) + +mentorshipRelationExtensionDAO = MentorshipRelationExtensionDAO() +userExtensionDAO = UserExtensionDAO() +OrganizationDAO = OrganizationDAO() +ProgramDAO = ProgramDAO() + +@mentorship_relation_ns.route("organizations//programs//send_request") +class ApplyForProgram(Resource): + @classmethod + @mentorship_relation_ns.doc("send_request") + @mentorship_relation_ns.expect(auth_header_parser, send_mentorship_extension_request_body) + @mentorship_relation_ns.response( + HTTPStatus.CREATED, f"{messages.MENTORSHIP_RELATION_WAS_SENT_SUCCESSFULLY}" + ) + @mentorship_relation_ns.response( + HTTPStatus.BAD_REQUEST, + f"{messages.NO_DATA_WAS_SENT}\n" + f"{messages.MATCH_EITHER_MENTOR_OR_MENTEE}\n" + f"{messages.MENTOR_ID_SAME_AS_MENTEE_ID}\n" + f"{messages.END_TIME_BEFORE_PRESENT}\n" + f"{messages.MENTOR_TIME_GREATER_THAN_MAX_TIME}\n" + f"{messages.MENTOR_TIME_LESS_THAN_MIN_TIME}\n" + f"{messages.MENTOR_NOT_AVAILABLE_TO_MENTOR}\n" + f"{messages.MENTEE_NOT_AVAIL_TO_BE_MENTORED}\n" + f"{messages.MENTOR_ALREADY_IN_A_RELATION}\n" + f"{messages.MENTEE_ALREADY_IN_A_RELATION}\n" + f"{messages.MENTOR_ID_FIELD_IS_MISSING}\n" + f"{messages.MENTEE_ID_FIELD_IS_MISSING}\n" + f"{messages.NOTES_FIELD_IS_MISSING}\n" + f"{messages.ORGANIZATION_DOES_NOT_EXIST}\n" + f"{messages.PROGRAM_DOES_NOT_EXIST}" + ) + @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.response( + HTTPStatus.NOT_FOUND, + f"{messages.MENTOR_DOES_NOT_EXIST}\n" + f"{messages.MENTEE_DOES_NOT_EXIST}" + ) + def post(cls,organization_id,program_id): + """ + Creates a new mentorship relation request. + + Also, sends an email notification to the recipient about new relation request. + + Input: + 1. Header: valid access token + 2. Body: A dict containing + - mentor_request_date,end_date: UNIX timestamp + - notes: description of relation request + + Returns: + Success or failure message. A mentorship request is send to the other + person whose ID is mentioned. The relation appears at /pending endpoint. + """ + + token = request.headers.environ["HTTP_AUTHORIZATION"] + is_wrong_token = validate_token(token) + + if not is_wrong_token: + try: + user_json = (AUTH_COOKIE["user"].value) + user = ast.literal_eval(user_json) + data = request.json + + if not data: + return messages.NO_DATA_WAS_SENT, HTTPStatus.BAD_REQUEST + + is_field_valid = expected_fields_validator(data, send_mentorship_extension_request_body) + if not is_field_valid.get("is_field_valid"): + return is_field_valid.get("message"), HTTPStatus.BAD_REQUEST + + is_valid = org_mentee_req_body_is_valid_data(data) + if is_valid != {}: + return is_valid, HTTPStatus.BAD_REQUEST + + # Checking whether organization exists + organization = OrganizationModel.query.filter_by(id=organization_id).first() + if not organization: + return messages.ORGANIZATION_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND + + # Checking whether program exists + program = ProgramModel.find_by_id(program_id) + if not program or (program.organization_id != organization_id): + return messages.PROGRAM_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND + + mentor_id = organization.rep_id + mentee_id = data['mentee_id'] + + mentorship_relation_data={} + mentorship_relation_data['mentee_id'] = mentee_id + mentorship_relation_data['mentor_id'] = int(mentor_id) + mentorship_relation_data['end_date'] = data['end_date'] + mentorship_relation_data['notes'] = data['notes'] + + response = http_response_checker(post_request_with_token("/mentorship_relation/send_request",token, mentorship_relation_data)) + if response.status_code == 201: + mentorshipRelationId = MentorshipRelationModel.query.filter_by(mentor_id=mentor_id).filter_by(mentee_id=mentee_id).first().id + return MentorshipRelationExtensionDAO.createMentorshipRelationExtension(program_id, mentorshipRelationId ,data['mentee_request_date']) + else: + return response.message, HTTPStatus.BAD_REQUEST + except ValueError as e: + return e, HTTPStatus.BAD_REQUEST + + return is_wrong_token \ No newline at end of file diff --git a/app/api/validations/mentorship_relation.py b/app/api/validations/mentorship_relation.py new file mode 100644 index 0000000..d577f10 --- /dev/null +++ b/app/api/validations/mentorship_relation.py @@ -0,0 +1,14 @@ +from app import messages + +def org_mentee_req_body_is_valid_data(data): + + # Verify if request body has required fields + if "mentee_id" not in data: + return messages.MENTEE_ID_FIELD_IS_MISSING + if "end_date" not in data: + return messages.END_DATE_FIELD_IS_MISSING + if "notes" not in data: + return messages.NOTES_FIELD_IS_MISSING + if "mentee_request_date" not in data: + return messages.MENTEE_REQUEST_DATE_FIELD_IS_MISSING + return {} \ No newline at end of file diff --git a/app/database/models/bit_schema/mentorship_relation_extension.py b/app/database/models/bit_schema/mentorship_relation_extension.py index 7c19033..8a7c1da 100644 --- a/app/database/models/bit_schema/mentorship_relation_extension.py +++ b/app/database/models/bit_schema/mentorship_relation_extension.py @@ -66,6 +66,15 @@ def find_by_id(cls, _id) -> "MentorshipRelationExtensionModel": _id: The id of a mentorship_relations_extension. """ return cls.query.filter_by(id=_id).first() + + @classmethod + def find_by_program_id(cls, _id) -> "MentorshipRelationExtensionModel": + + """Returns the mentorship_relations_extension that has the passed program id. + Args: + _id: The id of a program. + """ + return cls.query.filter_by(program_id=_id).first() def save_to_db(self) -> None: """Saves the model to the database.""" diff --git a/app/database/models/bit_schema/program.py b/app/database/models/bit_schema/program.py index 8c77a08..1f1cc21 100644 --- a/app/database/models/bit_schema/program.py +++ b/app/database/models/bit_schema/program.py @@ -116,7 +116,7 @@ def json(self): def __repr__(self): """Returns the program name, creation/start/end date and organization id.""" return ( - f"Program id is {self.program.id}\n" + f"Program id is {self.id}\n" f"Program name is {self.program_name}.\n" f"Organization's id is {self.organization_id}.\n" f"Program start date is {self.start_date}\n" diff --git a/app/messages.py b/app/messages.py index 2c44e2f..4f8b8e5 100644 --- a/app/messages.py +++ b/app/messages.py @@ -69,6 +69,7 @@ MENTOR_ID_FIELD_IS_MISSING = {"message": "Mentor ID field is missing."} MENTEE_ID_FIELD_IS_MISSING = {"message": "Mentee ID field is missing."} END_DATE_FIELD_IS_MISSING = {"message": "End date field is missing."} +MENTEE_REQUEST_DATE_FIELD_IS_MISSING = {"message": "Mentee request date field is missing."} NOTES_FIELD_IS_MISSING = {"message": "Notes field is missing."} USERNAME_FIELD_IS_MISSING = {"message": "The field username is missing."} PASSWORD_FIELD_IS_MISSING = {"message": "Password field is missing."} @@ -137,6 +138,9 @@ NO_DATA_FOR_UPDATING_PROFILE_WAS_SENT = { "message": "No data for updating profile was sent." } +NO_DATA_WAS_SENT = { + "message": "No data was sent." +} ADDITIONAL_INFORMATION_DOES_NOT_EXIST = { "message": "No additional information found with your data. Please provide them now." } diff --git a/tests/mentorship_relations/__init__.py b/tests/mentorship_relations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/mentorship_relations/test_api_create_mentee_org_relation.py b/tests/mentorship_relations/test_api_create_mentee_org_relation.py new file mode 100644 index 0000000..cf1db32 --- /dev/null +++ b/tests/mentorship_relations/test_api_create_mentee_org_relation.py @@ -0,0 +1,276 @@ +import time +import unittest +from unittest.mock import patch, Mock +from http import HTTPStatus, cookies +import requests +from requests.exceptions import HTTPError +# Flask +from flask import json +from flask_restx import marshal +# app +from app import messages +from app.database.sqlalchemy_extension import db +# Api Models +from app.api.models.user import full_user_api_model, public_user_personal_details_response_model +from app.api.models.organization import get_organization_response_model, update_organization_request_model +from app.api.models.mentorship_relation import send_mentorship_extension_request_body +# Test +from tests.base_test_case import BaseTestCase +from tests.test_data import user1, user2 +# Utils +from app.utils.date_converter import convert_timestamp_to_human_date +from app.api.request_api_utils import post_request, get_request, BASE_MS_API_URL, AUTH_COOKIE +# Database model +## ms schema +from app.database.models.ms_schema.user import UserModel +from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel +from app.database.models.ms_schema.tasks_list import TasksListModel +## bit schema +from app.database.models.bit_schema.user_extension import UserExtensionModel +from app.database.models.bit_schema.organization import OrganizationModel +from app.database.models.bit_schema.program import ProgramModel +from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel + +class TestCreateMenteeOrgRelationByOrgApi(BaseTestCase): + @patch("requests.get") + @patch("requests.post") + def setUp(self, mock_login, mock_get_user): + super(TestCreateMenteeOrgRelationByOrgApi, self).setUp() + # set access expiry 4 weeks from today's date (sc*min*hrrs*days) + access_expiry = time.time() + 60*60*24*28 + success_message = {"access_token": "this is fake token", "access_expiry": access_expiry} + success_code = HTTPStatus.OK + + # Mocking Login + mock_login_response = Mock() + mock_login_response.json.return_value = success_message + mock_login_response.status_code = success_code + mock_login.return_value = mock_login_response + mock_login.raise_for_status = json.dumps(success_code) + + expected_user = marshal(user1, full_user_api_model) + + mock_get_response = Mock() + mock_get_response.json.return_value = expected_user + mock_get_response.status_code = success_code + + mock_get_user.return_value = mock_get_response + mock_get_user.raise_for_status = json.dumps(success_code) + + user_login_success = { + "username": user1.get("username"), + "password": user1.get("password") + } + + with self.client: + login_response = self.client.post( + "/login", + data=json.dumps(user_login_success), + follow_redirects=True, + content_type="application/json", + ) + + # Creating test_user1 + test_user1 = UserModel( + name=user1["name"], + username=user1["username"], + password=user1["password"], + email=user1["email"], + terms_and_conditions_checked=user1["terms_and_conditions_checked"] + ) + test_user1.need_mentoring = user1["need_mentoring"] + test_user1.available_to_mentor = user1["available_to_mentor"] + + test_user1.save_to_db() + + self.test_user1_data = UserModel.find_by_email(test_user1.email) + + AUTH_COOKIE["user"] = marshal(self.test_user1_data, full_user_api_model) + + # Extending test_user1 + test_user1_extension = UserExtensionModel( + user_id=self.test_user1_data.id, + timezone="AUSTRALIA_MELBOURNE" + ) + test_user1_extension.is_organization_rep = True + test_user1_extension.save_to_db() + + # preparing organization + organization = OrganizationModel( + rep_id=self.test_user1_data.id, + name="Company ABC", + email="companyabc@mail.com", + address="506 Elizabeth St, Melbourne VIC 3000, Australia", + website="https://www.ames.net.au", + timezone="AUSTRALIA_MELBOURNE", + ) + organization.rep_department = "H&R Department" + organization.about = "This is about ABC" + organization.phone = "321-456-789" + organization.status = "DRAFT" + # joined one month prior to access date + organization.join_date = time.time() - 60*60*24*7 + + organization.save_to_db() + self.organization_data = OrganizationModel.find_by_representative(self.test_user1_data.id) + + # prepare expected representative object + self.expected_representative = marshal(self.test_user1_data, public_user_personal_details_response_model) + + # set start date one month from now, end date another month after that + start_date = time.time() + 60*60*24*28 + end_date = start_date + 60*60*24*28 + creation_date = start_date - 60*60*24*14 + program = ProgramModel( + program_name="Program A", + start_date=start_date, + end_date=end_date, + organization_id=self.organization_data, + ) + program.description = "This is about Program A." + program.target_skills = ["Python", "Ruby", "React"] + program.target_candidate = { + "target_candidate_gender": "Other", + "target_candidate_age": "Not Applicable", + "target_candidate_ethnicity": "Not Applicable", + "target_candidate_sexual_orientation": "Not Applicable", + "target_candidate_religion": "Not Applicable", + "target_candidate_physical_ability": "Not Applicable", + "target_candidate_mental_ability": "Not Applicable", + "target_candidate_socio_economic": "Not Applicable", + "target_candidate_highest_education": "Not Applicable", + "target_candidate_years_of_experience": "Not Applicable", + "target_candidate_other": "Non-Binary", + } + program.contact_type = "REMOTE" + program.zone = "GLOBAL" + program.status = "DRAFT" + program.creation_date = creation_date + program.save_to_db() + + self.program_data = ProgramModel.find_by_name("Program A") + + # mentee user + test_user2 = UserModel( + name=user2["name"], + username=user2["username"], + password=user2["password"], + email=user2["email"], + terms_and_conditions_checked=user2["terms_and_conditions_checked"] + ) + test_user2.need_mentoring = user2["need_mentoring"] + test_user2.available_to_mentor = user2["available_to_mentor"] + + test_user2.save_to_db() + self.test_user2_data = UserModel.find_by_email(test_user2.email) + + test_user2_extension = UserExtensionModel( + user_id=self.test_user2_data .id, + timezone="AUSTRALIA_MELBOURNE" + ) + test_user2_extension.is_organization_rep = True + test_user2_extension.save_to_db() + + self.correct_mentee_org_relation_body = { + "mentee_id": self.test_user2_data.id, + "mentee_request_date": time.time() + 60*60*24*7, + "end_date": time.time() + 2*60*60*24*7, + "notes": "Please Add" + } + + @patch("requests.post") + def test_api_dao_create_mentee_org_relation_successfully(self,mock_create_org_mentee_relation): + + success_message = messages.MENTORSHIP_RELATION_WAS_SENT_SUCCESSFULLY + success_code = HTTPStatus.BAD_REQUEST + + mock_response = Mock() + mock_response.json.return_value = success_message + mock_response.status_code = success_code + mock_create_org_mentee_relation.side_effect = requests.exceptions.HTTPError(response=mock_response) + mock_create_org_mentee_relation.return_value = mock_response + mock_create_org_mentee_relation.raise_for_status = json.dumps(success_code) + + with self.client: + response = self.client.post( + f"/organizations/{self.organization_data.id}/programs/{self.program_data.id}/send_request", + headers={"Authorization": AUTH_COOKIE["Authorization"].value}, + data=json.dumps( + dict(self.correct_mentee_org_relation_body) + ), + follow_redirects=True, + content_type="application/json", + ) + + tasks_list = TasksListModel() + tasks_list.save_to_db() + + test_org_mentee_relation = MentorshipRelationModel( + action_user_id = self.test_user1_data.id, + mentor_user = self.test_user1_data, + mentee_user = self.test_user2_data, + creation_date = time.time(), + end_date = self.correct_mentee_org_relation_body['end_date'], + state = 'PENDING', + notes = self.correct_mentee_org_relation_body['notes'], + tasks_list = tasks_list + ) + test_org_mentee_relation.save_to_db() + test_org_mentee_relation_data = MentorshipRelationModel.find_by_id(test_org_mentee_relation.id) + test_org_mentee_relation_extension = MentorshipRelationExtensionModel(self.program_data.id,test_org_mentee_relation_data.id) + test_org_mentee_relation_extension_data = MentorshipRelationExtensionModel.find_by_id(test_org_mentee_relation_extension.id) + + mock_create_org_mentee_relation.assert_called() + self.assertEqual(success_code, response.status_code) + self.assertEqual(response.json,success_message) + + + def test_api_dao_create_mentee_org_relation_invalid_program(self): + + invalid_program_id = 20 + response = self.client.post( + f"/organizations/{self.organization_data.id}/programs/{invalid_program_id}/send_request", + headers={"Authorization": AUTH_COOKIE["Authorization"].value}, + data=json.dumps( + dict(self.correct_mentee_org_relation_body) + ), + follow_redirects=True, + content_type="application/json", + ) + self.assertEqual(HTTPStatus.NOT_FOUND, response.status_code) + self.assertEqual(messages.PROGRAM_DOES_NOT_EXIST , response.json) + + @patch("requests.post") + def test_api_dao_create_mentee_org_relation_mentee_not_found(self,mock_create_org_mentee_relation): + + error_message = messages.MENTEE_DOES_NOT_EXIST + error_code = HTTPStatus.BAD_REQUEST + + mock_response = Mock() + mock_response.json.return_value = error_message + mock_response.status_code = error_code + mock_create_org_mentee_relation.side_effect = requests.exceptions.HTTPError(response=mock_response) + mock_create_org_mentee_relation.return_value = mock_response + mock_create_org_mentee_relation.raise_for_status = json.dumps(error_code) + + incorrect_mentee_org_relation_body = { + "mentee_id": 200, + "mentee_request_date": time.time() + 60*60*24*7, + "end_date": time.time() + 2*60*60*24*7, + "notes": "Please Add" + } + + response = self.client.post( + f"/organizations/{self.organization_data.id}/programs/{self.program_data.id}/send_request", + headers={"Authorization": AUTH_COOKIE["Authorization"].value}, + data=json.dumps( + dict(incorrect_mentee_org_relation_body) + ), + follow_redirects=True, + content_type="application/json", + ) + + self.assertEqual(error_code, response.status_code) + self.assertEqual(error_message, response.json) + +