├── .all-contributorsrc ├── .env.template ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ENV_SETUP_INSTRUCTION.md ├── GETTING_STARTED.md ├── PULL_REQUEST_TEMPLATE.md ├── REPORTING_GUIDELINES.md ├── config.yml └── workflows │ ├── documentation.yml │ └── main.yml ├── .gitignore ├── .pylintrc ├── LICENSE ├── Procfile ├── README.md ├── app ├── __init__.py ├── api │ ├── __init__.py │ ├── bit_extension.py │ ├── dao │ │ ├── __init__.py │ │ ├── organization.py │ │ ├── personal_background.py │ │ ├── program.py │ │ └── user_extension.py │ ├── jwt_extension.py │ ├── mail_extension.py │ ├── models │ │ ├── __init__.py │ │ ├── organization.py │ │ └── user.py │ ├── request_api_utils.py │ ├── resources │ │ ├── __init__.py │ │ ├── common.py │ │ ├── organizations.py │ │ └── users.py │ └── validations │ │ ├── __init__.py │ │ ├── organization.py │ │ ├── task_comment.py │ │ └── user.py ├── database │ ├── __init__.py │ ├── db_add_mock.py │ ├── db_types │ │ ├── ArrayOfEnum.py │ │ ├── JsonCustomType.py │ │ └── __init__.py │ ├── db_utils.py │ ├── models │ │ ├── __init__.py │ │ ├── bit_schema │ │ │ ├── __init__.py │ │ │ ├── mentorship_relation_extension.py │ │ │ ├── organization.py │ │ │ ├── personal_background.py │ │ │ ├── program.py │ │ │ └── user_extension.py │ │ └── ms_schema │ │ │ ├── __init__.py │ │ │ ├── mentorship_relation.py │ │ │ ├── task_comment.py │ │ │ ├── tasks_list.py │ │ │ └── user.py │ └── sqlalchemy_extension.py ├── messages.py └── utils │ ├── __init__.py │ ├── bit_constants.py │ ├── bitschema_utils.py │ ├── date_converter.py │ ├── decorator_utils.py │ ├── enum_utils.py │ ├── ms_constants.py │ └── validation_utils.py ├── config.py ├── docs ├── .gitignore ├── CODEOWNERS ├── ENVIRONMENT_VARIABLES.md ├── README.md ├── babel.config.js ├── docs │ ├── Commit-Message-Style-Guide.md │ ├── Environment-Setup-Instructions.md │ ├── Fork,-Clone-&-Remote.md │ ├── GSoC-2020-Maya-Treacy.md │ ├── Getting-Started.md │ ├── Home.md │ └── Project-Ideas.md ├── docusaurus.config.js ├── manual_tests │ └── test_register.md ├── package.json ├── sidebars.js ├── src │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.js │ │ └── styles.module.css ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ └── logo.png └── yarn.lock ├── requirements.txt ├── run.py └── tests ├── __init__.py ├── base_test_case.py ├── organizations ├── __init__.py ├── test_api_get_organization.py ├── test_api_list_organizations.py └── test_api_update_organization.py ├── programs ├── __init__.py ├── test_api_create_program.py ├── test_api_update_program.py └── test_get_program.py ├── test_app_config.py ├── test_data.py └── users ├── __init__.py ├── test_api_create_personal_background.py ├── test_api_create_user_additional_info.py ├── test_api_get_other_user_details.py ├── test_api_get_personal_background.py ├── test_api_get_user_additional_info.py ├── test_api_get_user_details.py ├── test_api_get_users_personal_details.py ├── test_api_login.py ├── test_api_register.py ├── test_api_update_personal_background.py ├── test_api_update_user_additional_info.py └── test_api_update_user_details.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "contributorsPerLine": 7, 7 | "contributorsSortAlphabetically": false, 8 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)", 9 | "skipCi": true, 10 | "contributors": [ 11 | { 12 | "login": "mtreacy002", 13 | "name": "Maya Treacy", 14 | "avatar_url": "https://avatars.githubusercontent.com/u/29667122?v=4", 15 | "profile": "https://github.com/mtreacy002", 16 | "contributions": [ 17 | "maintenance", 18 | "code", 19 | "doc", 20 | "userTesting", 21 | "test" 22 | ] 23 | }, 24 | { 25 | "login": "rpattath", 26 | "name": "Roshni Pattath", 27 | "avatar_url": "https://avatars.githubusercontent.com/u/26095715?v=4", 28 | "profile": "https://github.com/rpattath", 29 | "contributions": [ 30 | "maintenance" 31 | ] 32 | }, 33 | { 34 | "login": "Rahulm2310", 35 | "name": "Rahul Mohata", 36 | "avatar_url": "https://avatars.githubusercontent.com/u/54268438?v=4", 37 | "profile": "http://rahulm2310.github.io/Portfolio", 38 | "contributions": [ 39 | "code", 40 | "doc", 41 | "mentoring", 42 | "review" 43 | ] 44 | } 45 | ], 46 | "projectName": "bridge-in-tech-backend", 47 | "projectOwner": "anitab-org", 48 | "repoType": "github", 49 | "repoHost": "https://github.com" 50 | } 51 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | FLASK_ENVIRONMENT_CONFIG = 2 | SECRET_KEY = 3 | SECURITY_PASSWORD_SALT = 4 | MAIL_DEFAULT_SENDER = 5 | MAIL_SERVER = 6 | APP_MAIL_USERNAME = 7 | APP_MAIL_PASSWORD = 8 | MOCK_EMAIL = 9 | FLASK_APP=run.py 10 | DB_TYPE=postgresql 11 | DB_USERNAME= 12 | DB_PASSWORD= 13 | DB_ENDPOINT= 14 | DB_NAME= 15 | DB_TEST_NAME= 16 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## 1. Purpose 4 | 5 | A primary goal of AnitaB.org Open Source is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 6 | 7 | This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. 8 | 9 | We invite all those who participate in open source contributions to help us create safe and positive experiences for everyone. 10 | 11 | ## 2. Open Source Citizenship 12 | 13 | A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. 14 | 15 | Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. 16 | 17 | If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. 18 | 19 | ## 3. Expected Behavior 20 | 21 | The following behaviors are expected and requested of all community members: 22 | 23 | * Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. 24 | * Exercise consideration and respect in your speech and actions. 25 | * Attempt collaboration before conflict. 26 | * Refrain from demeaning, discriminatory, or harassing behavior and speech. 27 | * Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. 28 | * Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. 29 | 30 | ## 4. Unacceptable Behavior 31 | 32 | The following behaviors are considered harassment and are unacceptable within our community: 33 | 34 | * Violence, threats of violence or violent language directed against another person. 35 | * Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. 36 | * Posting or displaying sexually explicit or violent material. 37 | * Posting or threatening to post other people’s personally identifying information ("doxing"). 38 | * Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. 39 | * Inappropriate photography or recording. 40 | * Inappropriate physical contact. You should have someone’s consent before touching them. 41 | * Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. 42 | * Deliberate intimidation, stalking or following (online or in person). 43 | * Advocating for, or encouraging, any of the above behavior. 44 | * Sustained disruption of community events, including talks and presentations. 45 | 46 | ## 5. Consequences of Unacceptable Behavior 47 | 48 | Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. 49 | 50 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 51 | 52 | If a community member engages in unacceptable behavior, the community admins may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). 53 | 54 | ## 6. Reporting Guidelines 55 | If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community admin as soon as possible by emailing opensource@anitab.org. Please read [Reporting Guidelines](reporting_guidelines.md) for details. 56 | 57 | 58 | 59 | Additionally, community admins are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, community admins (or event managers) will also provide escorts as desired by the person experiencing distress. 60 | 61 | ## 7. Addressing Grievances 62 | 63 | Only permanent resolutions (such as bans) may be appealed. To appeal a decision of the working group, contact AnitaB.org staff at opensource@anitab.org with your appeal and we will review the case. 64 | 65 | 66 | ## 8. Scope 67 | 68 | We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. 69 | 70 | This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. 71 | 72 | ## 9. Contact info 73 | 74 | opensource@anitab.org 75 | 76 | ## 10. License and attribution 77 | 78 | This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). 79 | 80 | Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). 81 | 82 | Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) 83 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | * You can join us on [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com/). Each active repo has its own stream to direct questions to (for example #powerup or #portal). Bridge in Tech stream is [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech). 4 | * Remember that this is an inclusive community, committed to creating a safe, positive environment. See the full [Code of Conduct](CODE_OF_CONDUCT.md). 5 | * Follow our [Commit Message Style Guide](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Commit-Message-Style-Guide) when you commit your changes. 6 | * Please consider raising an issue before submitting a pull request (PR) to solve a problem that is not present in our [issue tracker](https://github.com/anitab-org/bridge-in-tech-backend/issues). This allows maintainers to first validate the issue you are trying to solve and also reference the PR to a specific issue. 7 | * When developing a new feature, include at least one test when applicable. 8 | * When submitting a PR, please follow [this template](PULL_REQUEST_TEMPLATE.md) (which will probably be already filled up once you create the PR). 9 | * When submitting a PR with changes to user interface (e.g.: new screen, ...), please add screenshots to the PR description. 10 | * When you are finished with your work, please squash your commits otherwise we will squash them on your PR (this can help us maintain a clear commit history). 11 | * When creating an issue to report a bug in the project, please follow our [bug_report.md](https://github.com/anitab-org/.github/blob/main/.github/ISSUE_TEMPLATE/bug_report.md) template. 12 | * Issues labeled as `First Timers Only` are meant for contributors who have not contributed to the project yet. Please choose other issues to contribute to, if you have already contributed to these types of issues. 13 | 14 | ## General Guidelines 15 | 16 | * If you’re just getting started work on an issue labeled `First Timers Only` in any project. Additional resources are available on our [website](http://www.systers.io). 17 | * In an active repository (not an archived one), choose an open issue from the issue list, claim it in the comments, and a maintainer will assign it to you. 18 | * After approval, you must make continuous notes on your progress in the issue while working. If there is not at least one comment every 3 days, the maintainer can reassign the issue. 19 | * Create a branch specific to the issue you're working on, so that you send a PR from that branch instead of the base branch on your fork. 20 | * If you’d like to create a new issue, please go through our issue list first (open as well as closed) and make sure the issues you are reporting do not replicate the existing issues. 21 | * Give a short description of what went wrong (like a root cause analysis and description of the fix), if that information is not already present in the issue. 22 | * If you have issues on multiple pages, report them separately. Do not combine them into a single issue. 23 | -------------------------------------------------------------------------------- /.github/GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | Hello Newcomer, here are some tips to help you get started contributing to this project. 2 | 3 | ## Get to know the project 4 | 5 | Here's what you can do to know more about the project: 6 | 7 | * Read Documentation available on [GitHub Wiki](https://github.com/anitab-org/bridge-in-tech-backend/wiki) and [README](https://github.com/anitab-org/bridge-in-tech-backend); 8 | * You can join the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel on [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com), to see or participate in the project's discussion; 9 | * You can browse the code on GitHub or even on your workspace after [cloning it](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Fork,-Clone-&-Remote#clone). 10 | 11 | ## Choose a working item 12 | 13 | 1. Check the [available issues](https://github.com/anitab-org/bridge-in-tech-backend/issues) (that anyone can contribute to) or [first timers only issues](https://github.com/anitab-org/bridge-in-tech-backend/issues) (just for first time contributors in this project); 14 | 1. Choose one issue you want to work on; 15 | 1. Ask maintainers, on the issue's comment section, if you can work on it; 16 | 1. Once you get approval you can start working on it! 17 | 18 | ## Start working 19 | 20 | Before you start working check the [Contribution Guidelines](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CONTRIBUTING.md) to make sure you can follow the best practises. 21 | In short: 22 | 23 | 1. [Fork the project](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Fork,-Clone-&-Remote#fork) into your profile; 24 | 1. [Clone the project](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Fork,-Clone-&-Remote#clone) into your workspace on your computer; 25 | 1. [Setup remotes](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Fork,-Clone-&-Remote#remote) to keep your develop branch with AnitaB.org repository; 26 | 1. Create a specific branch based from develop branch for your specific feature; 27 | 1. Start coding; 28 | 1. Make sure you follow this [Commit Message Style Guide](https://github.com/anitab-org/bridge-in-tech-backend/wiki/Commit-Message-Style-Guide); 29 | 1. Once you finish working on your issue, submit a Pull Request (PR) [(following the template provided)](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/PULL_REQUEST_TEMPLATE.md); 30 | 1. If the reviewers request changes on your PR, make the changes (this can be a back and forth process where you make changes according to reviewers feedback); 31 | 1. Make sure when you finish your changes you squash your commits into a single commit; 32 | 1. Once the reviewers approve your PR, they can merge and your code will finally be part of the main repository! 33 | 34 | # Other ways to contribute 35 | 36 | Do you know there are also other ways to contribute to the project besides working on an issue? 37 | As contributors, you can also: 38 | * 👀 **help review a PR** either by looking into the code and ensure that the code is clean and its logic makes sense. You can also run the app from PR branch to ensure the code works. Look for the PRs with `Status: Needs Review` label on the [Backend](https://github.com/anitab-org/bridge-in-tech-backend/pulls) and [Frontend](https://github.com/anitab-org/bridge-in-tech-web/pulls) repositories. 39 | * 💻 **test a PR** by running it manually on your local machine and write a report on whether or not the PR works. Find a PR with `Status: Needs testing` label which is the next step after that PR is approved by the reviewer/s. Here're good examples of testing report done on one of the [backend](https://github.com/anitab-org/bridge-in-tech-backend/pull/71#pullrequestreview-445274875) and [frontend](https://github.com/anitab-org/bridge-in-tech-web/pull/62#pullrequestreview-464955571) PRs. 40 | * 🔨 try to **break the app** by testing the application that runs from the existing code on the develop branch to find any bugs. If you find any, check if there is an issue already open for it, if there's none, report it on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Bugs and Fixes` topic to ask if you can open one. 41 | * 📚 **check documentations** and see if any area could be improved to help contributors understand the project better. If you find something, check if there is an issue already open for it, if there's none, report it on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Documentation` topic to get approval to open an issue. 42 | * 🎨 give suggestions on how to **improve the UI design**. Post your suggestion for a discussion on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Design/Mocks` topic. You might get approval to work on your proposed design and have it implemented as the app UI 😁. 43 | 44 | ✨ Happy Coding !!! ✨ 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | 5 | Fixes #ISSUE 6 | 7 | ### Type of Change: 8 | 9 | 10 | - Code 11 | - Quality Assurance 12 | - User Interface 13 | - Outreach 14 | - Documentation 15 | 16 | **Code/Quality Assurance Only** 17 | - Bug fix (non-breaking change which fixes an issue) 18 | - This change requires a documentation update (software upgrade on readme file) 19 | - New feature (non-breaking change which adds functionality pre-approved by mentors) 20 | 21 | 22 | 23 | ### How Has This Been Tested? 24 | 25 | 26 | 27 | ### Checklist: 28 | 29 | 30 | - [ ] My PR follows the style guidelines of this project 31 | - [ ] I have performed a self-review of my own code or materials 32 | - [ ] I have commented my code or provided relevant documentation, particularly in hard-to-understand areas 33 | - [ ] I have made corresponding changes to the documentation 34 | - [ ] Any dependent changes have been merged 35 | 36 | 37 | **Code/Quality Assurance Only** 38 | - [ ] My changes generate no new warnings 39 | - [ ] My PR currently breaks something (fix or feature that would cause existing functionality to not work as expected) 40 | - [ ] I have added tests that prove my fix is effective or that my feature works 41 | - [ ] New and existing unit tests pass locally with my changes 42 | - [ ] Any dependent changes have been published in downstream modules 43 | -------------------------------------------------------------------------------- /.github/REPORTING_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Reporting Guidelines 2 | 3 | If you believe someone is violating the code of conduct we ask that you report it to the community admins by emailing opensource@anitab.org. 4 | 5 | **All reports will be kept confidential**. In some cases we may determine that a public statement will need to be made. If that's the case, the identities of all victims and reporters will remain confidential unless those individuals instruct us otherwise. 6 | 7 | If you believe anyone is in physical danger, please notify appropriate emergency services first. If you are unsure what service or agency is appropriate to contact, include this in your report and we will attempt to notify them. 8 | 9 | In your report please include: 10 | 11 | * Your contact info for follow-up contact. 12 | * Names (legal, nicknames, or pseudonyms) of any individuals involved. 13 | * If there were other witnesses besides you, please try to include them as well. 14 | * When and where the incident occurred. Please be as specific as possible. 15 | * Your account of what occurred. 16 | * If there is a publicly available record (e.g. a mailing list archive or a public IRC logger) please include a link. 17 | * Any extra context you believe existed for the incident. 18 | * If you believe this incident is ongoing. 19 | * Any other information you believe we should have. 20 | 21 | 22 | 23 | ## What happens after you file a report? 24 | 25 | You will receive an email from the AnitaB.org Open Source's Code of Conduct response team acknowledging receipt as soon as possible, but within 48 hours. 26 | 27 | The working group will immediately meet to review the incident and determine: 28 | 29 | * What happened. 30 | * Whether this event constitutes a code of conduct violation. 31 | * What kind of response is appropriate. 32 | 33 | If this is determined to be an ongoing incident or a threat to physical safety, the team's immediate priority will be to protect everyone involved. This means we may delay an "official" response until we believe that the situation has ended and that everyone is physically safe. 34 | 35 | Once the team has a complete account of the events they will make a decision as to how to respond. Responses may include: 36 | 37 | Nothing (if we determine no code of conduct violation occurred). 38 | * A private reprimand from the Code of Conduct response team to the individual(s) involved. 39 | * A public reprimand. 40 | * An imposed vacation (i.e. asking someone to "take a week off" from a mailing list or IRC). 41 | * A permanent or temporary ban from some or all of AnitaB.org spaces (events, meetings, mailing lists, IRC, etc.) 42 | * A request to engage in mediation and/or an accountability plan. 43 | We'll respond within one week to the person who filed the report with either a resolution or an explanation of why the situation is not yet resolved. 44 | 45 | Once we've determined our final action, we'll contact the original reporter to let them know what action (if any) we'll be taking. We'll take into account feedback from the reporter on the appropriateness of our response, but our response will be determined by what will be best for community safety. 46 | 47 | Finally, the response team will make a report on the situation to the AnitaB.org's Open Source Board. The board may choose to issue a public report of the incident or take additional actions. 48 | 49 | 50 | 51 | ## Appealing the response 52 | 53 | Only permanent resolutions (such as bans) may be appealed. To appeal a decision of the working group, contact the community admins at opensource@anitab.org with your appeal and we will review the case. 54 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # Configuration for welcome - https://github.com/behaviorbot/welcome 2 | 3 | # Configuration for new-issue-welcome - https://github.com/behaviorbot/new-issue-welcome 4 | 5 | # Comment to be posted to on first time issues 6 | newIssueWelcomeComment: > 7 | Hello there!👋 Welcome to the project!💖 8 | 9 | 10 | Thank you and congrats🎉for opening your very first issue in this project. AnitaB.org is an inclusive community, committed to creating a safe and positive environment.🌸 Please adhere to our [Code of Conduct](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CODE_OF_CONDUCT.md).🙌 11 | You may submit a PR if you like! If you want to report a bug🐞 please follow our [Issue Template](https://github.com/anitab-org/.github/tree/main/.github/ISSUE_TEMPLATE). Also make sure you include steps to reproduce it and be patient while we get back to you.😄 12 | 13 | 14 | Feel free to join us on [AnitaB.org Open Source Zulip Community](https://anitab-org.zulipchat.com/).💖 We have different streams for each active repository for discussions.✨ Hope you have a great time there!😄 15 | 16 | 17 | # Configuration for new-pr-welcome - https://github.com/behaviorbot/new-pr-welcome 18 | 19 | # Comment to be posted to on PRs from first time contributors in your repository 20 | newPRWelcomeComment: > 21 | Hello there!👋 Welcome to the project!💖 22 | 23 | 24 | Thank you and congrats🎉 for opening your first pull request.✨ AnitaB.org is an inclusive community, committed to creating a safe and positive environment.🌸Please adhere to our [Code of Conduct](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CODE_OF_CONDUCT.md) and [Contribution Guidelines](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CONTRIBUTING.md).🙌.We will get back to you as soon as we can.😄 25 | 26 | 27 | Feel free to join us on [AnitaB.org Open Source Zulip Community](https://anitab-org.zulipchat.com/).💖 We have different streams for each active repository for discussions.✨ Hope you have a great time there!😄 28 | 29 | 30 | # Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge 31 | 32 | # Comment to be posted to on pull requests merged by a first time user 33 | firstPRMergeComment: > 34 | Congrats on merging your first pull request! 🎉🎉🎉 We here at AnitaB.org are proud of you! 35 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: documentation 2 | 3 | on: 4 | pull_request: 5 | branches: [develop] 6 | push: 7 | branches: [develop] 8 | 9 | jobs: 10 | checks: 11 | if: github.event_name != 'push' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: '12.x' 18 | - name: Test Build 19 | run: | 20 | cd docs/ 21 | if [ -e yarn.lock ]; then 22 | yarn install --frozen-lockfile 23 | elif [ -e package-lock.json ]; then 24 | npm ci 25 | else 26 | npm i 27 | fi 28 | npm run build 29 | gh-release: 30 | if: github.event_name != 'pull_request' 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v1 34 | - uses: actions/setup-node@v1 35 | with: 36 | node-version: '12.x' 37 | - uses: webfactory/ssh-agent@v0.5.0 38 | with: 39 | ssh-private-key: ${{ secrets.GH_PAGES_DEPLOY }} 40 | - name: Release to GitHub Pages 41 | env: 42 | USE_SSH: true 43 | GIT_USER: git 44 | run: | 45 | git config --global user.email "opensource@anitab.org" 46 | git config --global user.name "anitab-org" 47 | cd docs/ 48 | if [ -e yarn.lock ]; then 49 | yarn install --frozen-lockfile 50 | elif [ -e package-lock.json ]; then 51 | npm ci 52 | else 53 | npm i 54 | fi 55 | npm run deploy 56 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Lint Code and Run tests 5 | 6 | on: 7 | push: 8 | branches: [develop] 9 | pull_request: 10 | branches: [develop] 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-python@v2 18 | - uses: psf/black@20.8b1 19 | with: 20 | args: ". --check" 21 | 22 | build: 23 | needs: lint 24 | runs-on: Ubuntu-20.04 25 | strategy: 26 | matrix: 27 | python: [3.7, 3.9] 28 | 29 | env: 30 | DB_TYPE: postgresql 31 | DB_USERNAME: postgres 32 | DB_PASSWORD: postgres 33 | DB_ENDPOINT: localhost:5432 34 | DB_TEST_NAME: bit_schema_test 35 | 36 | services: 37 | postgres: 38 | image: postgres 39 | env: 40 | POSTGRES_PASSWORD: postgres 41 | ports: 42 | - 5432:5432 43 | # Set health checks to wait until postgres has started 44 | options: >- 45 | --health-cmd pg_isready 46 | --health-interval 10s 47 | --health-timeout 5s 48 | --health-retries 5 49 | 50 | steps: 51 | - uses: actions/checkout@v2 52 | - name: Connect to PostgreSQL 53 | run: | 54 | PGPASSWORD=postgres psql -h localhost -c 'CREATE DATABASE bit_schema_test;' -U postgres 55 | PGPASSWORD=postgres psql -h localhost -c 'CREATE SCHEMA bitschema;' -U postgres -d bit_schema_test 56 | PGPASSWORD=postgres psql -h localhost -c 'CREATE SCHEMA test_schema;' -U postgres -d bit_schema_test 57 | PGPASSWORD=postgres psql -h localhost -c 'create SCHEMA test_schema_2;' -U postgres -d bit_schema_test 58 | PGPASSWORD=postgres psql -h localhost -c '\dn;' -U postgres -d bit_schema_test 59 | PGPASSWORD=postgres psql -h localhost -c 'show search_path;' -U postgres -d bit_schema_test 60 | PGPASSWORD=postgres psql -h localhost -c "ALTER DATABASE bit_schema_test SET search_path TO bitschema,public;" -U postgres -d bit_schema_test 61 | PGPASSWORD=postgres psql -h localhost -c 'show search_path;' -U postgres -d bit_schema_test 62 | env: 63 | POSTGRES_HOST: localhost 64 | 65 | - name: Set up python 3.x 66 | uses: actions/setup-python@v2 67 | with: 68 | python-version: ${{ matrix.python }} 69 | - name: Install dependencies 70 | run: | 71 | python -m pip install --upgrade pip 72 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 73 | - name: Run tests and generate coverage report 74 | run: coverage run -m unittest discover tests -v 75 | #TODO- name: Upload coverage to Codecov 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm project settings 107 | .idea/ 108 | 109 | # vscode 110 | .vscode/ 111 | 112 | *.db 113 | 114 | # aws-eb 115 | .elasticbeanstalk/ 116 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=missing-docstring, C0301, R0801, E203, E266, E501, W503, R0401, R0911, R0912, C0330, W0511, R0903, C0303, C0412, E0602, C0103, C0413, W0212, W0614, W0401, E1101, W0613, W0611, R0902, R0913, C0415 3 | select = B,C,E,F,W,T4,B9, -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn run:application -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/anitab-org/bridge-in-tech-backend.svg?branch=develop)](https://travis-ci.org/anitab-org/bridge-in-tech-backend) 2 | [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) 3 | [![codecov](https://codecov.io/gh/anitab-org/bridge-in-tech-backend/branch/develop/graph/badge.svg)](https://codecov.io/gh/anitab-org/bridge-in-tech-backend) 4 | [![Python 3.7](https://img.shields.io/badge/python-3.7-blue.svg)](https://www.python.org/downloads/) 5 | 6 | # Bridge-In-Tech (backend) 7 | 8 | Bridge-In-Tech is an application that allows industries/companies, mentors and students to actively collaborate to one another. 9 | 10 | This is the backend client of [Bridge-In-Tech-Web](https://github.com/anitab-org/bridge-in-tech-web). 11 | 12 | ## Setup 13 | To start contributing to the project, setup the backend environment on your local machine by following the instructions on the [BIT Development Environment Setup Instruction](.github/ENV_SETUP_INSTRUCTION.md) wiki page. 14 | 15 | If you prefer to test the latest approved and merged version of the project on the remote server, you can use the [BridgeInTech Heroku Swagger UI server](https://bridgeintech-bit-heroku-psql.herokuapp.com) that already connected to the modified version of Mentorship System heroku backend server made specifically for BridgeInTech development. 16 | 17 | ## Branches 18 | 19 | This repository has the following branches: 20 | - **master**: This branch contains the deployment of the backend. 21 | - **develop**: This contains the latest code. All the contributing PRs must be sent to this branch. 22 | 23 | ### Auto-formatting with black 24 | 25 | We use [_Black_](https://github.com/psf/black) to format code automatically so that we don't have to worry about clean and 26 | readable code. To install _Black_: 27 | 28 | ``` 29 | pip install black 30 | ``` 31 | 32 | To run black: 33 | 34 | ``` 35 | black . 36 | ``` 37 | 38 | ## Project Documentation 39 | 40 | Documentation for the project is hosted [here](https://anitab-org.github.io/bridge-in-tech-backend). We use `Docusaurus` for maintaining the documentation of the project. 41 | 42 | This project has live documents that contain information on: 43 | - Planning - [BridgeInTech Proposal Review](https://docs.google.com/document/d/1uCDCWs8Xyo-3EaUnrLOs48mefXJIN235b1aAswA-s-Y/edit) 44 | - [Technical Discussion](https://docs.google.com/document/d/1Fi_dvc1f-J0uuTzzzU7pwZV8hAI9iTX5LlI_THcg_ks/edit#heading=h.gfxf99xhgujm) and Decision made throughout the development 45 | - [Meeting Minutes](https://docs.google.com/document/d/1QRHzy0IWgAE5bjkwI_Lp67Dv50aywGuUmzmWewQ1rpY/edit?usp=sharing) 46 | 47 | For a more complete information on BridgeInTech project, please go to [BridgeInTech Backend Wiki page](https://github.com/anitab-org/bridge-in-tech-backend/wiki). 48 | 49 | ## Contributing 50 | 51 | **This project is under active development** 52 | 53 | Please read our [Contributing Guidelines](.github/CONTRIBUTING.md), [Code of Conduct](.github/CODE_OF_CONDUCT.md) and [Reporting Guidelines](.github/REPORTING_GUIDELINES.md) thoroughly. 54 | 55 | ### Contributors 56 | 57 | Thanks goes to these people ([emoji key](https://github.com/all-contributors/all-contributors#emoji-key)): 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |

