diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..c68b352 --- /dev/null +++ b/.env.template @@ -0,0 +1,15 @@ +FLASK_ENVIRONMENT_CONFIG = +SECRET_KEY = +SECURITY_PASSWORD_SALT = +MAIL_DEFAULT_SENDER = +MAIL_SERVER = +APP_MAIL_USERNAME = +APP_MAIL_PASSWORD = +MOCK_EMAIL = +FLASK_APP=run.py +DB_TYPE=postgresql +DB_USERNAME= +DB_PASSWORD= +DB_ENDPOINT= +DB_NAME=bit_schema +DB_TEST_NAME=bit_schema_test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f6b593 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# PyCharm project settings +.idea/ + +# vscode +.vscode/ + +*.db + +# aws-eb +.elasticbeanstalk/ diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..af7090b --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: gunicorn run:application \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/bit_extension.py b/app/api/bit_extension.py new file mode 100644 index 0000000..3d0dcc6 --- /dev/null +++ b/app/api/bit_extension.py @@ -0,0 +1,21 @@ +from flask_restx import Api + +api = Api( + title="Bridge In Tech API", + version="1.0", + description="API documentation for the backend of Bridge In Tech. \n \n" + + "Bridge In Tech is an application inspired by the existing AnitaB.org Mentorship System, " + + "It encourages organizations to collaborate with the mentors and mentees on mentoring programs. \n \n" + + "The main repository of the Backend System can be found here: https://github.com/anitab-org/bridge-in-tech-backend \n \n" + + "The Web client for the Mentorship System can be found here: https://github.com/anitab-org/bridge-in-tech-web \n \n" + + "For more information about the project here's a link to our wiki guide: https://github.com/anitab-org/bridge-in-tech-backend/wiki" + # doc='/docs/' +) +api.namespaces.clear() + +# Adding namespaces +from app.api.resources.users import users_ns as user_namespace + +api.add_namespace(user_namespace, path="/") + + diff --git a/app/api/dao/__init__.py b/app/api/dao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/dao/user_extension.py b/app/api/dao/user_extension.py new file mode 100644 index 0000000..1ca4d8e --- /dev/null +++ b/app/api/dao/user_extension.py @@ -0,0 +1,37 @@ +from http import HTTPStatus +from typing import Dict +from sqlalchemy import func +from app.database.models.bit_schema.user_extension import UserExtensionModel +from app import messages + + +class UserExtensionDAO: + + """Data Access Object for Users_Extension functionalities""" + + @staticmethod + def create_user_extension(data): + """Creates a user_extension instance for a new registered user. + + Arguments: + data: A list containing user's id, boolean value of whether or not + the user is representing an organization, as well as their timezone + + Returns: + A dictionary containing "message" which indicates whether or not the user_exension was created successfully and "code" for the HTTP response code. + """ + + user_id = data["user_id"] + is_organization_rep = data["is_organization_rep"] + timezone = data["timezone"] + + user_extension = UserExtensionModel(user_id, is_organization_rep, timezone) + + user_extension.save_to_db() + + response = { + "message": f"{messages.USER_WAS_CREATED_SUCCESSFULLY}", + "code": f"{HTTPStatus.CREATED}", + } + + return response diff --git a/app/api/jwt_extension.py b/app/api/jwt_extension.py new file mode 100644 index 0000000..33b0aec --- /dev/null +++ b/app/api/jwt_extension.py @@ -0,0 +1,24 @@ +from flask_jwt_extended import JWTManager +from http import HTTPStatus +from app import messages +from app.api.bit_extension import api + +jwt = JWTManager() + +# This is needed for the error handlers to work with flask-restplus +jwt._set_error_handler_callbacks(api) + + +@jwt.expired_token_loader +def my_expired_token_callback(): + return messages.TOKEN_HAS_EXPIRED, HTTPStatus.UNAUTHORIZED + + +@jwt.invalid_token_loader +def my_invalid_token_callback(error_message): + return messages.TOKEN_IS_INVALID, HTTPStatus.UNAUTHORIZED + + +@jwt.unauthorized_loader +def my_unauthorized_request_callback(error_message): + return messages.AUTHORISATION_TOKEN_IS_MISSING, HTTPStatus.UNAUTHORIZED diff --git a/app/api/mail_extension.py b/app/api/mail_extension.py new file mode 100644 index 0000000..97987ea --- /dev/null +++ b/app/api/mail_extension.py @@ -0,0 +1,3 @@ +from flask_mail import Mail + +mail = Mail() diff --git a/app/api/models/__init__.py b/app/api/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/models/user_extension.py b/app/api/models/user_extension.py new file mode 100644 index 0000000..5345693 --- /dev/null +++ b/app/api/models/user_extension.py @@ -0,0 +1,24 @@ +from flask_restx import fields, Model +from app.utils.bitschema_utils import Timezone + +def add_models_to_namespace(api_namespace): + api_namespace.models[register_user_api_model.name] = register_user_api_model + +register_user_api_model = Model( + "User registration model", + { + "name": fields.String(required=True, description="User name"), + "username": fields.String(required=True, description="User username"), + "password": fields.String(required=True, description="User password"), + "email": fields.String(required=True, description="User email"), + "terms_and_conditions_checked": fields.Boolean( + required=True, description="User check Terms and Conditions value" + ), + "need_mentoring": fields.Boolean( + required=False, description="User need mentoring indication" + ), + "available_to_mentor": fields.Boolean( + required=False, description="User availability to mentor indication" + ), + }, +) diff --git a/app/api/ms_api_utils.py b/app/api/ms_api_utils.py new file mode 100644 index 0000000..32b4f71 --- /dev/null +++ b/app/api/ms_api_utils.py @@ -0,0 +1,58 @@ +from flask import jsonify +import requests +# from requests.exceptions import HTTPError +from flask import json + +from werkzeug.exceptions import HTTPException +import logging + +# set base url + +# for ms-api local server +BASE_MS_API_URL = "http://127.0.0.1:4000" + +# for ms-api heroku server +# BASE_MS_API_URL = "https://bridge-in-tech-ms-test.herokuapp.com" + +# @application.errorhandler(HTTPException) +# def handle_exception(e): +# """Return JSON instead of HTML for HTTP errors.""" +# # start with the correct headers and status code from the error +# response = e.get_response() +# # replace the body with JSON +# response.data = json.dumps({ +# "code": e.code, +# "name": e.name, +# "description": e.description, +# }) +# response.content_type = "application/json" +# return response + +# create instance +def post_request(request_url, data): + response = None, + try: + + response_raw = requests.post( + request_url, + json = data, + headers = {"Accept": "application/json"} + ) + response_raw.status_code = 201 + response_raw.encoding = "utf-8" + response = response_raw.json() + + except HTTPException as e: + response = e.get_response() + response.data = json.dumps({ + "code": e.code, + "name": e.name, + "description": e.description, + }) + response.content_type = "application/json" + + print(f"{response}") + return response + + + diff --git a/app/api/resources/__init__.py b/app/api/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/resources/users.py b/app/api/resources/users.py new file mode 100644 index 0000000..75d1de3 --- /dev/null +++ b/app/api/resources/users.py @@ -0,0 +1,47 @@ +from flask import request +from flask_restx import Resource, marshal, Namespace +from app.api.ms_api_utils import * +from app.api.dao.user_extension import UserExtensionDAO +from flask import json +from http import HTTPStatus +from app import messages +from app.api.models.user_extension import * + +users_ns = Namespace("Users", description="Operations related to users") +add_models_to_namespace(users_ns) + +DAO = UserExtensionDAO() + +@users_ns.route("register") +class UserRegister(Resource): + @classmethod + @users_ns.doc("create_user") + @users_ns.response(HTTPStatus.CREATED, "%s" % messages.USER_WAS_CREATED_SUCCESSFULLY) + @users_ns.response(HTTPStatus.BAD_REQUEST, "%s" % messages.PASSWORD_INPUT_BY_USER_HAS_INVALID_LENGTH) + @users_ns.response( + HTTPStatus.CONFLICT, + "%s\n%s\n%s" + % ( + messages.USER_USES_A_USERNAME_THAT_ALREADY_EXISTS, + messages.USER_USES_AN_EMAIL_ID_THAT_ALREADY_EXISTS, + ), + ) + @users_ns.response(HTTPStatus.INTERNAL_SERVER_ERROR, "%s" % messages.INTERNAL_SERVER_ERROR) + @users_ns.expect(register_user_api_model, validate=True) + + def post(cls): + """ + Creates a new user. + + The endpoint accepts user input related to Mentorship System API (name, username, password, email, + terms_and_conditions_checked(true/false), need_mentoring(true/false), + available_to_mentor(true/false)) and Bridge In Tech API (is_organization_rep and timezone). + A success message is displayed and verification email is sent to the user's email ID. + """ + + data = request.json + + # send POST /register request to MS API and return response + return post_request(f"{BASE_MS_API_URL}/register", data) + + diff --git a/app/api/validations/__init__.py b/app/api/validations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/validations/task_comment.py b/app/api/validations/task_comment.py new file mode 100644 index 0000000..13597cd --- /dev/null +++ b/app/api/validations/task_comment.py @@ -0,0 +1,22 @@ +from app import messages +from app.utils.validation_utils import validate_length, get_stripped_string + +COMMENT_MAX_LENGTH = 400 + + +def validate_task_comment_request_data(data): + if "comment" not in data: + return messages.COMMENT_FIELD_IS_MISSING + + comment = data["comment"] + + if not isinstance(comment, str): + return messages.COMMENT_NOT_IN_STRING_FORMAT + + is_valid = validate_length( + len(get_stripped_string(data["comment"])), 0, COMMENT_MAX_LENGTH, "comment" + ) + if not is_valid[0]: + return is_valid[1] + + return {} diff --git a/app/api/validations/user.py b/app/api/validations/user.py new file mode 100644 index 0000000..00ee224 --- /dev/null +++ b/app/api/validations/user.py @@ -0,0 +1,244 @@ +from app import messages +from app.utils.validation_utils import ( + is_name_valid, + is_email_valid, + is_username_valid, + validate_length, + get_stripped_string, +) + +# Field character limit + +NAME_MAX_LENGTH = 30 +NAME_MIN_LENGTH = 2 +USERNAME_MAX_LENGTH = 25 +USERNAME_MIN_LENGTH = 5 +PASSWORD_MAX_LENGTH = 64 +PASSWORD_MIN_LENGTH = 8 + +BIO_MAX_LENGTH = 450 +LOCATION_MAX_LENGTH = 60 +OCCUPATION_MAX_LENGTH = 60 +ORGANIZATION_MAX_LENGTH = 60 +SLACK_USERNAME_MAX_LENGTH = 60 +SKILLS_MAX_LENGTH = 450 +INTERESTS_MAX_LENGTH = 150 +SOCIALS_MAX_LENGTH = 400 + + +def validate_user_registration_request_data(data): + # Verify if request body has required fields + if "name" not in data: + return messages.NAME_FIELD_IS_MISSING + if "username" not in data: + return messages.USERNAME_FIELD_IS_MISSING + if "password" not in data: + return messages.PASSWORD_FIELD_IS_MISSING + if "email" not in data: + return messages.EMAIL_FIELD_IS_MISSING + if "terms_and_conditions_checked" not in data: + return messages.TERMS_AND_CONDITIONS_FIELD_IS_MISSING + + name = data["name"] + username = data["username"] + password = data["password"] + email = data["email"] + terms_and_conditions_checked = data["terms_and_conditions_checked"] + + if not ( + isinstance(name, str) + and isinstance(username, str) + and isinstance(password, str) + ): + return messages.NAME_USERNAME_AND_PASSWORD_NOT_IN_STRING_FORMAT + + is_valid = validate_length( + len(get_stripped_string(name)), NAME_MIN_LENGTH, NAME_MAX_LENGTH, "name" + ) + if not is_valid[0]: + return is_valid[1] + + is_valid = validate_length( + len(get_stripped_string(username)), + USERNAME_MIN_LENGTH, + USERNAME_MAX_LENGTH, + "username", + ) + if not is_valid[0]: + return is_valid[1] + + is_valid = validate_length( + len(get_stripped_string(password)), + PASSWORD_MIN_LENGTH, + PASSWORD_MAX_LENGTH, + "password", + ) + if not is_valid[0]: + return is_valid[1] + + # Verify business logic of request body + if not terms_and_conditions_checked: + return messages.TERMS_AND_CONDITIONS_ARE_NOT_CHECKED + + if not is_name_valid(name): + return messages.NAME_INPUT_BY_USER_IS_INVALID + + if not is_email_valid(email): + return messages.EMAIL_INPUT_BY_USER_IS_INVALID + + if not is_username_valid(username): + return messages.USERNAME_INPUT_BY_USER_IS_INVALID + + return {} + + +def validate_resend_email_request_data(data): + # Verify if request body has required fields + if "email" not in data: + return messages.EMAIL_FIELD_IS_MISSING + + email = data["email"] + if not is_email_valid(email): + return messages.EMAIL_INPUT_BY_USER_IS_INVALID + + return {} + + +def validate_update_profile_request_data(data): + # todo this does not check if non expected fields are being sent + + if not data: + return messages.NO_DATA_FOR_UPDATING_PROFILE_WAS_SENT + + username = data.get("username", None) + if username: + is_valid = validate_length( + len(get_stripped_string(username)), + USERNAME_MIN_LENGTH, + USERNAME_MAX_LENGTH, + "username", + ) + if not is_valid[0]: + return is_valid[1] + + if not is_username_valid(username): + return messages.NEW_USERNAME_INPUT_BY_USER_IS_INVALID + + name = data.get("name", None) + if name: + is_valid = validate_length( + len(get_stripped_string(name)), NAME_MIN_LENGTH, NAME_MAX_LENGTH, "name" + ) + if not is_valid[0]: + return is_valid[1] + + if not is_name_valid(name): + return messages.NAME_INPUT_BY_USER_IS_INVALID + + bio = data.get("bio", None) + if bio: + is_valid = validate_length( + len(get_stripped_string(bio)), 0, BIO_MAX_LENGTH, "bio" + ) + if not is_valid[0]: + return is_valid[1] + + location = data.get("location", None) + if location: + is_valid = validate_length( + len(get_stripped_string(location)), 0, LOCATION_MAX_LENGTH, "location" + ) + if not is_valid[0]: + return is_valid[1] + + occupation = data.get("occupation", None) + if occupation: + is_valid = validate_length( + len(get_stripped_string(occupation)), 0, OCCUPATION_MAX_LENGTH, "occupation" + ) + if not is_valid[0]: + return is_valid[1] + + organization = data.get("organization", None) + if organization: + is_valid = validate_length( + len(get_stripped_string(organization)), + 0, + ORGANIZATION_MAX_LENGTH, + "organization", + ) + if not is_valid[0]: + return is_valid[1] + + slack_username = data.get("slack_username", None) + if slack_username: + is_valid = validate_length( + len(get_stripped_string(slack_username)), + 0, + SLACK_USERNAME_MAX_LENGTH, + "slack_username", + ) + if not is_valid[0]: + return is_valid[1] + + social_media_links = data.get("social_media_links", None) + if social_media_links: + is_valid = validate_length( + len(get_stripped_string(social_media_links)), + 0, + SOCIALS_MAX_LENGTH, + "social_media_links", + ) + if not is_valid[0]: + return is_valid[1] + + skills = data.get("skills", None) + if skills: + is_valid = validate_length( + len(get_stripped_string(skills)), 0, SKILLS_MAX_LENGTH, "skills" + ) + if not is_valid[0]: + return is_valid[1] + + interests = data.get("interests", None) + if interests: + is_valid = validate_length( + len(get_stripped_string(interests)), 0, INTERESTS_MAX_LENGTH, "interests" + ) + if not is_valid[0]: + return is_valid[1] + + if "need_mentoring" in data and data["need_mentoring"] is None: + return messages.FIELD_NEED_MENTORING_IS_NOT_VALID + + if "available_to_mentor" in data and data["available_to_mentor"] is None: + return messages.FIELD_AVAILABLE_TO_MENTOR_IS_INVALID + + return {} + + +def validate_new_password(data): + if "current_password" not in data: + return messages.CURRENT_PASSWORD_FIELD_IS_MISSING + if "new_password" not in data: + return messages.NEW_PASSWORD_FIELD_IS_MISSING + + current_password = data["current_password"] + new_password = data["new_password"] + + if current_password == new_password: + return messages.USER_ENTERED_CURRENT_PASSWORD + + if " " in new_password: + return messages.USER_INPUTS_SPACE_IN_PASSWORD + + is_valid = validate_length( + len(get_stripped_string(new_password)), + PASSWORD_MIN_LENGTH, + PASSWORD_MAX_LENGTH, + "new_password", + ) + if not is_valid[0]: + return is_valid[1] + + return {} diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/db_add_mock.py b/app/database/db_add_mock.py new file mode 100644 index 0000000..91ee338 --- /dev/null +++ b/app/database/db_add_mock.py @@ -0,0 +1,440 @@ +from app.database.sqlalchemy_extension import db +from app.utils.bitschema_utils import * +from app.utils.enum_utils import * +from datetime import datetime, timezone + +def add_mock_data(): + + db.drop_all() + + 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 + from app.database.models.ms_schema.task_comment import TaskCommentModel + from app.database.models.bit_schema.organization import OrganizationModel + from app.database.models.bit_schema.program import ProgramModel + from app.database.models.bit_schema.user_extension import UserExtensionModel + from app.database.models.bit_schema.personal_background import PersonalBackgroundModel + from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel + + db.create_all() + + # for users table dummy data + + # add user1 who is also an admin + user1 = UserModel( + name = "testone", + username = "test0101", + password = "pbkdf2:sha256:50000$hY4PGrnp$b5c25743bc1308158d86b274af63e203ae4031061af5c7f9505c8420f50cae1d", + email = "rovexay139@prowerl.com", + terms_and_conditions_checked = True + ) + user1.available_to_mentor = True + user1.need_mentoring = True + user1.registration_date = 1589151600.93296 # Sunday, 10 May 2020 11:00:00.932 PM UTC+0 + user1.is_admin = True + user1.is_email_verified = True + user1.email_verification_date = "2020-05-11 09:39:37.950152" + + # add user2 + user2 = UserModel( + name = "testtwo", + username = "test0202", + password = "pbkdf2:sha256:50000$4CtuxCwF$8b6584ce767ba0d70312d285ea8013a0ef9480b55c1873f4376809761095c7d8", + email = "povey55047@whowlft.com", + terms_and_conditions_checked = True + ) + user2.available_to_mentor = True + user2.need_mentoring = True + user2.registration_date = 1589238000.95326 # Monday, 11 May 2020 11:00:00.953 PM UTC+0 + user1.is_admin = False + user2.is_email_verified = True + user2.email_verification_date = "2020-05-12 11:04:46.368948" + + # add user3 + user3 = UserModel( + name = "testthree", + username = "test0303", + password = "pbkdf2:sha256:50000$gGcSwNwu$1dba66ff19891770113f2ae4b1af003fd1ee5a2005af7c2f6654663ff03a281e", + email = "nohor47235@ximtyl.com", + terms_and_conditions_checked = True + ) + user3.available_to_mentor = True + user3.need_mentoring = False + user3.registration_date = 1589410800.82208 # Wednesday, 13 May 2020 11:00:00.822 PM UTC+0 + user3.is_admin = False + user3.is_email_verified = True + user3.email_verification_date = "2020-05-14 15:12:49.585829" + + # add user4 + user4 = UserModel( + name = "testfour", + username = "test0404", + password = "pbkdf2:sha256:50000$6zulJGqT$ea4d6f539654e49d7af38672798e0ec673beb4e7d70660ba6e884fd56d30c062", + email = "yeroy74921@whowlft.com", + terms_and_conditions_checked = True + ) + user4.available_to_mentor = False + user4.need_mentoring = True + user4.registration_date = 1589371200.61836 # Wednesday, 13 May 2020 12:00:00.618 PM UTC+0 + user4.is_admin = False + user4.is_email_verified = False + + # add users data + db.session.add(user1) + db.session.add(user2) + db.session.add(user3) + db.session.add(user4) + + # save to users table + db.session.commit() + + # for users_extension dummy data + + # user1 users_extension + user1_extension = UserExtensionModel( + user_id = user1.id, + is_organization_rep = True, + timezone = Timezone.AUSTRALIAN_EASTERN_STANDARD_TIME + ) + user1_extension.additional_info = {"phone": "03-88887777"} + + # user2 users_extension + user2_extension = UserExtensionModel( + user_id = user2.id, + is_organization_rep = False, + timezone = Timezone.ALASKA_STANDARD_TIME + ) + user2_extension.additional_info = { + "phone": "2130-99-11-99", "personal_website": "testtwo@github.io"} + + # user3 users_extension + user3_extension = UserExtensionModel( + user_id = user3.id, + is_organization_rep = True, + timezone = Timezone.CENTRAL_EUROPEAN_TIME + ) + user3_extension.additional_info = { + "mobile": "+44-5555-666-777", "personal_website": "testthree@github.io"} + + # user4 users_extension. Not yet confirm their email. + user4_extension = UserExtensionModel( + user_id = user4.id, + is_organization_rep = False, + timezone = Timezone.CHARLIE_TIME + ) + + # add users_extension data + db.session.add(user1_extension) + db.session.add(user2_extension) + db.session.add(user3_extension) + db.session.add(user4_extension) + + # save to users_extension table + db.session.commit() + + # for personal_background dummy data + + # user1 personal_background + user1_background = PersonalBackgroundModel( + user_id = user1.id, + gender = Gender.FEMALE, + age = Age.AGE_25_TO_34, + ethnicity = Ethnicity.CAUCASIAN, + sexual_orientation = SexualOrientation.LGBTIA, + religion = Religion.BUDDHISM, + physical_ability = PhysicalAbility.WITHOUT_DISABILITY, + mental_ability = MentalAbility.WITHOUT_DISORDER, + socio_economic = SocioEconomic.LOWER_MIDDLE, + highest_education = HighestEducation.BACHELOR, + years_of_experience = YearsOfExperience.UP_TO_10 + ) + user1_background.is_public = True + + # user2 personal_background + user2_background = PersonalBackgroundModel( + user_id = user2.id, + gender = Gender.OTHER, + age = Age.AGE_35_TO_44, + ethnicity = Ethnicity.ASIAN, + sexual_orientation = SexualOrientation.HETEROSEXUAL, + religion = Religion.OTHER, + physical_ability = PhysicalAbility.WITH_DISABILITY, + mental_ability = MentalAbility.WITHOUT_DISORDER, + socio_economic = SocioEconomic.BELOW_POVERTY, + highest_education = HighestEducation.HIGH_SCHOOL, + years_of_experience = YearsOfExperience.UP_TO_3 + ) + user2_background.others = {"religion": "Daoism"} + user2_background.is_public = True + + # user3 personal_background + user3_background = PersonalBackgroundModel( + user_id = user3.id, + gender = Gender.DECLINED, + age = Age.DECLINED, + ethnicity = Ethnicity.DECLINED, + sexual_orientation = SexualOrientation.DECLINED, + religion = Religion.DECLINED, + physical_ability = PhysicalAbility.DECLINED, + mental_ability = MentalAbility.DECLINED, + socio_economic = SocioEconomic.DECLINED, + highest_education = HighestEducation.DECLINED, + years_of_experience = YearsOfExperience.DECLINED + ) + user3_background.is_public = False + + # user4 has no background data because email has yet to be verified + + # add users background data + db.session.add(user1_background) + db.session.add(user2_background) + db.session.add(user3_background) + + # save to personal_background table + db.session.commit() + + # for organizations dummy data + + # organization1 data + organization1 = OrganizationModel( + rep_id = user1.id, + name = "ABC Pty Ltd", + email = "abc_pty_ltd@mail.com", + address = "506 Elizabeth St, Melbourne VIC 3000, Australia", + website = "abc_pty_ltd.com", + timezone = user1_extension.timezone + ) + organization1.status = OrganizationStatus.DRAFT + organization1.join_date = 1589284800 # Tuesday, 12 May 2020 12:00:00 PM UTC+0 + + # organization2 data + organization2 = OrganizationModel( + rep_id = user3.id, + name = "BCD.Co", + email = "bdc_co@mail.com", + address = "Novalisstraße 10, 10115 Berlin, Germany", + website = "bcd_co.com", + timezone = user3_extension.timezone + ) + organization2.rep_department = "IT Department" + organization2.about = "This is about us..." + organization2.phone = "+49-30-688364150" + organization2.status = OrganizationStatus.PUBLISH + organization2.join_date = 1589544000 # Friday, 15 May 2020 12:00:00 PM UTC+0 + + + # add organizations data + db.session.add(organization1) + db.session.add(organization2) + + # save to organizations table + db.session.commit() + + # for programs dummy data + + # program1 data + program1 = ProgramModel( + program_name = "Program A", + organization_id = organization1, + start_date = 1596236400, # Friday, 31 July 2020 11:00:00 PM UTC+0 + end_date = 1598828400, # Sunday, 30 August 2020 11:00:00 PM UTC+0 + ) + program1.creation_date = 1589457600 # Thursday, 14 May 2020 12:00:00 PM UTC+0 + + # program2 data + program2 = ProgramModel( + program_name = "Program B", + organization_id = organization2, + start_date = 1590062400, # Thursday, 21 May 2020 12:00:00 PM UTC+0 + end_date = 1596186000, # Friday, 31 July 2020 9:00:00 AM UTC+0 + ) + program2.description = "This is program B description..." + program2.target_skills = ["Python", "PostgreSQL", "ReactJS"] + program2.target_candidate = {"gender": Gender.FEMALE.value, "age": Age.AGE_45_TO_54.value} + program2.payment_amount = 2500.00 + program2.payment_currency = "GBP" + program2.contact_type = ContactType.FACE_TO_FACE + program2.zone = Zone.LOCAL + program2.student_responsibility = ["responsibility1", "responsibility2"] + program2.mentor_responsibility = ["responsibility1", "responsibility2"] + program2.organization_responsibility = ["responsibility1", "responsibility2"] + program2.student_requirements = ["requirement1", "requirement2"] + program2.mentor_requirements = ["requirement1", "requirement2"] + program2.resources_provided = ["resources1", "resources2"] + program2.contact_name = user3.name + program2.contact_department = "HR Department" + program2.program_address = organization2.address + program2.contact_phone = organization2.phone + program2.contact_mobile = user3_extension.additional_info.get("mobile") + program2.contact_email = user3.email + program2.program_website = organization2.website + program2.irc_channel = "bcd_co@zulip.chat" + program2.tags = program2.target_skills + program2.status = ProgramStatus.IN_PROGRESS + program2.creation_date = 1589544000 # Friday, 15 May 2020 12:00:00 PM UTC+0 + + # program3 data + program3 = ProgramModel( + program_name = "Program C", + organization_id = organization2, + start_date = 1594814400, # Wednesday, 15 July 2020 12:00:00 PM UTC+0 + end_date = 1598875200, # Monday, 31 August 2020 12:00:00 PM UTC+0 + ) + program3.description = "This is program C description..." + program3.target_skills = ["Dart", "Firebase", "Flutter"] + program3.target_candidate = {"physical_ability": PhysicalAbility.WITH_DISABILITY.value, "sexual_orientation": SexualOrientation.LGBTIA.value} + program3.payment_amount = 3500.00 + program3.payment_currency = "GBP" + program3.contact_type = ContactType.BOTH + program3.zone = Zone.NATIONAL + program3.student_responsibility = ["responsibility1", "responsibility2"] + program3.mentor_responsibility = ["responsibility1", "responsibility2"] + program3.organization_responsibility = ["responsibility1", "responsibility2"] + program3.student_requirements = ["requirement1", "requirement2"] + program3.mentor_requirements = ["requirement1", "requirement2"] + program3.resources_provided = ["resources1", "resources2"] + program3.contact_name = user3.name + program3.contact_department = "HR Department" + program3.program_address = organization2.address + program3.contact_phone = organization2.phone + program3.contact_mobile = user3_extension.additional_info.get("mobile") + program3.contact_email = user3.email + program3.program_website = organization2.website + program3.irc_channel = "bcd_co@zulip.chat" + program3.tags = program3.target_skills + program3.status = ProgramStatus.OPEN + program3.creation_date = 1589630400 # Saturday, 16 May 2020 12:00:00 PM UTC+0 + + + # add programs data + db.session.add(program1) + db.session.add(program2) + db.session.add(program3) + + # save to programs table + db.session.commit() + + # for mentorship_relations dummy data + + # 1st scenario. + # Program send request to mentor and mentee + + # prepare empty tasks_list for mentorship_relation1 + tasks_list_1 = TasksListModel() + db.session.add(tasks_list_1) + db.session.commit() + + # create mentorship_relation1 when program sending request to mentor + mentorship_relation1 = MentorshipRelationModel( + action_user_id = organization2.rep_id, + mentor_user = user1, + mentee_user = None, + creation_date = 1589630400, # Saturday, 16 May 2020 12:00:00 PM UTC+0 + end_date = program2.end_date, # Friday, 31 July 2020 9:00:00 AM UTC+0 == program2 end_date + state= MentorshipRelationState.PENDING, + notes = "Please be a mentor...", + tasks_list = tasks_list_1, + ) + mentorship_relation1.start_date = program2.start_date # Thursday, 21 May 2020 12:00:00 PM UTC+0 + + db.session.add(mentorship_relation1) + db.session.commit() + + + # initiate mentorship_relations_extension + mentorship_relations_extension1 = MentorshipRelationExtensionModel( + program_id = program2.id, + mentorship_relation_id = mentorship_relation1.id + ) + mentorship_relations_extension1.mentor_request_date = mentorship_relation1.creation_date + + db.session.add(mentorship_relations_extension1) + db.session.commit() + + + # later, mentor accepted program request + mentorship_relation1.action_id = user1.id + mentorship_relation1.notes = "ok, will do." + mentorship_relations_extension1.mentor_agreed_date = 1589803200 # Monday, 18 May 2020 12:00:00 PM UTC+0 + # update related tables + db.session.add(mentorship_relation1) + db.session.add(mentorship_relations_extension1) + db.session.commit() + + # then program send request to mentee + mentorship_relation1.action_id = organization2.rep_id + mentorship_relation1.mentee_id = user2.id + mentorship_relation1.notes = "You're invited to work with us as a mentee." + mentorship_relations_extension1.mentee_request_date = 1589803200 # Monday, 18 May 2020 12:00:00 PM UTC+0 + # update related tables + db.session.add(mentorship_relation1) + db.session.add(mentorship_relations_extension1) + db.session.commit() + + # mentee accepted program request + mentorship_relation1.action_id = user2.id + mentorship_relation1.notes = "sure, why not." + mentorship_relations_extension1.mentee_agreed_date = 1589976000 # Wednesday, 20 May 2020 12:00:00 PM UTC+0 + # update mentorship_relation state + mentorship_relation1.state = MentorshipRelationState.ACCEPTED + mentorship_relation1.accept_date = mentorship_relations_extension1.mentee_agreed_date + # update program status + program2.status = ProgramStatus.IN_PROGRESS + + # update related tables + db.session.add(mentorship_relation1) + db.session.add(mentorship_relations_extension1) + db.session.add(program2) + db.session.commit() + + # create list of tasks assigned at the beginning of the program + tasks_list_1_task_a = "this is task a for tasks list 1" + tasks_list_1_task_b = "this is task b for tasks list 1" + + tasks_list_1.add_task(description=tasks_list_1_task_a, created_at=1590062400) # Thursday, 21 May 2020 12:00:00 PM UTC+0 + tasks_list_1.add_task(description=tasks_list_1_task_b, created_at=1590062400) # Thursday, 21 May 2020 12:00:00 PM UTC+0 + + + db.session.add(tasks_list_1) + db.session.commit() + + # mentor comment on task a + tasks_list_1_task_comment_a = TaskCommentModel( + user_id = user1.id, + task_id = 1, + relation_id = 1, + comment = "Do you need help with the task?" + ) + tasks_list_1_task_comment_a.creation_date = 1590062400 # Thursday, 21 May 2020 12:00:00 PM UTC+0 == tasks a creation date + + db.session.add(tasks_list_1_task_comment_a) + db.session.commit() + + + # mentee responded to mentor comment + tasks_list_1_task_comment_a.user_id = user2.id + tasks_list_1_task_comment_a.comment = "Nope. All good" + tasks_list_1_task_comment_a.modification_date = 1590148800 # Friday, 22 May 2020 12:00:00 PM UTC+0 + + db.session.add(tasks_list_1_task_comment_a) + db.session.commit() + + # when task a completed + tasks_list_1.update_task( + task_id = 1, + is_done = True, + completed_at = 1590235200, # Saturday, 23 May 2020 12:00:00 PM UTC+0 + ) + + # mentor add comment on task a completion + tasks_list_1_task_comment_a.user_id = user1.id + tasks_list_1_task_comment_a.comment = "Well done!" + tasks_list_1_task_comment_a.modification_date = 1590235200 # Saturday, 23 May 2020 12:00:00 PM UTC+0 + + db.session.add(tasks_list_1) + db.session.add(tasks_list_1_task_comment_a) + db.session.commit() + + + \ No newline at end of file diff --git a/app/database/db_types/ArrayOfEnum.py b/app/database/db_types/ArrayOfEnum.py new file mode 100644 index 0000000..8df0f58 --- /dev/null +++ b/app/database/db_types/ArrayOfEnum.py @@ -0,0 +1,22 @@ +import re +from app.database.sqlalchemy_extension import db + + +class ArrayOfEnum(db.TypeDecorator): + + impl = db.ARRAY + + def bind_expression(self, bindvalue): + return db.cast(bindvalue, self) + + def result_processor(self, dialect, coltype): + super_rp = super(ArrayOfEnum, self).result_processor(dialect, coltype) + + def handle_raw_string(value): + inner = re.match(r"^{(.*)}$", value).group(1) + return inner.split(",") + + def process(value): + return super_rp(handle_raw_string(value)) + + return process diff --git a/app/database/db_types/JsonCustomType.py b/app/database/db_types/JsonCustomType.py new file mode 100644 index 0000000..1f9ed05 --- /dev/null +++ b/app/database/db_types/JsonCustomType.py @@ -0,0 +1,25 @@ +import json +from app.database.sqlalchemy_extension import db + + +class JsonCustomType(db.TypeDecorator): + """Enables JSON storage by encoding and decoding to Text field.""" + + impl = db.Text + + @classmethod + def process_bind_param(cls, value, dialect): + if value is None: + return "{}" + else: + return json.dumps(value) + + @classmethod + def process_result_value(cls, value, dialect): + if value is None: + return {} + else: + try: + return json.loads(value) + except (ValueError, TypeError): + return None diff --git a/app/database/db_types/__init__.py b/app/database/db_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/db_utils.py b/app/database/db_utils.py new file mode 100644 index 0000000..ae18f6c --- /dev/null +++ b/app/database/db_utils.py @@ -0,0 +1,6 @@ +from app.database.sqlalchemy_extension import db + + +def reset_database(): + db.drop_all() + db.create_all() diff --git a/app/database/models/__init__.py b/app/database/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/models/bit_schema/__init__.py b/app/database/models/bit_schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/models/bit_schema/mentorship_relation_extension.py b/app/database/models/bit_schema/mentorship_relation_extension.py new file mode 100644 index 0000000..66a25c1 --- /dev/null +++ b/app/database/models/bit_schema/mentorship_relation_extension.py @@ -0,0 +1,68 @@ +from datetime import date + +from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel +from app.database.sqlalchemy_extension import db + +class MentorshipRelationExtensionModel(db.Model): + """Defines attibutes of mentorship relation that are specific only to BridgeInTech. + + Attributes: + program_id: An integer for storing program id. + mentorship_relation_id: An integer for storing mentorship relation id. + mentor_agreed_date: A numeric for storing the date when mentor agreed to work in program. + mentee_agreed_date: A numeric for storing the date when mentee agreed to work in program. + """ + + __tablename__ = "mentorship_relations_extension" + __table_args__ = {"schema": "bitschema", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + program_id = db.Column(db.Integer, db.ForeignKey("bitschema.programs.id", ondelete="CASCADE"), nullable=False, unique=True) + mentorship_relation_id = db.Column(db.Integer, db.ForeignKey("public.mentorship_relations.id", ondelete="CASCADE"), nullable=False, unique=True) + mentor_request_date = db.Column(db.Numeric("16,6", asdecimal=False)) + mentor_agreed_date = db.Column(db.Numeric("16,6", asdecimal=False)) + mentee_request_date = db.Column(db.Numeric("16,6", asdecimal=False)) + mentee_agreed_date = db.Column(db.Numeric("16,6", asdecimal=False)) + + def __init__(self, program_id, mentorship_relation_id): + self.program_id = program_id + self.mentorship_relation_id = mentorship_relation_id + + # default values + self.mentor_request_date = None + self.mentor_agreed_date = None + self.mentee_request_date = None + self.mentee_agreed_date = None + + def json(self): + """Returns information of mentorship as a json object.""" + return { + "id": self.id, + "program_id": self.program_id, + "mentorship_relation_id": self.mentorship_relation_id, + "mentor_request_date": self.mentor_request_date, + "mentor_agreed_date": self.mentor_agreed_date, + "mentee_request_date": self.mentee_request_date, + "mentee_agreed_date": self.mentee_agreed_date, + } + + @classmethod + def find_by_id(cls, _id) -> "MentorshipRelationExtensionModel": + + """Returns the mentorship_relations_extension that has the passed id. + Args: + _id: The id of a mentorship_relations_extension. + """ + return cls.query.filter_by(id=_id).first() + + def save_to_db(self) -> None: + """Saves the model to the database.""" + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes the record of mentorship relation extension from the database.""" + self.tasks_list.delete_from_db() + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/bit_schema/organization.py b/app/database/models/bit_schema/organization.py new file mode 100644 index 0000000..8de8a27 --- /dev/null +++ b/app/database/models/bit_schema/organization.py @@ -0,0 +1,122 @@ +import time +from app.database.sqlalchemy_extension import db +from app.utils.bitschema_utils import OrganizationStatus, Timezone +from app.database.models.bit_schema.program import ProgramModel +from sqlalchemy import null + + +class OrganizationModel(db.Model): + """Defines attributes for the organization. + + Attributes: + rep_id: A string for storing the organization's rep id. + name: A string for storing organization's name. + email: A string for storing organization's email. + address: A string for storing the organization's address. + geoloc: A geolocation data using JSON format. + website: A string for storing the organization's website. + """ + + # Specifying database table used for OrganizationModel + __tablename__ = "organizations" + __table_args__ = {"schema": "bitschema", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + # Organization's representative data + rep_id = db.Column(db.Integer, db.ForeignKey("public.users.id"), unique=True) + rep_department = db.Column(db.String(150)) + + # Organization data + name = db.Column(db.String(150), nullable=False, unique=True) + email = db.Column(db.String(254), nullable=False, unique=True) + about = db.Column(db.String(500)) + address = db.Column(db.String(254)) + website = db.Column(db.String(150), nullable=False) + timezone = db.Column(db.Enum(Timezone)) + phone = db.Column(db.String(20)) + status = db.Column(db.Enum(OrganizationStatus)) + join_date = db.Column(db.Numeric("16,6", asdecimal=False)) + + # Programs relationship + programs = db.relationship( + ProgramModel, backref="organization", cascade="all,delete", passive_deletes=True + ) + + def __init__(self, rep_id, name, email, address, website, timezone): + """Initialises OrganizationModel class.""" + ## required fields + + self.rep_id = rep_id + self.name = name + self.email = email + self.address = address + self.website = website + self.timezone = timezone + + # default values + self.status = OrganizationStatus.DRAFT + self.join_date = time.time() + + + def json(self): + """Returns OrganizationModel object in json format.""" + return { + "id": self.id, + "rep_id": self.rep_id, + "rep_department": self.rep_department, + "name": self.name, + "email": self.email, + "about": self.about, + "address": self.address, + "website": self.website, + "timezone": self.timezone, + "phone": self.phone, + "status": self.status, + "join_date": self.join_date, + } + + def __repr__(self): + """Returns the organization.""" + return ( + f"Organization's representative is {self.rep_id}\n" + f"Organization's name is {self.c_name}.\n" + f"Organization's email is {self.c_email}\n" + f"Organization's address is {self.c_address}\n" + f"Organization's website is {self.c_website}\n" + f"Organization's timezone is {self.timezone}" + ) + + @classmethod + def find_by_id(cls, _id) -> "OrganizationModel": + + """Returns the Organization that has the passed id. + Args: + _id: The id of an Organization. + """ + return cls.query.filter_by(id=_id).first() + + @classmethod + def find_by_representative(cls, rep_id: int) -> "OrganizationModel": + """Returns the organization that has the representative id user searched for. """ + return cls.query.filter_by(rep_id=rep_id).first() + + @classmethod + def find_by_name(cls, name: str) -> "OrganizationModel": + """Returns the organization that has the name user searched for. """ + return cls.query.filter_by(c_name=name).first() + + @classmethod + def find_by_email(cls, email: str) -> "OrganizationModel": + """Returns the organization that has the email user searched for. """ + return cls.query.filter_by(c_email=email).first() + + def save_to_db(self) -> None: + """Adds an organization to the database. """ + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes an organization from the database. """ + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/bit_schema/personal_background.py b/app/database/models/bit_schema/personal_background.py new file mode 100644 index 0000000..574383f --- /dev/null +++ b/app/database/models/bit_schema/personal_background.py @@ -0,0 +1,119 @@ +from app.database.sqlalchemy_extension import db +from app.utils.bitschema_utils import * +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import null + + +class PersonalBackgroundModel(db.Model): + """Defines attributes for user's personal background. + + Attributes: + user_id: An integer for storing the user's id. + gender: A string for storing the user's gender. + age: A string for storing the user's age. + ethnicity: A string for storing the user's wthnicity. + sexual_orientation: A string for storing the user's sexual orientation. + religion: A string for storing the user's religion. + physical_ability: A string for storing the user's physical ability. + mental_ability: A string for storing the user's mental ability. + socio_economic: A string for storing the user's socio economic level. + highest_education: A string for storing the user's highest education level. + years_of_experience: A string for storing the user's length of expeprience in the It related area. + others: A JSON data type for storing users descriptions of 'other' fields. + is_public: A boolean indicating if user has agreed to display their personal background information publicly to other members. + """ + + # Specifying database table used for PersonalBackgroundModel + __tablename__ = "personal_backgrounds" + __table_args__ = {"schema": "bitschema", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + # User's personal background data + user_id = db.Column(db.Integer, db.ForeignKey("public.users.id", ondelete="CASCADE"), nullable=False, unique=True) + gender = db.Column(db.Enum(Gender)) + age = db.Column(db.Enum(Age)) + ethnicity = db.Column(db.Enum(Ethnicity)) + sexual_orientation = db.Column(db.Enum(SexualOrientation)) + religion = db.Column(db.Enum(Religion)) + physical_ability = db.Column(db.Enum(PhysicalAbility)) + mental_ability = db.Column(db.Enum(MentalAbility)) + socio_economic = db.Column(db.Enum(SocioEconomic)) + highest_education = db.Column(db.Enum(HighestEducation)) + years_of_experience = db.Column(db.Enum(YearsOfExperience)) + others = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) + is_public = db.Column(db.Boolean) + + def __init__(self, user_id, gender, age, ethnicity, sexual_orientation, religion, physical_ability, mental_ability, socio_economic, highest_education, years_of_experience): + """Initialises PersonalBackgroundModel class.""" + ## required fields + self.user_id = user_id + self.gender = gender + self.age = age + self.ethnicity = ethnicity + self.sexual_orientation = sexual_orientation + self.religion = religion + self.physical_ability = physical_ability + self.mental_ability = mental_ability + self.socio_economic = socio_economic + self.highest_education = highest_education + self.years_of_experience = years_of_experience + + # default values + self.others = None + self.is_public = False + + + def json(self): + """Returns PersonalBackgroundModel object in json format.""" + return { + "id": self.id, + "user_id": self.user_id, + "age": self.age, + "ethnicity": self.ethnicity, + "sexual_orientation": self.sexual_orientation, + "religion": self.religion, + "physical_ability": self.physical_ability, + "mental_ability": self.mental_ability, + "socio_economic": self.socio_economic, + "highest_education": self.highest_education, + "years_of_experience": self.years_of_experience, + "others": self.others, + "is_public": self.is_public, + } + + def __repr__(self): + """Returns user's background.""" + + return ( + f"Users's id is {self.user_id}.\n" + f"User's age is: {self.age}\n" + f"User's ethnicity is: {self.ethnicity}\n" + f"User's sexual orientation is: {self.sexual_orientation}\n" + f"User's religion is: {self.religion}\n" + f"User's physical ability is: {self.physical_ability}\n" + f"User's mental ability is: {self.mental_ability}\n" + f"User's socio economic category is: {self.socio_economic}\n" + f"User's highest level of education is: {self.highest_education}\n" + f"User's length of experience is: {self.years_of_experience}\n" + ) + + @classmethod + def find_by_id(cls, user_id) -> "PersonalBackgroundModel": + + """Returns the user's background that has the passed user id. + Args: + _id: The id of a user. + """ + return cls.query.filter_by(user_id=user_id).first() + + + def save_to_db(self) -> None: + """Adds user's personal background to the database. """ + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes user's personal background from the database. """ + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/bit_schema/program.py b/app/database/models/bit_schema/program.py new file mode 100644 index 0000000..0236d79 --- /dev/null +++ b/app/database/models/bit_schema/program.py @@ -0,0 +1,153 @@ +from app.database.sqlalchemy_extension import db +import time +# from datetime import datetime +from app.utils.bitschema_utils import ContactType, Zone, ProgramStatus +from sqlalchemy import BigInteger, ARRAY +from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import null +from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel + + +class ProgramModel(db.Model): + """Defines attributes for a program. + + Attributes: + program_name: A string for storing program name. + organization_id: An integer for storing organization's id. + start_date: A date for storing the program start date. + end_date: A date for storing the program end date. + """ + + # Specifying database table used for ProgramModel + __tablename__ = "programs" + __table_args__ = {"schema": "bitschema", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + program_name = db.Column(db.String(100), unique=True, nullable=False) + organization_id = db.Column(db.Integer, db.ForeignKey("bitschema.organizations.id", ondelete="CASCADE"), nullable=False) + start_date = db.Column(db.Numeric("16,6", asdecimal=False)) + end_date = db.Column(db.Numeric("16,6", asdecimal=False)) + description = db.Column(db.String(500)) + target_skills = db.Column(ARRAY(db.String(150))) + target_candidate = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) + payment_currency = db.Column(db.String(3)) + payment_amount = db.Column(BigInteger) + contact_type = db.Column(db.Enum(ContactType)) + zone = db.Column(db.Enum(Zone)) + student_responsibility = db.Column(ARRAY(db.String(250))) + mentor_responsibility = db.Column(ARRAY(db.String(250))) + organization_responsibility = db.Column(ARRAY(db.String(250))) + student_requirements = db.Column(ARRAY(db.String(250))) + mentor_requirements = db.Column(ARRAY(db.String(250))) + resources_provided = db.Column(ARRAY(db.String(250))) + contact_name = db.Column(db.String(50)) + contact_department = db.Column(db.String(150)) + program_address = db.Column(db.String(250)) + contact_phone = db.Column(db.String(20)) + contact_mobile = db.Column(db.String(20)) + contact_email = db.Column(db.String(254)) + program_website = db.Column(db.String(254)) + irc_channel = db.Column(db.String(254)) + tags = db.Column(ARRAY(db.String(150))) + status = db.Column(db.Enum(ProgramStatus)) + creation_date = db.Column(db.Numeric("16,6", asdecimal=False)) + mentorship_relation = db.relationship( + MentorshipRelationExtensionModel, + backref="program", + uselist=False, + cascade="all,delete", + passive_deletes=True, + ) + + """Initialises ProgramModel class.""" + ## required fields + def __init__(self, program_name, organization_id, start_date, end_date): + self.program_name = program_name + self.organization = organization_id + self.start_date = start_date + self.end_date = end_date + + # default value + self.target_candidate = None + self.status = ProgramStatus.DRAFT + self.creation_date = time.time() + + + def json(self): + """Returns ProgramModel object in json format.""" + return { + "id": self.id, + "program_name": self.program_name, + "organization_id": self.organization_id, + "start_date": self.start_date, + "end_date": self.end_date, + "description": self.description, + "target_skills": self.target_skills, + "target_candidate": self.target_candidate, + "payment_currency": self.payment_currency, + "payment_amount": self.payment_amount, + "contact_type": self.contact_type, + "zone": self.zone, + "student_responsibility": self.student_responsibility, + "mentor_responsibility": self.mentor_responsibility, + "organization_responsibility": self.organization_responsibility, + "student_requirements": self.student_requirements, + "mentor_requirements": self.mentor_requirements, + "resources_provided": self.resources_provided, + "contact_name": self.contact_name, + "contact_department": self.contact_department, + "program_address": self.program_address, + "contact_phone": self.contact_phone, + "contact_mobile": self.contact_mobile, + "contact_email": self.contact_email, + "program_website": self.program_website, + "irc_channel": self.irc_channel, + "tags": self.tags, + "status": self.status, + "creation_date": self.creation_date, + } + + def __repr__(self): + """Returns the program name, creation/start/end date and organization id.""" + return ( + f"Program name is {self.program_name}.\n" + f"Organization's id is {self.company_id}.\n" + f"Program start date is {self.start_date}\n" + f"Program end date is {self.end_date}\n" + f"Program creation date is {self.creation_date}\n" + ) + + @classmethod + def find_by_id(cls, _id) -> "ProgramModel": + + """Returns the Program that has the passed id. + Args: + _id: The id of a Program. + """ + return cls.query.filter_by(id=_id).first() + + @classmethod + def find_by_name(cls, program_name) -> "ProgramModel": + + """Returns the Program that has the passed name. + Args: + _id: The id of a Program. + """ + return cls.query.filter_by(program_name=program_name).first() + + @classmethod + def get_all_programs(cls, rep_id): + """Returns all the programs where representative id is the passed id. """ + return cls.query.filter_by(rep_id=rep_id).all() + + def save_to_db(self) -> None: + """Adds a program to the database. """ + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes a program from the database. """ + db.session.delete(self) + db.session.commit() \ No newline at end of file diff --git a/app/database/models/bit_schema/user_extension.py b/app/database/models/bit_schema/user_extension.py new file mode 100644 index 0000000..fd640e0 --- /dev/null +++ b/app/database/models/bit_schema/user_extension.py @@ -0,0 +1,74 @@ +from app.database.sqlalchemy_extension import db +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy import null +from app.utils.bitschema_utils import Timezone + + +class UserExtensionModel(db.Model): + """Defines attributes for a user that are specific only to BridgeInTech. + Attributes: + user_id: A string for storing user_id. + is_organization_rep: A boolean indicating that user is a organization representative. + additional_info: A json object for storing other information of the user with the specified id. + timezone: A string for storing user timezone information. + """ + + # Specifying database table used for UserExtensionModel + __tablename__ = "users_extension" + __table_args__ = {"schema": "bitschema", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + user_id = db.Column(db.Integer, db.ForeignKey("public.users.id", ondelete="CASCADE"), nullable=False, unique=True) + is_organization_rep = db.Column(db.Boolean) + additional_info = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) + timezone = db.Column(db.Enum(Timezone)) + + + """Initialises UserExtensionModel class.""" + ## required fields + def __init__(self, user_id, is_organization_rep, timezone): + self.user_id = user_id + self.timezone = timezone + + # default value + self.is_organization_rep = is_organization_rep + self.additional_info = None + + def json(self): + """Returns UserExtensionmodel object in json format.""" + return { + "id": self.id, + "user_id": self.user_id, + "is_organization_rep": self.is_organization_rep, + "timezone": self.timezone, + "additional_info": self.additional_info, + } + + def __repr__(self): + """Returns user's information that is specific to BridgeInTech.""" + + return ( + f"Users's id is {self.user_id}.\n" + f"User's as organization representative is: {self.is_organization_rep}\n" + f"User's timezone is: {self.timezone}\n" + ) + + @classmethod + def find_by_user_id(cls, user_id) -> "UserExtensionModel": + + """Returns the user extension that has the passed user id. + Args: + _id: The id of a user. + """ + return cls.query.filter_by(user_id=user_id).first() + + def save_to_db(self) -> None: + """Adds user's BridgeInTech specific data to the database. """ + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes user's BridgeInTech specific data from the database. """ + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/ms_schema/__init__.py b/app/database/models/ms_schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/models/ms_schema/mentorship_relation.py b/app/database/models/ms_schema/mentorship_relation.py new file mode 100644 index 0000000..8c48eb2 --- /dev/null +++ b/app/database/models/ms_schema/mentorship_relation.py @@ -0,0 +1,151 @@ +from datetime import date + +from app.database.models.ms_schema.tasks_list import TasksListModel +# from app.database.models.bitschema import MentorshipRelationExtensionModel +from app.database.sqlalchemy_extension import db +from app.utils.enum_utils import MentorshipRelationState + + +class MentorshipRelationModel(db.Model): + """Data Model representation of a mentorship relation. + + Attributes: + mentor_id: integer indicates the id of the mentor. + mentee_id: integer indicates the id of the mentee. + action_user_id: integer indicates id of action user. + mentor: relationship between UserModel and mentorship_relation. + mentee: relationship between UserModel and mentorship_relation. + creation_date: numeric that defines the date of creation of the mentorship. + accept_date: numeric that indicates the date of acceptance of mentorship without a program. + start_date: numeric that indicates the starting date of mentorship which also starts of the program (if any). + end_date: numeric that indicates the ending date of mentorship which also ends of the program (if any). + state: enumeration that indicates state of mentorship. + notes: string that indicates any notes. + tasks_list_id: integer indicates the id of the tasks_list + tasks_list: relationship between TasksListModel and mentorship_relation. + mentor_agreed: numeric that indicates the date when mentor accepted to a program. + mentee_agreed: numeric that indicates the date when mentee accepted to a program. + """ + + # Specifying database table used for MentorshipRelationModel + __tablename__ = "mentorship_relations" + __table_args__ = {"schema": "public", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + + # personal data + mentor_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) + mentee_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) + action_user_id = db.Column(db.Integer, nullable=False) + mentor = db.relationship( + # UserModel, + "UserModel", + backref="mentor_relations", + primaryjoin="MentorshipRelationModel.mentor_id == UserModel.id", + ) + mentee = db.relationship( + # UserModel, + "UserModel", + backref="mentee_relations", + primaryjoin="MentorshipRelationModel.mentee_id == UserModel.id", + ) + + creation_date = db.Column(db.Numeric("16,6", asdecimal=False), nullable=False) + + accept_date = db.Column(db.Numeric("16,6", asdecimal=False)) + + start_date = db.Column(db.Numeric("16,6", asdecimal=False)) + end_date = db.Column(db.Numeric("16,6", asdecimal=False)) + + state = db.Column(db.Enum(MentorshipRelationState), nullable=False) + notes = db.Column(db.String(400)) + + tasks_list_id = db.Column(db.Integer, db.ForeignKey("public.tasks_list.id")) + tasks_list = db.relationship( + TasksListModel, uselist=False, backref="mentorship_relation" + ) + + mentorship_relation_extension = db.relationship( + "MentorshipRelationExtensionModel", + backref="mentorship_relation", + uselist=False, + cascade="all,delete", + passive_deletes=True, + ) + + + # pass in parameters in a dictionary + def __init__( + self, + action_user_id, + mentor_user, + mentee_user, + creation_date, + end_date, + state, + notes, + tasks_list, + ): + + self.action_user_id = action_user_id + self.mentor = mentor_user + self.mentee = mentee_user # same as mentee_user.mentee_relations.append(self) + self.creation_date = creation_date + self.end_date = end_date + self.state = state + self.notes = notes + self.tasks_list = tasks_list + + def json(self): + """Returns information of mentorship as a json object.""" + return { + "id": self.id, + "action_user_id": self.action_user_id, + "mentor_id": self.mentor_id, + "mentee_id": self.mentee_id, + "creation_date": self.creation_date, + "accept_date": self.accept_date, + "start_date": self.start_date, + "end_date": self.end_date, + "state": self.state, + "notes": self.notes, + } + + # def __repr__(self): + # return "Mentorship Relation with id = %s, Mentor has id = %s and Mentee has id = %d" \ + # % (self.id, self.mentor_id, self.mentee_id) + + @classmethod + def find_by_id(cls, _id) -> "MentorshipRelationModel": + + """Returns the mentorship that has the passed id. + Args: + _id: The id of a mentorship. + """ + return cls.query.filter_by(id=_id).first() + + @classmethod + def is_empty(cls) -> bool: + """Returns True if the mentorship model is empty, and False otherwise.""" + return cls.query.first() is None + + @classmethod + def find_by_program_id(cls, program_id): + + """Returns list of mentorship that has the passed program id. + Args: + program_id: The id of a program which the mentorships related to. + """ + return cls.query.filter_by(program_id=program_id).first().all + + def save_to_db(self) -> None: + """Saves the model to the database.""" + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes the record of mentorship relation from the database.""" + self.tasks_list.delete_from_db() + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/ms_schema/task_comment.py b/app/database/models/ms_schema/task_comment.py new file mode 100644 index 0000000..d60eddd --- /dev/null +++ b/app/database/models/ms_schema/task_comment.py @@ -0,0 +1,107 @@ +from datetime import datetime + +from app.api.validations.task_comment import COMMENT_MAX_LENGTH +from app.database.sqlalchemy_extension import db + + +class TaskCommentModel(db.Model): + """Defines attributes for the task comment. + + Attributes: + task_id: An integer for storing the task's id. + user_id: An integer for storing the user's id. + relation_id: An integer for storing the relation's id. + creation_date: A float indicating comment's creation date. + modification_date: A float indicating the modification date. + comment: A string indicating the comment. + """ + + # Specifying database table used for TaskCommentModel + __tablename__ = "tasks_comments" + __table_args__ = {"schema": "public", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) + task_id = db.Column(db.Integer, db.ForeignKey("public.tasks_list.id")) + relation_id = db.Column(db.Integer, db.ForeignKey("public.mentorship_relations.id")) + creation_date = db.Column(db.Numeric("16,6", asdecimal=False), nullable=False) + modification_date = db.Column(db.Numeric("16,6", asdecimal=False)) + comment = db.Column(db.String(COMMENT_MAX_LENGTH), nullable=False) + + def __init__(self, user_id, task_id, relation_id, comment): + # required fields + self.user_id = user_id + self.task_id = task_id + self.relation_id = relation_id + self.comment = comment + + # default fields + self.creation_date = datetime.now().timestamp() + + def json(self): + """Returns information of task comment as a JSON object.""" + return { + "id": self.id, + "user_id": self.user_id, + "task_id": self.task_id, + "relation_id": self.relation_id, + "creation_date": self.creation_date, + "modification_date": self.modification_date, + "comment": self.comment, + } + + def __repr__(self): + """Returns the task and user ids, creation date and the comment.""" + return ( + f"User's id is {self.user_id}. Task's id is {self.task_id}. " + f"Comment was created on: {self.creation_date}\n" + f"Comment: {self.comment}" + ) + + @classmethod + def find_by_id(cls, _id): + """Returns the task comment that has the passed id. + Args: + _id: The id of the task comment. + """ + return cls.query.filter_by(id=_id).first() + + @classmethod + def find_all_by_task_id(cls, task_id, relation_id): + """Returns all task comments that has the passed task id. + Args: + task_id: The id of the task. + relation_id: The id of the relation. + """ + return cls.query.filter_by(task_id=task_id, relation_id=relation_id).all() + + @classmethod + def find_all_by_user_id(cls, user_id): + """Returns all task comments that has the passed user id. + Args: + user_id: The id of the user. + """ + return cls.query.filter_by(user_id=user_id).all() + + def modify_comment(self, comment): + """Changes the comment and the modification date. + Args: + comment: New comment. + """ + self.comment = comment + self.modification_date = datetime.now().timestamp() + + @classmethod + def is_empty(cls): + """Returns a boolean if the TaskCommentModel is empty or not.""" + return cls.query.first() is None + + def save_to_db(self): + """Adds a comment task to the database.""" + db.session.add(self) + db.session.commit() + + def delete_from_db(self): + """Deletes a comment task from the database.""" + db.session.delete(self) + db.session.commit() diff --git a/app/database/models/ms_schema/tasks_list.py b/app/database/models/ms_schema/tasks_list.py new file mode 100644 index 0000000..acf6f8d --- /dev/null +++ b/app/database/models/ms_schema/tasks_list.py @@ -0,0 +1,212 @@ +from enum import unique, Enum + +from app.database.db_types.JsonCustomType import JsonCustomType +from app.database.sqlalchemy_extension import db +from datetime import date + + +class TasksListModel(db.Model): + """Model representation of a list of tasks. + + Attributes: + id: Id of the list of tasks. + tasks: A list of tasks, using JSON format. + next_task_id: Id of the next task added to the list of tasks. + """ + + __tablename__ = "tasks_list" + __table_args__ = {"schema": "public", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + tasks = db.Column(JsonCustomType) + next_task_id = db.Column(db.Integer) + + def __init__(self, tasks: "TasksListModel" = None): + """Initializes tasks. + + Args: + tasks: A list of tasks. + + Raises: + A Value Error if the task is not initialized. + """ + + if tasks is None: + self.tasks = [] + self.next_task_id = 1 + else: + if isinstance(tasks, list): + self.tasks = [] + self.next_task_id = len(tasks) + 1 + else: + raise ValueError(TypeError) + + def add_task( + self, description: str, created_at: date, is_done=False, completed_at=None + ) -> None: + """Adds a task to the list of tasks. + + Args: + description: A description of the task added. + created_at: Date on which the task is created. + is_done: Boolean specifying completion of the task. + completed_at: Date on which task is completed. + """ + + task = { + TasksFields.ID.value: self.next_task_id, + TasksFields.DESCRIPTION.value: description, + TasksFields.IS_DONE.value: is_done, + TasksFields.CREATED_AT.value: created_at, + TasksFields.COMPLETED_AT.value: completed_at, + } + self.next_task_id += 1 + self.tasks = self.tasks + [task] + + def delete_task(self, task_id: int) -> None: + """Deletes a task from the list of tasks. + + Args: + task_id: Id of the task to be deleted. + """ + + new_list = list( + filter(lambda task: task[TasksFields.ID.value] != task_id, self.tasks) + ) + + self.tasks = new_list + self.save_to_db() + + def update_task( + self, + task_id: int, + description: str = None, + is_done: bool = None, + completed_at: date = None, + ) -> None: + """Updates a task. + + Args: + task_id: Id of the task to be updated. + description: A description of the task. + created_at: Date on which the task is created. + is_done: Boolean specifying completion of the task. + completed_at: Date on which task is completed. + """ + + new_list = [] + for task in self.tasks: + if task[TasksFields.ID.value] == task_id: + new_task = task.copy() + if description is not None: + new_task[TasksFields.DESCRIPTION.value] = description + + if is_done is not None: + new_task[TasksFields.IS_DONE.value] = is_done + + if completed_at is not None: + new_task[TasksFields.COMPLETED_AT.value] = completed_at + + new_list = new_list + [new_task] + continue + + new_list = new_list + [task] + + self.tasks = new_list + self.save_to_db() + + def find_task_by_id(self, task_id: int): + """Returns the task that has the specified id. + + Args: + task_id: Id of the task. + + Returns: + The task instance. + """ + task = list( + filter(lambda task: task[TasksFields.ID.value] == task_id, self.tasks) + ) + if len(task) == 0: + return None + else: + return task[0] + + def is_empty(self) -> bool: + """Checks if the list of tasks is empty. + + Returns: + Boolean; True if the task is empty, False otherwise. + """ + + return len(self.tasks) == 0 + + def json(self): + """Creates json object of the attributes of list of tasks. + + Returns: + Json objects of attributes of list of tasks. + """ + + return { + "id": self.id, + "mentorship_relation_id": self.mentorship_relation_id, + "tasks": self.tasks, + "next_task_id": self.next_task_id, + } + + def __repr__(self): + """Creates a representation of an object. + + Returns: + A string representation of the task object. + """ + + return "Task | id = %s; tasks = %s; next task id = %s" % ( + self.id, + self.tasks, + self.next_task_id, + ) + + @classmethod + def find_by_id(cls, _id: int): + """Finds a task with the specified id. + + Returns: + The task with the specified id. + """ + + return cls.query.filter_by(id=_id).first() + + def save_to_db(self) -> None: + """Adds a task to the database.""" + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes a task from the database.""" + db.session.delete(self) + db.session.commit() + + +@unique +class TasksFields(Enum): + """Represents a task attributes' name. + + Attributes: + ID: Id of a task. + DESCRIPTION: Description of a task. + IS_DONE: Boolean specifying the completion of the task. + COMPLETED_AT: The date on which the task is completed. + CREATED_AT: The date on which the task was created. + """ + + ID = "id" + DESCRIPTION = "description" + IS_DONE = "is_done" + COMPLETED_AT = "completed_at" + CREATED_AT = "created_at" + + def values(self): + """Returns a list containing a task.""" + return list(map(str, self)) diff --git a/app/database/models/ms_schema/user.py b/app/database/models/ms_schema/user.py new file mode 100644 index 0000000..3a83fa1 --- /dev/null +++ b/app/database/models/ms_schema/user.py @@ -0,0 +1,188 @@ +from typing import Optional + +from werkzeug.security import generate_password_hash, check_password_hash +import time +from app.database.sqlalchemy_extension import db +from sqlalchemy.types import JSON +from sqlalchemy import null +from app.database.models.bit_schema.personal_background import PersonalBackgroundModel +from app.database.models.bit_schema.organization import OrganizationModel +from app.database.models.bit_schema.user_extension import UserExtensionModel + + +class UserModel(db.Model): + """Defines attributes for the user. + + Attributes: + name: A string for storing the user's name. + username: A string for storing the user's username. + password: A string for storing the user's password. + email: A string for storing user email. + terms_and_conditions_checked: A boolean indicating if user has agreed to terms and conditions or not. + """ + + # Specifying database table used for UserModel + __tablename__ = "users" + __table_args__ = {"schema": "public", "extend_existing": True} + + id = db.Column(db.Integer, primary_key=True) + + # personal data + name = db.Column(db.String(30)) + username = db.Column(db.String(30), unique=True) + email = db.Column(db.String(254), unique=True) + + # security + password_hash = db.Column(db.String(100)) + + # registration + registration_date = db.Column(db.Float) + terms_and_conditions_checked = db.Column(db.Boolean) + + # admin + is_admin = db.Column(db.Boolean) + + # email verification + is_email_verified = db.Column(db.Boolean) + email_verification_date = db.Column(db.DateTime) + + # other info + current_mentorship_role = db.Column(db.Integer) + membership_status = db.Column(db.Integer) + + bio = db.Column(db.String(500)) + location = db.Column(db.String(80)) + occupation = db.Column(db.String(80)) + organization = db.Column(db.String(80)) + slack_username = db.Column(db.String(80)) + social_media_links = db.Column(db.String(500)) + skills = db.Column(db.String(500)) + interests = db.Column(db.String(200)) + resume_url = db.Column(db.String(200)) + photo_url = db.Column(db.String(200)) + need_mentoring = db.Column(db.Boolean) + available_to_mentor = db.Column(db.Boolean) + # one to many relation to personal background, + # on user delete, personal background will also be deleted. + background = db.relationship( + PersonalBackgroundModel, + backref="user", + uselist=False, + cascade="all,delete", + passive_deletes=True, + ) + # one to many relation to organization, + # on user delete, organization will be de-associated but still exist + # which will allow the organization to appoint a new representative. + organization = db.relationship(OrganizationModel, backref="user", uselist=False) + user_extension = db.relationship( + UserExtensionModel, + backref="user", + uselist=False, + cascade="all,delete", + passive_deletes=True, + ) + + def __init__(self, name, username, password, email, terms_and_conditions_checked): + """Initialises userModel class with name, username, password, email, and terms_and_conditions_checked. """ + ## required fields + + self.name = name + self.username = username + self.email = email + self.terms_and_conditions_checked = terms_and_conditions_checked + + # saving hash instead of saving password in plain text + self.set_password(password) + + # default values + self.is_admin = True if self.is_empty() else False # first user is admin + self.is_email_verified = False + self.registration_date = time.time() + + # optional fields + self.need_mentoring = False + self.available_to_mentor = False + # self.is_company_rep = False + + def json(self): + """Returns Usermodel object in json format.""" + return { + "id": self.id, + "name": self.name, + "username": self.username, + "password_hash": self.password_hash, + "email": self.email, + "terms_and_conditions_checked": self.terms_and_conditions_checked, + "registration_date": self.registration_date, + "is_admin": self.is_admin, + "is_email_verified": self.is_email_verified, + "email_verification_date": self.email_verification_date, + "current_mentorship_role": self.current_mentorship_role, + "membership_status": self.membership_status, + "bio": self.bio, + "location": self.location, + "occupation": self.occupation, + "organization": self.organization, + "slack_username": self.slack_username, + "social_media_links": self.social_media_links, + "skills": self.skills, + "interests": self.interests, + "resume_url": self.resume_url, + "photo_url": self.photo_url, + "need_mentoring": self.need_mentoring, + "available_to_mentor": self.available_to_mentor, + } + + def __repr__(self): + """Returns the user's name and username. """ + return "User name %s. Username is %s ." % (self.name, self.username) + + @classmethod + def find_by_username(cls, username: str) -> "UserModel": + """Returns the user that has the username we searched for. """ + return cls.query.filter_by(username=username).first() + + @classmethod + def find_by_email(cls, email: str) -> "UserModel": + """Returns the user that has the email we searched for. """ + return cls.query.filter_by(email=email).first() + + @classmethod + def find_by_id(cls, _id: int) -> "UserModel": + """Returns the user that has the id we searched for. """ + return cls.query.filter_by(id=_id).first() + + @classmethod + def get_all_admins(cls, is_admin=True): + """Returns all the admins. """ + return cls.query.filter_by(is_admin=is_admin).all() + + @classmethod + def is_empty(cls) -> bool: + """Returns a boolean if the Usermodel is empty or not. """ + return cls.query.first() is None + + @classmethod + def get_all_representatives(cls, is_company_rep=True): + """Returns all users who is a company representative. """ + return cls.query.filter_by(is_company_rep=is_company_rep).all() + + def set_password(self, password_plain_text: str) -> None: + """Sets user password when they create an account or when they are changing their password. """ + self.password_hash = generate_password_hash(password_plain_text) + + # checks if password is the same, using its hash + def check_password(self, password_plain_text: str) -> bool: + """Returns a boolean if password is the same as it hash or not. """ + return check_password_hash(self.password_hash, password_plain_text) + + def save_to_db(self) -> None: + """Adds a user to the database. """ + db.session.add(self) + db.session.commit() + + def delete_from_db(self) -> None: + """Deletes a user from the database. """ + db.session.delete(self) + db.session.commit() diff --git a/app/database/sqlalchemy_extension.py b/app/database/sqlalchemy_extension.py new file mode 100644 index 0000000..ddac6ce --- /dev/null +++ b/app/database/sqlalchemy_extension.py @@ -0,0 +1,11 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import MetaData + +db = SQLAlchemy(metadata=MetaData(naming_convention={ + 'pk': 'pk_%(table_name)s', + 'fk': 'fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s', + 'ix': 'ix_%(table_name)s_%(column_0_name)s', + 'uq': 'uq_%(table_name)s_%(column_0_name)s', + 'ck': 'ck_%(table_name)s_%(constraint_name)s', +})) diff --git a/app/messages.py b/app/messages.py new file mode 100644 index 0000000..fc7c9d1 --- /dev/null +++ b/app/messages.py @@ -0,0 +1,287 @@ +from app.api.validations.user import (PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH) + +# Invalid fields +NAME_INPUT_BY_USER_IS_INVALID = {"message": "Your name is invalid."} +EMAIL_INPUT_BY_USER_IS_INVALID = {"message": "Your email is invalid."} +USERNAME_INPUT_BY_USER_IS_INVALID = {"message": "Your username is invalid."} +NEW_USERNAME_INPUT_BY_USER_IS_INVALID = {"message": "Your new username is" " invalid."} +TOKEN_IS_INVALID = {"message": "The token is invalid!"} +USER_ID_IS_NOT_VALID = {"message": "User id is not valid."} +FIELD_NEED_MENTORING_IS_NOT_VALID = {"message": "Field need_mentoring is" " not valid."} +FIELD_AVAILABLE_TO_MENTOR_IS_INVALID = { + "message": "Field available_to_mentor" " is not valid." +} +PASSWORD_INPUT_BY_USER_HAS_INVALID_LENGTH={"message": f"The password field has to be longer than {PASSWORD_MIN_LENGTH} characters and shorter than {PASSWORD_MAX_LENGTH} characters."} + +# Not found +MENTORSHIP_RELATION_REQUEST_DOES_NOT_EXIST = { + "message": "This mentorship" " relation request does not" " exist." +} +MENTORSHIP_RELATION_DOES_NOT_EXIST = { + "message": "Mentorship relation does not" " exist." +} +USER_NOT_FOUND = {"message": "User not found."} +MENTOR_DOES_NOT_EXIST = {"message": "Mentor user does not exist."} +MENTEE_DOES_NOT_EXIST = {"message": "Mentee user does not exist."} +TASK_DOES_NOT_EXIST = {"message": "Task does not exist."} +USER_DOES_NOT_EXIST = {"message": "User does not exist."} +TASK_COMMENT_DOES_NOT_EXIST = {"message": "Task comment does not exist."} +TASK_COMMENT_WITH_GIVEN_TASK_ID_DOES_NOT_EXIST = { + "message": "Task comment with given task id does not exist." +} + +# Missing fields +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."} +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."} +NAME_FIELD_IS_MISSING = {"message": "Name field is missing."} +EMAIL_FIELD_IS_MISSING = {"message": "Email field is missing."} +TERMS_AND_CONDITIONS_FIELD_IS_MISSING = { + "message": "Terms and conditions" " field is missing." +} +CURRENT_PASSWORD_FIELD_IS_MISSING = {"message": "Current password field is" " missing."} +NEW_PASSWORD_FIELD_IS_MISSING = {"message": "New password field is missing."} +AUTHORISATION_TOKEN_IS_MISSING = {"message": "The authorization token is" " missing!"} +DESCRIPTION_FIELD_IS_MISSING = {"message": "Description field is missing."} +COMMENT_FIELD_IS_MISSING = {"message": "Comment field is missing."} + +# Admin +USER_IS_ALREADY_AN_ADMIN = {"message": "User is already an Admin."} +USER_CANNOT_BE_ASSIGNED_ADMIN_BY_USER = { + "message": "You cannot assign" " yourself as an Admin." +} +USER_IS_NOT_AN_ADMIN = {"message": "User is not an Admin."} +USER_ADMIN_STATUS_WAS_REVOKED = {"message": "User admin status was revoked."} +USER_CANT_DELETE = { + "message": "You cannot delete your account, since you are" " the only Admin left." +} +USER_CANNOT_REVOKE_ADMIN_STATUS = {"message": "You cannot revoke your admin" "status."} +USER_ASSIGN_NOT_ADMIN = { + "message": "You don't have admin status. You can't" " assign other user as admin." +} +USER_REVOKE_NOT_ADMIN = { + "message": "You don't have admin status. You can't" " revoke other admin user." +} +USER_IS_NOW_AN_ADMIN = {"message": "User is now an Admin."} + +# Mentor availability +MENTOR_NOT_AVAILABLE_TO_MENTOR = { + "message": "Mentor user is not available to" " mentor." +} +MENTOR_ALREADY_IN_A_RELATION = {"message": "Mentor user is already in a relationship."} + +# Mentee availability +MENTEE_NOT_AVAIL_TO_BE_MENTORED = { + "message": "Mentee user is not available" " to be mentored." +} +MENTEE_ALREADY_IN_A_RELATION = { + "message": "Mentee user is already in a" " relationship." +} + +# Mismatch of fields +MATCH_EITHER_MENTOR_OR_MENTEE = { + "message": "Your ID has to match either" " Mentor or Mentee IDs." +} +TASK_COMMENT_WAS_NOT_CREATED_BY_YOU = { + "message": "You have not created the comment and therefore cannot " "modify it." +} +TASK_COMMENT_WAS_NOT_CREATED_BY_YOU_DELETE = { + "message": "You have not created the comment and therefore cannot " "delete it." +} + +# Update +NO_DATA_FOR_UPDATING_PROFILE_WAS_SENT = { + "message": "No data for updating" "profile was sent." +} + +# Relation constraints +MENTOR_ID_SAME_AS_MENTEE_ID = { + "message": "You cannot have a mentorship" " relation with yourself." +} +END_TIME_BEFORE_PRESENT = {"message": "End date is invalid since date has" " passed."} +MENTOR_TIME_GREATER_THAN_MAX_TIME = { + "message": "Mentorship relation maximum" " duration is 6 months." +} +MENTOR_TIME_LESS_THAN_MIN_TIME = { + "message": "Mentorship relation minimum" " duration is 4 week." +} +CANT_ACCEPT_MENTOR_REQ_SENT_BY_USER = { + "message": "You cannot accept a" " mentorship request sent by yourself." +} +CANT_ACCEPT_UNINVOLVED_MENTOR_RELATION = { + "message": "You cannot accept a" + " mentorship relation where you are" + " not involved." +} +USER_CANT_REJECT_REQUEST_SENT_BY_USER = { + "message": "You cannot reject a" " mentorship request sent by yourself." +} +CANT_REJECT_UNINVOLVED_RELATION_REQUEST = { + "message": "You cannot reject a" + " mentorship relation where you are" + " not involved." +} +CANT_CANCEL_UNINVOLVED_REQUEST = { + "message": "You cannot cancel a mentorship" " relation where you are not involved." +} +CANT_DELETE_UNINVOLVED_REQUEST = { + "message": "You cannot delete a mentorship" " request that you did not create." +} +NOT_IN_MENTORED_RELATION_CURRENTLY = { + "message": "You are not in a current" " mentorship relation." +} +USER_IS_INVOLVED_IN_A_MENTORSHIP_RELATION = { + "message": "You are currently" " involved in a" " mentorship relation." +} +USER_NOT_INVOLVED_IN_THIS_MENTOR_RELATION = { + "message": "You are not involved" " in this mentorship relation." +} +USER_USES_A_USERNAME_THAT_ALREADY_EXISTS = { + "message": "A user with that" " username already exists." +} +USER_USES_AN_EMAIL_ID_THAT_ALREADY_EXISTS = { + "message": "A user with that " "email already exists." +} +USER_IS_NOT_REGISTERED_IN_THE_SYSTEM = { + "message": "You are not registered in" "the system." +} +NAME_LENGTH_GREATER_THAN_MAX_LIMIT = { + "message": "The {field_name} field has" + " to be shorter than {max_limit}" + " characters." +} +NAME_LENGTH_LESSER_THAN_MAX_LIMIT = { + "message": "The {field_name} field has to" + " be longer than {min_limit} characters" + " and shorter than {max_limit}" + " characters." +} +USER_INPUTS_INCORRECT_CONFIGURATION_VALUE = { + "message": "The environment" + " config value has to be within" + " these values: prod," + "dev, test." +} + +# Mentorship state +NOT_PENDING_STATE_RELATION = { + "message": "This mentorship relation is not in" " the pending state." +} +UNACCEPTED_STATE_RELATION = { + "message": "This mentorship relation status is" " not in the accepted state." +} +MENTORSHIP_RELATION_NOT_IN_ACCEPT_STATE = { + "message": "Mentorship relation is" " not in the accepted state." +} + +# Login errors +USER_ENTERED_INCORRECT_PASSWORD = {"message": "Current password is incorrect."} +USER_ENTERED_CURRENT_PASSWORD = { + "message": "New password should not be same " "as the current password." +} +EMAIL_EXPIRED_OR_TOKEN_IS_INVALID = { + "message": "The confirmation link is" " invalid or the token has expired." +} +WRONG_USERNAME_OR_PASSWORD = {"message": "Username or password is wrong."} +USER_HAS_NOT_VERIFIED_EMAIL_BEFORE_LOGIN = { + "message": "Please verify your" " email before login." +} +NAME_USERNAME_AND_PASSWORD_NOT_IN_STRING_FORMAT = { + "message": "Name, username" " and password must be in" " string format." +} +COMMENT_NOT_IN_STRING_FORMAT = {"message": "Comment must be in string format."} +TERMS_AND_CONDITIONS_ARE_NOT_CHECKED = { + "message": "Terms and conditions are" " not checked." +} +USER_INPUTS_SPACE_IN_PASSWORD = {"message": "Password shouldn't contain" " spaces."} +TOKEN_HAS_EXPIRED = { + "message": "The token has expired! Please, login again or refresh it." +} +TOKEN_SENT_TO_EMAIL_OF_USER = {"message": "Token sent to the user's email."} +EMAIL_VERIFICATION_MESSAGE = { + "message": "Check your email, a new verification" " email was sent." +} + +# Success messages +TASK_WAS_ALREADY_ACHIEVED = {"message": "Task was already achieved."} +MENTORSHIP_RELATION_WAS_SENT_SUCCESSFULLY = { + "message": "Mentorship relation" " was sent successfully." +} +MENTORSHIP_RELATION_WAS_ACCEPTED_SUCCESSFULLY = { + "message": "Mentorship" " relation was accepted" " successfully." +} +MENTORSHIP_RELATION_WAS_DELETED_SUCCESSFULLY = { + "message": "Mentorship" " relation was deleted" " successfully." +} +MENTORSHIP_RELATION_WAS_REJECTED_SUCCESSFULLY = { + "message": "Mentorship" " relation was" " rejected successfully." +} +MENTORSHIP_RELATION_WAS_CANCELLED_SUCCESSFULLY = { + "message": "Mentorship relation was cancelled successfully." +} +TASK_WAS_CREATED_SUCCESSFULLY = {"message": "Task was created successfully."} +TASK_WAS_DELETED_SUCCESSFULLY = {"message": "Task was deleted successfully."} +TASK_WAS_ACHIEVED_SUCCESSFULLY = {"message": "Task was achieved" " successfully."} +USER_WAS_CREATED_SUCCESSFULLY = { + "message": "User was created successfully." + "A confirmation email has been sent via" + " email. After confirming your email you " + "can login." +} +ACCEPT_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Accept mentorship" " relations with success." +} +REJECTED_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Rejected mentorship" " relations with success." +} +CANCELLED_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Cancelled" " mentorship" " relations with success." +} +DELETED_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Deleted mentorship" " relations with success." +} +RETURNED_PAST_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Returned " "past mentorship " "relations with success." +} +RETURNED_CURRENT_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Returned" " current mentorship" " relation" " with success." +} +RETURNED_PENDING_MENTORSHIP_RELATIONS_WITH_SUCCESS = { + "message": "Returned" " pending mentorship" " relations" " with success." +} +DELETE_TASK_WITH_SUCCESS = {"message": "Delete task with success."} +UPDATED_TASK_WITH_SUCCESS = {"message": "Updated task with success."} +USER_SUCCESSFULLY_CREATED = {"message": "User successfully created."} +USER_SUCCESSFULLY_DELETED = {"message": "User was deleted successfully."} +USER_SUCCESSFULLY_UPDATED = {"message": "User was updated successfully."} +PASSWORD_SUCCESSFULLY_UPDATED = {"message": "Password was updated " "successfully."} +TASK_COMMENT_WAS_CREATED_SUCCESSFULLY = { + "message": "Task comment was created successfully." +} +TASK_COMMENT_WAS_UPDATED_SUCCESSFULLY = { + "message": "Task comment was updated successfully." +} +TASK_COMMENT_WAS_DELETED_SUCCESSFULLY = { + "message": "Task comment was deleted successfully." +} +LIST_TASK_COMMENTS_WITH_SUCCESS = { + "message": "List task comments from a mentorship relation with success." +} + +# confimation +ACCOUNT_ALREADY_CONFIRMED = {"message": "Account already confirmed."} +USER_ALREADY_CONFIRMED_ACCOUNT = {"message": "You already confirm your email."} +ACCOUNT_ALREADY_CONFIRMED_AND_THANKS = { + "message": "You have confirmed your" " account. Thanks!" +} + +# Miscellaneous +VALIDATION_ERROR = {"message": "Validation error."} +INVALID_END_DATE = { + "message": "Validation error. End date represented by the timestamp is invalid." +} +NOT_IMPLEMENTED = {"message": "Not implemented."} +INTERNAL_SERVER_ERROR = {"message": "An unexpected server error occurs while processing your request. Please try again later."} \ No newline at end of file diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/bitschema_utils.py b/app/utils/bitschema_utils.py new file mode 100644 index 0000000..4fc23e3 --- /dev/null +++ b/app/utils/bitschema_utils.py @@ -0,0 +1,202 @@ +from enum import Enum, unique +# from sqlalchemy import Enum +# from sqlalchemy.dialects.postgresql import ENUM + +@unique +class ProgramStatus(Enum): + DRAFT = "Draft" + OPEN = "Open" + IN_PROGRESS = "In_Progress" + COMPLETED = "Completed" + CLOSED = "Closed" + + def programStatus(self): + return list(map(str, self)) + +@unique +class OrganizationStatus(Enum): + DRAFT = "Draft" + PUBLISH = "Publish" + ARCHIVED = "Archived" + + def OrganizationStatus(self): + return list(map(str, self)) + +@unique +class Gender(Enum): + FEMALE = "Female" + MALE = "Male" + OTHER = "Other" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def gender(self): + return list(map(str, self)) + +@unique +class Age(Enum): + UNDER_18 = "Under 18" + AGE_18_TO_20 = "Between 18 to 20 yo" + AGE_21_TO_24 = "Between 21 to 24 yo" + AGE_25_TO_34 = "Between 25 to 34 yo" + AGE_35_TO_44 = "Between 35 to 44 yo" + AGE_45_TO_54 = "Between 45 to 54 yo" + AGE_55_TO_64 = "Between 55 to 64 yo" + ABOVE_65_YO = "Above 65 yo" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def age(self): + return list(map(str, self)) + +@unique +class Ethnicity(Enum): + AFRICAN_AMERICAN = "African-American/Black" + CAUCASIAN = "Caucasian/White" + HISPANIC = "Hispanic/Latinx" + NATIVE_AMERICAN = "Native American/Alaska Native/First Nations" + MIDDLE_EASTERN = "Middle Eastern/North African (MENA)" + ASIAN = "Asian" + OTHER = "Other" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def ethnicity(self): + return list(map(str, self)) + +@unique +class SexualOrientation(Enum): + HETEROSEXUAL = "Heterosexual/Straight" + LGBTIA = "LGBTIA+" + OTHER = "Other" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def sexualOrientation(self): + return list(map(str, self)) + +@unique +class Religion(Enum): + CHRISTIANITY = "Christianity" + JUDAISM = "Judaism" + ISLAM = "Islam" + HINDUISM = "Hinduism" + BUDDHISM = "Buddhism" + OTHER = "Other" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def religion(self): + return list(map(str, self)) + +@unique +class PhysicalAbility(Enum): + WITH_DISABILITY = "With/had limited physical ability (or with/had some type of physical disability/ies)" + WITHOUT_DISABILITY = "Without/have no limitation to physical ability/ies" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def physicalAbility(self): + return list(map(str, self)) + +@unique +class MentalAbility(Enum): + WITH_DISORDER = "With/previously had some type of mental disorders" + WITHOUT_DISORDER = "Without/have no mental disorders" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def mentalAbility(self): + return list(map(str, self)) + +@unique +class SocioEconomic(Enum): + UPPER = "Upper class/Elite" + UPPER_MIDDLE = "Upper Middle class (or High-level Professionals/white collars e.g. enginers/accountants/lawyers/architects/managers/directors" + LOWER_MIDDLE = "Lower Middle class (e.g. blue collars in skilled trades/Paralegals/Bank tellers/Sales/Clerical-Admin/other support workers)" + WORKING = "Working class (e.g. craft workers, factory labourers, restaurant/delivery services workers" + BELOW_POVERTY = "Underclass, working but with wages under poverty line, receiving Social Benefit from Government" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def socioEconomic(self): + return list(map(str, self)) + +@unique +class HighestEducation(Enum): + BELOW_HIGH_SCHOOL = "Have/did not completed High School" + HIGH_SCHOOL = "High School Diploma" + ASSOCIATE = "Associate Degree" + BACHELOR = "Bachelor's Degree" + MASTER = "Master's Degree" + PHD = "PhD or other Doctorate Degrees" + OTHER = "Other" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def highestEducation(self): + return list(map(str, self)) + +@unique +class YearsOfExperience(Enum): + UNDER_ONE = "Less than a year" + UP_TO_3 = "Up to 3 years" + UP_TO_5 = "Up to 5 years" + UP_TO_10 = "Up to 10 year" + OVER_10 = "Over 10 years of experience" + DECLINED = "Prefer not to say" + NOT_APPLICABLE = "Not Applicable" + + def yearsOfExperience(self): + return list(map(str, self)) + +@unique +class ContactType(Enum): + FACE_TO_FACE = "Face-to-face" + REMOTE = "Remote" + BOTH = "Both Remote and Face-to-face" + + def contactType(self): + return list(map(str, self)) + +@unique +class Zone(Enum): + LOCAL = "Local", + NATIONAL = "National", + GLOBAL = "Global", + + def zone(self): + return list(map(str, self)) + +@unique +class Timezone(Enum): + CAPE_VERDE_TIME = "UTC-01:00/Cape Verde Time" + NEWFOUNDLAND_STANDARD_TIME = "UTC-03:30/NewFoundland_Standard_Time" + ATLANTTIC_STANDARD_TIME = "UTC-04:00/Atlantic Standard Time" + EASTERN_STANDARD_TIME = "UTC-05:00/Eastern Standard Time" + CENTRAL_STANDARD_TIME = "UTC-06:00/Central Standard Time" + MOUNTAIN_STANDARD_TIME = "UTC-07:00/Mountain Standard Time" + PACIFIC_STANDARD_TIME = "UTC-08:00/Pacific Standard Time" + ALASKA_STANDARD_TIME = "UTC-09:00/Alaska Standard Time" + HAWAII_ALEUTIAN_STANDARD_TIME = "UTC-10:00/Hawaii-Aleutian Standard Time" + SAMOA_STANDARD_TIME = "UTC-11:00/Samoa Standard Time" + GREENWICH_MEAN_TIME = "UTC+00:00/Greenwich Mean Time and Western European Time" + CENTRAL_EUROPEAN_TIME = "UTC+01:00/Central European Time" + WEST_AFRICA_TIME = "UTC+01:00/West Africa Time" + EASTERN_EUROPEAN_TIME = "UTC+02:00/Eastern European Time" + CENTRAL_SOUTH_AFRICA_TIME = "UTC+02:00/Central and South Africa Standard Time" + EAST_AFRICA_TIME = "UTC+03:00/East Africa Time" + MOSKOW_TIME = "UTC+03:00/Moskow Time" + CHARLIE_TIME = "UTC+03:00/Charlie Time - Middle East Time" + DELTA_TIME = "UTC+04:00/Delta Time - Middle East Time" + INDIA_STANDARD_TIME = "UTC+05:30/India Standard Time" + CHINA_STANDARD_TIME = "UTC+08:00/China Standard TIme" + AUSTRALIAN_WESTERN_STANDARD_TIME = "UTC+08:00/Australian Western Standard Time" + AUSTRALIAN_CENTRAL_SOUTH_STANDARD_TIME = "UTC+09:30/Australian Central and South Standard Time" + AUSTRALIAN_EASTERN_STANDARD_TIME = "UTC+10:00/Australian Eastern Standard Time" + NEW_ZEALAND_STANDARD_TIME = "UTC+12:00/New Zealand Standard Time" + + + + def timezone(self): + return list(map(str, self)) diff --git a/app/utils/decorator_utils.py b/app/utils/decorator_utils.py new file mode 100644 index 0000000..0105309 --- /dev/null +++ b/app/utils/decorator_utils.py @@ -0,0 +1,46 @@ +""" +This module is used to define decorators for the app +""" +from app import messages +from http import HTTPStatus +from app.database.models.ms_schema.user import UserModel + + +def email_verification_required(user_function): + """ + This function is used to validate the + input function i.e. user_function + It will check if the user given as a + parameter to user_function + exists and have its email verified + """ + + def check_verification(*args, **kwargs): + """ + Function to validate the input function ie user_function + - It will return error 404 if user doesn't exist + - It will return error 400 if user hasn't verified email + + Parameters: + Function to be validated can have any type of argument + - list + - dict + """ + + if kwargs: + user = UserModel.find_by_id(kwargs["user_id"]) + else: + user = UserModel.find_by_id(args[0]) + + # verify if user exists + if user: + if not user.is_email_verified: + return ( + messages.USER_HAS_NOT_VERIFIED_EMAIL_BEFORE_LOGIN, + HTTPStatus.FORBIDDEN, + ) + return user_function(*args, **kwargs) + else: + return messages.USER_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND + + return check_verification diff --git a/app/utils/enum_utils.py b/app/utils/enum_utils.py new file mode 100644 index 0000000..610bdd6 --- /dev/null +++ b/app/utils/enum_utils.py @@ -0,0 +1,16 @@ +from enum import IntEnum, unique + + +@unique +class MentorshipRelationState(IntEnum): + PENDING = 1 + ACCEPTED = 2 + REJECTED = 3 + CANCELLED = 4 + COMPLETED = 5 + + + def values(self): + return list(map(int, self)) + + diff --git a/app/utils/validation_utils.py b/app/utils/validation_utils.py new file mode 100644 index 0000000..ffbc916 --- /dev/null +++ b/app/utils/validation_utils.py @@ -0,0 +1,145 @@ +"""This module is to check the validity of input for the "name", "email", "username" fields +against predetermined patterns as well as to ensure the string input is within the accepted length. + +For the "name" field to be valid, it may contain one or more character from: +- letter "a" to "z" and/or "A" to "Z", +- any of the whitespace characters, and/or +- special character "-". + +For the "email" field to be valid, it must have the following structure: +> the first section, which may contain one or more character from: + - letter "a" to "z" and/or "A" to "Z", + - number "0" to "9", + - special character "_", ".", "+", and/or "-". +> followed by the "@" character, +> followed by the second section, which may contain one or more character from: + - letter "a" to "z" and/or "A" to "Z", + - number "0" to "9", and/or + - special character "-". +> followed by the escaped character ".", +> followed by the third section, which may contain one or more character from: + - letter "a" to "z" and/or "A" to "Z", + - number "0" to "9", + - special character "-" and/or ".". + +For the "username" field to be valid, it may contain one or more character from: + - letter "a" to "z" and/or "A" to "Z", + - number "0" to "9", and/or + - special character "-". +""" +import re + +name_regex = r"(^[a-zA-Z\s\-]+$)" +email_regex = r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)" +username_regex = r"(^[a-zA-Z0-9_]+$)" + + +def is_name_valid(name): + """Checks if name input is within the acceptable pattern defined in name_regex. + + Args: + name: string input for name. + + Return: + True: if string input for name is within the acceptable name_regex pattern. + False: if string input for name is not according to name_regex pattern. + """ + return re.match(name_regex, name) + + +def is_email_valid(email): + """Checks if email input is within the acceptable pattern defined in email_regex. + + Args: + email: string input for email. + + Return: + True: if string input for email is within the acceptable email_regex pattern. + False: if string input for email is not according to email_regex pattern. + """ + return re.match(email_regex, email) + + +def is_username_valid(username): + """Checks if username input is within the acceptable pattern defined in username_regex. + + Args: + name: string input for username. + + Return: + True: if string input for username is within the acceptable username_regex pattern. + False: if string input for username is not according to username_regex pattern. + """ + return re.match(username_regex, username) + + +def validate_length(field_length, min_length, max_length, field_name): + """Validates string input. + + Checks the length of the string which is inserted in a particular field against the given values. + + Args: + field_length: length of the string input in a given field. + min_length: minimum acceptable string length. + max_length: maximum acceptable string length. + field_name: the name of the field where the string is inserted. + + Returns: + False, error_msg: if string input is either less than the minimum length, or more than the maximum length. + True, {}: if string input is longer or equals to the minimum length, and less than or equals to the maximum length. + """ + if not (min_length <= field_length <= max_length): + if min_length <= 0: + error_msg = { + "message": get_length_validation_error_message( + field_name, None, max_length + ) + } + else: + error_msg = { + "message": get_length_validation_error_message( + field_name, min_length, max_length + ) + } + return False, error_msg + else: + return True, {} + + +def get_length_validation_error_message(field_name, min_length, max_length): + """Returns an error message which content depends on the given keys. + + Args: + field_name: the name of the field where the string is inserted. + min_length: minimum acceptable string length. + max_length: maximum acceptable string length. + + Returns: + - error message if minimum length is not determined. + - error message if minimum length is determined. + """ + if min_length is None: + return "The {field_name} field has to be shorter than {max_limit} characters.".format( + field_name=field_name, max_limit=max_length + 1 + ) + else: + return ( + "The {field_name} field has to be longer than {min_limit} " + "characters and shorter than {max_limit} characters.".format( + field_name=field_name, + min_limit=min_length - 1, + max_limit=max_length + 1, + ) + ) + + +def get_stripped_string(string_with_whitespaces): + """Returns a new string from key argument that has been cleaned from whitespaces (split and joined by delimiter ""). + + Args: + string_with_whitespaces: string input that has whitespaces. + + Return: + A new string which is the string_with_whitespaces with whitespaces been removed. + """ + return "".join(string_with_whitespaces.split()) diff --git a/config.py b/config.py new file mode 100644 index 0000000..ae9490e --- /dev/null +++ b/config.py @@ -0,0 +1,122 @@ +import os +from datetime import timedelta + +def get_mock_email_config() -> bool: + MOCK_EMAIL = os.getenv("MOCK_EMAIL") + + #if MOCK_EMAIL env variable is set + if MOCK_EMAIL: + # MOCK_EMAIL is case insensitive + MOCK_EMAIL = MOCK_EMAIL.lower() + + if MOCK_EMAIL=="true": + return True + elif MOCK_EMAIL=="false": + return False + else: + # if MOCK_EMAIL env variable is set a wrong value + raise ValueError( + "MOCK_EMAIL environment variable is optional if set, it has to be valued as either 'True' or 'False'" + ) + else: + # Default behaviour is to send the email if MOCK_EMAIL is not set + return False + +class BaseConfig(object): + DEBUG = False + TESTING = False + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # Flask JWT settings + JWT_ACCESS_TOKEN_EXPIRES = timedelta(weeks=1) + JWT_REFRESH_TOKEN_EXPIRES = timedelta(weeks=4) + + # Security + SECRET_KEY = os.getenv("SECRET_KEY", None) + BCRYPT_LOG_ROUNDS = 13 + WTF_CSRF_ENABLED = True + + # mail settings + MAIL_SERVER = os.getenv("MAIL_SERVER") + MAIL_PORT = 465 + MAIL_USE_TLS = False + MAIL_USE_SSL = True + + # email authentication + MAIL_USERNAME = os.getenv("APP_MAIL_USERNAME") + MAIL_PASSWORD = os.getenv("APP_MAIL_PASSWORD") + + # mail accounts + MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER") + + @staticmethod + def build_db_uri( + db_type_arg=os.getenv("DB_TYPE"), + db_user_arg=os.getenv("DB_USERNAME"), + db_password_arg=os.getenv("DB_PASSWORD"), + db_endpoint_arg=os.getenv("DB_ENDPOINT"), + db_name_arg=os.getenv("DB_NAME"), + ): + """Build remote database uri using specific environment variables.""" + + return "{db_type}://{db_user}:{db_password}@{db_endpoint}/{db_name}".format( + db_type=db_type_arg, + db_user=db_user_arg, + db_password=db_password_arg, + db_endpoint=db_endpoint_arg, + db_name=db_name_arg, + ) + +class LocalConfig(BaseConfig): + """Local configuration.""" + + DEBUG = True + + # Using a local postgre database + # SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema" + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() + +class DevelopmentConfig(BaseConfig): + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + DEBUG = True + + # Using elephantsql - BridgeInTech remote db + # https://bridge-in-tech-bit-test.herokuapp.com + SQLALCHEMY_DATABASE_URI = os.getenv('DB_REMOTE_URL') + +class TestingConfig(BaseConfig): + TESTING = True + MOCK_EMAIL = True + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_TEST_URL') + + # Using a local postgre database + SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema_test" + # SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_test_uri() + +class StagingConfig(BaseConfig): + """Staging configuration.""" + + DEBUG = True + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() + MOCK_EMAIL = False + +class ProductionConfig(BaseConfig): + # SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() + MOCK_EMAIL = False + +def get_env_config() -> str: + flask_config_name = os.getenv("FLASK_ENVIRONMENT_CONFIG", "dev") + if flask_config_name not in ["prod", "test", "dev", "local", "stag"]: + raise ValueError( + "The environment config value has to be within these values: prod, dev, test, local, stag." + ) + return CONFIGURATION_MAPPER[flask_config_name] + +CONFIGURATION_MAPPER = { + "dev": "config.DevelopmentConfig", + "prod": "config.ProductionConfig", + "stag": "config.StagingConfig", + "local": "config.LocalConfig", + "test": "config.TestingConfig", +} \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..e5baea5 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,47 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S + +schema_names = public,bitschema \ No newline at end of file diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..774bf93 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,120 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger("alembic.env") + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata + +from flask import current_app + +config.set_main_option( + "sqlalchemy.url", + current_app.config.get("SQLALCHEMY_DATABASE_URI").replace("%", "%%"), +) +import re + +schema_names = re.split(r',\s*', "public,bitschema") + +target_metadata = current_app.extensions['migrate'].db.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, "autogenerate", False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info("No changes in schema detected.") + else: + schema_name = context.opts["x_schema_name"] + + upgrade_ops = script.upgrade_ops_list[-1] + downgrade_ops = script.downgrade_ops_list[-1] + + for op in upgrade_ops.ops + downgrade_ops.ops: + op.schema = schema_name + if hasattr(op, "ops"): + for sub_op in op.ops: + sub_op.schema = schema_name + + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + for schema_name in ["public", "bitschema"]: + connection.execute("SET search_path TO %s" % schema_name) + connection.dialect.default_schema_name = schema_name + context.configure( + connection=connection, + target_metadata=target_metadata, + include_schema=True, + upgrade_token="%s_upgrades" % schema_name, + downgrade_token="%s_downgrades" % schema_name, + process_revision_directives=process_revision_directives, + **current_app.extensions["migrate"].configure_args, + x_schema_name=schema_name + ) + + with context.begin_transaction(): + context.run_migrations(schema_name=schema_name) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..0cb3ac8 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,41 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(schema_name): + globals()["upgrade_%s" % schema_name]() + + +def downgrade(schema_name): + globals()["downgrade_%s" % schema_name]() + +<% + import re + schema_names = re.split(r',\s*', "public,bitschema") +%> + + +% for schema_name in schema_names: + +def upgrade_${schema_name}(): + ${context.get("%s_upgrades" % schema_name, "pass")} + + +def downgrade_${schema_name}(): + ${context.get("%s_downgrades" % schema_name, "pass")} + +% endfor diff --git a/migrations/versions/9a818ccb2f0a_initial_migration.py b/migrations/versions/9a818ccb2f0a_initial_migration.py new file mode 100644 index 0000000..c30a2bb --- /dev/null +++ b/migrations/versions/9a818ccb2f0a_initial_migration.py @@ -0,0 +1,220 @@ +"""Initial migration + +Revision ID: 9a818ccb2f0a +Revises: +Create Date: 2020-06-01 11:30:07.366041 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql +from app.database.db_types.JsonCustomType import JsonCustomType + +# revision identifiers, used by Alembic. +revision = '9a818ccb2f0a' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(schema_name): + globals()["upgrade_%s" % schema_name]() + + +def downgrade(schema_name): + globals()["downgrade_%s" % schema_name]() + +def upgrade_public(): + pass + +def downgrade_public(): + pass + +def upgrade_bitschema(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('tasks_list', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tasks', JsonCustomType, nullable=True), + sa.Column('next_task_id', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_tasks_list')), + schema='public' + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=30), nullable=True), + sa.Column('username', sa.String(length=30), nullable=True), + sa.Column('email', sa.String(length=254), nullable=True), + sa.Column('password_hash', sa.String(length=100), nullable=True), + sa.Column('registration_date', sa.Float(), nullable=True), + sa.Column('terms_and_conditions_checked', sa.Boolean(), nullable=True), + sa.Column('is_admin', sa.Boolean(), nullable=True), + sa.Column('is_email_verified', sa.Boolean(), nullable=True), + sa.Column('email_verification_date', sa.DateTime(), nullable=True), + sa.Column('current_mentorship_role', sa.Integer(), nullable=True), + sa.Column('membership_status', sa.Integer(), nullable=True), + sa.Column('bio', sa.String(length=500), nullable=True), + sa.Column('location', sa.String(length=80), nullable=True), + sa.Column('occupation', sa.String(length=80), nullable=True), + sa.Column('slack_username', sa.String(length=80), nullable=True), + sa.Column('social_media_links', sa.String(length=500), nullable=True), + sa.Column('skills', sa.String(length=500), nullable=True), + sa.Column('interests', sa.String(length=200), nullable=True), + sa.Column('resume_url', sa.String(length=200), nullable=True), + sa.Column('photo_url', sa.String(length=200), nullable=True), + sa.Column('need_mentoring', sa.Boolean(), nullable=True), + sa.Column('available_to_mentor', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('email', name=op.f('uq_users_email')), + sa.UniqueConstraint('username', name=op.f('uq_users_username')), + schema='public' + ) + op.create_table('mentorship_relations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mentor_id', sa.Integer(), nullable=True), + sa.Column('mentee_id', sa.Integer(), nullable=True), + sa.Column('action_user_id', sa.Integer(), nullable=False), + sa.Column('creation_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=False), + sa.Column('accept_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('start_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('end_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('state', sa.Enum('PENDING', 'ACCEPTED', 'REJECTED', 'CANCELLED', 'COMPLETED', name='mentorshiprelationstate'), nullable=False), + sa.Column('notes', sa.String(length=400), nullable=True), + sa.Column('tasks_list_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['mentee_id'], ['public.users.id'], name=op.f('fk_mentorship_relations_mentee_id_users')), + sa.ForeignKeyConstraint(['mentor_id'], ['public.users.id'], name=op.f('fk_mentorship_relations_mentor_id_users')), + sa.ForeignKeyConstraint(['tasks_list_id'], ['public.tasks_list.id'], name=op.f('fk_mentorship_relations_tasks_list_id_tasks_list')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mentorship_relations')), + schema='public' + ) + op.create_table('tasks_comments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('task_id', sa.Integer(), nullable=True), + sa.Column('relation_id', sa.Integer(), nullable=True), + sa.Column('creation_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=False), + sa.Column('modification_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('comment', sa.String(length=400), nullable=False), + sa.ForeignKeyConstraint(['relation_id'], ['public.mentorship_relations.id'], name=op.f('fk_tasks_comments_relation_id_mentorship_relations')), + sa.ForeignKeyConstraint(['task_id'], ['public.tasks_list.id'], name=op.f('fk_tasks_comments_task_id_tasks_list')), + sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], name=op.f('fk_tasks_comments_user_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_tasks_comments')), + schema='public' + ) + + op.create_table('organizations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('rep_id', sa.Integer(), nullable=True), + sa.Column('rep_department', sa.String(length=150), nullable=True), + sa.Column('name', sa.String(length=150), nullable=False), + sa.Column('email', sa.String(length=254), nullable=False), + sa.Column('about', sa.String(length=500), nullable=True), + sa.Column('address', sa.String(length=254), nullable=True), + sa.Column('website', sa.String(length=150), nullable=False), + sa.Column('timezone', sa.Enum('CAPE_VERDE_TIME', 'NEWFOUNDLAND_STANDARD_TIME', 'ATLANTTIC_STANDARD_TIME', 'EASTERN_STANDARD_TIME', 'CENTRAL_STANDARD_TIME', 'MOUNTAIN_STANDARD_TIME', 'PACIFIC_STANDARD_TIME', 'ALASKA_STANDARD_TIME', 'HAWAII_ALEUTIAN_STANDARD_TIME', 'SAMOA_STANDARD_TIME', 'GREENWICH_MEAN_TIME', 'CENTRAL_EUROPEAN_TIME', 'WEST_AFRICA_TIME', 'EASTERN_EUROPEAN_TIME', 'CENTRAL_SOUTH_AFRICA_TIME', 'EAST_AFRICA_TIME', 'MOSKOW_TIME', 'CHARLIE_TIME', 'DELTA_TIME', 'INDIA_STANDARD_TIME', 'CHINA_STANDARD_TIME', 'AUSTRALIAN_WESTERN_STANDARD_TIME', 'AUSTRALIAN_CENTRAL_SOUTH_STANDARD_TIME', 'AUSTRALIAN_EASTERN_STANDARD_TIME', 'NEW_ZEALAND_STANDARD_TIME', name='timezone'), nullable=True), + sa.Column('phone', sa.String(length=20), nullable=True), + sa.Column('status', sa.Enum('DRAFT', 'PUBLISH', 'ARCHIVED', name='organizationstatus'), nullable=True), + sa.Column('join_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.ForeignKeyConstraint(['rep_id'], ['public.users.id'], name=op.f('fk_organizations_rep_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_organizations')), + sa.UniqueConstraint('email', name=op.f('uq_organizations_email')), + sa.UniqueConstraint('name', name=op.f('uq_organizations_name')), + sa.UniqueConstraint('rep_id', name=op.f('uq_organizations_rep_id')), + schema='bitschema' + ) + op.create_table('personal_backgrounds', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('gender', sa.Enum('FEMALE', 'MALE', 'OTHER', 'DECLINED', 'NOT_APPLICABLE', name='gender'), nullable=True), + sa.Column('age', sa.Enum('UNDER_18', 'AGE_18_TO_20', 'AGE_21_TO_24', 'AGE_25_TO_34', 'AGE_35_TO_44', 'AGE_45_TO_54', 'AGE_55_TO_64', 'ABOVE_65_YO', 'DECLINED', 'NOT_APPLICABLE', name='age'), nullable=True), + sa.Column('ethnicity', sa.Enum('AFRICAN_AMERICAN', 'CAUCASIAN', 'HISPANIC', 'NATIVE_AMERICAN', 'MIDDLE_EASTERN', 'ASIAN', 'OTHER', 'DECLINED', 'NOT_APPLICABLE', name='ethnicity'), nullable=True), + sa.Column('sexual_orientation', sa.Enum('HETEROSEXUAL', 'LGBTIA', 'OTHER', 'DECLINED', 'NOT_APPLICABLE', name='sexualorientation'), nullable=True), + sa.Column('religion', sa.Enum('CHRISTIANITY', 'JUDAISM', 'ISLAM', 'HINDUISM', 'BUDDHISM', 'OTHER', 'DECLINED', 'NOT_APPLICABLE', name='religion'), nullable=True), + sa.Column('physical_ability', sa.Enum('WITH_DISABILITY', 'WITHOUT_DISABILITY', 'DECLINED', 'NOT_APPLICABLE', name='physicalability'), nullable=True), + sa.Column('mental_ability', sa.Enum('WITH_DISORDER', 'WITHOUT_DISORDER', 'DECLINED', 'NOT_APPLICABLE', name='mentalability'), nullable=True), + sa.Column('socio_economic', sa.Enum('UPPER', 'UPPER_MIDDLE', 'LOWER_MIDDLE', 'WORKING', 'BELOW_POVERTY', 'DECLINED', 'NOT_APPLICABLE', name='socioeconomic'), nullable=True), + sa.Column('highest_education', sa.Enum('BELOW_HIGH_SCHOOL', 'HIGH_SCHOOL', 'ASSOCIATE', 'BACHELOR', 'MASTER', 'PHD', 'OTHER', 'DECLINED', 'NOT_APPLICABLE', name='highesteducation'), nullable=True), + sa.Column('years_of_experience', sa.Enum('UNDER_ONE', 'UP_TO_3', 'UP_TO_5', 'UP_TO_10', 'OVER_10', 'DECLINED', 'NOT_APPLICABLE', name='yearsofexperience'), nullable=True), + sa.Column('others', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('is_public', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], name=op.f('fk_personal_backgrounds_user_id_users'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_personal_backgrounds')), + sa.UniqueConstraint('user_id', name=op.f('uq_personal_backgrounds_user_id')), + schema='bitschema' + ) + op.create_table('users_extension', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('is_organization_rep', sa.Boolean(), nullable=True), + sa.Column('additional_info', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('timezone', sa.Enum('CAPE_VERDE_TIME', 'NEWFOUNDLAND_STANDARD_TIME', 'ATLANTTIC_STANDARD_TIME', 'EASTERN_STANDARD_TIME', 'CENTRAL_STANDARD_TIME', 'MOUNTAIN_STANDARD_TIME', 'PACIFIC_STANDARD_TIME', 'ALASKA_STANDARD_TIME', 'HAWAII_ALEUTIAN_STANDARD_TIME', 'SAMOA_STANDARD_TIME', 'GREENWICH_MEAN_TIME', 'CENTRAL_EUROPEAN_TIME', 'WEST_AFRICA_TIME', 'EASTERN_EUROPEAN_TIME', 'CENTRAL_SOUTH_AFRICA_TIME', 'EAST_AFRICA_TIME', 'MOSKOW_TIME', 'CHARLIE_TIME', 'DELTA_TIME', 'INDIA_STANDARD_TIME', 'CHINA_STANDARD_TIME', 'AUSTRALIAN_WESTERN_STANDARD_TIME', 'AUSTRALIAN_CENTRAL_SOUTH_STANDARD_TIME', 'AUSTRALIAN_EASTERN_STANDARD_TIME', 'NEW_ZEALAND_STANDARD_TIME', name='timezone'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['public.users.id'], name=op.f('fk_users_extension_user_id_users'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users_extension')), + sa.UniqueConstraint('user_id', name=op.f('uq_users_extension_user_id')), + schema='bitschema' + ) + op.create_table('programs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('program_name', sa.String(length=100), nullable=False), + sa.Column('organization_id', sa.Integer(), nullable=False), + sa.Column('start_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('end_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('target_skills', sa.ARRAY(sa.String(length=150)), nullable=True), + sa.Column('target_candidate', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('payment_currency', sa.String(length=3), nullable=True), + sa.Column('payment_amount', sa.BigInteger(), nullable=True), + sa.Column('contact_type', sa.Enum('FACE_TO_FACE', 'REMOTE', 'BOTH', name='contacttype'), nullable=True), + sa.Column('zone', sa.Enum('LOCAL', 'NATIONAL', 'GLOBAL', name='zone'), nullable=True), + sa.Column('student_responsibility', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('mentor_responsibility', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('organization_responsibility', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('student_requirements', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('mentor_requirements', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('resources_provided', sa.ARRAY(sa.String(length=250)), nullable=True), + sa.Column('contact_name', sa.String(length=50), nullable=True), + sa.Column('contact_department', sa.String(length=150), nullable=True), + sa.Column('program_address', sa.String(length=250), nullable=True), + sa.Column('contact_phone', sa.String(length=20), nullable=True), + sa.Column('contact_mobile', sa.String(length=20), nullable=True), + sa.Column('contact_email', sa.String(length=254), nullable=True), + sa.Column('program_website', sa.String(length=254), nullable=True), + sa.Column('irc_channel', sa.String(length=254), nullable=True), + sa.Column('tags', sa.ARRAY(sa.String(length=150)), nullable=True), + sa.Column('status', sa.Enum('DRAFT', 'OPEN', 'IN_PROGRESS', 'COMPLETED', 'CLOSED', name='programstatus'), nullable=True), + sa.Column('creation_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['bitschema.organizations.id'], name=op.f('fk_programs_organization_id_organizations'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_programs')), + sa.UniqueConstraint('program_name', name=op.f('uq_programs_program_name')), + schema='bitschema' + ) + op.create_table('mentorship_relations_extension', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('program_id', sa.Integer(), nullable=False), + sa.Column('mentorship_relation_id', sa.Integer(), nullable=False), + sa.Column('mentor_request_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('mentor_agreed_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('mentee_request_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.Column('mentee_agreed_date', sa.Numeric(precision='16,6', asdecimal=False), nullable=True), + sa.ForeignKeyConstraint(['mentorship_relation_id'], ['public.mentorship_relations.id'], name=op.f('fk_mentorship_relations_extension_mentorship_relation_id_mentorship_relations'), ondelete='CASCADE'), + sa.ForeignKeyConstraint(['program_id'], ['bitschema.programs.id'], name=op.f('fk_mentorship_relations_extension_program_id_programs'), ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id', name=op.f('pk_mentorship_relations_extension')), + sa.UniqueConstraint('mentorship_relation_id', name=op.f('uq_mentorship_relations_extension_mentorship_relation_id')), + sa.UniqueConstraint('program_id', name=op.f('uq_mentorship_relations_extension_program_id')), + schema='bitschema' + ) + # ### end Alembic commands ### + + +def downgrade_bitschema(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('tasks_comments', schema='public') + op.drop_table('mentorship_relations', schema='public') + op.drop_table('users', schema='public') + op.drop_table('tasks_list', schema='public') + op.drop_table('mentorship_relations_extension', schema='bitschema') + op.drop_table('programs', schema='bitschema') + op.drop_table('users_extension', schema='bitschema') + op.drop_table('personal_backgrounds', schema='bitschema') + op.drop_table('organizations', schema='bitschema') + # ### end Alembic commands ### + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..88dbd64 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,78 @@ +alembic==1.4.2 +aniso8601==3.0.0 +appdirs==1.4.3 +APScheduler==3.5.1 +asn1crypto==0.24.0 +attrs==19.3.0 +awsebcli==3.14.1 +bcrypt==3.1.7 +black==19.3b0 +blessed==1.15.0 +blinker==1.4 +botocore==1.10.48 +cached-property==1.4.3 +cement==2.8.2 +certifi==2018.4.16 +cffi==1.11.5 +chardet==3.0.4 +click==6.7 +colorama==0.3.9 +coverage==4.5.1 +cryptography==2.8 +docker==3.4.0 +docker-compose==1.21.2 +docker-pycreds==0.3.0 +dockerpty==0.4.1 +docopt==0.6.2 +docutils==0.14 +entrypoints==0.3 +Flask==1.1.1 +Flask-JWT-Extended==3.10.0 +Flask-Mail==0.9.1 +Flask-Migrate==2.5.2 +flask-restplus==0.11.0 +flask-restx==0.1.1 +Flask-SQLAlchemy==2.3.2 +Flask-Testing==0.7.1 +future==0.16.0 +gunicorn==20.0.4 +idna==2.6 +importlib-metadata==1.6.0 +itsdangerous==0.24 +Jinja2==2.11.2 +jmespath==0.9.3 +jsonschema==2.6.0 +Mako==1.1.2 +MarkupSafe==1.1.1 +mccabe==0.6.1 +paramiko==2.7.1 +pathspec==0.5.5 +psycopg2-binary==2.8.3 +pycodestyle==2.5.0 +pycparser==2.18 +pyflakes==2.1.1 +PyJWT==1.4.2 +PyMySQL==0.9.2 +PyNaCl==1.3.0 +pyrsistent==0.16.0 +python-dateutil==2.7.3 +python-dotenv==0.8.2 +python-editor==1.0.4 +pytz==2018.4 +PyYAML==3.13 +regex==2020.2.20 +requests==2.18.4 +semantic-version==2.5.0 +six==1.11.0 +SQLAlchemy==1.2.7 +tabulate==0.7.5 +termcolor==1.1.0 +texttable==0.9.1 +toml==0.10.0 +typed-ast==1.4.1 +tzlocal==1.5.1 +urllib3==1.22 +wcwidth==0.1.7 +websocket-client==0.48.0 +Werkzeug==0.15.3 +zipp==3.1.0 diff --git a/run.py b/run.py new file mode 100644 index 0000000..fc0d83d --- /dev/null +++ b/run.py @@ -0,0 +1,94 @@ +import os +from flask import Flask, jsonify +# from flask_restx import Resource, Api +from config import get_env_config +from flask_migrate import Migrate, MigrateCommand + + + +def create_app(config_filename: str) -> Flask: + # instantiate the app + app = Flask(__name__, instance_relative_config=True) + + # setup application environment + app.config.from_object(config_filename) + app.url_map.strict_slashes = False + + from app.database.sqlalchemy_extension import db + + db.init_app(app) + + 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 + from app.database.models.ms_schema.task_comment import TaskCommentModel + from app.database.models.bit_schema.organization import OrganizationModel + from app.database.models.bit_schema.program import ProgramModel + from app.database.models.bit_schema.user_extension import UserExtensionModel + from app.database.models.bit_schema.personal_background import PersonalBackgroundModel + from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel + + migrate = Migrate(app, db) + + from app.api.jwt_extension import jwt + + jwt.init_app(app) + + from app.api.bit_extension import api + + api.init_app(app) + + from app.api.mail_extension import mail + + mail.init_app(app) + + return app + +application = create_app(get_env_config()) + +@application.before_first_request +def create_tables(): + from app.database.sqlalchemy_extension import db + + 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 + from app.database.models.ms_schema.task_comment import TaskCommentModel + from app.database.models.bit_schema.organization import OrganizationModel + from app.database.models.bit_schema.program import ProgramModel + from app.database.models.bit_schema.user_extension import UserExtensionModel + from app.database.models.bit_schema.personal_background import PersonalBackgroundModel + from app.database.models.bit_schema.mentorship_relation_extension import MentorshipRelationExtensionModel + + # uncomment the line below if no dummy data needed on INITIAL setup! + # Warning !!! Do not uncomment if this is not your INITIAL setup to database! + # db.create_all() + + # uncomment lines below if you want to add dummy data on INITIAL setup! + # !!! Warning!!! Treat this with caution as it will mess up your db!! + # Warning !!! Do not uncomment if this is not your INITIAL setup to database! + + # from app.database.db_add_mock import add_mock_data # uncomment here + # add_mock_data() + + + @application.shell_context_processor + def make_shell_context(): + return { + "db": db, + "UserModel": UserModel, + "MentorshipRelationModel": MentorshipRelationModel, + "TaskListModel": TasksListModel, + "TaskCommentModel": TaskCommentModel, + "OrganizationModel": OrganizationModel, + "ProgramModel": ProgramModel, + "UserExtensionModel": UserExtensionModel, + "PersonalBackgroundModel": PersonalBackgroundModel, + "MentorshipRelationExtensionModel": MentorshipRelationExtensionModel, + } + + # uncomment the lines below if you want to test querying database + + +if __name__ == "__main__": + application.run(port=5000) \ No newline at end of file