Maya Treacy

🚧 💻 📖 📓 ⚠️

Roshni Pattath

🚧

Rahul Mohata

💻 📖 🧑‍🏫 👀
69 | 70 | 71 | 72 | 73 | 74 | 75 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. 76 | Contributions of any kind welcome! 77 | 78 | ## Contact 79 | 80 | If you have any questions or want to discuss something about this repo, feel free to join our [Zulip Community](http://anitab-org.zulipchat.com/)! If you are a new contributor, head over to this project's stream [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) on Zulip to see ongoing discussions. 81 | 82 | 83 | 84 | ## License 85 | 86 | The project is licensed under the GNU General Public License v3.0. Learn more about it in the [LICENSE](LICENSE) file. 87 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/__init__.py -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/api/__init__.py -------------------------------------------------------------------------------- /app/api/bit_extension.py: -------------------------------------------------------------------------------- 1 | from flask_restx import Api 2 | 3 | api = Api( 4 | title="Bridge In Tech API", 5 | version="1.0", 6 | description="API documentation for the backend of Bridge In Tech. \n \n" 7 | + "Bridge In Tech is an application inspired by the existing AnitaB.org Mentorship System, " 8 | + "It encourages organizations to collaborate with the mentors and mentees on mentoring programs. \n \n" 9 | + "The main repository of the Backend System can be found here: https://github.com/anitab-org/bridge-in-tech-backend \n \n" 10 | + "The Web client for the Bridge In Tech can be found here: https://github.com/anitab-org/bridge-in-tech-web \n \n" 11 | + "For more information about the project here's a link to our wiki guide: https://github.com/anitab-org/bridge-in-tech-backend/wiki" 12 | # doc='/docs/' 13 | ) 14 | api.namespaces.clear() 15 | 16 | # Adding namespaces 17 | from app.api.resources.users import users_ns as user_namespace 18 | 19 | api.add_namespace(user_namespace, path="/") 20 | 21 | from app.api.resources.organizations import organizations_ns as organization_namespace 22 | 23 | api.add_namespace(organization_namespace, path="/") 24 | -------------------------------------------------------------------------------- /app/api/dao/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/api/dao/__init__.py -------------------------------------------------------------------------------- /app/api/dao/personal_background.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from http import HTTPStatus 3 | from flask import json 4 | from app.database.models.bit_schema.personal_background import PersonalBackgroundModel 5 | from app import messages 6 | from app.api.request_api_utils import AUTH_COOKIE 7 | from app.utils.bitschema_utils import * 8 | 9 | 10 | class PersonalBackgroundDAO: 11 | 12 | """Data Access Object for Personal_Background functionalities""" 13 | 14 | @staticmethod 15 | def get_user_personal_background_info(user_id): 16 | """Retrieves a user's perrsonal background information using a specified ID. 17 | 18 | Arguments: 19 | user_id: The ID of the user to be searched. 20 | 21 | Returns: 22 | The PersonalBackgroundModel class of the user whose ID was searched, containing their additional information. 23 | """ 24 | 25 | result = PersonalBackgroundModel.find_by_user_id(user_id) 26 | if result: 27 | return { 28 | "user_id": result.user_id, 29 | "gender": result.gender.value, 30 | "age": result.age.value, 31 | "ethnicity": result.ethnicity.value, 32 | "sexual_orientation": result.sexual_orientation.value, 33 | "religion": result.religion.value, 34 | "physical_ability": result.physical_ability.value, 35 | "mental_ability": result.mental_ability.value, 36 | "socio_economic": result.socio_economic.value, 37 | "highest_education": result.highest_education.value, 38 | "years_of_experience": result.years_of_experience.value, 39 | "gender_other": result.others["gender_other"], 40 | "ethnicity_other": result.others["ethnicity_other"], 41 | "sexual_orientation_other": result.others["sexual_orientation_other"], 42 | "religion_other": result.others["religion_other"], 43 | "physical_ability_other": result.others["physical_ability_other"], 44 | "mental_ability_other": result.others["mental_ability_other"], 45 | "socio_economic_other": result.others["socio_economic_other"], 46 | "highest_education_other": result.others["highest_education_other"], 47 | "is_public": result.is_public, 48 | } 49 | 50 | @staticmethod 51 | def update_user_personal_background(data): 52 | """Creates or Updates a personal_background instance. 53 | 54 | Arguments: 55 | data: A list containing user's id, and user's background details (gender, 56 | age, ethnicity, sexual_orientation, religion, physical_ability, mental_ability, 57 | socio_economic, highest_education, years_of_experience, others) as well as 58 | whether or not user agrees to make their personal background information 59 | public to other members of BridgeInTech. 60 | 61 | Returns: 62 | A dictionary containing "message" which indicates whether or not the user_exension 63 | was created or updated successfully and "code" for the HTTP response code. 64 | """ 65 | 66 | others_data = {} 67 | try: 68 | others_data["gender_other"] = data["gender_other"] 69 | others_data["ethnicity_other"] = data["ethnicity_other"] 70 | others_data["sexual_orientation_other"] = data["sexual_orientation_other"] 71 | others_data["religion_other"] = data["religion_other"] 72 | others_data["physical_ability_other"] = data["physical_ability_other"] 73 | others_data["mental_ability_other"] = data["mental_ability_other"] 74 | others_data["socio_economic_other"] = data["socio_economic_other"] 75 | others_data["highest_education_other"] = data["highest_education_other"] 76 | except KeyError as e: 77 | return e, HTTPStatus.BAD_REQUEST 78 | 79 | user_json = AUTH_COOKIE["user"].value 80 | user = ast.literal_eval(user_json) 81 | existing_personal_background = PersonalBackgroundModel.find_by_user_id( 82 | int(user["id"]) 83 | ) 84 | if not existing_personal_background: 85 | try: 86 | personal_background = PersonalBackgroundModel( 87 | user_id=int(user["id"]), 88 | gender=Gender(data["gender"]).name, 89 | age=Age(data["age"]).name, 90 | ethnicity=Ethnicity(data["ethnicity"]).name, 91 | sexual_orientation=SexualOrientation( 92 | data["sexual_orientation"] 93 | ).name, 94 | religion=Religion(data["religion"]).name, 95 | physical_ability=PhysicalAbility(data["physical_ability"]).name, 96 | mental_ability=MentalAbility(data["mental_ability"]).name, 97 | socio_economic=SocioEconomic(data["socio_economic"]).name, 98 | highest_education=HighestEducation(data["highest_education"]).name, 99 | years_of_experience=YearsOfExperience( 100 | data["years_of_experience"] 101 | ).name, 102 | ) 103 | personal_background.others = others_data 104 | personal_background.is_public = data["is_public"] 105 | except KeyError as e: 106 | return e, HTTPStatus.BAD_REQUEST 107 | 108 | personal_background.save_to_db() 109 | return messages.PERSONAL_BACKGROUND_SUCCESSFULLY_CREATED, HTTPStatus.CREATED 110 | 111 | try: 112 | existing_personal_background.gender = Gender(data["gender"]).name 113 | existing_personal_background.age = Age(data["age"]).name 114 | existing_personal_background.ethnicity = Ethnicity(data["ethnicity"]).name 115 | existing_personal_background.sexual_orientation = SexualOrientation( 116 | data["sexual_orientation"] 117 | ).name 118 | existing_personal_background.religion = Religion(data["religion"]).name 119 | existing_personal_background.physical_ability = PhysicalAbility( 120 | data["physical_ability"] 121 | ).name 122 | existing_personal_background.mental_ability = MentalAbility( 123 | data["mental_ability"] 124 | ).name 125 | existing_personal_background.socio_economic = SocioEconomic( 126 | data["socio_economic"] 127 | ).name 128 | existing_personal_background.highest_education = HighestEducation( 129 | data["highest_education"] 130 | ).name 131 | existing_personal_background.years_of_experience = YearsOfExperience( 132 | data["years_of_experience"] 133 | ).name 134 | existing_personal_background.is_public = data["is_public"] 135 | except KeyError as e: 136 | return e, HTTPStatus.BAD_REQUEST 137 | 138 | existing_personal_background.others = others_data 139 | existing_personal_background.save_to_db() 140 | 141 | return messages.PERSONAL_BACKGROUND_SUCCESSFULLY_UPDATED, HTTPStatus.OK 142 | -------------------------------------------------------------------------------- /app/api/dao/user_extension.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from http import HTTPStatus 3 | from flask import json 4 | from app.database.models.bit_schema.user_extension import UserExtensionModel 5 | from app import messages 6 | from app.api.request_api_utils import AUTH_COOKIE 7 | from app.utils.bitschema_utils import Timezone 8 | from typing import Dict 9 | 10 | 11 | class UserExtensionDAO: 12 | 13 | """Data Access Object for Users_Extension functionalities""" 14 | 15 | @staticmethod 16 | def get_user_additional_data_info(user_id: int) -> dict: 17 | """Retrieves a user's additional information using a specified ID. 18 | 19 | Arguments: 20 | user_id: The ID of the user to be searched. 21 | 22 | Returns: 23 | The UserExtensionModel class of the user whose ID was searched, containing their additional information. 24 | """ 25 | 26 | result = UserExtensionModel.find_by_user_id(user_id) 27 | if result: 28 | try: 29 | phone = result.additional_info["phone"] 30 | mobile = result.additional_info["mobile"] 31 | personal_website = result.additional_info["personal_website"] 32 | return { 33 | "user_id": result.user_id, 34 | "is_organization_rep": result.is_organization_rep, 35 | "timezone": result.timezone.value, 36 | "phone": phone, 37 | "mobile": mobile, 38 | "personal_website": personal_website, 39 | } 40 | except TypeError: 41 | return { 42 | "user_id": result.user_id, 43 | "is_organization_rep": result.is_organization_rep, 44 | "timezone": result.timezone.value, 45 | "phone": "", 46 | "mobile": "", 47 | "personal_website": "", 48 | } 49 | return 50 | 51 | @staticmethod 52 | def update_user_additional_info(data: Dict[str, str]) -> tuple: 53 | """Updates a user_extension instance. 54 | Arguments: 55 | data: A list containing user's id, boolean value of whether or not 56 | the user is representing an organization, as well as their timezone 57 | Returns: 58 | A dictionary containing "message" which indicates whether or not 59 | the user_exension was updated successfully and "code" for the HTTP response code. 60 | """ 61 | 62 | timezone_value = data["timezone"] 63 | timezone = Timezone(timezone_value).name 64 | 65 | user_json = AUTH_COOKIE["user"].value 66 | user = ast.literal_eval(user_json) 67 | user_additional_info = UserExtensionModel.find_by_user_id(int(user["id"])) 68 | if not user_additional_info: 69 | user_additional_info = UserExtensionModel(int(user["id"]), timezone) 70 | return update( 71 | user_additional_info, 72 | data, 73 | timezone, 74 | messages.ADDITIONAL_INFO_SUCCESSFULLY_CREATED, 75 | HTTPStatus.CREATED, 76 | ) 77 | return update( 78 | user_additional_info, 79 | data, 80 | timezone, 81 | messages.ADDITIONAL_INFO_SUCCESSFULLY_UPDATED, 82 | HTTPStatus.OK, 83 | ) 84 | 85 | 86 | def update( 87 | user: Dict[str, str], 88 | data: Dict[str, str], 89 | timezone: str, 90 | success_message: str, 91 | status_code: int, 92 | ) -> tuple: 93 | additional_info_data = {} 94 | try: 95 | additional_info_data["phone"] = data["phone"] 96 | additional_info_data["mobile"] = data["mobile"] 97 | additional_info_data["personal_website"] = data["personal_website"] 98 | user.is_organization_rep = data["is_organization_rep"] 99 | except KeyError as e: 100 | return e, HTTPStatus.BAD_REQUEST 101 | 102 | user.timezone = timezone 103 | user.additional_info = additional_info_data 104 | user.save_to_db() 105 | 106 | return success_message, status_code 107 | -------------------------------------------------------------------------------- /app/api/jwt_extension.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | from flask_jwt_extended import JWTManager 3 | from app import messages 4 | from app.api.bit_extension import api 5 | 6 | jwt = JWTManager() 7 | 8 | # This is needed for the error handlers to work with flask-restplus 9 | jwt._set_error_handler_callbacks(api) 10 | 11 | 12 | @jwt.expired_token_loader 13 | def my_expired_token_callback(): 14 | return messages.TOKEN_HAS_EXPIRED, HTTPStatus.UNAUTHORIZED 15 | 16 | 17 | @jwt.invalid_token_loader 18 | def my_invalid_token_callback(): 19 | return messages.TOKEN_IS_INVALID, HTTPStatus.UNAUTHORIZED 20 | 21 | 22 | @jwt.unauthorized_loader 23 | def my_unauthorized_request_callback(): 24 | return messages.AUTHORISATION_TOKEN_IS_MISSING, HTTPStatus.UNAUTHORIZED 25 | -------------------------------------------------------------------------------- /app/api/mail_extension.py: -------------------------------------------------------------------------------- 1 | from flask_mail import Mail 2 | 3 | mail = Mail() 4 | -------------------------------------------------------------------------------- /app/api/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/api/models/__init__.py -------------------------------------------------------------------------------- /app/api/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/api/resources/__init__.py -------------------------------------------------------------------------------- /app/api/resources/common.py: -------------------------------------------------------------------------------- 1 | from flask_restx import reqparse 2 | 3 | auth_header_parser = reqparse.RequestParser() 4 | auth_header_parser.add_argument( 5 | "Authorization", 6 | required=True, 7 | help="Authentication access token. E.g.: Bearer ", 8 | location="headers", 9 | ) 10 | -------------------------------------------------------------------------------- /app/api/validations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/api/validations/__init__.py -------------------------------------------------------------------------------- /app/api/validations/organization.py: -------------------------------------------------------------------------------- 1 | from app import messages 2 | from app.utils.bitschema_utils import OrganizationStatus, Timezone, Zone, ProgramStatus 3 | from app.utils.validation_utils import is_email_valid, is_phone_valid 4 | from app.database.models.bit_schema.program import ProgramModel 5 | 6 | 7 | def validate_update_organization(data): 8 | try: 9 | email = data["email"] 10 | phone = data["phone"] 11 | if not is_email_valid(email): 12 | return messages.EMAIL_INPUT_BY_USER_IS_INVALID 13 | if not is_phone_valid(phone): 14 | return messages.PHONE_OR_MOBILE_IS_NOT_IN_NUMBER_FORMAT 15 | except ValueError as e: 16 | return e 17 | try: 18 | timezone_value = data["timezone"] 19 | timezone = Timezone(timezone_value).name 20 | except ValueError: 21 | return messages.TIMEZONE_INPUT_IS_INVALID 22 | except KeyError: 23 | return messages.TIMEZONE_FIELD_IS_MISSING 24 | try: 25 | status_value = data["status"] 26 | status = OrganizationStatus(status_value).name 27 | except ValueError: 28 | return messages.ORGANIZATION_STATUS_INPUT_IS_INVALID 29 | except KeyError: 30 | return messages.ORGANIZATION_OR_PROGRAM_STATUS_FIELD_IS_MISSING 31 | 32 | 33 | def validate_update_program(data): 34 | if "program_name" not in data: 35 | return messages.PROGRAM_NAME_IS_MISSING 36 | 37 | email = data["contact_email"] 38 | if not email: 39 | return messages.EMAIL_FIELD_IS_MISSING 40 | if not is_email_valid(email): 41 | return messages.EMAIL_INPUT_BY_USER_IS_INVALID 42 | 43 | phone = data["contact_phone"] 44 | if not phone: 45 | return messages.PHONE_FIELD_IS_MISSING 46 | if not is_phone_valid(phone): 47 | return messages.PHONE_OR_MOBILE_IS_NOT_IN_NUMBER_FORMAT 48 | 49 | mobile = data["contact_mobile"] 50 | if mobile: 51 | if not is_phone_valid(mobile): 52 | return messages.PHONE_OR_MOBILE_IS_NOT_IN_NUMBER_FORMAT 53 | try: 54 | status_value = data["status"] 55 | status = ProgramStatus(status_value).name 56 | except ValueError: 57 | return messages.PROGRAM_STATUS_INPUT_IS_INVALID 58 | except KeyError: 59 | return messages.ORGANIZATION_OR_PROGRAM_STATUS_FIELD_IS_MISSING 60 | -------------------------------------------------------------------------------- /app/api/validations/task_comment.py: -------------------------------------------------------------------------------- 1 | from app import messages 2 | from app.utils.validation_utils import validate_length, get_stripped_string 3 | 4 | COMMENT_MAX_LENGTH = 400 5 | 6 | 7 | def validate_task_comment_request_data(data): 8 | if "comment" not in data: 9 | return messages.COMMENT_FIELD_IS_MISSING 10 | 11 | comment = data["comment"] 12 | 13 | if not isinstance(comment, str): 14 | return messages.COMMENT_NOT_IN_STRING_FORMAT 15 | 16 | is_valid = validate_length( 17 | len(get_stripped_string(data["comment"])), 0, COMMENT_MAX_LENGTH, "comment" 18 | ) 19 | if not is_valid[0]: 20 | return is_valid[1] 21 | 22 | return {} 23 | -------------------------------------------------------------------------------- /app/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/database/__init__.py -------------------------------------------------------------------------------- /app/database/db_types/ArrayOfEnum.py: -------------------------------------------------------------------------------- 1 | import re 2 | from app.database.sqlalchemy_extension import db 3 | 4 | 5 | class ArrayOfEnum(db.TypeDecorator): 6 | 7 | impl = db.ARRAY 8 | 9 | def bind_expression(self, bindvalue): 10 | return db.cast(bindvalue, self) 11 | 12 | def result_processor(self, dialect, coltype): 13 | super_rp = super(ArrayOfEnum, self).result_processor(dialect, coltype) 14 | 15 | def handle_raw_string(value): 16 | inner = re.match(r"^{(.*)}$", value).group(1) 17 | return inner.split(",") 18 | 19 | def process(value): 20 | return super_rp(handle_raw_string(value)) 21 | 22 | return process 23 | -------------------------------------------------------------------------------- /app/database/db_types/JsonCustomType.py: -------------------------------------------------------------------------------- 1 | import json 2 | from app.database.sqlalchemy_extension import db 3 | 4 | 5 | class JsonCustomType(db.TypeDecorator): 6 | """Enables JSON storage by encoding and decoding to Text field.""" 7 | 8 | impl = db.Text 9 | 10 | @classmethod 11 | def process_bind_param(cls, value, dialect): 12 | if value is None: 13 | return "{}" 14 | return json.dumps(value) 15 | 16 | @classmethod 17 | def process_result_value(cls, value, dialect): 18 | if value is None: 19 | return {} 20 | try: 21 | return json.loads(value) 22 | except (ValueError, TypeError): 23 | return None 24 | -------------------------------------------------------------------------------- /app/database/db_types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/database/db_types/__init__.py -------------------------------------------------------------------------------- /app/database/db_utils.py: -------------------------------------------------------------------------------- 1 | from app.database.sqlalchemy_extension import db 2 | 3 | 4 | def reset_database(): 5 | db.drop_all() 6 | db.create_all() 7 | -------------------------------------------------------------------------------- /app/database/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/database/models/__init__.py -------------------------------------------------------------------------------- /app/database/models/bit_schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/database/models/bit_schema/__init__.py -------------------------------------------------------------------------------- /app/database/models/bit_schema/mentorship_relation_extension.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel 4 | from app.database.sqlalchemy_extension import db 5 | 6 | 7 | class MentorshipRelationExtensionModel(db.Model): 8 | """Defines attibutes of mentorship relation that are specific only to BridgeInTech. 9 | 10 | Attributes: 11 | program_id: An integer for storing program id. 12 | mentorship_relation_id: An integer for storing mentorship relation id. 13 | mentor_agreed_date: A numeric for storing the date when mentor agreed to work in program. 14 | mentee_agreed_date: A numeric for storing the date when mentee agreed to work in program. 15 | """ 16 | 17 | __tablename__ = "mentorship_relations_extension" 18 | __table_args__ = {"schema": "bitschema", "extend_existing": True} 19 | 20 | id = db.Column(db.Integer, primary_key=True) 21 | 22 | program_id = db.Column( 23 | db.Integer, 24 | db.ForeignKey("bitschema.programs.id", ondelete="CASCADE"), 25 | nullable=False, 26 | unique=True, 27 | ) 28 | mentorship_relation_id = db.Column( 29 | db.Integer, 30 | db.ForeignKey("public.mentorship_relations.id", ondelete="CASCADE"), 31 | nullable=False, 32 | unique=True, 33 | ) 34 | mentor_request_date = db.Column(db.Numeric("16,6", asdecimal=False)) 35 | mentor_agreed_date = db.Column(db.Numeric("16,6", asdecimal=False)) 36 | mentee_request_date = db.Column(db.Numeric("16,6", asdecimal=False)) 37 | mentee_agreed_date = db.Column(db.Numeric("16,6", asdecimal=False)) 38 | 39 | def __init__(self, program_id, mentorship_relation_id): 40 | self.program_id = program_id 41 | self.mentorship_relation_id = mentorship_relation_id 42 | 43 | # default values 44 | self.mentor_request_date = None 45 | self.mentor_agreed_date = None 46 | self.mentee_request_date = None 47 | self.mentee_agreed_date = None 48 | 49 | def json(self): 50 | """Returns information of mentorship as a json object.""" 51 | return { 52 | "id": self.id, 53 | "program_id": self.program_id, 54 | "mentorship_relation_id": self.mentorship_relation_id, 55 | "mentor_request_date": self.mentor_request_date, 56 | "mentor_agreed_date": self.mentor_agreed_date, 57 | "mentee_request_date": self.mentee_request_date, 58 | "mentee_agreed_date": self.mentee_agreed_date, 59 | } 60 | 61 | @classmethod 62 | def find_by_id(cls, _id) -> "MentorshipRelationExtensionModel": 63 | 64 | """Returns the mentorship_relations_extension that has the passed id. 65 | Args: 66 | _id: The id of a mentorship_relations_extension. 67 | """ 68 | return cls.query.filter_by(id=_id).first() 69 | 70 | def save_to_db(self) -> None: 71 | """Saves the model to the database.""" 72 | db.session.add(self) 73 | db.session.commit() 74 | 75 | def delete_from_db(self) -> None: 76 | """Deletes the record of mentorship relation extension from the database.""" 77 | self.tasks_list.delete_from_db() 78 | db.session.delete(self) 79 | db.session.commit() 80 | -------------------------------------------------------------------------------- /app/database/models/bit_schema/organization.py: -------------------------------------------------------------------------------- 1 | import time 2 | from sqlalchemy import null 3 | from app.database.sqlalchemy_extension import db 4 | from app.utils.bitschema_utils import OrganizationStatus, Timezone 5 | from app.database.models.bit_schema.program import ProgramModel 6 | 7 | 8 | class OrganizationModel(db.Model): 9 | """Defines attributes for the organization. 10 | 11 | Attributes: 12 | rep_id: A string for storing the organization's rep id. 13 | name: A string for storing organization's name. 14 | email: A string for storing organization's email. 15 | address: A string for storing the organization's address. 16 | geoloc: A geolocation data using JSON format. 17 | website: A string for storing the organization's website. 18 | """ 19 | 20 | # Specifying database table used for OrganizationModel 21 | __tablename__ = "organizations" 22 | __table_args__ = {"schema": "bitschema", "extend_existing": True} 23 | 24 | id = db.Column(db.Integer, primary_key=True) 25 | 26 | # Organization's representative data 27 | rep_id = db.Column(db.Integer, db.ForeignKey("public.users.id"), unique=True) 28 | rep_department = db.Column(db.String(150)) 29 | 30 | # Organization data 31 | name = db.Column(db.String(150), nullable=False, unique=True) 32 | email = db.Column(db.String(254), nullable=False, unique=True) 33 | about = db.Column(db.String(500)) 34 | address = db.Column(db.String(254)) 35 | website = db.Column(db.String(150), nullable=False) 36 | timezone = db.Column(db.Enum(Timezone)) 37 | phone = db.Column(db.String(20)) 38 | status = db.Column(db.Enum(OrganizationStatus)) 39 | join_date = db.Column(db.Numeric("16,6", asdecimal=False)) 40 | 41 | # Programs relationship 42 | programs = db.relationship( 43 | ProgramModel, backref="organization", cascade="all,delete", passive_deletes=True 44 | ) 45 | 46 | def __init__(self, rep_id, name, email, address, website, timezone): 47 | """Initialises OrganizationModel class.""" 48 | ## required fields 49 | 50 | self.rep_id = rep_id 51 | self.name = name 52 | self.email = email 53 | self.address = address 54 | self.website = website 55 | self.timezone = timezone 56 | 57 | # default values 58 | self.status = OrganizationStatus.DRAFT 59 | self.join_date = time.time() 60 | 61 | def json(self): 62 | """Returns OrganizationModel object in json format.""" 63 | return { 64 | "id": self.id, 65 | "rep_id": self.rep_id, 66 | "rep_department": self.rep_department, 67 | "name": self.name, 68 | "email": self.email, 69 | "about": self.about, 70 | "address": self.address, 71 | "website": self.website, 72 | "timezone": self.timezone, 73 | "phone": self.phone, 74 | "status": self.status, 75 | "join_date": self.join_date, 76 | } 77 | 78 | def __repr__(self): 79 | """Returns the organization.""" 80 | return ( 81 | f"Organization's id is {self.id}\n" 82 | f"Organization's representative is {self.rep_id}\n" 83 | f"Organization's name is {self.name}.\n" 84 | f"Organization's email is {self.email}\n" 85 | f"Organization's address is {self.address}\n" 86 | f"Organization's website is {self.website}\n" 87 | f"Organization's timezone is {self.timezone}" 88 | ) 89 | 90 | @classmethod 91 | def find_by_id(cls, _id) -> "OrganizationModel": 92 | 93 | """Returns the Organization that has the passed id. 94 | Args: 95 | _id: The id of an Organization. 96 | """ 97 | return cls.query.filter_by(id=_id).first() 98 | 99 | @classmethod 100 | def find_by_representative(cls, rep_id: int) -> "OrganizationModel": 101 | """Returns the organization that has the representative id user searched for.""" 102 | return cls.query.filter_by(rep_id=rep_id).first() 103 | 104 | @classmethod 105 | def find_by_name(cls, name: str) -> "OrganizationModel": 106 | """Returns the organization that has the name user searched for.""" 107 | return cls.query.filter_by(name=name).first() 108 | 109 | @classmethod 110 | def find_by_email(cls, email: str) -> "OrganizationModel": 111 | """Returns the organization that has the email user searched for.""" 112 | return cls.query.filter_by(email=email).first() 113 | 114 | def save_to_db(self) -> None: 115 | """Adds an organization to the database.""" 116 | db.session.add(self) 117 | db.session.commit() 118 | 119 | def delete_from_db(self) -> None: 120 | """Deletes an organization from the database.""" 121 | db.session.delete(self) 122 | db.session.commit() 123 | -------------------------------------------------------------------------------- /app/database/models/bit_schema/personal_background.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import null 2 | from sqlalchemy.dialects.postgresql import JSONB 3 | from app.database.sqlalchemy_extension import db 4 | from app.utils.bitschema_utils import * 5 | 6 | 7 | class PersonalBackgroundModel(db.Model): 8 | """Defines attributes for user's personal background. 9 | 10 | Attributes: 11 | user_id: An integer for storing the user's id. 12 | gender: A string for storing the user's gender. 13 | age: A string for storing the user's age. 14 | ethnicity: A string for storing the user's wthnicity. 15 | sexual_orientation: A string for storing the user's sexual orientation. 16 | religion: A string for storing the user's religion. 17 | physical_ability: A string for storing the user's physical ability. 18 | mental_ability: A string for storing the user's mental ability. 19 | socio_economic: A string for storing the user's socio economic level. 20 | highest_education: A string for storing the user's highest education level. 21 | years_of_experience: A string for storing the user's length of expeprience in the It related area. 22 | others: A JSON data type for storing users descriptions of 'other' fields. 23 | is_public: A boolean indicating if user has agreed to display their personal background information publicly to other members. 24 | """ 25 | 26 | # Specifying database table used for PersonalBackgroundModel 27 | __tablename__ = "personal_backgrounds" 28 | __table_args__ = {"schema": "bitschema", "extend_existing": True} 29 | 30 | id = db.Column(db.Integer, primary_key=True) 31 | 32 | # User's personal background data 33 | user_id = db.Column( 34 | db.Integer, 35 | db.ForeignKey("public.users.id", ondelete="CASCADE"), 36 | nullable=False, 37 | unique=True, 38 | ) 39 | gender = db.Column(db.Enum(Gender)) 40 | age = db.Column(db.Enum(Age)) 41 | ethnicity = db.Column(db.Enum(Ethnicity)) 42 | sexual_orientation = db.Column(db.Enum(SexualOrientation)) 43 | religion = db.Column(db.Enum(Religion)) 44 | physical_ability = db.Column(db.Enum(PhysicalAbility)) 45 | mental_ability = db.Column(db.Enum(MentalAbility)) 46 | socio_economic = db.Column(db.Enum(SocioEconomic)) 47 | highest_education = db.Column(db.Enum(HighestEducation)) 48 | years_of_experience = db.Column(db.Enum(YearsOfExperience)) 49 | others = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) 50 | is_public = db.Column(db.Boolean) 51 | 52 | def __init__( 53 | self, 54 | user_id, 55 | gender, 56 | age, 57 | ethnicity, 58 | sexual_orientation, 59 | religion, 60 | physical_ability, 61 | mental_ability, 62 | socio_economic, 63 | highest_education, 64 | years_of_experience, 65 | ): 66 | """Initialises PersonalBackgroundModel class.""" 67 | ## required fields 68 | self.user_id = user_id 69 | self.gender = gender 70 | self.age = age 71 | self.ethnicity = ethnicity 72 | self.sexual_orientation = sexual_orientation 73 | self.religion = religion 74 | self.physical_ability = physical_ability 75 | self.mental_ability = mental_ability 76 | self.socio_economic = socio_economic 77 | self.highest_education = highest_education 78 | self.years_of_experience = years_of_experience 79 | 80 | # default values 81 | self.others = None 82 | self.is_public = False 83 | 84 | def json(self): 85 | """Returns PersonalBackgroundModel object in json format.""" 86 | return { 87 | "id": self.id, 88 | "user_id": self.user_id, 89 | "age": self.age, 90 | "ethnicity": self.ethnicity, 91 | "sexual_orientation": self.sexual_orientation, 92 | "religion": self.religion, 93 | "physical_ability": self.physical_ability, 94 | "mental_ability": self.mental_ability, 95 | "socio_economic": self.socio_economic, 96 | "highest_education": self.highest_education, 97 | "years_of_experience": self.years_of_experience, 98 | "others": self.others, 99 | "is_public": self.is_public, 100 | } 101 | 102 | def __repr__(self): 103 | """Returns user's background.""" 104 | 105 | return ( 106 | f"User's id is {self.user_id}.\n" 107 | f"User's age is: {self.age}\n" 108 | f"User's ethnicity is: {self.ethnicity}\n" 109 | f"User's sexual orientation is: {self.sexual_orientation}\n" 110 | f"User's religion is: {self.religion}\n" 111 | f"User's physical ability is: {self.physical_ability}\n" 112 | f"User's mental ability is: {self.mental_ability}\n" 113 | f"User's socio economic category is: {self.socio_economic}\n" 114 | f"User's highest level of education is: {self.highest_education}\n" 115 | f"User's length of experience is: {self.years_of_experience}\n" 116 | ) 117 | 118 | @classmethod 119 | def find_by_user_id(cls, user_id) -> "PersonalBackgroundModel": 120 | 121 | """Returns the user's background that has the passed user id. 122 | Args: 123 | _id: The id of a user. 124 | """ 125 | return cls.query.filter_by(user_id=user_id).first() 126 | 127 | def save_to_db(self) -> None: 128 | """Adds user's personal background to the database.""" 129 | db.session.add(self) 130 | db.session.commit() 131 | 132 | def delete_from_db(self) -> None: 133 | """Deletes user's personal background from the database.""" 134 | db.session.delete(self) 135 | db.session.commit() 136 | -------------------------------------------------------------------------------- /app/database/models/bit_schema/program.py: -------------------------------------------------------------------------------- 1 | import time 2 | from sqlalchemy import null 3 | from sqlalchemy import BigInteger, ARRAY 4 | from sqlalchemy.dialects.postgresql import JSONB 5 | from app.database.sqlalchemy_extension import db 6 | from app.utils.bitschema_utils import ContactType, Zone, ProgramStatus 7 | 8 | # from app.database.models.ms_schema.mentorship_relation import MentorshipRelationModel 9 | from app.database.models.bit_schema.mentorship_relation_extension import ( 10 | MentorshipRelationExtensionModel, 11 | ) 12 | 13 | 14 | class ProgramModel(db.Model): 15 | """Defines attributes for a program. 16 | 17 | Attributes: 18 | program_name: A string for storing program name. 19 | organization_id: An integer for storing organization's id. 20 | start_date: A date for storing the program start date. 21 | end_date: A date for storing the program end date. 22 | """ 23 | 24 | # Specifying database table used for ProgramModel 25 | __tablename__ = "programs" 26 | __table_args__ = {"schema": "bitschema", "extend_existing": True} 27 | 28 | id = db.Column(db.Integer, primary_key=True) 29 | 30 | program_name = db.Column(db.String(100), unique=True, nullable=False) 31 | organization_id = db.Column( 32 | db.Integer, 33 | db.ForeignKey("bitschema.organizations.id", ondelete="CASCADE"), 34 | nullable=False, 35 | ) 36 | start_date = db.Column(db.Numeric("16,6", asdecimal=False)) 37 | end_date = db.Column(db.Numeric("16,6", asdecimal=False)) 38 | description = db.Column(db.String(500)) 39 | target_skills = db.Column(ARRAY(db.String(150))) 40 | target_candidate = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) 41 | payment_currency = db.Column(db.String(3)) 42 | payment_amount = db.Column(BigInteger) 43 | contact_type = db.Column(db.Enum(ContactType)) 44 | zone = db.Column(db.Enum(Zone)) 45 | student_responsibility = db.Column(ARRAY(db.String(250))) 46 | mentor_responsibility = db.Column(ARRAY(db.String(250))) 47 | organization_responsibility = db.Column(ARRAY(db.String(250))) 48 | student_requirements = db.Column(ARRAY(db.String(250))) 49 | mentor_requirements = db.Column(ARRAY(db.String(250))) 50 | resources_provided = db.Column(ARRAY(db.String(250))) 51 | contact_name = db.Column(db.String(50)) 52 | contact_department = db.Column(db.String(150)) 53 | program_address = db.Column(db.String(250)) 54 | contact_phone = db.Column(db.String(20)) 55 | contact_mobile = db.Column(db.String(20)) 56 | contact_email = db.Column(db.String(254)) 57 | program_website = db.Column(db.String(254)) 58 | irc_channel = db.Column(db.String(254)) 59 | tags = db.Column(ARRAY(db.String(150))) 60 | status = db.Column(db.Enum(ProgramStatus)) 61 | creation_date = db.Column(db.Numeric("16,6", asdecimal=False)) 62 | mentorship_relation = db.relationship( 63 | MentorshipRelationExtensionModel, 64 | backref="program", 65 | uselist=False, 66 | cascade="all,delete", 67 | passive_deletes=True, 68 | ) 69 | 70 | """Initialises ProgramModel class.""" 71 | ## required fields 72 | def __init__(self, program_name, organization_id, start_date, end_date): 73 | self.program_name = program_name 74 | self.organization = organization_id 75 | self.start_date = start_date 76 | self.end_date = end_date 77 | 78 | # default value 79 | self.target_candidate = None 80 | self.status = ProgramStatus.DRAFT 81 | self.creation_date = time.time() 82 | 83 | def json(self): 84 | """Returns ProgramModel object in json format.""" 85 | return { 86 | "id": self.id, 87 | "program_name": self.program_name, 88 | "organization_id": self.organization_id, 89 | "start_date": self.start_date, 90 | "end_date": self.end_date, 91 | "description": self.description, 92 | "target_skills": self.target_skills, 93 | "target_candidate": self.target_candidate, 94 | "payment_currency": self.payment_currency, 95 | "payment_amount": self.payment_amount, 96 | "contact_type": self.contact_type, 97 | "zone": self.zone, 98 | "student_responsibility": self.student_responsibility, 99 | "mentor_responsibility": self.mentor_responsibility, 100 | "organization_responsibility": self.organization_responsibility, 101 | "student_requirements": self.student_requirements, 102 | "mentor_requirements": self.mentor_requirements, 103 | "resources_provided": self.resources_provided, 104 | "contact_name": self.contact_name, 105 | "contact_department": self.contact_department, 106 | "program_address": self.program_address, 107 | "contact_phone": self.contact_phone, 108 | "contact_mobile": self.contact_mobile, 109 | "contact_email": self.contact_email, 110 | "program_website": self.program_website, 111 | "irc_channel": self.irc_channel, 112 | "tags": self.tags, 113 | "status": self.status, 114 | "creation_date": self.creation_date, 115 | } 116 | 117 | def __repr__(self): 118 | """Returns the program name, creation/start/end date and organization id.""" 119 | return ( 120 | f"Program id is {self.program.id}\n" 121 | f"Program name is {self.program_name}.\n" 122 | f"Organization's id is {self.organization_id}.\n" 123 | f"Program start date is {self.start_date}\n" 124 | f"Program end date is {self.end_date}\n" 125 | f"Program creation date is {self.creation_date}\n" 126 | ) 127 | 128 | @classmethod 129 | def find_by_id(cls, _id) -> "ProgramModel": 130 | 131 | """Returns the Program that has the passed id. 132 | Args: 133 | _id: The id of a Program. 134 | """ 135 | return cls.query.filter_by(id=_id).first() 136 | 137 | @classmethod 138 | def find_by_name(cls, program_name) -> "ProgramModel": 139 | 140 | """Returns the Program that has the passed name. 141 | Args: 142 | program_name: The name of a Program. 143 | """ 144 | return cls.query.filter_by(program_name=program_name).first() 145 | 146 | @classmethod 147 | def get_all_programs_by_organization(cls, organization_id): 148 | """Returns list of programs that has the passed organization id. 149 | Args: 150 | _id: The id of an Organization. 151 | """ 152 | return cls.query.filter_by(organization_id=organization_id).all() 153 | 154 | @classmethod 155 | def get_all_programs_by_representative(cls, rep_id): 156 | """Returns list of programs that where their representative ID is the passedid.""" 157 | return cls.query.filter_by(rep_id=rep_id).all() 158 | 159 | def save_to_db(self) -> None: 160 | """Adds a program to the database.""" 161 | try: 162 | db.session.add(self) 163 | db.session.commit() 164 | except Exception as e: 165 | db.session.rollback() 166 | raise e 167 | 168 | def delete_from_db(self) -> None: 169 | """Deletes a program from the database.""" 170 | db.session.delete(self) 171 | db.session.commit() 172 | -------------------------------------------------------------------------------- /app/database/models/bit_schema/user_extension.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import null 2 | from sqlalchemy.dialects.postgresql import JSONB 3 | from app.database.sqlalchemy_extension import db 4 | from app.utils.bitschema_utils import Timezone 5 | 6 | 7 | class UserExtensionModel(db.Model): 8 | """Defines attributes for a user that are specific only to BridgeInTech. 9 | Attributes: 10 | user_id: A string for storing user_id. 11 | is_organization_rep: A boolean indicating that user is a organization representative. 12 | additional_info: A json object for storing other information of the user with the specified id. 13 | timezone: A string for storing user timezone information. 14 | """ 15 | 16 | # Specifying database table used for UserExtensionModel 17 | __tablename__ = "users_extension" 18 | __table_args__ = {"schema": "bitschema", "extend_existing": True} 19 | 20 | id = db.Column(db.Integer, primary_key=True) 21 | 22 | user_id = db.Column( 23 | db.Integer, 24 | db.ForeignKey("public.users.id", ondelete="CASCADE"), 25 | nullable=False, 26 | unique=True, 27 | ) 28 | is_organization_rep = db.Column(db.Boolean) 29 | additional_info = db.Column(JSONB(none_as_null=False), default=JSONB.NULL) 30 | timezone = db.Column(db.Enum(Timezone)) 31 | 32 | """Initialises UserExtensionModel class.""" 33 | ## required fields 34 | def __init__(self, user_id, timezone): 35 | self.user_id = user_id 36 | self.timezone = timezone 37 | 38 | # default value 39 | self.is_organization_rep = False 40 | self.additional_info = None 41 | 42 | def json(self): 43 | """Returns UserExtensionmodel object in json format.""" 44 | return { 45 | "id": self.id, 46 | "user_id": self.user_id, 47 | "is_organization_rep": self.is_organization_rep, 48 | "timezone": self.timezone, 49 | "additional_info": self.additional_info, 50 | } 51 | 52 | def __repr__(self): 53 | """Returns user's information that is specific to BridgeInTech.""" 54 | 55 | return ( 56 | f"Users's id is {self.user_id}.\n" 57 | f"User's as organization representative is: {self.is_organization_rep}\n" 58 | f"User's timezone is: {self.timezone}\n" 59 | ) 60 | 61 | @classmethod 62 | def find_by_user_id(cls, user_id) -> "UserExtensionModel": 63 | 64 | """Returns the user extension that has the passed user id. 65 | Args: 66 | _id: The id of a user. 67 | """ 68 | return cls.query.filter_by(user_id=user_id).first() 69 | 70 | def save_to_db(self) -> None: 71 | """Adds user's BridgeInTech specific data to the database.""" 72 | db.session.add(self) 73 | db.session.commit() 74 | 75 | def delete_from_db(self) -> None: 76 | """Deletes user's BridgeInTech specific data from the database.""" 77 | db.session.delete(self) 78 | db.session.commit() 79 | -------------------------------------------------------------------------------- /app/database/models/ms_schema/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/database/models/ms_schema/__init__.py -------------------------------------------------------------------------------- /app/database/models/ms_schema/mentorship_relation.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from app.database.models.ms_schema.tasks_list import TasksListModel 4 | 5 | # from app.database.models.bitschema import MentorshipRelationExtensionModel 6 | from app.database.sqlalchemy_extension import db 7 | from app.utils.enum_utils import MentorshipRelationState 8 | 9 | 10 | class MentorshipRelationModel(db.Model): 11 | """Data Model representation of a mentorship relation. 12 | 13 | Attributes: 14 | mentor_id: integer indicates the id of the mentor. 15 | mentee_id: integer indicates the id of the mentee. 16 | action_user_id: integer indicates id of action user. 17 | mentor: relationship between UserModel and mentorship_relation. 18 | mentee: relationship between UserModel and mentorship_relation. 19 | creation_date: numeric that defines the date of creation of the mentorship. 20 | accept_date: numeric that indicates the date of acceptance of mentorship without a program. 21 | start_date: numeric that indicates the starting date of mentorship which also starts of the program (if any). 22 | end_date: numeric that indicates the ending date of mentorship which also ends of the program (if any). 23 | state: enumeration that indicates state of mentorship. 24 | notes: string that indicates any notes. 25 | tasks_list_id: integer indicates the id of the tasks_list 26 | tasks_list: relationship between TasksListModel and mentorship_relation. 27 | mentor_agreed: numeric that indicates the date when mentor accepted to a program. 28 | mentee_agreed: numeric that indicates the date when mentee accepted to a program. 29 | """ 30 | 31 | # Specifying database table used for MentorshipRelationModel 32 | __tablename__ = "mentorship_relations" 33 | __table_args__ = {"schema": "public", "extend_existing": True} 34 | 35 | id = db.Column(db.Integer, primary_key=True) 36 | 37 | # personal data 38 | mentor_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) 39 | mentee_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) 40 | action_user_id = db.Column(db.Integer, nullable=False) 41 | mentor = db.relationship( 42 | # UserModel, 43 | "UserModel", 44 | backref="mentor_relations", 45 | primaryjoin="MentorshipRelationModel.mentor_id == UserModel.id", 46 | ) 47 | mentee = db.relationship( 48 | # UserModel, 49 | "UserModel", 50 | backref="mentee_relations", 51 | primaryjoin="MentorshipRelationModel.mentee_id == UserModel.id", 52 | ) 53 | 54 | creation_date = db.Column(db.Numeric("16,6", asdecimal=False), nullable=False) 55 | 56 | accept_date = db.Column(db.Numeric("16,6", asdecimal=False)) 57 | 58 | start_date = db.Column(db.Numeric("16,6", asdecimal=False)) 59 | end_date = db.Column(db.Numeric("16,6", asdecimal=False)) 60 | 61 | state = db.Column(db.Enum(MentorshipRelationState), nullable=False) 62 | notes = db.Column(db.String(400)) 63 | 64 | tasks_list_id = db.Column(db.Integer, db.ForeignKey("public.tasks_list.id")) 65 | tasks_list = db.relationship( 66 | TasksListModel, uselist=False, backref="mentorship_relation" 67 | ) 68 | 69 | mentorship_relation_extension = db.relationship( 70 | "MentorshipRelationExtensionModel", 71 | backref="mentorship_relation", 72 | uselist=False, 73 | cascade="all,delete", 74 | passive_deletes=True, 75 | ) 76 | 77 | # pass in parameters in a dictionary 78 | def __init__( 79 | self, 80 | action_user_id, 81 | mentor_user, 82 | mentee_user, 83 | creation_date, 84 | end_date, 85 | state, 86 | notes, 87 | tasks_list, 88 | ): 89 | 90 | self.action_user_id = action_user_id 91 | self.mentor = mentor_user 92 | self.mentee = mentee_user # same as mentee_user.mentee_relations.append(self) 93 | self.creation_date = creation_date 94 | self.end_date = end_date 95 | self.state = state 96 | self.notes = notes 97 | self.tasks_list = tasks_list 98 | 99 | def json(self): 100 | """Returns information of mentorship as a json object.""" 101 | return { 102 | "id": self.id, 103 | "action_user_id": self.action_user_id, 104 | "mentor_id": self.mentor_id, 105 | "mentee_id": self.mentee_id, 106 | "creation_date": self.creation_date, 107 | "accept_date": self.accept_date, 108 | "start_date": self.start_date, 109 | "end_date": self.end_date, 110 | "state": self.state, 111 | "notes": self.notes, 112 | } 113 | 114 | @classmethod 115 | def find_by_id(cls, _id) -> "MentorshipRelationModel": 116 | 117 | """Returns the mentorship that has the passed id. 118 | Args: 119 | _id: The id of a mentorship. 120 | """ 121 | return cls.query.filter_by(id=_id).first() 122 | 123 | @classmethod 124 | def is_empty(cls) -> bool: 125 | """Returns True if the mentorship model is empty, and False otherwise.""" 126 | return cls.query.first() is None 127 | 128 | @classmethod 129 | def find_by_program_id(cls, program_id): 130 | 131 | """Returns list of mentorship that has the passed program id. 132 | Args: 133 | program_id: The id of a program which the mentorships related to. 134 | """ 135 | return cls.query.filter_by(program_id=program_id).first().all 136 | 137 | def save_to_db(self) -> None: 138 | """Saves the model to the database.""" 139 | db.session.add(self) 140 | db.session.commit() 141 | 142 | def delete_from_db(self) -> None: 143 | """Deletes the record of mentorship relation from the database.""" 144 | self.tasks_list.delete_from_db() 145 | db.session.delete(self) 146 | db.session.commit() 147 | -------------------------------------------------------------------------------- /app/database/models/ms_schema/task_comment.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from app.api.validations.task_comment import COMMENT_MAX_LENGTH 4 | from app.database.sqlalchemy_extension import db 5 | 6 | 7 | class TaskCommentModel(db.Model): 8 | """Defines attributes for the task comment. 9 | 10 | Attributes: 11 | task_id: An integer for storing the task's id. 12 | user_id: An integer for storing the user's id. 13 | relation_id: An integer for storing the relation's id. 14 | creation_date: A float indicating comment's creation date. 15 | modification_date: A float indicating the modification date. 16 | comment: A string indicating the comment. 17 | """ 18 | 19 | # Specifying database table used for TaskCommentModel 20 | __tablename__ = "tasks_comments" 21 | __table_args__ = {"schema": "public", "extend_existing": True} 22 | 23 | id = db.Column(db.Integer, primary_key=True) 24 | user_id = db.Column(db.Integer, db.ForeignKey("public.users.id")) 25 | task_id = db.Column(db.Integer, db.ForeignKey("public.tasks_list.id")) 26 | relation_id = db.Column(db.Integer, db.ForeignKey("public.mentorship_relations.id")) 27 | creation_date = db.Column(db.Numeric("16,6", asdecimal=False), nullable=False) 28 | modification_date = db.Column(db.Numeric("16,6", asdecimal=False)) 29 | comment = db.Column(db.String(COMMENT_MAX_LENGTH), nullable=False) 30 | 31 | def __init__(self, user_id, task_id, relation_id, comment): 32 | # required fields 33 | self.user_id = user_id 34 | self.task_id = task_id 35 | self.relation_id = relation_id 36 | self.comment = comment 37 | 38 | # default fields 39 | self.creation_date = datetime.now().timestamp() 40 | 41 | def json(self): 42 | """Returns information of task comment as a JSON object.""" 43 | return { 44 | "id": self.id, 45 | "user_id": self.user_id, 46 | "task_id": self.task_id, 47 | "relation_id": self.relation_id, 48 | "creation_date": self.creation_date, 49 | "modification_date": self.modification_date, 50 | "comment": self.comment, 51 | } 52 | 53 | def __repr__(self): 54 | """Returns the task and user ids, creation date and the comment.""" 55 | return ( 56 | f"User's id is {self.user_id}. Task's id is {self.task_id}. " 57 | f"Comment was created on: {self.creation_date}\n" 58 | f"Comment: {self.comment}" 59 | ) 60 | 61 | @classmethod 62 | def find_by_id(cls, _id): 63 | """Returns the task comment that has the passed id. 64 | Args: 65 | _id: The id of the task comment. 66 | """ 67 | return cls.query.filter_by(id=_id).first() 68 | 69 | @classmethod 70 | def find_all_by_task_id(cls, task_id, relation_id): 71 | """Returns all task comments that has the passed task id. 72 | Args: 73 | task_id: The id of the task. 74 | relation_id: The id of the relation. 75 | """ 76 | return cls.query.filter_by(task_id=task_id, relation_id=relation_id).all() 77 | 78 | @classmethod 79 | def find_all_by_user_id(cls, user_id): 80 | """Returns all task comments that has the passed user id. 81 | Args: 82 | user_id: The id of the user. 83 | """ 84 | return cls.query.filter_by(user_id=user_id).all() 85 | 86 | def modify_comment(self, comment): 87 | """Changes the comment and the modification date. 88 | Args: 89 | comment: New comment. 90 | """ 91 | self.comment = comment 92 | self.modification_date = datetime.now().timestamp() 93 | 94 | @classmethod 95 | def is_empty(cls): 96 | """Returns a boolean if the TaskCommentModel is empty or not.""" 97 | return cls.query.first() is None 98 | 99 | def save_to_db(self): 100 | """Adds a comment task to the database.""" 101 | db.session.add(self) 102 | db.session.commit() 103 | 104 | def delete_from_db(self): 105 | """Deletes a comment task from the database.""" 106 | db.session.delete(self) 107 | db.session.commit() 108 | -------------------------------------------------------------------------------- /app/database/models/ms_schema/tasks_list.py: -------------------------------------------------------------------------------- 1 | from enum import unique, Enum 2 | from datetime import date 3 | from app.database.db_types.JsonCustomType import JsonCustomType 4 | from app.database.sqlalchemy_extension import db 5 | 6 | 7 | class TasksListModel(db.Model): 8 | """Model representation of a list of tasks. 9 | 10 | Attributes: 11 | id: Id of the list of tasks. 12 | tasks: A list of tasks, using JSON format. 13 | next_task_id: Id of the next task added to the list of tasks. 14 | """ 15 | 16 | __tablename__ = "tasks_list" 17 | __table_args__ = {"schema": "public", "extend_existing": True} 18 | 19 | id = db.Column(db.Integer, primary_key=True) 20 | tasks = db.Column(JsonCustomType) 21 | next_task_id = db.Column(db.Integer) 22 | 23 | def __init__(self, tasks: "TasksListModel" = None): 24 | """Initializes tasks. 25 | 26 | Args: 27 | tasks: A list of tasks. 28 | 29 | Raises: 30 | A Value Error if the task is not initialized. 31 | """ 32 | 33 | if not tasks: 34 | self.tasks = [] 35 | self.next_task_id = 1 36 | else: 37 | if isinstance(tasks, list): 38 | self.tasks = [] 39 | self.next_task_id += 1 40 | else: 41 | raise TypeError("task is not initialized") 42 | 43 | def add_task( 44 | self, description: str, created_at: date, is_done=False, completed_at=None 45 | ) -> None: 46 | """Adds a task to the list of tasks. 47 | 48 | Args: 49 | description: A description of the task added. 50 | created_at: Date on which the task is created. 51 | is_done: Boolean specifying completion of the task. 52 | completed_at: Date on which task is completed. 53 | """ 54 | 55 | task = { 56 | TasksFields.ID.value: self.next_task_id, 57 | TasksFields.DESCRIPTION.value: description, 58 | TasksFields.IS_DONE.value: is_done, 59 | TasksFields.CREATED_AT.value: created_at, 60 | TasksFields.COMPLETED_AT.value: completed_at, 61 | } 62 | self.next_task_id += 1 63 | self.tasks = self.tasks + [task] 64 | 65 | def delete_task(self, task_id: int) -> None: 66 | """Deletes a task from the list of tasks. 67 | 68 | Args: 69 | task_id: Id of the task to be deleted. 70 | """ 71 | new_list = [] 72 | for task in self.tasks: 73 | if task[TasksFields.ID.value] != task_id: 74 | new_list.append(task) 75 | 76 | self.tasks = new_list 77 | self.save_to_db() 78 | 79 | def update_task( 80 | self, 81 | task_id: int, 82 | description: str = None, 83 | is_done: bool = None, 84 | completed_at: date = None, 85 | ) -> None: 86 | """Updates a task. 87 | 88 | Args: 89 | task_id: Id of the task to be updated. 90 | description: A description of the task. 91 | created_at: Date on which the task is created. 92 | is_done: Boolean specifying completion of the task. 93 | completed_at: Date on which task is completed. 94 | """ 95 | 96 | new_list = [] 97 | for task in self.tasks: 98 | if task[TasksFields.ID.value] == task_id: 99 | new_task = task.copy() 100 | if not description: 101 | new_task[TasksFields.DESCRIPTION.value] = description 102 | 103 | if not is_done: 104 | new_task[TasksFields.IS_DONE.value] = is_done 105 | 106 | if not completed_at: 107 | new_task[TasksFields.COMPLETED_AT.value] = completed_at 108 | 109 | new_list += [new_task] 110 | continue 111 | 112 | new_list += [task] 113 | 114 | self.tasks = new_list 115 | self.save_to_db() 116 | 117 | def find_task_by_id(self, task_id: int): 118 | """Returns the task that has the specified id. 119 | 120 | Args: 121 | task_id: Id of the task. 122 | 123 | Returns: 124 | The task instance. 125 | """ 126 | task = list( 127 | filter(lambda task: task[TasksFields.ID.value] == task_id, self.tasks) 128 | ) 129 | if not task: 130 | return None 131 | return task[0] 132 | 133 | def is_empty(self) -> bool: 134 | """Checks if the list of tasks is empty. 135 | 136 | Returns: 137 | Boolean; True if the task is empty, False otherwise. 138 | """ 139 | 140 | return self.tasks 141 | 142 | def json(self): 143 | """Creates json object of the attributes of list of tasks. 144 | 145 | Returns: 146 | Json objects of attributes of list of tasks. 147 | """ 148 | 149 | return { 150 | "id": self.id, 151 | "mentorship_relation_id": self.mentorship_relation_id, 152 | "tasks": self.tasks, 153 | "next_task_id": self.next_task_id, 154 | } 155 | 156 | def __repr__(self): 157 | """Creates a representation of an object. 158 | 159 | Returns: 160 | A string representation of the task object. 161 | """ 162 | 163 | return ( 164 | f"Task id is {self.id}\n" 165 | f"Tasks list is {self.tasks}\n" 166 | f"Next task id is {self.next_task_id}\n" 167 | ) 168 | 169 | @classmethod 170 | def find_by_id(cls, _id: int): 171 | """Finds a task with the specified id. 172 | 173 | Returns: 174 | The task with the specified id. 175 | """ 176 | 177 | return cls.query.filter_by(id=_id).first() 178 | 179 | def save_to_db(self) -> None: 180 | """Adds a task to the database.""" 181 | db.session.add(self) 182 | db.session.commit() 183 | 184 | def delete_from_db(self) -> None: 185 | """Deletes a task from the database.""" 186 | db.session.delete(self) 187 | db.session.commit() 188 | 189 | 190 | @unique 191 | class TasksFields(Enum): 192 | """Represents a task attributes' name. 193 | 194 | Attributes: 195 | ID: Id of a task. 196 | DESCRIPTION: Description of a task. 197 | IS_DONE: Boolean specifying the completion of the task. 198 | COMPLETED_AT: The date on which the task is completed. 199 | CREATED_AT: The date on which the task was created. 200 | """ 201 | 202 | ID = "id" 203 | DESCRIPTION = "description" 204 | IS_DONE = "is_done" 205 | COMPLETED_AT = "completed_at" 206 | CREATED_AT = "created_at" 207 | 208 | def values(self): 209 | """Returns a list containing a task.""" 210 | return list(map(str, self)) 211 | -------------------------------------------------------------------------------- /app/database/sqlalchemy_extension.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | from sqlalchemy import MetaData 4 | 5 | db = SQLAlchemy( 6 | metadata=MetaData( 7 | naming_convention={ 8 | "pk": "pk_%(table_name)s", 9 | "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", 10 | "ix": "ix_%(table_name)s_%(column_0_name)s", 11 | "uq": "uq_%(table_name)s_%(column_0_name)s", 12 | "ck": "ck_%(table_name)s_%(constraint_name)s", 13 | } 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/app/utils/__init__.py -------------------------------------------------------------------------------- /app/utils/bit_constants.py: -------------------------------------------------------------------------------- 1 | # This file keeps all Constants from BridgeInTech backend 2 | 3 | # from OrganizationDAO constants 4 | DEFAULT_PAGE = 1 5 | DEFAULT_ORGANIZATIONS_PER_PAGE = 10 6 | MAX_ORGANIZATIONS_PER_PAGE = 50 7 | DEFAULT_PROGRAMS_PER_PAGE = 10 8 | MAX_PROGRAMS_PER_PAGE = 50 9 | -------------------------------------------------------------------------------- /app/utils/date_converter.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import pytz 3 | import dateutil.parser 4 | 5 | 6 | def convert_timestamp_to_human_date(unix_time, timezone): 7 | date = datetime.datetime.utcfromtimestamp(unix_time) 8 | date_with_utc_timezone = pytz.UTC.localize(date) 9 | user_timezone = pytz.timezone(timezone) 10 | date_in_user_timezone = date_with_utc_timezone.astimezone(user_timezone) 11 | date_format = "%Y-%m-%d %H:%M %Z%z" 12 | return date_in_user_timezone.strftime(date_format) 13 | 14 | 15 | def convert_human_date_to_timestamp(input_date, timezone): 16 | try: 17 | user_input_date = datetime.datetime.strptime(input_date, "%Y-%m-%d %H:%M") 18 | user_timezone = pytz.timezone(timezone) 19 | date_in_user_timezone = user_timezone.localize(user_input_date) 20 | utc_timezone = pytz.UTC 21 | date_in_utc_timezone = date_in_user_timezone.astimezone(utc_timezone) 22 | epoch = datetime.datetime(1970, 1, 1, 0, 0, 0, tzinfo=pytz.UTC) 23 | except ValueError: 24 | raise ValueError("Incorrect date format, should be YYYY-MM-DD HH:MM") 25 | 26 | return (date_in_utc_timezone - epoch).total_seconds() 27 | -------------------------------------------------------------------------------- /app/utils/decorator_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module is used to define decorators for the app 3 | """ 4 | from http import HTTPStatus 5 | from collections import namedtuple 6 | from app import messages 7 | from app.database.models.ms_schema.user import UserModel 8 | 9 | 10 | def email_verification_required(user_function): 11 | """ 12 | This function is used to validate the 13 | input function i.e. user_function 14 | It will check if the user given as a 15 | parameter to user_function 16 | exists and have its email verified 17 | """ 18 | 19 | def check_verification(*args, **kwargs): 20 | """ 21 | Function to validate the input function ie user_function 22 | - It will return error 404 if user doesn't exist 23 | - It will return error 400 if user hasn't verified email 24 | 25 | Parameters: 26 | Function to be validated can have any type of argument 27 | - list 28 | - dict 29 | """ 30 | 31 | if kwargs: 32 | user = UserModel.find_by_id(kwargs["user_id"]) 33 | else: 34 | user = UserModel.find_by_id(args[0]) 35 | 36 | # verify if user exists 37 | if user: 38 | if not user.is_email_verified: 39 | return ( 40 | messages.USER_HAS_NOT_VERIFIED_EMAIL_BEFORE_LOGIN, 41 | HTTPStatus.FORBIDDEN, 42 | ) 43 | return user_function(*args, **kwargs) 44 | return messages.USER_DOES_NOT_EXIST, HTTPStatus.NOT_FOUND 45 | 46 | return check_verification 47 | 48 | 49 | def http_response_namedtuple_converter(user_function): 50 | def tuple_to_namedtuple_http_response(result): 51 | HttpNamedTupleResponse = namedtuple( 52 | "HttpNamedTupleResponse", ["message", "status_code"] 53 | ) 54 | converted_result = HttpNamedTupleResponse(*result) 55 | return user_function(converted_result) 56 | 57 | return tuple_to_namedtuple_http_response 58 | -------------------------------------------------------------------------------- /app/utils/enum_utils.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum, unique 2 | 3 | 4 | @unique 5 | class MentorshipRelationState(IntEnum): 6 | PENDING = 1 7 | ACCEPTED = 2 8 | REJECTED = 3 9 | CANCELLED = 4 10 | COMPLETED = 5 11 | 12 | def values(self): 13 | return list(map(int, self)) 14 | -------------------------------------------------------------------------------- /app/utils/ms_constants.py: -------------------------------------------------------------------------------- 1 | # This file keeps all Constants from Mentorship System backend 2 | # that does not have relevant files on BridgeInTech 3 | 4 | 5 | # from UserDAO constants 6 | DEFAULT_PAGE = 1 7 | DEFAULT_USERS_PER_PAGE = 10 8 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import timedelta 3 | 4 | 5 | def get_mock_email_config() -> bool: 6 | MOCK_EMAIL = os.getenv("MOCK_EMAIL") 7 | 8 | # if MOCK_EMAIL env variable is set 9 | if MOCK_EMAIL: 10 | # MOCK_EMAIL is case insensitive 11 | MOCK_EMAIL = MOCK_EMAIL.lower() 12 | 13 | if MOCK_EMAIL == "true": 14 | return True 15 | elif MOCK_EMAIL == "false": 16 | return False 17 | else: 18 | # if MOCK_EMAIL env variable is set a wrong value 19 | raise ValueError( 20 | "MOCK_EMAIL environment variable is optional if set, it has to be valued as either 'True' or 'False'" 21 | ) 22 | else: 23 | # Default behaviour is to send the email if MOCK_EMAIL is not set 24 | return False 25 | 26 | 27 | class BaseConfig(object): 28 | DEBUG = False 29 | TESTING = False 30 | SQLALCHEMY_TRACK_MODIFICATIONS = False 31 | 32 | # Flask JWT settings 33 | JWT_ACCESS_TOKEN_EXPIRES = timedelta(weeks=1) 34 | JWT_REFRESH_TOKEN_EXPIRES = timedelta(weeks=4) 35 | PROPAGATE_EXCEPTION = True 36 | 37 | # Security 38 | SECRET_KEY = os.getenv("SECRET_KEY", None) 39 | BCRYPT_LOG_ROUNDS = 13 40 | WTF_CSRF_ENABLED = True 41 | 42 | # mail settings 43 | MAIL_SERVER = os.getenv("MAIL_SERVER") 44 | MAIL_PORT = 465 45 | MAIL_USE_TLS = False 46 | MAIL_USE_SSL = True 47 | 48 | # email authentication 49 | MAIL_USERNAME = os.getenv("APP_MAIL_USERNAME") 50 | MAIL_PASSWORD = os.getenv("APP_MAIL_PASSWORD") 51 | 52 | # mail accounts 53 | MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER") 54 | 55 | DB_TYPE = (os.getenv("DB_TYPE"),) 56 | DB_USERNAME = (os.getenv("DB_USERNAME"),) 57 | DB_PASSWORD = (os.getenv("DB_PASSWORD"),) 58 | DB_ENDPOINT = (os.getenv("DB_ENDPOINT"),) 59 | DB_NAME = os.getenv("DB_NAME") 60 | DB_TEST_NAME = os.getenv("DB_TEST_NAME") 61 | 62 | @staticmethod 63 | def build_db_uri( 64 | db_type_arg=os.getenv("DB_TYPE"), 65 | db_user_arg=os.getenv("DB_USERNAME"), 66 | db_password_arg=os.getenv("DB_PASSWORD"), 67 | db_endpoint_arg=os.getenv("DB_ENDPOINT"), 68 | db_name_arg=os.getenv("DB_NAME"), 69 | ): 70 | return f"{db_type_arg}://{db_user_arg}:{db_password_arg}@{db_endpoint_arg}/{db_name_arg}" 71 | 72 | @staticmethod 73 | def build_db_test_uri( 74 | db_type_arg=os.getenv("DB_TYPE"), 75 | db_user_arg=os.getenv("DB_USERNAME"), 76 | db_password_arg=os.getenv("DB_PASSWORD"), 77 | db_endpoint_arg=os.getenv("DB_ENDPOINT"), 78 | db_name_arg=os.getenv("DB_TEST_NAME"), 79 | ): 80 | return f"{db_type_arg}://{db_user_arg}:{db_password_arg}@{db_endpoint_arg}/{db_name_arg}" 81 | 82 | 83 | class LocalConfig(BaseConfig): 84 | """Local configuration.""" 85 | 86 | DEBUG = True 87 | 88 | # Using a local postgre database 89 | SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema" 90 | 91 | # SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() 92 | 93 | 94 | class DevelopmentConfig(BaseConfig): 95 | DEBUG = True 96 | 97 | SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() 98 | 99 | 100 | class TestingConfig(BaseConfig): 101 | TESTING = True 102 | MOCK_EMAIL = True 103 | 104 | # Using a local postgre database 105 | # SQLALCHEMY_DATABASE_URI = "postgresql:///bit_schema_test" 106 | 107 | SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_test_uri() 108 | 109 | 110 | class StagingConfig(BaseConfig): 111 | """Staging configuration.""" 112 | 113 | DEBUG = True 114 | SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() 115 | 116 | 117 | class ProductionConfig(BaseConfig): 118 | SQLALCHEMY_DATABASE_URI = BaseConfig.build_db_uri() 119 | 120 | 121 | def get_env_config() -> str: 122 | flask_config_name = os.getenv("FLASK_ENVIRONMENT_CONFIG", "dev") 123 | if flask_config_name not in ["prod", "test", "dev", "local", "stag"]: 124 | raise ValueError( 125 | "The environment config value has to be within these values: prod, dev, test, local, stag." 126 | ) 127 | return CONFIGURATION_MAPPER[flask_config_name] 128 | 129 | 130 | CONFIGURATION_MAPPER = { 131 | "dev": "config.DevelopmentConfig", 132 | "prod": "config.ProductionConfig", 133 | "stag": "config.StagingConfig", 134 | "local": "config.LocalConfig", 135 | "test": "config.TestingConfig", 136 | } 137 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mtreacy002 2 | -------------------------------------------------------------------------------- /docs/ENVIRONMENT_VARIABLES.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | To run the backend you need to export environment variables. 4 | 5 | These are the needed environment variables: 6 | ``` 7 | export FLASK_ENVIRONMENT_CONFIG= 8 | export SECRET_KEY= 9 | export SECURITY_PASSWORD_SALT= 10 | export MAIL_DEFAULT_SENDER= 11 | export MAIL_SERVER= 12 | export APP_MAIL_USERNAME= 13 | export APP_MAIL_PASSWORD= 14 | export MOCK_EMAIL = 15 | ``` 16 | 17 | ## Environment Variables Description 18 | 19 | ### Run Configuration 20 | 21 | | Environment Variable | Description | Example | 22 | |--------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------| 23 | | FLASK_ENVIRONMENT_CONFIG | Short running environment name so that Flask know which configuration to load. Currently, there are 4 options for this: `dev`, `test`, `local` and `prod`. If you want to use a simple local database (e.g.: sqlite) then set your environment config value as `local`. To use the development environment with a remote database configuration you should use `dev` as a value. | local | 24 | 25 | 26 | These are the currently available run configurations: 27 | - **local:** Local environment used when developing locally 28 | - **dev:** Development environment used when developing with a remote database configuration 29 | - **test:** Testing environment used when running tests 30 | - **prod:** Production environment used when a server is deployed 31 | 32 | ### Security 33 | 34 | | Environment Variable | Description | Example | 35 | |------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| 36 | | SECRET_KEY | Variable used to encrypt or hash sensitive data. JWT based authentication uses this variable to calculate the hash of the access token. Its also used to calculate the password hash to avoid saving it in plain text. This is in string format. | 'some random key' | 37 | | SECURITY_PASSWORD_SALT | Variable used for email confirmation token generation. This is in string format. | 'some password salt' | 38 | 39 | ### Email Verification 40 | 41 | Email verification is when a user registers into the application and to be able to login, this user has to verify/confirm its email. To do this the user has to go to its emails and click the link from the mail sent by our application email. 42 | 43 | | Environment Variable | Description | Example | 44 | |----------------------|-----------------------------------------------------------------------------------------------------------------|------------------------| 45 | | MAIL_DEFAULT_SENDER | Email used to send email verification emails. This is in string format. | 'some_username@gmail.com' | 46 | | MAIL_SERVER | SMTP server address/name used by the email account that sends the verification emails. This is in string format. | 'smtp.gmail.com' | 47 | | APP_MAIL_USERNAME | Username of the email account used to send verification emails. This is in string format. | 'some_username' | 48 | | APP_MAIL_PASSWORD | Password of the email account used to send verification emails. This is in string format. | 'some_password' | 49 | 50 | _Note:_ 51 | - In the examples we use Gmail account example, but you are not restricted to use a Gmail account to send the verification email. If you use other email providers make sure to research about the correct SMTP server name. 52 | - The `'` character may be optional for environment variables without space on them. 53 | 54 | 55 | ### Mock Email Service 56 | 57 | The email sending behaviour can be mocked by setting **MOCK_EMAIL** to 'True'. When set to 'True' it pipes the email as terminal output. Setting it to 'False' (or not setting it) will result in sending emails. 58 | 59 | ## Exporting environment variables 60 | 61 | Assume that KEY is the name of the variable and VALUE is the actual value of the environment variable. 62 | To export an environment variable you have to run: 63 | ``` 64 | export KEY=VALUE 65 | ``` 66 | 67 | - Example: 68 | ``` 69 | export FLASK_ENVIRONMENT_CONFIG=dev 70 | ``` 71 | 72 | Windows user should use **set** instead of **export** for setting these environment variables.
73 | Another way to do this in flask applications is by having a file called `.env` which will have all of the environment variables. When a flask application runs, it will load these variables from the file. 74 | 75 | - Content of `.env` file: 76 | 77 | ``` 78 | FLASK_ENVIRONMENT_CONFIG=dev 79 | SECRET_KEY='some_random_key' 80 | (...) 81 | ``` 82 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Bridge-In-Tech Backend Docs 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ``` 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ``` 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ``` 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ``` 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | 35 | ## Deploying to Surge 36 | 37 | Surge is a static web hosting platform. Tt is used to deploy our spen source programs docusaurus from the command line in a minute. 38 | 39 | Deploy using surge with the following steps: 40 | 41 | 1. Install Surge using npm by running the following command: 42 | ``` 43 | npm install --g surge 44 | ``` 45 | 2. To build the static files of the site for production in the root/docs directory of project, run: 46 | ``` 47 | npm run build 48 | ``` 49 | 3. Run this command inside the root/docs directory of project: 50 | ``` 51 | surge build/ 52 | ``` 53 | or 54 | 55 | Deploy the site to existing domain using the command: 56 | ``` 57 | surge build/ https://bit-backend-docs.surge.sh 58 | ``` 59 | 60 | First-time users of Surge would be prompted to create an account from the command line (happens only once). 61 | 62 | Confirm that the site you want to publish is in the build directory, a randomly generated subdomain *.surge.sh subdomain is always given (which can be edited). 63 | 64 | -------------------------------------------------------------------------------- /docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/docs/Commit-Message-Style-Guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Commit-Message-Style-Guide 3 | title: Commit Message Style Guide 4 | --- 5 | 6 | # Commit Message Structure 7 | 8 | A commit message should consist of two distinct parts separated by a blank line: the `title` and an optional `body`. The layout looks like this: 9 | 10 | ``` 11 | type: subject 12 | 13 | body 14 | ``` 15 | 16 | ## Title 17 | 18 | The title should consist of the `type` of the change and `subject` separated by a colon `:`. Title should be no longer than 50 characters. 19 | 20 | **Type** 21 | 22 | The type is contained within the title and can be one of these types: 23 | 24 | **feat**: a new feature 25 | **fix**: a bug fix 26 | **docs**: changes to documentation 27 | **style**: formatting, missing semi colons, etc; no code change 28 | **refactor**: refactoring production code 29 | **test**: adding tests, refactoring test; no production code change 30 | **chore**: updating build tasks, package manager configs, etc; no production code change 31 | 32 | **Subject** 33 | 34 | Should begin with a capital letter and not end with a period. 35 | 36 | Use an imperative tone to describe what a commit does, rather than what it did. For example, use change; not changed or changes. 37 | 38 | ## Body 39 | 40 | If the changes made in a commit are complex, they should be explained in the commit body. Use the body to explain the what and why of a commit, not the how. 41 | 42 | When writing a body, the blank line between the title and the body is required and you should limit the length of each line to no more than 72 characters. 43 | 44 | **Examples** 45 | 46 | Without body 47 | 48 | ``` 49 | docs: update screenshots in the documentation 50 | ``` 51 | or 52 | 53 | With body 54 | 55 | ``` 56 | fix: fix crash caused by new libraries 57 | 58 | After merging PRs #126 and #130 crashes were occurring. 59 | These crashes were because of deprecated functions. 60 | Found a solution here (https://stackoverflow.com/questions/22718185) 61 | This will resolve issue #140 62 | ``` -------------------------------------------------------------------------------- /docs/docs/Fork,-Clone-&-Remote.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Fork,-Clone-&-Remote 3 | title: Fork, Clone & Remote 4 | --- 5 | 6 | ## Fork 7 | 8 | _**Note**: This is only needed if you want to contribute to the project_. 9 | 10 | If you want to contribute to the project you will have to create your own copy of the project on GitHub. You can do this by clicking the **Fork** button that can be found on the top right corner of the [landing page](https://github.com/anitab-org/bridge-in-tech-backend) of the repository. 11 | 12 | Screen Shot 2020-05-09 at 4 41 25 pm 13 | 14 | ## Clone 15 | 16 | _**Note**: For this you need to install [git](https://git-scm.com) on your machine. You can download the git tool from [here](https://git-scm.com/downloads)_. 17 | 18 | * If you have forked the project, run the following command - 19 | 20 | `git clone https://github.com/YOUR_GITHUB_USER_NAME/bridge-in-tech-backend` 21 | 22 | where `YOUR_GITHUB_USER_NAME` is your GitHub handle. 23 | 24 | If you haven't forked the project, run the following command - 25 | 26 | `git clone https://github.com/anitab-org/bridge-in-tech-backend` 27 | 28 | ## Remote 29 | 30 | _**Note**: This is only needed if you want to contribute to the project_. 31 | 32 | When a repository is cloned, it has a default remote named origin that points to your fork on GitHub, not the original repository it was forked from. To keep track of the original repository, you should add another remote named upstream. For this project it can be done by running the following command - 33 | 34 | `git remote add upstream https://github.com/anitab-org/bridge-in-tech-backend` 35 | 36 | You can check that the previous command worked by running git remote -v. You should see the following output: 37 | 38 | ``` 39 | $ git remote -v 40 | origin https://github.com/YOUR_GITHUB_USER_NAME/bridge-in-tech-backend (fetch) 41 | origin https://github.com/YOUR_GITHUB_USER_NAME/bridge-in-tech-backend (push) 42 | upstream https://github.com/anitab-org/bridge-in-tech-backend.git (fetch) 43 | upstream https://github.com/anitab-org/bridge-in-tech-backend.git (push) 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /docs/docs/Getting-Started.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Getting-Started 3 | title: Getting Started 4 | --- 5 | 6 | Hello Newcomer, here are some tips to help you get started contributing to this project. 7 | 8 | ## Get to know the project 9 | 10 | Here's what you can do to know more about the project: 11 | 12 | * Read documentation available on this website and [README](https://github.com/anitab-org/bridge-in-tech-backend); 13 | * You can join the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel on [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com), to see or participate in the project's discussion; 14 | * You can browse the code on GitHub or even on your workspace after [cloning it](./Fork,-Clone-&-Remote.md#clone). 15 | 16 | ## Choose a working item 17 | 18 | 1. Check the [available issues](https://github.com/anitab-org/bridge-in-tech-backend/issues) (that anyone can contribute to) or [first timers only issues](https://github.com/anitab-org/bridge-in-tech-backend/issues) (just for first time contributors in this project); 19 | 1. Choose one issue you want to work on; 20 | 1. Ask maintainers, on the issue's comment section, if you can work on it; 21 | 1. Once you get approval you can start working on it! 22 | 23 | ## Start working 24 | 25 | Before you start working check the [Contribution Guidelines](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CONTRIBUTING.md) to make sure you can follow the best practises. 26 | In short: 27 | 28 | 1. [Fork the project](./Fork,-Clone-&-Remote.md#fork) into your profile; 29 | 1. [Clone the project](./Fork,-Clone-&-Remote.md#clone) into your workspace on your computer; 30 | 1. [Setup remotes](./Fork,-Clone-&-Remote.md#remote) to keep your develop branch with AnitaB.org repository; 31 | 1. Create a specific branch based from develop branch for your specific feature; 32 | 1. Start coding; 33 | 1. Make sure you follow this [Commit Message Style Guide](./Commit-Message-Style-Guide.md); 34 | 1. Once you finish working on your issue, submit a Pull Request (PR) [(following the template provided)](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/PULL_REQUEST_TEMPLATE.md); 35 | 1. If the reviewers request changes on your PR, make the changes (this can be a back and forth process where you make changes according to reviewers feedback); 36 | 1. Make sure when you finish your changes you squash your commits into a single commit; 37 | 1. Once the reviewers approve your PR, they can merge and your code will finally be part of the main repository! 38 | 39 | # Other ways to contribute 40 | 41 | Do you know there are also other ways to contribute to the project besides working on an issue? 42 | As contributors, you can also: 43 | * 👀 **help review a PR** either by looking into the code and ensure that the code is clean and its logic makes sense. You can also run the app from PR branch to ensure the code works. Look for the PRs with `Status: Needs Review` label on the [Backend](https://github.com/anitab-org/bridge-in-tech-backend/pulls) and [Frontend](https://github.com/anitab-org/bridge-in-tech-web/pulls) repositories. 44 | * 💻 **test a PR** by running it manually on your local machine and write a report on whether or not the PR works. Find a PR with `Status: Needs testing` label which is the next step after that PR is approved by the reviewer/s. Here're good examples of testing report done on one of the [backend](https://github.com/anitab-org/bridge-in-tech-backend/pull/71#pullrequestreview-445274875) and [frontend](https://github.com/anitab-org/bridge-in-tech-web/pull/62#pullrequestreview-464955571) PRs. 45 | * 🔨 try to **break the app** by testing the application that runs from the existing code on the develop branch to find any bugs. If you find any, check if there is an issue already open for it, if there's none, report it on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Bugs and Fixes` topic to ask if you can open one. 46 | * 📚 **check documentations** and see if any area could be improved to help contributors understand the project better. If you find something, check if there is an issue already open for it, if there's none, report it on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Documentation` topic to get approval to open an issue. 47 | * 🎨 give suggestions on how to **improve the UI design**. Post your suggestion for a discussion on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) channel under `Design/Mocks` topic. You might get approval to work on your proposed design and have it implemented as the app UI 😁. 48 | 49 | ✨ Happy Coding !!! ✨ 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /docs/docs/Home.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Home 3 | title: Home 4 | slug: / 5 | --- 6 | 7 | [![](https://img.shields.io/github/forks/anitab-org/bridge-in-tech-backend?style=social)](https://github.com/anitab-org/bridge-in-tech-backend) 8 | [![](https://img.shields.io/github/stars/anitab-org/bridge-in-tech-backend?style=social)](https://github.com/anitab-org/bridge-in-tech-backend) 9 | [![project chat](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) 10 | [![GPLv3 License](https://img.shields.io/badge/License-GPL%20v3-yellow.svg)](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/LICENSE) 11 | [![Open Source](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/anitab-org/bridge-in-tech-backend) 12 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/anitab-org/bridge-in-tech-backend/pulse) 13 | [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/PULL_REQUEST_TEMPLATE.md) 14 | 15 | Welcome to the bridge-in-tech-backend! 16 | 17 | # About 18 | 19 | Bridge-In-Tech is an application inspired by the existing [AnitaB.org Mentorship System](https://github.com/anitab-org/mentorship-backend/wiki). It encourages organizations to collaborate with the mentors and mentees on mentoring programs. Through Bridge-In-Tech, an organization can offer a mentorship program to a mentor and a mentee that is customised to meet the needed skills set within its organisation while providing a safety and supportive environment for these mentor/mentee to work in. This repository is the backend of the system. The tech stack used as backend is Python-Flask-SQLAlchemy with PostgreSQL as database. 20 | 21 | This project was proposed as [an original project of Google Summer of Code 2020](https://summerofcode.withgoogle.com/organizations/4752039663370240/), by [Maya Treacy](https://github.com/mtreacy002). Maya is going to be mentored by [Ramit Sawhney](https://github.com/ramitsawhney27), [Meenakshi Dhanani](https://github.com/meenakshi-dhanani), [Foong Min Wong](https://github.com/foongminwong) and [Isabel Costa](https://github.com/isabelcosta). Isabel will also have a double role as the project manager. You can see Maya's [proposal here](https://docs.google.com/document/d/1ZZCOoWdn2yb6N3qrrgK0SRDQPcx2Pp7kz3yDXDRSHUs/edit#heading=h.h1xuf6xi92mt). 22 | 23 | The frontend of this application will be a Web application using ReactJS. The link to the repository can be found [here](https://github.com/anitab-org/bridge-in-tech-web). 24 | 25 | # AnitaB.org Open Source 26 | 27 | AnitaB.org is an international community for all women involved in the technical aspects of computing. We welcome the participation of women technologists of all ages and at any stage of their studies or careers. 28 | 29 | We engage our community by contributing to open source, collaborating with the global community, and learning/enhancing our coding skills. We are committed to providing a safe, positive online community for our many volunteers that offer their skills, time, and commitment to our projects. 30 | 31 | In order to engage with the community, you can sign up on [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com/) 32 | 33 | # Code of Conduct 34 | 35 | A primary goal of AnitaB.org is to be inclusive of the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe, and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). 36 | 37 | See the full [Code of Conduct](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/CODE_OF_CONDUCT.md) and the [Reporting Guidelines](https://github.com/anitab-org/bridge-in-tech-backend/blob/develop/.github/REPORTING_GUIDELINES.md). 38 | 39 | # Contact 40 | 41 | You can reach us at [AnitaB.org Open Source Zulip](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech). This project is discussed on the [#bridge-in-tech](https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech) stream. 42 | -------------------------------------------------------------------------------- /docs/docs/Project-Ideas.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: Project-Ideas 3 | title: Project Ideas 4 | --- 5 | 6 | ## Origin 7 | BridgeInTech (BIT) was founded by Maya Treacy as an original project for AnitaB.org Open Source for a project submitted to Google Summer of Code 2020. 8 | 9 | ## Project Summary 10 | Bridge In Tech is an application that allows industries/companies, mentors, and students to actively collaborate with one another. There is a backend written with Python and a Web application written in Javascript and React. 11 | 12 | ## Status 13 | Web app at https://bridge-in-tech-web-heroku.herokuapp.com/ 14 | 15 | Backend API at https://bridgeintech-bit-heroku-psql.herokuapp.com/ 16 | 17 | ## Repo Links 18 | [Bridge In Tech (Backend)](https://github.com/anitab-org/bridge-in-tech-backend) 19 | [Bridge In Tech (Web)](https://github.com/anitab-org/bridge-in-tech-web) 20 | 21 | ## Project ideas 22 | ### Backend + Frontend (full features) 23 | 24 | | Idea | Description | 25 | | ---- | ----------- | 26 | | Users data service | Ideally there will be one single point to keep user data across all AnitaB Open Source Projects. Here we start with the Mentorship System (MS) and BridgeInTech (BIT). The service needs to have a single database that holds user data, a backend server which provides API endpoints for both MS and BIT on user related data, and a frontend web server which can be used by Admins of a particular project to manage their users data. 27 | | Remote servers | At the moment there is a bottleneck on the Heroku remote servers for BridgeInTech application due to BIT complex architecture clashes with Heroku limited services on free tier option. Find a solution or alternative remote hosting to solve this issue on both backend and frontend. 28 | | Forgot password | Allow user to reset their password on Login page if they forgot their password | 29 | | Deactivate account | Ability for a user to shut down an account, removing any sensitive data, while still keeping data integrity. This is very important if a user wishes to be removed from the app. | 30 | | Third-party apps authentication (OAuth) | Authenticate using Slack, Facebook, Twitter, Google+, etc | 31 | | App Admin API endpoints and Dashboard | Allow the BridgeInTech Admin user to manage other users, organisations and programs through a dashboard within the application | 32 | | Notifications | Define settings configuration for types and frequency of notifications the user receives / Different types of notifications: Push Notifications; Email and in-app notifications screen; | 33 | | Apply to a program | Allow a user to apply to a program offered by an organization | 34 | | Send Request to a mentor or mentee | Allow organization to request a mentor or mentee to work on their program | 35 | | Alternative solution to keeping user token as Cookies | Currently on both backend and frontend, user token is saved as a cookie. As this may raise a security issue, a better solution to deal with user authentication is needed. | 36 | | ... | ... | ... | 37 | 38 | ### Backend Only 39 | | Idea | Description | Difficulty | 40 | | ---- | ----------- | ---------- | 41 | | Mentorship System and BridgeInTech code integration | Ensure BridgeInTech and Mentorship System can be fully integrated by applying BridgeInTech's Mentorship System related code base on the Mentorship System repository without breaking the existing features on both applications. | Medium | 42 | | Add another representative to organization | Allow an Organization representative to add another user to become the organization representative to help manage programs | Medium | 43 | | ... | ... | ... | 44 | 45 | ### Frontend Only 46 | | Idea | Description | 47 | | ---- | ----------- | 48 | | User's portfolio page | Allow user to view a porfolio page (their own and other's) and see an overview of their/other's activities in BIT (programs involved, feedback from mentors/mentees, etc). | 49 | | Implement redesign of the app | Implement redesign of the app to have a consistent design across screens and follow AnitaB.org branding styles | 50 | | Organization Dashboard | Allow user who is a representative of an organization see an overview of the activities relevant to the programs their organization is offering. | 51 | | Program's progress page | Allow user to see the progress of the program (ratio to completion, task/s status, etc) | 52 | | ... | ... | 53 | 54 | 55 | ## Development Environment 56 | #### Backend Development Environment 57 | 58 | * Technologies Used: Python 59 | * Difficulty: Novice to Intermediate 60 | 61 | #### Web Development Environment 62 | 63 | * Technologies Used: HTML, CSS, JavaScript, React 64 | * Difficulty: Novice to Intermediate 65 | 66 | ## Communicate with Us on Zulip! 67 | If you have an idea of how to improve Bridge In Tech, drop us a message on the #bridge-in-tech stream to discuss it :) -------------------------------------------------------------------------------- /docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'Bridge-In-Tech Backend', 3 | tagline: 'Documentation for Bridge-In-Tech backend', 4 | url: 'https://anitab-org.github.io', 5 | baseUrl: '/bridge-in-tech-backend/', 6 | onBrokenLinks: 'warn', 7 | onBrokenMarkdownLinks: 'warn', 8 | favicon: 'img/favicon.ico', 9 | organizationName: 'anitab-org', 10 | projectName: 'bridge-in-tech-backend', 11 | themeConfig: { 12 | announcementBar: { 13 | id: 'support_us', 14 | content: 15 | '⭐️ If you like Bridge-In-Tech-Backend, give it a star on GitHub! ⭐️', 16 | backgroundColor: '#fafbfc', 17 | textColor: '#091E42', 18 | }, 19 | colorMode: { 20 | defaultMode: "light", 21 | }, 22 | navbar: { 23 | title: 'Bridge-In-Tech Backend', 24 | hideOnScroll: true, 25 | logo: { 26 | alt: 'AnitaB.org logo', 27 | src: 'img/logo.png', 28 | }, 29 | items: [ 30 | { 31 | to: 'docs/', 32 | activeBasePath: 'docs', 33 | label: 'Docs', 34 | position: 'left', 35 | }, 36 | { 37 | href:'https://www.anitab.org', 38 | label: 'AnitaB.org', 39 | position: 'right', 40 | }, 41 | { 42 | href:'https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech', 43 | label: 'Zulip', 44 | position: 'right', 45 | }, 46 | { 47 | href: 'https://github.com/anitab-org/bridge-in-tech-backend', 48 | label: 'GitHub', 49 | position: 'right', 50 | }, 51 | ], 52 | }, 53 | footer: { 54 | style: 'dark', 55 | links: [ 56 | { 57 | title: 'Docs', 58 | items: [ 59 | { 60 | label: 'Bridge-In-Tech-Web Docs', 61 | href: 'https://github.com/anitab-org/bridge-in-tech-web/wiki', 62 | } 63 | ], 64 | }, 65 | { 66 | title: 'Community', 67 | items: [ 68 | { 69 | label: 'Zulip', 70 | href: 'https://anitab-org.zulipchat.com/#narrow/stream/237630-bridge-in-tech', 71 | }, 72 | { 73 | label: 'Twitter', 74 | href: 'https://twitter.com/anitab_org', 75 | }, 76 | ], 77 | }, 78 | { 79 | title: 'More', 80 | items: [ 81 | { 82 | label: 'GitHub', 83 | href: 'https://github.com/anitab-org/bridge-in-tech-backend', 84 | }, 85 | { 86 | label: 'Blog', 87 | href:'https://medium.com/anitab-org-open-source' 88 | } 89 | ], 90 | }, 91 | ], 92 | copyright: ` 93 |
94 | 95 | 96 | 97 | 98 |
99 | Copyright © ${new Date().getFullYear()} AnitaB.org 100 | 101 | `, 102 | }, 103 | }, 104 | presets: [ 105 | [ 106 | '@docusaurus/preset-classic', 107 | { 108 | docs: { 109 | sidebarPath: require.resolve('./sidebars.js'), 110 | editUrl: 111 | 'https://github.com/anitab-org/bridge-in-tech-backend/edit/develop/docs', 112 | }, 113 | theme: { 114 | customCss: require.resolve('./src/css/custom.css'), 115 | }, 116 | }, 117 | ], 118 | ], 119 | }; 120 | -------------------------------------------------------------------------------- /docs/manual_tests/test_register.md: -------------------------------------------------------------------------------- 1 | 2 | ### Test Description 📜 : /register [method = POST] 3 | 4 | - #### Positive Tests ✅ : 5 | 6 | - **Screenshot/gif** 📸 : 7 |
8 | 9 | ![post-register-positive](https://user-images.githubusercontent.com/56113566/120222706-01edad00-c25e-11eb-96b0-d0a6d62534e8.gif) 10 | 11 | - **Expected Result** 📝 : A new user should be created. 12 | 13 |
14 | 15 | - **Screenshot/gif** 📸 : 16 | 17 |
18 | 19 | ![register-positive-1](https://user-images.githubusercontent.com/56113566/120224846-d7055800-c261-11eb-8989-2d8da8de84c1.png) 20 | 21 | - **Expected Result** 📝 : A new user should be created. 22 |
23 | 24 | ![register-positive-1-res](https://user-images.githubusercontent.com/56113566/120224850-d8cf1b80-c261-11eb-8801-59f7fa4a3fa5.png) 25 | 26 |
27 | 28 | - **Screenshot/gif** 📸 : 29 |
30 | 31 | ![register-positive-2](https://user-images.githubusercontent.com/56113566/120225485-f8b30f00-c262-11eb-85d6-4805e2620372.png) 32 | 33 | - **Expected Result** 📝 : A new user should be created. 34 |
35 | 36 | ![register-positive-1-res](https://user-images.githubusercontent.com/56113566/120224850-d8cf1b80-c261-11eb-8801-59f7fa4a3fa5.png) 37 | 38 |
39 | 40 | - #### Negative Tests ❌ : 41 | 42 | 43 | - **Screenshot/gif** 📸 : 44 | - If a username **already exists**. 45 |
46 | 47 | ![register-negative-6](https://user-images.githubusercontent.com/56113566/120224858-dc62a280-c261-11eb-84dd-7d1014b43b7e.png) 48 | 49 | - **Expected Result** 📝 : A new user shouldn't be created. 50 |
51 | 52 | ![register-negative-6-res](https://user-images.githubusercontent.com/56113566/120224864-de2c6600-c261-11eb-8e7b-391be3281f6d.png) 53 | 54 |
55 | 56 | - **Screenshot/gif** 📸 : 57 | - If user email **already exists**. 58 |
59 | 60 | ![register-negative-7](https://user-images.githubusercontent.com/56113566/120224867-df5d9300-c261-11eb-900c-b8ef91aa3ae4.png) 61 | 62 | - **Expected Result** 📝 : A new user shouldn't be created. 63 | 64 |
65 | 66 | ![register-negative-7-res](https://user-images.githubusercontent.com/56113566/120224876-e4224700-c261-11eb-8c6c-792e1e51289f.png) 67 | 68 | 69 | 70 | - **Screenshot/gif** 📸 : 71 | - If name attribute is **invalid**. 72 |
73 | 74 | ![post-register-negative-1](https://user-images.githubusercontent.com/56113566/120222960-6f014280-c25e-11eb-8aa8-691fec8c8e8b.png) 75 | 76 | - **Expected Result** 📝 : A new user shouldn't be created. 77 | 78 |
79 | 80 | ![post-register-negative-1-res](https://user-images.githubusercontent.com/56113566/120222966-745e8d00-c25e-11eb-99ec-5054111a0957.png) 81 | 82 |
83 | 84 | - **Screenshot/gif** 📸 : 85 | - If user email is **invalid**. 86 |
87 | 88 | ![post-register-negative-2](https://user-images.githubusercontent.com/56113566/120222980-79bbd780-c25e-11eb-9345-6e588e518cdf.png) 89 | 90 | - **Expected Result** 📝 : A new user shouldn't be created. 91 |
92 | 93 | ![post-register-negative-2-res](https://user-images.githubusercontent.com/56113566/120222985-7aed0480-c25e-11eb-8156-e71a64c1ed92.png) 94 | 95 |
96 | 97 | - **Screenshot/gif** 📸 : 98 | - Password field has to be at **least 8 characters**. 99 |
100 | 101 | ![post-register-negative-3](https://user-images.githubusercontent.com/56113566/120222996-804a4f00-c25e-11eb-910e-b93e97b44b45.png) 102 | 103 | - **Expected Result** 📝 : A new user shouldn't be created. 104 |
105 | 106 | ![post-register-negative-3-res](https://user-images.githubusercontent.com/56113566/120222998-817b7c00-c25e-11eb-9d50-daded22e17ba.png) 107 | 108 |
109 | 110 | - **Screenshot/gif** 📸 : 111 | - Username field has to be **atleast 5-25 characters**. 112 |
113 | 114 | ![post-register-negative-4](https://user-images.githubusercontent.com/56113566/120223004-84766c80-c25e-11eb-8b3d-478c32473f1b.png) 115 | 116 | - **Expected Result** 📝 : A new user shouldn't be created. 117 |
118 | 119 | ![post-register-negative-4-res](https://user-images.githubusercontent.com/56113566/120223012-86d8c680-c25e-11eb-9e11-381ca4d3b4f0.png) 120 | 121 |
122 | 123 | - **Screenshot/gif** 📸 : 124 | - Terms and condition are **not checked**. 125 |
126 | 127 | ![post-register-negative-5](https://user-images.githubusercontent.com/56113566/120223026-8b9d7a80-c25e-11eb-87b3-cf8b85ee742e.png) 128 | 129 | - **Expected Result** 📝 : A new user shouldn't be created. 130 | 131 |
132 | 133 | ![post-register-negative-5-res](https://user-images.githubusercontent.com/56113566/120223043-91935b80-c25e-11eb-97be-fab631b367b7.png) 134 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear" 13 | }, 14 | "dependencies": { 15 | "@docusaurus/core": "2.0.0-alpha.70", 16 | "@docusaurus/preset-classic": "2.0.0-alpha.70", 17 | "@mdx-js/react": "^1.6.21", 18 | "clsx": "^1.1.1", 19 | "react": "^16.8.4", 20 | "react-dom": "^16.8.4" 21 | }, 22 | "browserslist": { 23 | "production": [ 24 | ">0.5%", 25 | "not dead", 26 | "not op_mini all" 27 | ], 28 | "development": [ 29 | "last 1 chrome version", 30 | "last 1 firefox version", 31 | "last 1 safari version" 32 | ] 33 | } 34 | } -------------------------------------------------------------------------------- /docs/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: [ 3 | { 4 | type: "doc", 5 | id: "Home", 6 | }, 7 | { 8 | type: 'link', 9 | label: 'About AnitaB.org', 10 | href: 'https://anitab.org/' 11 | }, 12 | { 13 | type:"link", 14 | label:"About BridgeInTech", 15 | href:"https://medium.com/@mtreacy002/bridgeintech-by-anitab-org-85745853865d?source=friends_link&sk=fcb616ef0ac7e1e85ac6b58b513a56ee" 16 | }, 17 | { 18 | type: "doc", 19 | id: "Getting-Started", 20 | }, 21 | { 22 | type:"category", 23 | label:"Documentation", 24 | items:[ 25 | { 26 | type:"category", 27 | label:"Development", 28 | items:[ 29 | { 30 | type: "doc", 31 | id: "Fork,-Clone-&-Remote", 32 | }, 33 | { 34 | type:"doc", 35 | id:"Environment-Setup-Instructions" 36 | }, 37 | { 38 | type:"link", 39 | label:"BIT Heroku Swagger UI server (NOTE:NOT WORKING DUE TO EXTERNAL ISSUE)", 40 | href:"https://bridgeintech-bit-heroku-psql.herokuapp.com" 41 | } 42 | ] 43 | }, 44 | { 45 | type:"category", 46 | label:"Work In Progress", 47 | items:[ 48 | { 49 | type:"link", 50 | label:"Community Bonding - Proposal Review", 51 | href:"https://docs.google.com/document/d/1uCDCWs8Xyo-3EaUnrLOs48mefXJIN235b1aAswA-s-Y/edit" 52 | }, 53 | { 54 | type:"link", 55 | label:"Technical Discussions", 56 | href:"https://docs.google.com/document/d/1Fi_dvc1f-J0uuTzzzU7pwZV8hAI9iTX5LlI_THcg_ks/edit" 57 | }, 58 | { 59 | type:"link", 60 | label:"Weekly Scrum Progress Updates", 61 | href:"https://docs.google.com/document/d/1GFeYF7LQV7gG0Cu0lE6Yuit0FVhuUxV-B6c1jBxbro8/edit#heading=h.urum71w6ib58" 62 | }, 63 | { 64 | type:"link", 65 | label:"Roadmap to production", 66 | href:"https://docs.google.com/document/d/1LfB3fSk6Thqwz1_g5foJKnTHDIkxHH69OOsKUTFsUHY/edit?usp=sharing" 67 | }, 68 | ] 69 | }, 70 | { 71 | type:"category", 72 | label:"Meeting minutes", 73 | items:[ 74 | { 75 | type:"link", 76 | label:"GSoC20 Meetings Minutes", 77 | href:"https://docs.google.com/document/d/1QRHzy0IWgAE5bjkwI_Lp67Dv50aywGuUmzmWewQ1rpY/edit?usp=sharing" 78 | }, 79 | { 80 | type:"link", 81 | label:"BIT Open Session Minutes", 82 | href:"https://docs.google.com/document/d/1vMmLOC8txyOZdNfdFh6keB5rw7uEbL1F6dS_JdcnD0g/edit#" 83 | } 84 | ] 85 | }, 86 | { 87 | type:"doc", 88 | id:"Project-Ideas" 89 | } 90 | ] 91 | }, 92 | { 93 | type:"category", 94 | label:"GSoc Students", 95 | items:[ 96 | { 97 | type:"category", 98 | label:"GSoC 2020", 99 | items:[ 100 | { 101 | type:"doc", 102 | id:"GSoC-2020-Maya-Treacy" 103 | }, 104 | { 105 | type:"link", 106 | label:"Final Report", 107 | href:"https://gist.github.com/mtreacy002/0e6968595f4b0aa6df8538c6c1155642" 108 | } 109 | ] 110 | } 111 | ] 112 | } 113 | ] 114 | }; 115 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #25c2a0; 11 | --ifm-color-primary-dark: rgb(33, 175, 144); 12 | --ifm-color-primary-darker: rgb(31, 165, 136); 13 | --ifm-color-primary-darkest: rgb(26, 136, 112); 14 | --ifm-color-primary-light: rgb(70, 203, 174); 15 | --ifm-color-primary-lighter: rgb(102, 212, 189); 16 | --ifm-color-primary-lightest: rgb(146, 224, 208); 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | -------------------------------------------------------------------------------- /docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Layout from '@theme/Layout'; 4 | import Link from '@docusaurus/Link'; 5 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | import styles from './styles.module.css'; 8 | 9 | function Home() { 10 | const context = useDocusaurusContext(); 11 | const {siteConfig = {}} = context; 12 | return ( 13 | 16 |
17 |
18 |

{siteConfig.title}

19 |

{siteConfig.tagline}

20 |
21 | 27 | Get Started 28 | 29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | 36 | export default Home; 37 | -------------------------------------------------------------------------------- /docs/src/pages/styles.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/docs/static/img/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | APScheduler==3.6.3 2 | Flask==1.1.2 3 | Flask-Cors==3.0.9 4 | Flask-JWT-Extended==3.24.1 5 | Flask-Mail==0.9.1 6 | Flask-Migrate==2.5.3 7 | flask-restx==0.2.0 8 | Flask-SQLAlchemy==2.3.2 9 | Flask-Testing==0.8.0 10 | gunicorn==20.0.4 11 | psycopg2-binary==2.8.6 12 | PyMySQL==0.10.1 13 | pytest-cov==2.10.1 14 | python-dotenv==0.14.0 15 | regex==2020.9.27 16 | requests==2.24.0 17 | werkzeug==0.16.1 18 | python-dateutil==2.8.2 19 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, jsonify 3 | from flask_migrate import Migrate, MigrateCommand 4 | from flask_cors import CORS 5 | from config import get_env_config 6 | 7 | cors = CORS() 8 | 9 | 10 | def create_app(config_filename: str) -> Flask: 11 | # instantiate the app 12 | app = Flask(__name__, instance_relative_config=True) 13 | 14 | # setup application environment 15 | app.config.from_object(config_filename) 16 | app.url_map.strict_slashes = False 17 | 18 | from app.database.sqlalchemy_extension import db 19 | 20 | db.init_app(app) 21 | 22 | from app.database.models.ms_schema.user import UserModel 23 | from app.database.models.ms_schema.mentorship_relation import ( 24 | MentorshipRelationModel, 25 | ) 26 | from app.database.models.ms_schema.tasks_list import TasksListModel 27 | from app.database.models.ms_schema.task_comment import TaskCommentModel 28 | from app.database.models.bit_schema.organization import OrganizationModel 29 | from app.database.models.bit_schema.program import ProgramModel 30 | from app.database.models.bit_schema.user_extension import UserExtensionModel 31 | from app.database.models.bit_schema.personal_background import ( 32 | PersonalBackgroundModel, 33 | ) 34 | from app.database.models.bit_schema.mentorship_relation_extension import ( 35 | MentorshipRelationExtensionModel, 36 | ) 37 | 38 | migrate = Migrate(app, db) 39 | 40 | cors.init_app( 41 | app, 42 | resources={ 43 | r"*": {"origins": {"http://localhost:3000", "https://anitab-org.github.io"}} 44 | }, 45 | ) 46 | 47 | from app.api.jwt_extension import jwt 48 | 49 | jwt.init_app(app) 50 | 51 | from app.api.bit_extension import api 52 | 53 | api.init_app(app) 54 | 55 | from app.api.mail_extension import mail 56 | 57 | mail.init_app(app) 58 | 59 | return app 60 | 61 | 62 | application = create_app(get_env_config()) 63 | 64 | 65 | @application.before_first_request 66 | def create_tables(): 67 | 68 | from app.database.sqlalchemy_extension import db 69 | 70 | from app.database.models.ms_schema.user import UserModel 71 | from app.database.models.ms_schema.mentorship_relation import ( 72 | MentorshipRelationModel, 73 | ) 74 | from app.database.models.ms_schema.tasks_list import TasksListModel 75 | from app.database.models.ms_schema.task_comment import TaskCommentModel 76 | from app.database.models.bit_schema.organization import OrganizationModel 77 | from app.database.models.bit_schema.program import ProgramModel 78 | from app.database.models.bit_schema.user_extension import UserExtensionModel 79 | from app.database.models.bit_schema.personal_background import ( 80 | PersonalBackgroundModel, 81 | ) 82 | from app.database.models.bit_schema.mentorship_relation_extension import ( 83 | MentorshipRelationExtensionModel, 84 | ) 85 | 86 | db.create_all() 87 | 88 | @application.shell_context_processor 89 | def make_shell_context(): 90 | return { 91 | "db": db, 92 | "UserModel": UserModel, 93 | "MentorshipRelationModel": MentorshipRelationModel, 94 | "TaskListModel": TasksListModel, 95 | "TaskCommentModel": TaskCommentModel, 96 | "OrganizationModel": OrganizationModel, 97 | "ProgramModel": ProgramModel, 98 | "UserExtensionModel": UserExtensionModel, 99 | "PersonalBackgroundModel": PersonalBackgroundModel, 100 | "MentorshipRelationExtensionModel": MentorshipRelationExtensionModel, 101 | } 102 | 103 | 104 | if __name__ == "__main__": 105 | application.run(port=5000) 106 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/tests/__init__.py -------------------------------------------------------------------------------- /tests/base_test_case.py: -------------------------------------------------------------------------------- 1 | from flask_testing import TestCase 2 | 3 | from app.database.models.ms_schema.user import UserModel 4 | from run import application 5 | from app.database.sqlalchemy_extension import db 6 | 7 | from tests.test_data import test_admin_user 8 | 9 | 10 | class BaseTestCase(TestCase): 11 | @classmethod 12 | def create_app(cls): 13 | application.config.from_object("config.TestingConfig") 14 | 15 | # Setting up test environment variables 16 | application.config["SECRET_KEY"] = "TEST_SECRET_KEY" 17 | application.config["SECURITY_PASSWORD_SALT"] = "TEST_SECURITY_PWD_SALT" 18 | return application 19 | 20 | def setUp(self): 21 | 22 | db.create_all() 23 | 24 | @classmethod 25 | def tearDown(cls): 26 | db.session.remove() 27 | db.drop_all() 28 | -------------------------------------------------------------------------------- /tests/organizations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/tests/organizations/__init__.py -------------------------------------------------------------------------------- /tests/organizations/test_api_get_organization.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus, cookies 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from app.database.sqlalchemy_extension import db 11 | from tests.base_test_case import BaseTestCase 12 | from app.api.request_api_utils import ( 13 | post_request, 14 | get_request, 15 | BASE_MS_API_URL, 16 | AUTH_COOKIE, 17 | ) 18 | from app.api.models.user import full_user_api_model 19 | from app.api.models.organization import ( 20 | get_organization_response_model, 21 | update_organization_request_model, 22 | ) 23 | from tests.test_data import user1 24 | from app.database.models.ms_schema.user import UserModel 25 | from app.database.models.bit_schema.user_extension import UserExtensionModel 26 | from app.database.models.bit_schema.organization import OrganizationModel 27 | from app.utils.date_converter import convert_timestamp_to_human_date 28 | 29 | 30 | class TestGetOrganizationApi(BaseTestCase): 31 | @patch("requests.get") 32 | @patch("requests.post") 33 | def setUp(self, mock_login, mock_get_user): 34 | super(TestGetOrganizationApi, self).setUp() 35 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 36 | access_expiry = time.time() + 60 * 60 * 24 * 28 37 | success_message = { 38 | "access_token": "this is fake token", 39 | "access_expiry": access_expiry, 40 | } 41 | success_code = HTTPStatus.OK 42 | 43 | mock_login_response = Mock() 44 | mock_login_response.json.return_value = success_message 45 | mock_login_response.status_code = success_code 46 | mock_login.return_value = mock_login_response 47 | mock_login.raise_for_status = json.dumps(success_code) 48 | 49 | expected_user = marshal(user1, full_user_api_model) 50 | 51 | mock_get_response = Mock() 52 | mock_get_response.json.return_value = expected_user 53 | mock_get_response.status_code = success_code 54 | 55 | mock_get_user.return_value = mock_get_response 56 | mock_get_user.raise_for_status = json.dumps(success_code) 57 | 58 | user_login_success = { 59 | "username": user1.get("username"), 60 | "password": user1.get("password"), 61 | } 62 | 63 | with self.client: 64 | login_response = self.client.post( 65 | "/login", 66 | data=json.dumps(user_login_success), 67 | follow_redirects=True, 68 | content_type="application/json", 69 | ) 70 | 71 | test_user1 = UserModel( 72 | name=user1["name"], 73 | username=user1["username"], 74 | password=user1["password"], 75 | email=user1["email"], 76 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 77 | ) 78 | test_user1.need_mentoring = user1["need_mentoring"] 79 | test_user1.available_to_mentor = user1["available_to_mentor"] 80 | 81 | test_user1.save_to_db() 82 | self.test_user1_data = UserModel.find_by_email(test_user1.email) 83 | AUTH_COOKIE["user"] = marshal(self.test_user1_data, full_user_api_model) 84 | 85 | def test_api_dao_get_organization_successfully(self): 86 | organization = OrganizationModel( 87 | rep_id=self.test_user1_data.id, 88 | name="Company ABC", 89 | email="companyabc@mail.com", 90 | address="506 Elizabeth St, Melbourne VIC 3000, Australia", 91 | website="https://www.ames.net.au", 92 | timezone="AUSTRALIA_MELBOURNE", 93 | ) 94 | organization.rep_department = "H&R Department" 95 | organization.about = "This is about ABC" 96 | organization.phone = "321-456-789" 97 | organization.status = "DRAFT" 98 | # joined one month prior to access date 99 | join_date = time.time() - 60 * 60 * 24 * 7 100 | organization.join_date = join_date 101 | 102 | db.session.add(organization) 103 | db.session.commit() 104 | 105 | organization1_data = OrganizationModel.find_by_representative( 106 | self.test_user1_data.id 107 | ) 108 | 109 | test_user_extension = UserExtensionModel( 110 | user_id=self.test_user1_data.id, timezone="AUSTRALIA_MELBOURNE" 111 | ) 112 | test_user_extension.is_organization_rep = True 113 | test_user_extension.save_to_db() 114 | 115 | response_organization = { 116 | "id": organization1_data.id, 117 | "representative_id": self.test_user1_data.id, 118 | "representative_name": self.test_user1_data.name, 119 | "representative_department": "H&R Department", 120 | "organization_name": "Company ABC", 121 | "email": "companyabc@mail.com", 122 | "about": "This is about ABC", 123 | "address": "506 Elizabeth St, Melbourne VIC 3000, Australia", 124 | "website": "https://www.ames.net.au", 125 | "timezone": "Australia/Melbourne", 126 | "phone": "321-456-789", 127 | "status": "Draft", 128 | "join_date": convert_timestamp_to_human_date( 129 | join_date, "Australia/Melbourne" 130 | ), 131 | } 132 | success_code = HTTPStatus.OK 133 | 134 | response = self.client.get( 135 | "/organization", 136 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 137 | follow_redirects=True, 138 | content_type="application/json", 139 | ) 140 | self.assertEqual(response.json, response_organization) 141 | self.assertEqual(response.status_code, success_code) 142 | 143 | def test_api_dao_get_organization_not_exist(self): 144 | test_user_extension = UserExtensionModel( 145 | user_id=self.test_user1_data.id, timezone="AUSTRALIA_MELBOURNE" 146 | ) 147 | test_user_extension.is_organization_rep = True 148 | test_user_extension.save_to_db() 149 | 150 | response = self.client.get( 151 | "/organization", 152 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 153 | follow_redirects=True, 154 | content_type="application/json", 155 | ) 156 | self.assertEqual(HTTPStatus.NOT_FOUND, response.status_code) 157 | self.assertEqual(messages.ORGANIZATION_DOES_NOT_EXIST, response.json) 158 | 159 | def test_api_dao_get_organization_not_representative(self): 160 | test_user_extension = UserExtensionModel( 161 | user_id=self.test_user1_data.id, timezone="AUSTRALIA_MELBOURNE" 162 | ) 163 | test_user_extension.is_organization_rep = False 164 | test_user_extension.save_to_db() 165 | 166 | response = self.client.get( 167 | "/organization", 168 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 169 | follow_redirects=True, 170 | content_type="application/json", 171 | ) 172 | self.assertEqual(HTTPStatus.FORBIDDEN, response.status_code) 173 | self.assertEqual(messages.NOT_ORGANIZATION_REPRESENTATIVE, response.json) 174 | -------------------------------------------------------------------------------- /tests/programs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/tests/programs/__init__.py -------------------------------------------------------------------------------- /tests/test_app_config.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from datetime import timedelta 4 | 5 | from flask import current_app 6 | from flask_testing import TestCase 7 | 8 | from config import BaseConfig 9 | from run import application 10 | 11 | 12 | class TestTestingConfig(TestCase): 13 | def create_app(self): 14 | application.config.from_object("config.TestingConfig") 15 | 16 | secret_key = os.getenv("SECRET_KEY", None) 17 | application.config["SECRET_KEY"] = ( 18 | secret_key if secret_key else "TEST_SECRET_KEY" 19 | ) 20 | return application 21 | 22 | def test_app_testing_config(self): 23 | self.assertIsNotNone(application.config["SECRET_KEY"]) 24 | self.assertFalse(application.config["DEBUG"]) 25 | self.assertTrue(application.config["TESTING"]) 26 | self.assertFalse(application.config["SQLALCHEMY_TRACK_MODIFICATIONS"]) 27 | self.assertTrue(application.config["MOCK_EMAIL"]) 28 | self.assertIsNotNone(current_app) 29 | 30 | def test_get_db_uri_function(self): 31 | 32 | expected_result = "db_type_example://db_user_example:db_password_example@db_endpoint_example/db_name_example" 33 | actual_result = BaseConfig.build_db_uri( 34 | db_type_arg="db_type_example", 35 | db_user_arg="db_user_example", 36 | db_password_arg="db_password_example", 37 | db_endpoint_arg="db_endpoint_example", 38 | db_name_arg="db_name_example", 39 | ) 40 | self.assertEqual(expected_result, actual_result) 41 | 42 | 43 | class TestDevelopmentConfig(TestCase): 44 | def create_app(self): 45 | application.config.from_object("config.DevelopmentConfig") 46 | 47 | secret_key = os.getenv("SECRET_KEY", None) 48 | application.config["SECRET_KEY"] = ( 49 | secret_key if secret_key else "TEST_SECRET_KEY" 50 | ) 51 | 52 | return application 53 | 54 | def test_app_development_config(self): 55 | self.assertIsNotNone(application.config["SECRET_KEY"]) 56 | self.assertTrue(application.config["DEBUG"]) 57 | self.assertFalse(application.config["TESTING"]) 58 | self.assertFalse(application.config["SQLALCHEMY_TRACK_MODIFICATIONS"]) 59 | self.assertIsNotNone(current_app) 60 | 61 | 62 | class TestStagingConfig(TestCase): 63 | def create_app(self): 64 | application.config.from_object("config.StagingConfig") 65 | 66 | secret_key = os.getenv("SECRET_KEY", None) 67 | application.config["SECRET_KEY"] = ( 68 | secret_key if secret_key else "TEST_SECRET_KEY" 69 | ) 70 | 71 | return application 72 | 73 | def test_app_development_config(self): 74 | self.assertIsNotNone(application.config["SECRET_KEY"]) 75 | self.assertTrue(application.config["DEBUG"]) 76 | self.assertFalse(application.config["TESTING"]) 77 | self.assertFalse(application.config["SQLALCHEMY_TRACK_MODIFICATIONS"]) 78 | self.assertIsNotNone(current_app) 79 | 80 | 81 | class TestLocalConfig(TestCase): 82 | def create_app(self): 83 | application.config.from_object("config.LocalConfig") 84 | 85 | secret_key = os.getenv("SECRET_KEY", None) 86 | application.config["SECRET_KEY"] = ( 87 | secret_key if secret_key else "TEST_SECRET_KEY" 88 | ) 89 | return application 90 | 91 | def test_app_development_config(self): 92 | self.assertIsNotNone(application.config["SECRET_KEY"]) 93 | self.assertTrue(application.config["DEBUG"]) 94 | self.assertFalse(application.config["TESTING"]) 95 | self.assertFalse(application.config["SQLALCHEMY_TRACK_MODIFICATIONS"]) 96 | self.assertIsNotNone(current_app) 97 | 98 | 99 | class TestProductionConfig(TestCase): 100 | def create_app(self): 101 | application.config.from_object("config.ProductionConfig") 102 | 103 | secret_key = os.getenv("SECRET_KEY", None) 104 | application.config["SECRET_KEY"] = ( 105 | secret_key if secret_key else "TEST_SECRET_KEY" 106 | ) 107 | 108 | return application 109 | 110 | def test_app_production_config(self): 111 | self.assertIsNotNone(application.config["SECRET_KEY"]) 112 | self.assertFalse(application.config["DEBUG"]) 113 | self.assertFalse(application.config["TESTING"]) 114 | self.assertFalse(application.config["SQLALCHEMY_TRACK_MODIFICATIONS"]) 115 | self.assertIsNotNone(current_app) 116 | 117 | 118 | if __name__ == "__main__": 119 | unittest.main() 120 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | test_admin_user = { 2 | "name": "Admin", 3 | "email": "admin@email.com", 4 | "username": "admin_username", 5 | "password": "admin_pwd", 6 | "terms_and_conditions_checked": True, 7 | } 8 | 9 | test_admin_user_2 = { 10 | "name": "Admin_2", 11 | "email": "admin2@email.com", 12 | "username": "admin2_username", 13 | "password": "admin2_pwd", 14 | "terms_and_conditions_checked": True, 15 | } 16 | 17 | test_admin_user_3 = { 18 | "name": "Admin_3", 19 | "email": "admin3@email.com", 20 | "username": "admin3_username", 21 | "password": "admin3_pwd", 22 | "terms_and_conditions_checked": True, 23 | } 24 | 25 | 26 | user1 = { 27 | "name": "User", 28 | "email": "user1@email.com", 29 | "username": "user1", 30 | "password": "user1_pwd", 31 | "terms_and_conditions_checked": True, 32 | "available_to_mentor": True, 33 | "need_mentoring": True, 34 | } 35 | 36 | user2 = { 37 | "name": "Userb", 38 | "email": "user2@email.com", 39 | "username": "user2", 40 | "password": "user2_pwd", 41 | "terms_and_conditions_checked": True, 42 | "available_to_mentor": False, 43 | "need_mentoring": True, 44 | } 45 | 46 | user3 = { 47 | "name": "s_t-r$a/n'ge name", 48 | "email": "user3@email.com", 49 | "username": "user3", 50 | "password": "user3_pwd", 51 | "terms_and_conditions_checked": True, 52 | "available_to_mentor": True, 53 | "need_mentoring": False, 54 | } 55 | 56 | user4 = { 57 | "name": "user4@email.com", 58 | "email": "user4@email.com", 59 | "username": "user4", 60 | "password": "user4_pwd", 61 | "terms_and_conditions_checked": True, 62 | } 63 | 64 | program1 = { 65 | "program_name": "Program One", 66 | "organizations": { 67 | "rep_id": { 68 | "name": "User", 69 | "email": "user1@email.com", 70 | "username": "user1", 71 | "password": "user1_pwd", 72 | "terms_and_conditions_checked": True, 73 | "available_to_mentor": True, 74 | "need_mentoring": True, 75 | }, 76 | "name": "Company ABC", 77 | "email": "companyabc@mail.com", 78 | "address": "506 Elizabeth St, Melbourne VIC 3000, Australia", 79 | "website": "https://www.ames.net.au", 80 | "timezone": "UTC-09:00/Alaska Standard Time", 81 | }, 82 | "start_date": 1596236400, 83 | "end_date": 1598828400, 84 | "target_candidate": None, 85 | "status": "Draft", 86 | "creation_date": 1589457600, 87 | } 88 | 89 | organization1 = { 90 | "rep_id": { 91 | "name": "User", 92 | "email": "user1@email.com", 93 | "username": "user1", 94 | "password": "user1_pwd", 95 | "terms_and_conditions_checked": True, 96 | "available_to_mentor": True, 97 | "need_mentoring": True, 98 | }, 99 | "name": "Company ABC", 100 | "email": "companyabc@mail.com", 101 | "address": "506 Elizabeth St, Melbourne VIC 3000, Australia", 102 | "website": "https://www.ames.net.au", 103 | "timezone": "UTC-09:00/Alaska Standard Time", 104 | } 105 | -------------------------------------------------------------------------------- /tests/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anitab-org/bridge-in-tech-backend/57d7cee75558573cd8e5685fbe383ec395467e92/tests/users/__init__.py -------------------------------------------------------------------------------- /tests/users/test_api_create_user_additional_info.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import time 3 | import unittest 4 | from http import HTTPStatus, cookies 5 | from unittest.mock import patch, Mock 6 | import requests 7 | from requests.exceptions import HTTPError 8 | from flask import json 9 | from flask_restx import marshal 10 | from app import messages 11 | from tests.base_test_case import BaseTestCase 12 | from app.api.request_api_utils import ( 13 | post_request, 14 | get_request, 15 | BASE_MS_API_URL, 16 | AUTH_COOKIE, 17 | ) 18 | from app.api.models.user import full_user_api_model, get_user_extension_response_model 19 | from tests.test_data import user1 20 | from app.database.models.ms_schema.user import UserModel 21 | from app.database.models.bit_schema.user_extension import UserExtensionModel 22 | 23 | 24 | class TestCreateUserAdditionalInfoApi(BaseTestCase): 25 | @patch("requests.get") 26 | @patch("requests.post") 27 | def setUp(self, mock_login, mock_get_user): 28 | super(TestCreateUserAdditionalInfoApi, self).setUp() 29 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 30 | access_expiry = time.time() + 60 * 60 * 24 * 28 31 | success_message = { 32 | "access_token": "this is fake token", 33 | "access_expiry": access_expiry, 34 | } 35 | success_code = HTTPStatus.OK 36 | 37 | mock_login_response = Mock() 38 | mock_login_response.json.return_value = success_message 39 | mock_login_response.status_code = success_code 40 | mock_login.return_value = mock_login_response 41 | mock_login.raise_for_status = json.dumps(success_code) 42 | 43 | expected_user = marshal(user1, full_user_api_model) 44 | 45 | mock_get_response = Mock() 46 | mock_get_response.json.return_value = expected_user 47 | mock_get_response.status_code = success_code 48 | 49 | mock_get_user.return_value = mock_get_response 50 | mock_get_user.raise_for_status = json.dumps(success_code) 51 | 52 | user_login_success = { 53 | "username": user1.get("username"), 54 | "password": user1.get("password"), 55 | } 56 | 57 | with self.client: 58 | login_response = self.client.post( 59 | "/login", 60 | data=json.dumps(user_login_success), 61 | follow_redirects=True, 62 | content_type="application/json", 63 | ) 64 | 65 | test_user = UserModel( 66 | name=user1["name"], 67 | username=user1["username"], 68 | password=user1["password"], 69 | email=user1["email"], 70 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 71 | ) 72 | test_user.need_mentoring = user1["need_mentoring"] 73 | test_user.available_to_mentor = user1["available_to_mentor"] 74 | 75 | test_user.save_to_db() 76 | 77 | self.test_user_data = UserModel.find_by_email(test_user.email) 78 | 79 | AUTH_COOKIE["user"] = marshal(self.test_user_data, full_user_api_model) 80 | 81 | self.correct_payload_additional_info = { 82 | "is_organization_rep": True, 83 | "timezone": "Australia/Melbourne", 84 | "phone": "123-456-789", 85 | "mobile": "", 86 | "personal_website": "", 87 | } 88 | 89 | def test_api_dao_create_user_additional_info_successfully(self): 90 | success_message = messages.ADDITIONAL_INFO_SUCCESSFULLY_CREATED 91 | success_code = HTTPStatus.CREATED 92 | 93 | with self.client: 94 | response = self.client.put( 95 | "/user/additional_info", 96 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 97 | data=json.dumps(dict(self.correct_payload_additional_info)), 98 | follow_redirects=True, 99 | content_type="application/json", 100 | ) 101 | 102 | test_user_additional_info_data = UserExtensionModel.query.filter_by( 103 | user_id=self.test_user_data.id 104 | ).first() 105 | self.assertEqual(test_user_additional_info_data.user_id, self.test_user_data.id) 106 | self.assertEqual( 107 | test_user_additional_info_data.is_organization_rep, 108 | self.correct_payload_additional_info["is_organization_rep"], 109 | ) 110 | self.assertEqual( 111 | test_user_additional_info_data.timezone.value, 112 | self.correct_payload_additional_info["timezone"], 113 | ) 114 | self.assertEqual( 115 | test_user_additional_info_data.additional_info["phone"], 116 | self.correct_payload_additional_info["phone"], 117 | ) 118 | self.assertEqual( 119 | test_user_additional_info_data.additional_info["mobile"], 120 | self.correct_payload_additional_info["mobile"], 121 | ) 122 | self.assertEqual( 123 | test_user_additional_info_data.additional_info["personal_website"], 124 | self.correct_payload_additional_info["personal_website"], 125 | ) 126 | self.assertEqual(response.json, success_message) 127 | self.assertEqual(response.status_code, success_code) 128 | 129 | def test_api_dao_create_user_additional_info_invalid_payload(self): 130 | error_message = messages.PHONE_OR_MOBILE_IS_NOT_IN_NUMBER_FORMAT 131 | error_code = HTTPStatus.BAD_REQUEST 132 | 133 | test_user_additional_info = { 134 | "is_organization_rep": True, 135 | "timezone": "Australia/Melbourne", 136 | "phone": "128abc", 137 | "mobile": "", 138 | "personal_website": "", 139 | } 140 | 141 | with self.client: 142 | response = self.client.put( 143 | "/user/additional_info", 144 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 145 | data=json.dumps(dict(test_user_additional_info)), 146 | follow_redirects=True, 147 | content_type="application/json", 148 | ) 149 | 150 | test_user_additional_info_data = UserExtensionModel.query.filter_by( 151 | user_id=self.test_user_data.id 152 | ).first() 153 | self.assertEqual(test_user_additional_info_data, None) 154 | self.assertEqual(response.json, error_message) 155 | self.assertEqual(response.status_code, error_code) 156 | 157 | 158 | if __name__ == "__main__": 159 | unittest.main() 160 | -------------------------------------------------------------------------------- /tests/users/test_api_get_other_user_details.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus, cookies 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from tests.base_test_case import BaseTestCase 11 | from app.api.request_api_utils import ( 12 | post_request, 13 | get_request, 14 | BASE_MS_API_URL, 15 | AUTH_COOKIE, 16 | ) 17 | from app.api.models.user import full_user_api_model, get_user_extension_response_model 18 | from tests.test_data import user1, user2, user3 19 | from app.database.models.ms_schema.user import UserModel 20 | from app.api.models.user import public_user_personal_details_response_model 21 | 22 | 23 | class TestGetOtherUserPersonalDetailsApi(BaseTestCase): 24 | @patch("requests.get") 25 | @patch("requests.post") 26 | def setUp(self, mock_login, mock_get_user): 27 | super(TestGetOtherUserPersonalDetailsApi, self).setUp() 28 | 29 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 30 | access_expiry = time.time() + 60 * 60 * 24 * 28 31 | success_message = { 32 | "access_token": "this is fake token", 33 | "access_expiry": access_expiry, 34 | } 35 | success_code = HTTPStatus.OK 36 | 37 | mock_login_response = Mock() 38 | mock_login_response.json.return_value = success_message 39 | mock_login_response.status_code = success_code 40 | mock_login.return_value = mock_login_response 41 | mock_login.raise_for_status = json.dumps(success_code) 42 | 43 | expected_user = marshal(user1, full_user_api_model) 44 | 45 | mock_get_response = Mock() 46 | mock_get_response.json.return_value = expected_user 47 | mock_get_response.status_code = success_code 48 | 49 | mock_get_user.return_value = mock_get_response 50 | mock_get_user.raise_for_status = json.dumps(success_code) 51 | 52 | user_login_success = { 53 | "username": user1.get("username"), 54 | "password": user1.get("password"), 55 | } 56 | 57 | with self.client: 58 | login_response = self.client.post( 59 | "/login", 60 | data=json.dumps(user_login_success), 61 | follow_redirects=True, 62 | content_type="application/json", 63 | ) 64 | 65 | test_user1 = UserModel( 66 | name=user1["name"], 67 | username=user1["username"], 68 | password=user1["password"], 69 | email=user1["email"], 70 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 71 | ) 72 | test_user1.need_mentoring = user1["need_mentoring"] 73 | test_user1.available_to_mentor = user1["available_to_mentor"] 74 | 75 | test_user1.save_to_db() 76 | test_user1_data = UserModel.find_by_email(test_user1.email) 77 | AUTH_COOKIE["user"] = marshal(test_user1_data, full_user_api_model) 78 | 79 | @patch("requests.get") 80 | def test_api_other_user_personal_details_with_correct_token(self, mock_get_users): 81 | expected_response = { 82 | "id": 2, 83 | "username": "usertest", 84 | "name": "User test", 85 | "slack_username": "Just any slack name", 86 | "bio": "Just any bio", 87 | "location": "Just any location", 88 | "occupation": "Just any occupation", 89 | "current_organization": "Just any organization", 90 | "interests": "Just any interests", 91 | "skills": "Just any skills", 92 | "need_mentoring": True, 93 | "available_to_mentor": True, 94 | "is_available": True, 95 | } 96 | success_code = HTTPStatus.OK 97 | 98 | mock_get_response = Mock() 99 | mock_get_response.json.return_value = expected_response 100 | mock_get_response.status_code = success_code 101 | 102 | mock_get_users.return_value = mock_get_response 103 | mock_get_users.raise_for_status = json.dumps(success_code) 104 | 105 | with self.client: 106 | get_response = self.client.get( 107 | "/users/2", 108 | headers={ 109 | "Authorization": AUTH_COOKIE["Authorization"].value, 110 | "Accept": "application/json", 111 | }, 112 | follow_redirects=True, 113 | ) 114 | 115 | mock_get_users.assert_called() 116 | self.assertEqual(get_response.json, expected_response) 117 | self.assertEqual(get_response.status_code, success_code) 118 | 119 | @patch("requests.get") 120 | def test_api_other_user_personal_details_with_token_expired(self, mock_get_users): 121 | error_message = messages.TOKEN_HAS_EXPIRED 122 | error_code = HTTPStatus.UNAUTHORIZED 123 | 124 | mock_response = Mock() 125 | mock_error = Mock() 126 | http_error = requests.exceptions.HTTPError() 127 | mock_response.raise_for_status.side_effect = http_error 128 | mock_get_users.return_value = mock_response 129 | mock_error.json.return_value = error_message 130 | mock_error.status_code = error_code 131 | mock_get_users.side_effect = requests.exceptions.HTTPError(response=mock_error) 132 | 133 | with self.client: 134 | get_response = self.client.get( 135 | "/users", 136 | headers={ 137 | "Authorization": AUTH_COOKIE["Authorization"].value, 138 | "Accept": "application/json", 139 | }, 140 | follow_redirects=True, 141 | ) 142 | mock_get_users.assert_called() 143 | self.assertEqual(get_response.json, error_message) 144 | self.assertEqual(get_response.status_code, error_code) 145 | -------------------------------------------------------------------------------- /tests/users/test_api_get_user_additional_info.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import time 3 | import unittest 4 | from http import HTTPStatus, cookies 5 | from unittest.mock import patch, Mock 6 | import requests 7 | from requests.exceptions import HTTPError 8 | from flask import json 9 | from flask_restx import marshal 10 | from app.database.sqlalchemy_extension import db 11 | from app import messages 12 | from tests.base_test_case import BaseTestCase 13 | from app.api.request_api_utils import ( 14 | post_request, 15 | get_request, 16 | BASE_MS_API_URL, 17 | AUTH_COOKIE, 18 | ) 19 | from app.api.models.user import full_user_api_model, get_user_extension_response_model 20 | from tests.test_data import user1 21 | from app.database.models.ms_schema.user import UserModel 22 | from app.database.models.bit_schema.user_extension import UserExtensionModel 23 | 24 | 25 | class TestGetUserAdditionalInfoApi(BaseTestCase): 26 | @patch("requests.get") 27 | @patch("requests.post") 28 | def setUp(self, mock_login, mock_get_user): 29 | super(TestGetUserAdditionalInfoApi, self).setUp() 30 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 31 | access_expiry = time.time() + 60 * 60 * 24 * 28 32 | success_message = { 33 | "access_token": "this is fake token", 34 | "access_expiry": access_expiry, 35 | } 36 | success_code = HTTPStatus.OK 37 | 38 | mock_login_response = Mock() 39 | mock_login_response.json.return_value = success_message 40 | mock_login_response.status_code = success_code 41 | mock_login.return_value = mock_login_response 42 | mock_login.raise_for_status = json.dumps(success_code) 43 | 44 | expected_user = marshal(user1, full_user_api_model) 45 | 46 | mock_get_response = Mock() 47 | mock_get_response.json.return_value = expected_user 48 | mock_get_response.status_code = success_code 49 | 50 | mock_get_user.return_value = mock_get_response 51 | mock_get_user.raise_for_status = json.dumps(success_code) 52 | 53 | user_login_success = { 54 | "username": user1.get("username"), 55 | "password": user1.get("password"), 56 | } 57 | 58 | with self.client: 59 | login_response = self.client.post( 60 | "/login", 61 | data=json.dumps(user_login_success), 62 | follow_redirects=True, 63 | content_type="application/json", 64 | ) 65 | 66 | test_user = UserModel( 67 | name=user1["name"], 68 | username=user1["username"], 69 | password=user1["password"], 70 | email=user1["email"], 71 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 72 | ) 73 | test_user.need_mentoring = user1["need_mentoring"] 74 | test_user.available_to_mentor = user1["available_to_mentor"] 75 | 76 | db.session.add(test_user) 77 | db.session.commit() 78 | 79 | self.test_user_data = UserModel.find_by_email(test_user.email) 80 | 81 | AUTH_COOKIE["user"] = marshal(self.test_user_data, full_user_api_model) 82 | 83 | self.response_additional_info = { 84 | "user_id": self.test_user_data.id, 85 | "is_organization_rep": True, 86 | "timezone": "Australia/Melbourne", 87 | "phone": "123-456-789", 88 | "mobile": "", 89 | "personal_website": "", 90 | } 91 | 92 | def test_api_dao_get_user_additional_info_successfully(self): 93 | success_message = self.response_additional_info 94 | success_code = HTTPStatus.OK 95 | 96 | # prepare existing additional info 97 | additional_info = { 98 | "phone": self.response_additional_info["phone"], 99 | "mobile": self.response_additional_info["mobile"], 100 | "personal_website": self.response_additional_info["personal_website"], 101 | } 102 | 103 | user_extension = UserExtensionModel( 104 | self.response_additional_info["user_id"], 105 | "AUSTRALIA_MELBOURNE", 106 | ) 107 | user_extension.is_organization_rep = self.response_additional_info[ 108 | "is_organization_rep" 109 | ] 110 | user_extension.additional_info = additional_info 111 | 112 | db.session.add(user_extension) 113 | db.session.commit() 114 | 115 | with self.client: 116 | response = self.client.get( 117 | "/user/additional_info", 118 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 119 | follow_redirects=True, 120 | content_type="application/json", 121 | ) 122 | 123 | test_user_additional_info_data = UserExtensionModel.query.filter_by( 124 | user_id=self.test_user_data.id 125 | ).first() 126 | self.assertEqual( 127 | test_user_additional_info_data.user_id, response.json["user_id"] 128 | ) 129 | self.assertEqual( 130 | test_user_additional_info_data.is_organization_rep, 131 | response.json["is_organization_rep"], 132 | ) 133 | self.assertEqual( 134 | test_user_additional_info_data.timezone.value, response.json["timezone"] 135 | ) 136 | self.assertEqual( 137 | test_user_additional_info_data.additional_info["phone"], 138 | response.json["phone"], 139 | ) 140 | self.assertEqual( 141 | test_user_additional_info_data.additional_info["mobile"], 142 | response.json["mobile"], 143 | ) 144 | self.assertEqual( 145 | test_user_additional_info_data.additional_info["personal_website"], 146 | response.json["personal_website"], 147 | ) 148 | self.assertEqual(response.json, success_message) 149 | self.assertEqual(response.status_code, success_code) 150 | 151 | def test_api_dao_get_non_existence_additional_info(self): 152 | error_message = messages.ADDITIONAL_INFORMATION_DOES_NOT_EXIST 153 | error_code = HTTPStatus.NOT_FOUND 154 | 155 | with self.client: 156 | response = self.client.get( 157 | "/user/additional_info", 158 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 159 | follow_redirects=True, 160 | content_type="application/json", 161 | ) 162 | 163 | test_user_additional_info_data = UserExtensionModel.query.filter_by( 164 | user_id=self.test_user_data.id 165 | ).first() 166 | self.assertEqual(test_user_additional_info_data, None) 167 | self.assertEqual(response.json, error_message) 168 | self.assertEqual(response.status_code, error_code) 169 | 170 | 171 | if __name__ == "__main__": 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /tests/users/test_api_get_user_details.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import time 3 | import unittest 4 | from http import HTTPStatus, cookies 5 | from unittest.mock import patch, Mock 6 | import requests 7 | from requests.exceptions import HTTPError 8 | from flask import json 9 | from flask_restx import marshal 10 | from app import messages 11 | from tests.base_test_case import BaseTestCase 12 | from app.api.request_api_utils import post_request, BASE_MS_API_URL, AUTH_COOKIE 13 | from app.api.resources.users import MyProfilePersonalDetails 14 | from app.api.models.user import full_user_api_model 15 | from tests.test_data import user1 16 | 17 | 18 | class TestGetUserDetailsApi(BaseTestCase): 19 | @patch("requests.get") 20 | @patch("requests.post") 21 | def setUp(self, mock_login, mock_get_user): 22 | super(TestGetUserDetailsApi, self).setUp() 23 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 24 | access_expiry = time.time() + 60 * 60 * 24 * 28 25 | success_message = { 26 | "access_token": "this is fake token", 27 | "access_expiry": access_expiry, 28 | } 29 | success_code = HTTPStatus.OK 30 | 31 | mock_login_response = Mock() 32 | mock_login_response.json.return_value = success_message 33 | mock_login_response.status_code = success_code 34 | mock_login.return_value = mock_login_response 35 | mock_login.raise_for_status = json.dumps(success_code) 36 | 37 | expected_user = marshal(user1, full_user_api_model) 38 | 39 | mock_get_response = Mock() 40 | mock_get_response.json.return_value = expected_user 41 | mock_get_response.status_code = success_code 42 | 43 | mock_get_user.return_value = mock_get_response 44 | mock_get_user.raise_for_status = json.dumps(success_code) 45 | 46 | user_login_success = { 47 | "username": user1.get("username"), 48 | "password": user1.get("password"), 49 | } 50 | 51 | with self.client: 52 | login_response = self.client.post( 53 | "/login", 54 | data=json.dumps(user_login_success), 55 | follow_redirects=True, 56 | content_type="application/json", 57 | ) 58 | 59 | def test_api_get_user_details_with_correct_token(self): 60 | 61 | user_json = AUTH_COOKIE["user"].value 62 | user = ast.literal_eval(user_json) 63 | success_code = HTTPStatus.OK 64 | 65 | with self.client: 66 | get_response = self.client.get( 67 | "/user/personal_details", 68 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 69 | follow_redirects=True, 70 | ) 71 | 72 | self.assertEqual(get_response.json, user) 73 | self.assertEqual(get_response.status_code, success_code) 74 | 75 | @patch("requests.get") 76 | def test_api_get_user_details_with_incorrect_token(self, mock_get_user): 77 | error_message = messages.TOKEN_IS_INVALID 78 | error_code = HTTPStatus.UNAUTHORIZED 79 | 80 | with self.client: 81 | get_response = self.client.get( 82 | "/user/personal_details", 83 | headers={"Authorization": "Bearer Token incorrect"}, 84 | follow_redirects=True, 85 | ) 86 | 87 | self.assertEqual(get_response.json, error_message) 88 | self.assertEqual(get_response.status_code, error_code) 89 | 90 | 91 | if __name__ == "__main__": 92 | unittest.main() 93 | -------------------------------------------------------------------------------- /tests/users/test_api_get_users_personal_details.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus, cookies 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from tests.base_test_case import BaseTestCase 11 | from app.api.request_api_utils import ( 12 | post_request, 13 | get_request, 14 | BASE_MS_API_URL, 15 | AUTH_COOKIE, 16 | ) 17 | from app.api.models.user import full_user_api_model, get_user_extension_response_model 18 | from tests.test_data import user1, user2, user3 19 | from app.database.models.ms_schema.user import UserModel 20 | from app.api.models.user import public_user_personal_details_response_model 21 | 22 | 23 | class TestGetUsersListPersonalDetailsApi(BaseTestCase): 24 | @patch("requests.get") 25 | @patch("requests.post") 26 | def setUp(self, mock_login, mock_get_user): 27 | super(TestGetUsersListPersonalDetailsApi, self).setUp() 28 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 29 | access_expiry = time.time() + 60 * 60 * 24 * 28 30 | success_message = { 31 | "access_token": "this is fake token", 32 | "access_expiry": access_expiry, 33 | } 34 | success_code = HTTPStatus.OK 35 | 36 | mock_login_response = Mock() 37 | mock_login_response.json.return_value = success_message 38 | mock_login_response.status_code = success_code 39 | mock_login.return_value = mock_login_response 40 | mock_login.raise_for_status = json.dumps(success_code) 41 | 42 | expected_user = marshal(user1, full_user_api_model) 43 | 44 | mock_get_response = Mock() 45 | mock_get_response.json.return_value = expected_user 46 | mock_get_response.status_code = success_code 47 | 48 | mock_get_user.return_value = mock_get_response 49 | mock_get_user.raise_for_status = json.dumps(success_code) 50 | 51 | user_login_success = { 52 | "username": user1.get("username"), 53 | "password": user1.get("password"), 54 | } 55 | 56 | with self.client: 57 | login_response = self.client.post( 58 | "/login", 59 | data=json.dumps(user_login_success), 60 | follow_redirects=True, 61 | content_type="application/json", 62 | ) 63 | 64 | test_user1 = UserModel( 65 | name=user1["name"], 66 | username=user1["username"], 67 | password=user1["password"], 68 | email=user1["email"], 69 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 70 | ) 71 | test_user1.need_mentoring = user1["need_mentoring"] 72 | test_user1.available_to_mentor = user1["available_to_mentor"] 73 | 74 | test_user1.save_to_db() 75 | test_user1_data = UserModel.find_by_email(test_user1.email) 76 | AUTH_COOKIE["user"] = marshal(test_user1_data, full_user_api_model) 77 | 78 | @patch("requests.get") 79 | def test_api_list_users_personal_details_with_correct_token(self, mock_get_users): 80 | test_user2 = UserModel( 81 | name=user2["name"], 82 | username=user2["username"], 83 | password=user2["password"], 84 | email=user2["email"], 85 | terms_and_conditions_checked=user2["terms_and_conditions_checked"], 86 | ) 87 | test_user2.need_mentoring = user2["need_mentoring"] 88 | test_user2.available_to_mentor = user2["available_to_mentor"] 89 | test_user2.is_email_verified = True 90 | 91 | test_user3 = UserModel( 92 | name=user3["name"], 93 | username=user3["username"], 94 | password=user3["password"], 95 | email=user3["email"], 96 | terms_and_conditions_checked=user3["terms_and_conditions_checked"], 97 | ) 98 | test_user3.need_mentoring = user3["need_mentoring"] 99 | test_user3.available_to_mentor = user3["available_to_mentor"] 100 | test_user3.is_email_verified = True 101 | 102 | test_user2.save_to_db() 103 | test_user3.save_to_db() 104 | 105 | test_user2_data = UserModel.find_by_email(test_user2.email) 106 | test_user3_data = UserModel.find_by_email(test_user3.email) 107 | 108 | expected_list = [ 109 | marshal(test_user2_data, public_user_personal_details_response_model), 110 | marshal(test_user3_data, public_user_personal_details_response_model), 111 | ] 112 | success_code = HTTPStatus.OK 113 | 114 | mock_get_response = Mock() 115 | mock_get_response.json.return_value = expected_list 116 | mock_get_response.status_code = success_code 117 | 118 | mock_get_users.return_value = mock_get_response 119 | mock_get_users.raise_for_status = json.dumps(success_code) 120 | 121 | with self.client: 122 | get_response = self.client.get( 123 | "/users", 124 | headers={ 125 | "Authorization": AUTH_COOKIE["Authorization"].value, 126 | "search": "", 127 | "page": None, 128 | "per_page": None, 129 | "Accept": "application/json", 130 | }, 131 | follow_redirects=True, 132 | ) 133 | 134 | mock_get_users.assert_called() 135 | self.assertEqual(get_response.json, expected_list) 136 | self.assertEqual(get_response.status_code, success_code) 137 | 138 | @patch("requests.get") 139 | def test_api_list_users_personal_details_with_token_expired(self, mock_get_users): 140 | error_message = messages.TOKEN_HAS_EXPIRED 141 | error_code = HTTPStatus.UNAUTHORIZED 142 | 143 | mock_response = Mock() 144 | mock_error = Mock() 145 | http_error = requests.exceptions.HTTPError() 146 | mock_response.raise_for_status.side_effect = http_error 147 | mock_get_users.return_value = mock_response 148 | mock_error.json.return_value = error_message 149 | mock_error.status_code = error_code 150 | mock_get_users.side_effect = requests.exceptions.HTTPError(response=mock_error) 151 | 152 | with self.client: 153 | get_response = self.client.get( 154 | "/users", 155 | headers={ 156 | "Authorization": AUTH_COOKIE["Authorization"].value, 157 | "search": "", 158 | "page": None, 159 | "per_page": None, 160 | "Accept": "application/json", 161 | }, 162 | follow_redirects=True, 163 | ) 164 | 165 | mock_get_users.assert_called() 166 | self.assertEqual(get_response.json, error_message) 167 | self.assertEqual(get_response.status_code, error_code) 168 | -------------------------------------------------------------------------------- /tests/users/test_api_login.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from tests.base_test_case import BaseTestCase 11 | from app.api.request_api_utils import post_request, BASE_MS_API_URL 12 | from app.api.resources.users import LoginUser 13 | from app.api.models.user import full_user_api_model 14 | from app.database.models.ms_schema.user import UserModel 15 | from app.database.models.bit_schema.user_extension import UserExtensionModel 16 | from tests.test_data import user1 17 | 18 | 19 | class TestUserLoginApi(BaseTestCase): 20 | def setUp(self): 21 | super(TestUserLoginApi, self).setUp() 22 | 23 | test_user1 = UserModel( 24 | name=user1["name"], 25 | username=user1["username"], 26 | password=user1["password"], 27 | email=user1["email"], 28 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 29 | ) 30 | test_user1.need_mentoring = user1["need_mentoring"] 31 | test_user1.available_to_mentor = user1["available_to_mentor"] 32 | 33 | test_user1.save_to_db() 34 | self.test_user1_data = UserModel.find_by_email(test_user1.email) 35 | self.expected_user = marshal(self.test_user1_data, full_user_api_model) 36 | 37 | @patch("requests.get") 38 | @patch("requests.post") 39 | def test_api_login_successful(self, mock_login, mock_get_user): 40 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 41 | access_expiry = time.time() + 60 * 60 * 24 * 28 42 | success_message = { 43 | "access_token": "this is fake token", 44 | "access_expiry": access_expiry, 45 | } 46 | success_code = HTTPStatus.OK 47 | 48 | mock_response = Mock() 49 | mock_response.json.return_value = success_message 50 | mock_response.status_code = success_code 51 | mock_login.return_value = mock_response 52 | mock_login.raise_for_status = json.dumps(success_code) 53 | 54 | user_login_success = { 55 | "username": user1.get("username"), 56 | "password": user1.get("password"), 57 | } 58 | 59 | expected_user = marshal(user1, full_user_api_model) 60 | 61 | mock_get_response = Mock() 62 | mock_get_response.json.return_value = expected_user 63 | mock_get_response.status_code = success_code 64 | 65 | mock_get_user.return_value = mock_get_response 66 | mock_get_user.raise_for_status = json.dumps(success_code) 67 | 68 | with self.client: 69 | response = self.client.post( 70 | "/login", 71 | data=json.dumps(user_login_success), 72 | follow_redirects=True, 73 | content_type="application/json", 74 | ) 75 | 76 | mock_login.assert_called() 77 | self.assertEqual(response.json, success_message) 78 | self.assertEqual(response.status_code, success_code) 79 | 80 | @patch("requests.post") 81 | def test_api_wrong_password(self, mock_login): 82 | error_message = messages.WRONG_USERNAME_OR_PASSWORD 83 | error_code = HTTPStatus.UNAUTHORIZED 84 | 85 | mock_response = Mock() 86 | mock_error = Mock() 87 | http_error = requests.exceptions.HTTPError() 88 | mock_response.raise_for_status.side_effect = http_error 89 | mock_login.return_value = mock_response 90 | mock_error.json.return_value = error_message 91 | mock_error.status_code = error_code 92 | mock_login.side_effect = requests.exceptions.HTTPError(response=mock_error) 93 | 94 | user_wrong_data = { 95 | "username": "user_wrong_data", 96 | "password": "user_wrong_data", 97 | } 98 | 99 | with self.client: 100 | response = self.client.post( 101 | "/login", 102 | data=json.dumps(user_wrong_data), 103 | follow_redirects=True, 104 | content_type="application/json", 105 | ) 106 | 107 | mock_login.assert_called() 108 | self.assertEqual(response.json, error_message) 109 | self.assertEqual(response.status_code, error_code) 110 | 111 | @patch("requests.post") 112 | def test_api_internal_server_error(self, mock_login): 113 | error_message = messages.INTERNAL_SERVER_ERROR 114 | error_code = HTTPStatus.INTERNAL_SERVER_ERROR 115 | 116 | mock_response = Mock() 117 | mock_error = Mock() 118 | http_error = requests.exceptions.HTTPError() 119 | mock_response.raise_for_status.side_effect = http_error 120 | mock_login.return_value = mock_response 121 | mock_error.json.return_value = error_message 122 | mock_error.status_code = error_code 123 | mock_login.side_effect = requests.exceptions.HTTPError(response=mock_error) 124 | 125 | user_server_error = { 126 | "username": "user_server_error", 127 | "password": "user_server_error", 128 | } 129 | with self.client: 130 | response = self.client.post( 131 | "/login", 132 | data=json.dumps(user_server_error), 133 | follow_redirects=True, 134 | content_type="application/json", 135 | ) 136 | 137 | mock_login.assert_called() 138 | self.assertEqual(response.json, error_message) 139 | self.assertEqual(response.status_code, error_code) 140 | 141 | 142 | if __name__ == "__main__": 143 | unittest.main() 144 | -------------------------------------------------------------------------------- /tests/users/test_api_update_user_additional_info.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus, cookies 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from tests.base_test_case import BaseTestCase 11 | from app.api.request_api_utils import ( 12 | post_request, 13 | get_request, 14 | BASE_MS_API_URL, 15 | AUTH_COOKIE, 16 | ) 17 | from app.api.models.user import full_user_api_model, get_user_extension_response_model 18 | from tests.test_data import user1 19 | from app.database.models.ms_schema.user import UserModel 20 | from app.database.models.bit_schema.user_extension import UserExtensionModel 21 | 22 | 23 | class TestUpdateUserAdditionalInfoApi(BaseTestCase): 24 | @patch("requests.get") 25 | @patch("requests.post") 26 | def setUp(self, mock_login, mock_get_user): 27 | super(TestUpdateUserAdditionalInfoApi, self).setUp() 28 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 29 | access_expiry = time.time() + 60 * 60 * 24 * 28 30 | success_message = { 31 | "access_token": "this is fake token", 32 | "access_expiry": access_expiry, 33 | } 34 | success_code = HTTPStatus.OK 35 | 36 | mock_login_response = Mock() 37 | mock_login_response.json.return_value = success_message 38 | mock_login_response.status_code = success_code 39 | mock_login.return_value = mock_login_response 40 | mock_login.raise_for_status = json.dumps(success_code) 41 | 42 | expected_user = marshal(user1, full_user_api_model) 43 | 44 | mock_get_response = Mock() 45 | mock_get_response.json.return_value = expected_user 46 | mock_get_response.status_code = success_code 47 | 48 | mock_get_user.return_value = mock_get_response 49 | mock_get_user.raise_for_status = json.dumps(success_code) 50 | 51 | user_login_success = { 52 | "username": user1.get("username"), 53 | "password": user1.get("password"), 54 | } 55 | 56 | with self.client: 57 | login_response = self.client.post( 58 | "/login", 59 | data=json.dumps(user_login_success), 60 | follow_redirects=True, 61 | content_type="application/json", 62 | ) 63 | 64 | test_user = UserModel( 65 | name=user1["name"], 66 | username=user1["username"], 67 | password=user1["password"], 68 | email=user1["email"], 69 | terms_and_conditions_checked=user1["terms_and_conditions_checked"], 70 | ) 71 | test_user.need_mentoring = user1["need_mentoring"] 72 | test_user.available_to_mentor = user1["available_to_mentor"] 73 | 74 | test_user.save_to_db() 75 | 76 | self.test_user_data = UserModel.find_by_email(test_user.email) 77 | 78 | AUTH_COOKIE["user"] = marshal(self.test_user_data, full_user_api_model) 79 | 80 | self.correct_payload_update_additional_info = { 81 | "is_organization_rep": True, 82 | "timezone": "Australia/Melbourne", 83 | "phone": "123-456-789", 84 | "mobile": "", 85 | "personal_website": "", 86 | } 87 | 88 | def test_api_dao_update_user_additional_info_successfully(self): 89 | success_message = messages.ADDITIONAL_INFO_SUCCESSFULLY_UPDATED 90 | success_code = HTTPStatus.OK 91 | 92 | # prepare existing additional info 93 | additional_info = {"phone": "123-456-456", "mobile": "", "personal_website": ""} 94 | 95 | user_extension = UserExtensionModel( 96 | user_id=self.test_user_data.id, timezone="EUROPE_KIEV" 97 | ) 98 | user_extension.is_organization_rep = False 99 | user_extension.additional_info = additional_info 100 | 101 | user_extension.save_to_db() 102 | 103 | with self.client: 104 | response = self.client.put( 105 | "/user/additional_info", 106 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 107 | data=json.dumps(dict(self.correct_payload_update_additional_info)), 108 | follow_redirects=True, 109 | content_type="application/json", 110 | ) 111 | 112 | test_user_additional_info_data = UserExtensionModel.query.filter_by( 113 | user_id=self.test_user_data.id 114 | ).first() 115 | self.assertEqual( 116 | test_user_additional_info_data.is_organization_rep, 117 | self.correct_payload_update_additional_info["is_organization_rep"], 118 | ) 119 | self.assertEqual( 120 | test_user_additional_info_data.timezone.value, 121 | self.correct_payload_update_additional_info["timezone"], 122 | ) 123 | self.assertEqual( 124 | test_user_additional_info_data.additional_info["phone"], 125 | self.correct_payload_update_additional_info["phone"], 126 | ) 127 | self.assertEqual( 128 | test_user_additional_info_data.additional_info["mobile"], 129 | self.correct_payload_update_additional_info["mobile"], 130 | ) 131 | self.assertEqual( 132 | test_user_additional_info_data.additional_info["personal_website"], 133 | self.correct_payload_update_additional_info["personal_website"], 134 | ) 135 | self.assertEqual(response.json, success_message) 136 | self.assertEqual(response.status_code, success_code) 137 | 138 | def test_api_dao_update_additional_info_with_invalid_timezone(self): 139 | error_message = messages.TIMEZONE_INPUT_IS_INVALID 140 | error_code = HTTPStatus.BAD_REQUEST 141 | 142 | # prepare existing additional info 143 | additional_info = {"phone": "123-456-456", "mobile": "", "personal_website": ""} 144 | 145 | user_extension = UserExtensionModel( 146 | user_id=self.test_user_data.id, timezone="EUROPE_KIEV" 147 | ) 148 | user_extension.is_organization_rep = False 149 | user_extension.additional_info = additional_info 150 | 151 | user_extension.save_to_db() 152 | 153 | invalid_timezone_update_additional_info = { 154 | "is_organization_rep": True, 155 | "timezone": "some random timezone", 156 | "phone": "123-456-789", 157 | "mobile": "", 158 | "personal_website": "", 159 | } 160 | 161 | with self.client: 162 | response = self.client.put( 163 | "/user/additional_info", 164 | headers={"Authorization": AUTH_COOKIE["Authorization"].value}, 165 | data=json.dumps(dict(invalid_timezone_update_additional_info)), 166 | follow_redirects=True, 167 | content_type="application/json", 168 | ) 169 | 170 | self.assertEqual(response.json, error_message) 171 | self.assertEqual(response.status_code, error_code) 172 | 173 | 174 | if __name__ == "__main__": 175 | unittest.main() 176 | -------------------------------------------------------------------------------- /tests/users/test_api_update_user_details.py: -------------------------------------------------------------------------------- 1 | import time 2 | import unittest 3 | from http import HTTPStatus, cookies 4 | from unittest.mock import patch, Mock 5 | import requests 6 | from requests.exceptions import HTTPError 7 | from flask import json 8 | from flask_restx import marshal 9 | from app import messages 10 | from tests.base_test_case import BaseTestCase 11 | from app.api.request_api_utils import post_request, BASE_MS_API_URL, AUTH_COOKIE 12 | from app.api.resources.users import MyProfilePersonalDetails 13 | from app.api.models.user import update_user_details_request_body_model 14 | from tests.test_data import user1 15 | 16 | 17 | class TestUpdateUserDetailsApi(BaseTestCase): 18 | @patch("requests.post") 19 | def setUp(self, mock_login): 20 | super(TestUpdateUserDetailsApi, self).setUp() 21 | # set access expiry 4 weeks from today's date (sc*min*hrrs*days) 22 | access_expiry = time.time() + 60 * 60 * 24 * 28 23 | success_login_message = { 24 | "access_token": "this is fake token", 25 | "access_expiry": access_expiry, 26 | } 27 | success_login_code = HTTPStatus.OK 28 | 29 | mock_login_response = Mock() 30 | mock_login_response.json.return_value = success_login_message 31 | mock_login_response.status_code = success_login_code 32 | mock_login.return_value = mock_login_response 33 | mock_login.raise_for_status = json.dumps(success_login_code) 34 | 35 | user_login_success = { 36 | "username": user1.get("username"), 37 | "password": user1.get("password"), 38 | } 39 | 40 | with self.client: 41 | login_response = self.client.post( 42 | "/login", 43 | data=json.dumps(user_login_success), 44 | follow_redirects=True, 45 | content_type="application/json", 46 | ) 47 | 48 | self.access_token_header = "Bearer this is fake token" 49 | self.user_new_details = { 50 | "name": "test update", 51 | "username": "testupdate", 52 | "bio": "string", 53 | "location": "string", 54 | "occupation": "string", 55 | "current_organization": "string", 56 | "slack_username": "string", 57 | "social_media_links": "string", 58 | "skills": "string", 59 | "interests": "string", 60 | "resume_url": "string", 61 | "photo_url": "string", 62 | "need_mentoring": True, 63 | "available_to_mentor": True, 64 | } 65 | 66 | @patch("requests.put") 67 | def test_api_update_user_details_with_correct_payload(self, mock_update_user): 68 | expected_put_message = messages.USER_SUCCESSFULLY_UPDATED 69 | expected_put_code = HTTPStatus.OK 70 | 71 | mock_update_response = Mock() 72 | mock_update_response.json.return_value = expected_put_message 73 | mock_update_response.status_code = expected_put_code 74 | 75 | mock_update_user.return_value = mock_update_response 76 | mock_update_user.raise_for_status = json.dumps(expected_put_code) 77 | 78 | with self.client: 79 | put_response = self.client.put( 80 | "/user/personal_details", 81 | data=json.dumps(self.user_new_details), 82 | content_type="application/json", 83 | headers={"Authorization": self.access_token_header}, 84 | follow_redirects=True, 85 | ) 86 | 87 | mock_update_user.assert_called() 88 | self.assertEqual(put_response.json, expected_put_message) 89 | self.assertEqual(put_response.status_code, expected_put_code) 90 | 91 | @patch("requests.put") 92 | def test_api_update_user_details_with_new_username_invalid(self, mock_update_user): 93 | user_invalid_details = { 94 | "name": "test update", 95 | "username": "testupdate?", 96 | "bio": "string", 97 | "location": "string", 98 | "occupation": "string", 99 | "current_organization": "string", 100 | "slack_username": "string", 101 | "social_media_links": "string", 102 | "skills": "string", 103 | "interests": "string", 104 | "resume_url": "string", 105 | "photo_url": "string", 106 | "need_mentoring": True, 107 | "available_to_mentor": True, 108 | } 109 | 110 | error_message = messages.NEW_USERNAME_INPUT_BY_USER_IS_INVALID 111 | error_code = HTTPStatus.BAD_REQUEST 112 | 113 | mock_response = Mock() 114 | mock_error = Mock() 115 | http_error = requests.exceptions.HTTPError() 116 | mock_response.raise_for_status.side_effect = http_error 117 | mock_update_user.return_value = mock_response 118 | mock_error.json.return_value = error_message 119 | mock_error.status_code = error_code 120 | mock_update_user.side_effect = requests.exceptions.HTTPError( 121 | response=mock_error 122 | ) 123 | 124 | with self.client: 125 | put_response = self.client.put( 126 | "/user/personal_details", 127 | data=json.dumps(user_invalid_details), 128 | content_type="application/json", 129 | headers={"Authorization": self.access_token_header}, 130 | follow_redirects=True, 131 | ) 132 | 133 | mock_update_user.assert_not_called() 134 | self.assertEqual(put_response.json, error_message) 135 | self.assertEqual(put_response.status_code, error_code) 136 | 137 | @patch("requests.put") 138 | def test_api_update_user_details_with_internal_server_error(self, mock_update_user): 139 | 140 | error_message = messages.INTERNAL_SERVER_ERROR 141 | error_code = HTTPStatus.INTERNAL_SERVER_ERROR 142 | 143 | mock_response = Mock() 144 | mock_error = Mock() 145 | http_error = requests.exceptions.HTTPError() 146 | mock_response.raise_for_status.side_effect = http_error 147 | mock_update_user.return_value = mock_response 148 | mock_error.json.return_value = error_message 149 | mock_error.status_code = error_code 150 | mock_update_user.side_effect = requests.exceptions.HTTPError( 151 | response=mock_error 152 | ) 153 | 154 | with self.client: 155 | put_response = self.client.put( 156 | "/user/personal_details", 157 | data=json.dumps(self.user_new_details), 158 | content_type="application/json", 159 | headers={"Authorization": self.access_token_header}, 160 | follow_redirects=True, 161 | ) 162 | 163 | mock_update_user.assert_called() 164 | self.assertEqual(put_response.json, error_message) 165 | self.assertEqual(put_response.status_code, error_code) 166 | 167 | 168 | if __name__ == "__main__": 169 | unittest.main() 170 | --------------------------------------------------------------------------------