From 5f37641039ffee5b01b170c88ed2795ddd760132 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 18 Dec 2023 10:25:09 -0500 Subject: [PATCH 01/17] added create_partners endpoint --- backend/dto/__init__.py | 1 + backend/dto/user/invite_user.py | 15 +++++ backend/routes/partners.py | 110 ++++++++++++++++++++++++++------ 3 files changed, 105 insertions(+), 21 deletions(-) create mode 100644 backend/dto/user/invite_user.py diff --git a/backend/dto/__init__.py b/backend/dto/__init__.py index c11852e1..c2152b28 100644 --- a/backend/dto/__init__.py +++ b/backend/dto/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa: F401 from .user.register_user import RegisterUserDTO from .user.login_user import LoginUserDTO +from .user.invite_user import InviteUserDTO diff --git a/backend/dto/user/invite_user.py b/backend/dto/user/invite_user.py new file mode 100644 index 00000000..5acdb1a9 --- /dev/null +++ b/backend/dto/user/invite_user.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel, EmailStr +from typing import Optional +from enum import Enum + +class MemberRole(str, Enum): + ADMIN = "Administrator" + PUBLISHER = "Publisher" + MEMBER = "Member" + SUBSCRIBER = "Subscriber" + + +class InviteUserDTO(BaseModel): + email: EmailStr + role: MemberRole + diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 72058e98..f208b358 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -1,11 +1,17 @@ from backend.auth.jwt import min_role_required from backend.mixpanel.mix import track_to_mp from backend.database.models.user import User, UserRole -from flask import Blueprint, abort, current_app, request +from flask import Blueprint, abort, current_app, request,jsonify from flask_jwt_extended import get_jwt from flask_jwt_extended.view_decorators import jwt_required - from ..database import Partner, PartnerMember, MemberRole, db +from ..dto import InviteUserDTO +from flask_mail import Message + + + + + from ..schemas import ( CreatePartnerSchema, AddMemberSchema, @@ -33,32 +39,90 @@ def get_partners(partner_id: int): @jwt_required() @min_role_required(UserRole.PUBLIC) @validate(json=CreatePartnerSchema) -def create_partner(): +def create(): """Create a contributing partner. Cannot be called in production environments """ - if current_app.env == "production": - abort(418) + body = request.context.json + # if current_app.env == "production": + # abort(418) + + if body.name is not None and body.url is not None and body.contact_email is not None and body.name!="" and body.url!="" and body.contact_email!="": + + """ + check if instance already is in the db + """ + try: + partner = partner_to_orm(request.context.json) + except Exception: + abort(400) + partner_query_email= Partner.query.filter_by(contact_email=body.contact_email).first() + partner_query_url= Partner.query.filter_by(url=body.url).first() + + if partner_query_email: + if partner_query_email.contact_email==partner.contact_email: + return { + "status": "error", + "message":"Error. Entered email or url details matches existing record.", + + },400 + if partner_query_url: + if partner_query_url.url==partner.url: + return { + "status": "error", + "message":"Error. Entered email or url details matches existing record.", + + },400 + + + + """ + add to database if all fields are present, and instance not already in db. + """ + + + created = partner.create() + resp = jsonify( + { + "status": "ok", + "message": "Successfully registered.", + "item": partner_orm_to_json(created), + } + ) + + + make_admin = PartnerMember( + partner_id=created.id, + user_id=get_jwt()["sub"], + role=MemberRole.ADMIN, + ) + make_admin.create() + + track_to_mp(request, "create_partner", { + "partner_name": partner.name, + "partner_contact": partner.contact_email + }) + return resp,200 + else: + + """ + missing values/fields in the form + """ + required_keys=["name","url","contact_email"] + return { + "status": "error", + "message": "Failed to create partner. Please include all of the following" + " fields: " + ", ".join(required_keys), + }, 400 + + + + + - try: - partner = partner_to_orm(request.context.json) - except Exception: - abort(400) - created = partner.create() - make_admin = PartnerMember( - partner_id=created.id, - user_id=get_jwt()["sub"], - role=MemberRole.ADMIN, - ) - make_admin.create() - track_to_mp(request, "create_partner", { - "partner_name": partner.name, - "partner_contact": partner.contact_email - }) - return partner_orm_to_json(created) @bp.route("/", methods=["GET"]) @@ -193,3 +257,7 @@ def add_member_to_partner(partner_id: int): "role": partner_member.role, }) return partner_member_orm_to_json(created) + + + + \ No newline at end of file From d12efe90067e94822135e9d644cf59f87acb15b5 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Wed, 20 Dec 2023 23:47:26 -0500 Subject: [PATCH 02/17] flask-mail edits --- backend/api.py | 1 + backend/config.py | 41 +++++++--- backend/routes/partners.py | 164 +++++++++++++++++++++---------------- 3 files changed, 126 insertions(+), 80 deletions(-) diff --git a/backend/api.py b/backend/api.py index 22766fbe..2ae8aad2 100644 --- a/backend/api.py +++ b/backend/api.py @@ -41,6 +41,7 @@ def register_extensions(app: Flask): jwt.init_app(app) Mail(app) CORS(app, resources={r"/api/*": {"origins": "*"}}) + def register_commands(app: Flask): diff --git a/backend/config.py b/backend/config.py index cffc281b..da4e5c0c 100644 --- a/backend/config.py +++ b/backend/config.py @@ -23,16 +23,36 @@ class Config(object): POSTGRES_DB = os.environ.get("POSTGRES_DB", "police_data") # Flask-Mail SMTP server settings - MAIL_SERVER = os.environ.get("MAIL_SERVER") - MAIL_PORT = os.environ.get("MAIL_PORT") - MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) - MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) - MAIL_USERNAME = os.environ.get("MAIL_USERNAME") - MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") - MAIL_DEFAULT_SENDER = os.environ.get( - "MAIL_DEFAULT_SENDER", - "National Police Data Coalition <{email}>".format(email=MAIL_USERNAME), - ) + # MAIL_SERVER = os.environ.get("MAIL_SERVER") + # MAIL_PORT = os.environ.get("MAIL_PORT") + # MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) + # MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) + # MAIL_USERNAME = os.environ.get("MAIL_USERNAME") + # MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") + # MAIL_DEFAULT_SENDER = os.environ.get( + # "MAIL_DEFAULT_SENDER", + # "National Police Data Coalition <{email}>".format(email=MAIL_USERNAME), + # ) + + # MAIL_SERVER = os.environ.get("MAIL_SERVER") + # MAIL_PORT = os.environ.get("MAIL_PORT") + # MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) + # MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) + # MAIL_USERNAME = os.environ.get("MAIL_USERNAME") + # MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") + # MAIL_DEFAULT_SENDER = os.environ.get( + # "MAIL_DEFAULT_SENDER", + # "National Police Data Coalition <{email}>".format(email=MAIL_USERNAME), + # ) + + #Testing configurations with Mailtrap Email testing, all the configurations will be different--go to mailtrap for more information + MAIL_SERVER='sandbox.smtp.mailtrap.io' + MAIL_PORT = 2525 + MAIL_USERNAME = '30a682ceaa0416' + MAIL_PASSWORD = 'dbf502527604b1' + MAIL_USE_TLS = True + MAIL_USE_SSL = False + # Flask-User settings USER_APP_NAME = ( @@ -95,6 +115,7 @@ class TestingConfig(Config): SECRET_KEY = "my-secret-key" JWT_SECRET_KEY = "my-jwt-secret-key" MIXPANEL_TOKEN = "mixpanel-token" + def get_config_from_env(env: str) -> Config: diff --git a/backend/routes/partners.py b/backend/routes/partners.py index f208b358..8b8474cc 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -7,6 +7,8 @@ from ..database import Partner, PartnerMember, MemberRole, db from ..dto import InviteUserDTO from flask_mail import Message +from ..config import TestingConfig + @@ -44,78 +46,27 @@ def create(): Cannot be called in production environments """ - body = request.context.json - # if current_app.env == "production": - # abort(418) - - if body.name is not None and body.url is not None and body.contact_email is not None and body.name!="" and body.url!="" and body.contact_email!="": - - """ - check if instance already is in the db - """ - try: - partner = partner_to_orm(request.context.json) - except Exception: - abort(400) - partner_query_email= Partner.query.filter_by(contact_email=body.contact_email).first() - partner_query_url= Partner.query.filter_by(url=body.url).first() - - if partner_query_email: - if partner_query_email.contact_email==partner.contact_email: - return { - "status": "error", - "message":"Error. Entered email or url details matches existing record.", - - },400 - if partner_query_url: - if partner_query_url.url==partner.url: - return { - "status": "error", - "message":"Error. Entered email or url details matches existing record.", - - },400 - - - - """ - add to database if all fields are present, and instance not already in db. - """ - - - created = partner.create() - resp = jsonify( - { - "status": "ok", - "message": "Successfully registered.", - "item": partner_orm_to_json(created), - } - ) + if current_app.env == "production": + abort(418) - - make_admin = PartnerMember( - partner_id=created.id, - user_id=get_jwt()["sub"], - role=MemberRole.ADMIN, - ) - make_admin.create() - - track_to_mp(request, "create_partner", { - "partner_name": partner.name, - "partner_contact": partner.contact_email - }) - return resp,200 - else: + try: + partner = partner_to_orm(request.context.json) + except Exception: + abort(400) - """ - missing values/fields in the form - """ - required_keys=["name","url","contact_email"] - return { - "status": "error", - "message": "Failed to create partner. Please include all of the following" - " fields: " + ", ".join(required_keys), - }, 400 + created = partner.create() + make_admin = PartnerMember( + partner_id=created.id, + user_id=get_jwt()["sub"], + role=MemberRole.ADMIN, + ) + make_admin.create() + track_to_mp(request, "create_partner", { + "partner_name": partner.name, + "partner_contact": partner.contact_email + }) + return partner_orm_to_json(created) @@ -260,4 +211,77 @@ def add_member_to_partner(partner_id: int): - \ No newline at end of file + + + +@bp.route("/invite",methods=["POST"]) +@jwt_required() +@min_role_required(MemberRole.ADMIN) +@validate(auth=True,json=InviteUserDTO) +def invite_user(): + + """ + Testing scenarios + + 1) Should not work for user that is not an Admin + 2) Should work for someone who is an Admin of an organization + 3) If a user is invited to an organization, message should be appropriate + 4) If a user is not invited to an organization, message should be appropriate + + After TODO + + 1) Check if the invitations are being added to the correct tables. + + + + """ + + + + + body: InviteUserDTO = request.context.json + mail = current_app.extensions.get('mail') + + + + user = User.query.filter_by(email=body.email).first() + + if user is not None: + + #TODO:handle logic to add user's to the partner member table + try: + msg = Message("Invitation to join NPDC partner organization!", sender=TestingConfig.MAIL_USERNAME, recipients=['paul@mailtrap.io']) + msg.body = "You are a registered user of NPDC and were invited to a partner organization. Please log on to accept or decline the invitation." + mail.send(msg) + return { + "status": "ok", + "message": "User notified of their invitation through email!" + }, 200 + + except: + return { + "status":"error", + "message":"Something went wrong! Please try again!" + },500 + + + + else: + try: + msg = Message("Invitation to join NPDC index!",sender=TestingConfig.MAIL_USERNAME , + recipients=['paul@mailtrap.io']) + msg.body = ("You are not a registered user of NPDC and were invited to a partner organization. Please register with NPDC index.") + mail.send(msg) + + return { + "status": "ok", + "message": "User is not registered with the NPDC index. Email sent to user notifying them to register." + }, 200 + + except: + return { + "status":"error", + "message":"Something went wrong! Please try again!" + },500 + + From 65246094ce8f4e96ce45a9273a28621efd995c01 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Sat, 23 Dec 2023 13:39:35 -0500 Subject: [PATCH 03/17] invite-user edits, getting weird error while trying to insert/query from new defined models --- backend/database/models/partner.py | 16 +++++ backend/database/models/user.py | 4 ++ backend/dto/user/invite_user.py | 2 + backend/routes/auth.py | 12 ++++ backend/routes/partners.py | 102 ++++++++++++++++++++++++++--- backend/schemas.py | 2 + 6 files changed, 129 insertions(+), 9 deletions(-) diff --git a/backend/database/models/partner.py b/backend/database/models/partner.py index 24eccc6b..3914b136 100644 --- a/backend/database/models/partner.py +++ b/backend/database/models/partner.py @@ -23,6 +23,19 @@ def get_value(self): else: return 5 +class Invitation(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + partner_id = db.Column(db.Integer, db.ForeignKey('partner.id'),primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'),primary_key=True) + role = db.Column(db.Enum(MemberRole), nullable=False) + is_accepted = db.Column(db.Boolean, default=False) # default to not accepted invite + + +class StagedInvitation(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + partner_id = db.Column(db.Integer, db.ForeignKey('partner.id'),primary_key=True) + email = db.Column(db.String,unique=True,primary_key=True) + role = db.Column(db.Enum(MemberRole), nullable=False) class PartnerMember(db.Model, CrudMixin): __tablename__ = "partner_user" @@ -62,3 +75,6 @@ class Partner(db.Model, CrudMixin): def __repr__(self): """Represent instance as a unique string.""" return f"" + + + \ No newline at end of file diff --git a/backend/database/models/user.py b/backend/database/models/user.py index ab72c569..6e91f687 100644 --- a/backend/database/models/user.py +++ b/backend/database/models/user.py @@ -57,6 +57,8 @@ def get_value(self): return 4 + + # Define the User data-model. class User(db.Model, UserMixin, CrudMixin): """The SQL dataclass for an Incident.""" @@ -96,3 +98,5 @@ def verify_password(self, pw): def get_by_email(email): return User.query.filter(User.email == email).first() + + diff --git a/backend/dto/user/invite_user.py b/backend/dto/user/invite_user.py index 5acdb1a9..28315da8 100644 --- a/backend/dto/user/invite_user.py +++ b/backend/dto/user/invite_user.py @@ -10,6 +10,8 @@ class MemberRole(str, Enum): class InviteUserDTO(BaseModel): + partner_id: int email: EmailStr role: MemberRole + diff --git a/backend/routes/auth.py b/backend/routes/auth.py index f3e505b5..0e15e141 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -87,6 +87,18 @@ def register(): db.session.add(user) db.session.commit() token = create_access_token(identity=user.id) + + """ + code to handle adding staged_invitations-->invitations for users who have just signed up for NPDC + """ + staged_invite = StagedInvitation.query.filter_by(email=user.email).all() + if staged_invite is not None and len(staged_invite)>0: + for instance in staged_invite: + new_invitation = Invitation(user_id=user.id,role=instance.role,partner_id=instance.partner_id) + db.session.add(new_invitation) + db.session.commit() + StagedInvitation.query.filter_by(email=user.email).delete() + resp = jsonify( { "status": "ok", diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 8b8474cc..0e3019da 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -4,7 +4,7 @@ from flask import Blueprint, abort, current_app, request,jsonify from flask_jwt_extended import get_jwt from flask_jwt_extended.view_decorators import jwt_required -from ..database import Partner, PartnerMember, MemberRole, db +from ..database import Partner, PartnerMember, MemberRole, db, Invitation, StagedInvitation from ..dto import InviteUserDTO from flask_mail import Message from ..config import TestingConfig @@ -216,21 +216,23 @@ def add_member_to_partner(partner_id: int): @bp.route("/invite",methods=["POST"]) @jwt_required() -@min_role_required(MemberRole.ADMIN) +@min_role_required(UserRole.PUBLIC) @validate(auth=True,json=InviteUserDTO) def invite_user(): + + #TODO : Only Admins of an organization can invite """ Testing scenarios 1) Should not work for user that is not an Admin 2) Should work for someone who is an Admin of an organization - 3) If a user is invited to an organization, message should be appropriate - 4) If a user is not invited to an organization, message should be appropriate - After TODO - 1) Check if the invitations are being added to the correct tables. + 3) (TESTED) If a user already exists and is invited to an organization, message should be appropriate + 4) (TESTED) If a user does not exist and is not invited to an organization, message should be appropriate + + 5)Make sure all db changes Invitations, and Staged Invitations are happening as expected @@ -246,10 +248,15 @@ def invite_user(): user = User.query.filter_by(email=body.email).first() + # if user is already registered with NPDC, add them to Invitations Table, and send out an email notification if user is not None: + - #TODO:handle logic to add user's to the partner member table - try: + try: + new_invitation= Invitation(partner_id=body.partner_id, user_id=user.id,role=body.role) + db.session.add(new_invitation) + db.session.commit() + msg = Message("Invitation to join NPDC partner organization!", sender=TestingConfig.MAIL_USERNAME, recipients=['paul@mailtrap.io']) msg.body = "You are a registered user of NPDC and were invited to a partner organization. Please log on to accept or decline the invitation." mail.send(msg) @@ -264,10 +271,30 @@ def invite_user(): "message":"Something went wrong! Please try again!" },500 - + + # new_invitation= Invitation(partner_id=body.partner_id, user_id=user.id,role=body.role) + # # new_invitation.create() + # db.session.add(new_invitation) + # db.session.commit() + + # msg = Message("Invitation to join NPDC partner organization!", sender=TestingConfig.MAIL_USERNAME, recipients=['paul@mailtrap.io']) + # msg.body = "You are a registered user of NPDC and were invited to a partner organization. Please log on to accept or decline the invitation." + # mail.send(msg) + # return { + # "status": "ok", + # "message": "User notified of their invitation through email!" + # }, 200 + + + + #if user not registered with NPDC, add the invitation for them in StagedInvitations Table, and send out an email notification else: try: + + new_staged_invite = StagedInvitation(partner_id=body.partner_id,email=body.email,role=body.role) + db.session.add(new_staged_invite) + db.session.commit() msg = Message("Invitation to join NPDC index!",sender=TestingConfig.MAIL_USERNAME , recipients=['paul@mailtrap.io']) msg.body = ("You are not a registered user of NPDC and were invited to a partner organization. Please register with NPDC index.") @@ -284,4 +311,61 @@ def invite_user(): "message":"Something went wrong! Please try again!" },500 + + + # new_staged_invite = StagedInvitation(partner_id=body.partner_id,email=body.email,role=body.role) + + # db.session.add(new_staged_invite) + # db.session.commit() + # msg = Message("Invitation to join NPDC index!",sender=TestingConfig.MAIL_USERNAME , + # recipients=['paul@mailtrap.io']) + # msg.body = ("You are not a registered user of NPDC and were invited to a partner organization. Please register with NPDC index.") + # mail.send(msg) + + # return { + # "status": "ok", + # "message": "User is not registered with the NPDC index. Email sent to user notifying them to register." + # }, 200 + + + + +@bp.route("/invitations",methods=["GET"]) +@jwt_required() +@validate() +#only defined for testing environment +def get_invitations(): + if current_app.env == "production": + abort(418) + invitation_found = Invitation.query.filter_by(email="harsharauniyar1@gmail.com",partner_id=10) + + if invitation_found is not None: + return "Found in DB" + elif invitation_found is None: + return "Did not find in DB" + + +@bp.route("/stagedinvitations",methods=["GET"]) +@jwt_required() +@validate() +#only defined for testing environment +def stagedinvitations(): + if current_app.env == "production": + abort(418) + staged_invitations = StagedInvitation.query.all() + + + invitations_data = [ + { + 'id': staged_invitation.id, + 'email': staged_invitation.email, + 'role': staged_invitation.role, + 'partner_id':staged_invitation.partner_id, + } + for staged_invitation in staged_invitations + ] + + return jsonify({'staged_invitations': invitations_data}) + + diff --git a/backend/schemas.py b/backend/schemas.py index b7f1a97e..e31dfc70 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -163,6 +163,7 @@ def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: CreateLegalCaseSchema = schema_create(LegalCase) + class CreateIncidentSchema(_BaseCreateIncidentSchema, _IncidentMixin): victims: Optional[List[CreateVictimSchema]] perpetrators: Optional[List[CreatePerpetratorSchema]] @@ -211,6 +212,7 @@ def schema_get(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: LegalCaseSchema = schema_get(LegalCase) + class IncidentSchema(_BaseIncidentSchema, _IncidentMixin): victims: List[VictimSchema] perpetrators: List[PerpetratorSchema] From 744b4ec386303fc817807c78502b65d8ac986f1c Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Sat, 20 Jan 2024 12:52:17 -0500 Subject: [PATCH 04/17] adding epic 2 apis --- backend/api.py | 1 - backend/config.py | 57 ++--- backend/database/models/partner.py | 37 ++- backend/database/models/user.py | 4 - backend/dto/user/invite_user.py | 4 +- backend/routes/auth.py | 13 +- backend/routes/partners.py | 365 +++++++++++++++++++---------- backend/schemas.py | 2 - 8 files changed, 301 insertions(+), 182 deletions(-) diff --git a/backend/api.py b/backend/api.py index 2ae8aad2..22766fbe 100644 --- a/backend/api.py +++ b/backend/api.py @@ -41,7 +41,6 @@ def register_extensions(app: Flask): jwt.init_app(app) Mail(app) CORS(app, resources={r"/api/*": {"origins": "*"}}) - def register_commands(app: Flask): diff --git a/backend/config.py b/backend/config.py index da4e5c0c..9fd87de2 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,7 +1,6 @@ import os from dotenv import load_dotenv from datetime import timedelta - if os.environ.get("FLASK_ENV") != "production": load_dotenv() @@ -23,36 +22,33 @@ class Config(object): POSTGRES_DB = os.environ.get("POSTGRES_DB", "police_data") # Flask-Mail SMTP server settings - # MAIL_SERVER = os.environ.get("MAIL_SERVER") - # MAIL_PORT = os.environ.get("MAIL_PORT") - # MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) - # MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) - # MAIL_USERNAME = os.environ.get("MAIL_USERNAME") - # MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") - # MAIL_DEFAULT_SENDER = os.environ.get( - # "MAIL_DEFAULT_SENDER", - # "National Police Data Coalition <{email}>".format(email=MAIL_USERNAME), - # ) - - # MAIL_SERVER = os.environ.get("MAIL_SERVER") - # MAIL_PORT = os.environ.get("MAIL_PORT") - # MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) - # MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) - # MAIL_USERNAME = os.environ.get("MAIL_USERNAME") - # MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") - # MAIL_DEFAULT_SENDER = os.environ.get( - # "MAIL_DEFAULT_SENDER", - # "National Police Data Coalition <{email}>".format(email=MAIL_USERNAME), - # ) - - #Testing configurations with Mailtrap Email testing, all the configurations will be different--go to mailtrap for more information - MAIL_SERVER='sandbox.smtp.mailtrap.io' - MAIL_PORT = 2525 - MAIL_USERNAME = '30a682ceaa0416' - MAIL_PASSWORD = 'dbf502527604b1' - MAIL_USE_TLS = True - MAIL_USE_SSL = False + """ + Config settings for email sending + Put all of the information in your dotenv + config file + """ + MAIL_SERVER = os.environ.get("MAIL_SERVER") + MAIL_PORT = os.environ.get("MAIL_PORT") + MAIL_USE_SSL = bool(os.environ.get("MAIL_USE_SSL")) + MAIL_USE_TLS = bool(os.environ.get("MAIL_USER_TLS")) + MAIL_USERNAME = os.environ.get("MAIL_USERNAME") + MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") + MAIL_DEFAULT_SENDER = os.environ.get( + "MAIL_DEFAULT_SENDER", + "National Police Data Coalition <{email}>".format( + email=MAIL_USERNAME), + ) + """ + Testing configurations with Mailtrap Email testing, all the configurations + will be different--go to mailtrap for more information + """ + # MAIL_SERVER = 'sandbox.smtp.mailtrap.io' + # MAIL_PORT = 2525 + # MAIL_USERNAME = '30a682ceaa0416' + # MAIL_PASSWORD = 'dbf502527604b1' + # MAIL_USE_TLS = True + # MAIL_USE_SSL = False # Flask-User settings USER_APP_NAME = ( @@ -115,7 +111,6 @@ class TestingConfig(Config): SECRET_KEY = "my-secret-key" JWT_SECRET_KEY = "my-jwt-secret-key" MIXPANEL_TOKEN = "mixpanel-token" - def get_config_from_env(env: str) -> Config: diff --git a/backend/database/models/partner.py b/backend/database/models/partner.py index 3914b136..144cb497 100644 --- a/backend/database/models/partner.py +++ b/backend/database/models/partner.py @@ -23,20 +23,42 @@ def get_value(self): else: return 5 + class Invitation(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) - partner_id = db.Column(db.Integer, db.ForeignKey('partner.id'),primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('user.id'),primary_key=True) + partner_id = db.Column( + db.Integer, db.ForeignKey('partner.id'), primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) role = db.Column(db.Enum(MemberRole), nullable=False) - is_accepted = db.Column(db.Boolean, default=False) # default to not accepted invite - + is_accepted = db.Column(db.Boolean, default=False) + # default to not accepted invite + + def serialize(self): + return { + 'id': self.id, + 'partner_id': self.partner_id, + 'user_id': self.user_id, + 'role': self.role, + 'is_accepted': self.is_accepted, + } + class StagedInvitation(db.Model): id = db.Column(db.Integer, primary_key=True, autoincrement=True) - partner_id = db.Column(db.Integer, db.ForeignKey('partner.id'),primary_key=True) - email = db.Column(db.String,unique=True,primary_key=True) + partner_id = db.Column( + db.Integer, db.ForeignKey('partner.id'), primary_key=True) + email = db.Column(db.String, unique=True, primary_key=True) role = db.Column(db.Enum(MemberRole), nullable=False) + def serialize(self): + return { + 'id': self.id, + 'partner_id': self.partner_id, + 'email': self.email, + 'role': self.role + } + + class PartnerMember(db.Model, CrudMixin): __tablename__ = "partner_user" id = db.Column(db.Integer, primary_key=True, autoincrement=True) @@ -75,6 +97,3 @@ class Partner(db.Model, CrudMixin): def __repr__(self): """Represent instance as a unique string.""" return f"" - - - \ No newline at end of file diff --git a/backend/database/models/user.py b/backend/database/models/user.py index 6e91f687..ab72c569 100644 --- a/backend/database/models/user.py +++ b/backend/database/models/user.py @@ -57,8 +57,6 @@ def get_value(self): return 4 - - # Define the User data-model. class User(db.Model, UserMixin, CrudMixin): """The SQL dataclass for an Incident.""" @@ -98,5 +96,3 @@ def verify_password(self, pw): def get_by_email(email): return User.query.filter(User.email == email).first() - - diff --git a/backend/dto/user/invite_user.py b/backend/dto/user/invite_user.py index 28315da8..14acabec 100644 --- a/backend/dto/user/invite_user.py +++ b/backend/dto/user/invite_user.py @@ -1,7 +1,7 @@ from pydantic import BaseModel, EmailStr -from typing import Optional from enum import Enum + class MemberRole(str, Enum): ADMIN = "Administrator" PUBLISHER = "Publisher" @@ -13,5 +13,3 @@ class InviteUserDTO(BaseModel): partner_id: int email: EmailStr role: MemberRole - - diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 0e15e141..d4ddb844 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -10,7 +10,7 @@ from pydantic.main import BaseModel from ..auth import min_role_required, user_manager from ..mixpanel.mix import track_to_mp -from ..database import User, UserRole, db +from ..database import User, UserRole, db, Invitation, StagedInvitation from ..dto import LoginUserDTO, RegisterUserDTO from ..schemas import UserSchema, validate @@ -89,15 +89,20 @@ def register(): token = create_access_token(identity=user.id) """ - code to handle adding staged_invitations-->invitations for users who have just signed up for NPDC + code to handle adding staged_invitations-->invitations for users + who have just signed up for NPDC """ staged_invite = StagedInvitation.query.filter_by(email=user.email).all() - if staged_invite is not None and len(staged_invite)>0: + if staged_invite is not None and len(staged_invite) > 0: for instance in staged_invite: - new_invitation = Invitation(user_id=user.id,role=instance.role,partner_id=instance.partner_id) + new_invitation = Invitation( + user_id=user.id, + role=instance.role, + partner_id=instance.partner_id) db.session.add(new_invitation) db.session.commit() StagedInvitation.query.filter_by(email=user.email).delete() + db.session.commit() resp = jsonify( { diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 0e3019da..6159187b 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -1,19 +1,21 @@ +from datetime import datetime from backend.auth.jwt import min_role_required from backend.mixpanel.mix import track_to_mp from backend.database.models.user import User, UserRole -from flask import Blueprint, abort, current_app, request,jsonify +from flask import Blueprint, abort, current_app, request, jsonify from flask_jwt_extended import get_jwt from flask_jwt_extended.view_decorators import jwt_required -from ..database import Partner, PartnerMember, MemberRole, db, Invitation, StagedInvitation +from ..database import ( + Partner, + PartnerMember, + MemberRole, + db, + Invitation, + StagedInvitation, +) from ..dto import InviteUserDTO from flask_mail import Message from ..config import TestingConfig - - - - - - from ..schemas import ( CreatePartnerSchema, AddMemberSchema, @@ -41,7 +43,7 @@ def get_partners(partner_id: int): @jwt_required() @min_role_required(UserRole.PUBLIC) @validate(json=CreatePartnerSchema) -def create(): +def create_partner(): """Create a contributing partner. Cannot be called in production environments @@ -68,13 +70,6 @@ def create(): }) return partner_orm_to_json(created) - - - - - - - @bp.route("/", methods=["GET"]) @jwt_required() @@ -210,162 +205,276 @@ def add_member_to_partner(partner_id: int): return partner_member_orm_to_json(created) +# user can join org they were invited to +@bp.route("/join", methods=["POST"]) +@jwt_required() +@min_role_required(UserRole.PUBLIC) +def join_organization(): + try: + body = request.get_json() + user_exists = PartnerMember.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"]).first() + if user_exists: + return { + "status" : "Error", + "message": "User already in the organization" + }, 400 + else: + new_member = PartnerMember( + user_id=body["user_id"], + partner_id=body["partner_id"], + role=body["role"], + date_joined=datetime.now(), + is_active=True + ) + db.session.add(new_member) + db.session.commit() + Invitation.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"]).delete() + db.session.commit() + return { + "status": "ok", + "message": "Successfully joined partner organization" + } , 200 + except Exception: + db.session.rollback() + return { + "status": "Error", + "message": "Something went wrong!" + }, 500 + finally: + db.session.close() - - -@bp.route("/invite",methods=["POST"]) +# user can leave org they already joined +@bp.route("/leave", methods=["DELETE"]) @jwt_required() @min_role_required(UserRole.PUBLIC) -@validate(auth=True,json=InviteUserDTO) -def invite_user(): - - - #TODO : Only Admins of an organization can invite +def leave_organization(): """ - Testing scenarios - - 1) Should not work for user that is not an Admin - 2) Should work for someone who is an Admin of an organization - - - 3) (TESTED) If a user already exists and is invited to an organization, message should be appropriate - 4) (TESTED) If a user does not exist and is not invited to an organization, message should be appropriate - - 5)Make sure all db changes Invitations, and Staged Invitations are happening as expected - - - + remove from PartnerMember table """ + try: + body = request.get_json() + result = PartnerMember.query.filter_by( + user_id=body["user_id"], partner_id=body["partner_id"]).delete() + db.session.commit() + if result > 0: + return { + "status": "ok", + "message": "Succesfully left organization" + }, 200 + else: + return { + "status": "Error", + "message": "Not a member of this organization" + }, 400 + except Exception: + db.session.rollback() + return { + "status": "Error", + "message": "Something went wrong!" + } + finally: + db.session.close() - - +# inviting anyone to NPDC +@bp.route("/invite", methods=["POST"]) +@jwt_required() +@min_role_required(MemberRole.ADMIN) +@validate(auth=True, json=InviteUserDTO) +def invite_user(): body: InviteUserDTO = request.context.json mail = current_app.extensions.get('mail') - - - user = User.query.filter_by(email=body.email).first() - - # if user is already registered with NPDC, add them to Invitations Table, and send out an email notification if user is not None: - - - try: - new_invitation= Invitation(partner_id=body.partner_id, user_id=user.id,role=body.role) - db.session.add(new_invitation) - db.session.commit() - - msg = Message("Invitation to join NPDC partner organization!", sender=TestingConfig.MAIL_USERNAME, recipients=['paul@mailtrap.io']) - msg.body = "You are a registered user of NPDC and were invited to a partner organization. Please log on to accept or decline the invitation." - mail.send(msg) + invitation_exists = Invitation.query.filter_by( + partner_id=body.partner_id, user_id=user.id).first() + if invitation_exists: return { - "status": "ok", - "message": "User notified of their invitation through email!" - }, 200 - - except: - return { - "status":"error", - "message":"Something went wrong! Please try again!" - },500 - - - # new_invitation= Invitation(partner_id=body.partner_id, user_id=user.id,role=body.role) - # # new_invitation.create() - # db.session.add(new_invitation) - # db.session.commit() - - # msg = Message("Invitation to join NPDC partner organization!", sender=TestingConfig.MAIL_USERNAME, recipients=['paul@mailtrap.io']) - # msg.body = "You are a registered user of NPDC and were invited to a partner organization. Please log on to accept or decline the invitation." - # mail.send(msg) - # return { - # "status": "ok", - # "message": "User notified of their invitation through email!" - # }, 200 - - - - - #if user not registered with NPDC, add the invitation for them in StagedInvitations Table, and send out an email notification + "status": "error", + "message": "Invitation already sent to this user!" + }, 500 + else: + try: + new_invitation = Invitation( + partner_id=body.partner_id, user_id=user.id, role=body.role) + db.session.add(new_invitation) + db.session.commit() + + msg = Message("Invitation to join NPDC partner organization!", + sender=TestingConfig.MAIL_USERNAME, + recipients=[body.email]) + msg.body = """You are a registered user of NPDC and were invited + to a partner organization. Please log on to accept or decline + the invitation at https://dev.nationalpolicedata.org/.""" + mail.send(msg) + return { + "status": "ok", + "message": "User notified of their invitation!" + }, 200 + + except Exception: + return { + "status": "error", + "message": "Something went wrong! Please try again!" + }, 500 else: try: - new_staged_invite = StagedInvitation(partner_id=body.partner_id,email=body.email,role=body.role) + new_staged_invite = StagedInvitation( + partner_id=body.partner_id, email=body.email, role=body.role) db.session.add(new_staged_invite) db.session.commit() - msg = Message("Invitation to join NPDC index!",sender=TestingConfig.MAIL_USERNAME , - recipients=['paul@mailtrap.io']) - msg.body = ("You are not a registered user of NPDC and were invited to a partner organization. Please register with NPDC index.") + msg = Message("Invitation to join NPDC index!", + sender=TestingConfig.MAIL_USERNAME, + recipients=[body.email]) + msg.body = """You are not a registered user of NPDC and were + invited to a partner organization. Please register + with NPDC index at + https://dev.nationalpolicedata.org/.""" mail.send(msg) return { "status": "ok", - "message": "User is not registered with the NPDC index. Email sent to user notifying them to register." + "message": """User is not registered with the NPDC index. + Email sent to user notifying them to register.""" }, 200 - - except: + + except Exception: return { - "status":"error", - "message":"Something went wrong! Please try again!" - },500 - - - - # new_staged_invite = StagedInvitation(partner_id=body.partner_id,email=body.email,role=body.role) - - # db.session.add(new_staged_invite) - # db.session.commit() - # msg = Message("Invitation to join NPDC index!",sender=TestingConfig.MAIL_USERNAME , - # recipients=['paul@mailtrap.io']) - # msg.body = ("You are not a registered user of NPDC and were invited to a partner organization. Please register with NPDC index.") - # mail.send(msg) - - # return { - # "status": "ok", - # "message": "User is not registered with the NPDC index. Email sent to user notifying them to register." - # }, 200 - - - - -@bp.route("/invitations",methods=["GET"]) + "status": "error", + "message": "Something went wrong! Please try again!" + }, 500 + + +# admin can remove any member from a partner organization +@bp.route("/remove_member", methods=['DELETE']) +@jwt_required() +@min_role_required(MemberRole.ADMIN) +def remove_member(): + body = request.get_json() + try: + user_found = PartnerMember.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"] + ).delete() + db.session.commit() + if user_found > 0: + return { + "status" : "ok", + "message" : "Member successfully deleted from Organization" + } , 200 + else: + return { + "status" : "Error", + "message" : "Member is not part of the Organization" + + } , 400 + except Exception as e: + db.session.rollback() + return str(e) + finally: + db.session.close() + + +# admin can withdraw invitations that have been sent out +@bp.route("/withdraw_invitation", methods=['DELETE']) +@jwt_required() +@min_role_required(MemberRole.ADMIN) +def withdraw_invitation(): + body = request.get_json() + try: + user_found = Invitation.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"] + ).delete() + db.session.commit() + if user_found > 0: + return { + "status" : "ok", + "message" : "Member's invitation withdrawn from Organization" + } , 200 + else: + return { + "status" : "Error", + "message" : "Member is not invited to the Organization" + + } , 400 + except Exception as e: + db.session.rollback() + return str(e) + finally: + db.session.close() + + +# admin can change roles of any user +@bp.route("/role_change", methods=["PATCH"]) +@jwt_required() +@min_role_required(MemberRole.ADMIN) +def role_change(): + body = request.get_json() + try: + user_found = PartnerMember.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"] + ).first() + if user_found: + user_found.role = body["role"] + db.session.commit() + return { + "status" : "ok", + "message" : "Role has been updated!" + }, 200 + else: + return { + "status" : "Error", + "message" : "User not found in this organization" + }, 400 + except Exception as e: + db.session.rollback + return str(e) + finally: + db.session.close() + + +# view invitations table +@bp.route("/invitations", methods=["GET"]) @jwt_required() @validate() -#only defined for testing environment +# only defined for testing environment def get_invitations(): if current_app.env == "production": abort(418) - invitation_found = Invitation.query.filter_by(email="harsharauniyar1@gmail.com",partner_id=10) + try: + all_records = Invitation.query.all() + records_list = [record.serialize() for record in all_records] + return jsonify(records_list) - if invitation_found is not None: - return "Found in DB" - elif invitation_found is None: - return "Did not find in DB" + except Exception as e: + return str(e) -@bp.route("/stagedinvitations",methods=["GET"]) +# view staged invitations table +@bp.route("/stagedinvitations", methods=["GET"]) @jwt_required() @validate() -#only defined for testing environment +# only defined for testing environment def stagedinvitations(): if current_app.env == "production": abort(418) staged_invitations = StagedInvitation.query.all() - - invitations_data = [ { 'id': staged_invitation.id, 'email': staged_invitation.email, - 'role': staged_invitation.role, - 'partner_id':staged_invitation.partner_id, + 'role': staged_invitation.role, + 'partner_id': staged_invitation.partner_id, } for staged_invitation in staged_invitations ] return jsonify({'staged_invitations': invitations_data}) - - - diff --git a/backend/schemas.py b/backend/schemas.py index e31dfc70..b7f1a97e 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -163,7 +163,6 @@ def schema_create(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: CreateLegalCaseSchema = schema_create(LegalCase) - class CreateIncidentSchema(_BaseCreateIncidentSchema, _IncidentMixin): victims: Optional[List[CreateVictimSchema]] perpetrators: Optional[List[CreatePerpetratorSchema]] @@ -212,7 +211,6 @@ def schema_get(model_type: DeclarativeMeta, **kwargs) -> ModelMetaclass: LegalCaseSchema = schema_get(LegalCase) - class IncidentSchema(_BaseIncidentSchema, _IncidentMixin): victims: List[VictimSchema] perpetrators: List[PerpetratorSchema] From 3b39569346cd9975bd246c8ab74aac0f4b9974dc Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 20:40:11 -0500 Subject: [PATCH 05/17] adding requested changes --- backend/routes/partners.py | 181 ++++++++++++++----------------------- montonic.py | 0 sys_design_notes | 1 + 3 files changed, 69 insertions(+), 113 deletions(-) create mode 100644 montonic.py create mode 100644 sys_design_notes diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 6159187b..09f8eb12 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -18,10 +18,8 @@ from ..config import TestingConfig from ..schemas import ( CreatePartnerSchema, - AddMemberSchema, partner_orm_to_json, partner_member_orm_to_json, - partner_member_to_orm, partner_to_orm, validate, ) @@ -146,66 +144,92 @@ class Config: } """ -@bp.route("//members/add", methods=["POST"]) +# inviting anyone to NPDC +@bp.route("/invite", methods=["POST"]) @jwt_required() -@min_role_required(UserRole.PUBLIC) -@validate(json=AddMemberSchema) -def add_member_to_partner(partner_id: int): - """Add a member to a partner. - - TODO: Allow the API to accept a user email instad of a user id - TODO: Use the partner ID from the API path instead of the request body - The `partner_member_to_orm` function seems very picky about the input. - I wasn't able to get it to accept a dict or a PartnerMember object. - - Cannot be called in production environments - """ - if current_app.env == "production": - abort(418) - - # Ensure that the user has premission to add a member to this partner. +@min_role_required(MemberRole.ADMIN) +@validate(auth=True, json=InviteUserDTO) +def add_member_to_partner(): + body: InviteUserDTO = request.context.json jwt_decoded = get_jwt() current_user = User.get(jwt_decoded["sub"]) association = db.session.query(PartnerMember).filter( PartnerMember.user_id == current_user.id, - PartnerMember.partner_id == partner_id, + PartnerMember.partner_id == body.partner_id, ).first() if ( association is None or not association.is_administrator() - or not association.partner_id == partner_id + or not association.partner_id == body.partner_id ): abort(403) + mail = current_app.extensions.get('mail') + user = User.query.filter_by(email=body.email).first() + if user is not None: + invitation_exists = Invitation.query.filter_by( + partner_id=body.partner_id, user_id=user.id).first() + if invitation_exists: + return { + "status": "error", + "message": "Invitation already sent to this user!" + }, 500 + else: + try: + new_invitation = Invitation( + partner_id=body.partner_id, user_id=user.id, role=body.role) + db.session.add(new_invitation) + db.session.commit() - # TODO: Allow the API to accept a user email instad of a user id - # user_obj = User.get_by_email(request.context.json.user_email) - # if user_obj is None: - # abort(400) - - # new_member = PartnerMember( - # partner_id=partner_id, - # user_id=user_obj.id, - # role=request.context.json.role, - # ) + msg = Message("Invitation to join NPDC partner organization!", + sender=TestingConfig.MAIL_USERNAME, + recipients=[body.email]) + msg.body = """You are a registered user of NPDC and were invited + to a partner organization. Please log on to accept or decline + the invitation at https://dev.nationalpolicedata.org/.""" + mail.send(msg) + return { + "status": "ok", + "message": "User notified of their invitation!" + }, 200 - try: - partner_member = partner_member_to_orm(request.context.json) - except Exception: - abort(400) + except Exception: + return { + "status": "error", + "message": "Something went wrong! Please try again!" + }, 500 + else: + try: - created = partner_member.create() + new_staged_invite = StagedInvitation( + partner_id=body.partner_id, email=body.email, role=body.role) + db.session.add(new_staged_invite) + db.session.commit() + msg = Message("Invitation to join NPDC index!", + sender=TestingConfig.MAIL_USERNAME, + recipients=[body.email]) + msg.body = """You are not a registered user of NPDC and were + invited to a partner organization. Please register + with NPDC index at + https://dev.nationalpolicedata.org/.""" + mail.send(msg) - track_to_mp(request, "add_partner_member", { - "partner_id": partner_id, - "user_id": partner_member.user_id, - "role": partner_member.role, - }) - return partner_member_orm_to_json(created) + return { + "status": "ok", + "message": """User is not registered with the NPDC index. + Email sent to user notifying them to register.""" + }, 200 + except Exception: + return { + "status": "error", + "message": "Something went wrong! Please try again!" + }, 500 # user can join org they were invited to + + @bp.route("/join", methods=["POST"]) @jwt_required() @min_role_required(UserRole.PUBLIC) @@ -280,78 +304,9 @@ def leave_organization(): finally: db.session.close() - -# inviting anyone to NPDC -@bp.route("/invite", methods=["POST"]) -@jwt_required() -@min_role_required(MemberRole.ADMIN) -@validate(auth=True, json=InviteUserDTO) -def invite_user(): - body: InviteUserDTO = request.context.json - mail = current_app.extensions.get('mail') - user = User.query.filter_by(email=body.email).first() - if user is not None: - invitation_exists = Invitation.query.filter_by( - partner_id=body.partner_id, user_id=user.id).first() - if invitation_exists: - return { - "status": "error", - "message": "Invitation already sent to this user!" - }, 500 - else: - try: - new_invitation = Invitation( - partner_id=body.partner_id, user_id=user.id, role=body.role) - db.session.add(new_invitation) - db.session.commit() - - msg = Message("Invitation to join NPDC partner organization!", - sender=TestingConfig.MAIL_USERNAME, - recipients=[body.email]) - msg.body = """You are a registered user of NPDC and were invited - to a partner organization. Please log on to accept or decline - the invitation at https://dev.nationalpolicedata.org/.""" - mail.send(msg) - return { - "status": "ok", - "message": "User notified of their invitation!" - }, 200 - - except Exception: - return { - "status": "error", - "message": "Something went wrong! Please try again!" - }, 500 - else: - try: - - new_staged_invite = StagedInvitation( - partner_id=body.partner_id, email=body.email, role=body.role) - db.session.add(new_staged_invite) - db.session.commit() - msg = Message("Invitation to join NPDC index!", - sender=TestingConfig.MAIL_USERNAME, - recipients=[body.email]) - msg.body = """You are not a registered user of NPDC and were - invited to a partner organization. Please register - with NPDC index at - https://dev.nationalpolicedata.org/.""" - mail.send(msg) - - return { - "status": "ok", - "message": """User is not registered with the NPDC index. - Email sent to user notifying them to register.""" - }, 200 - - except Exception: - return { - "status": "error", - "message": "Something went wrong! Please try again!" - }, 500 +# admin can remove any member from a partner organization -# admin can remove any member from a partner organization @bp.route("/remove_member", methods=['DELETE']) @jwt_required() @min_role_required(MemberRole.ADMIN) @@ -422,7 +377,7 @@ def role_change(): user_id=body["user_id"], partner_id=body["partner_id"] ).first() - if user_found: + if user_found and user_found.role != "Admin": user_found.role = body["role"] db.session.commit() return { diff --git a/montonic.py b/montonic.py new file mode 100644 index 00000000..e69de29b diff --git a/sys_design_notes b/sys_design_notes new file mode 100644 index 00000000..1d2f0149 --- /dev/null +++ b/sys_design_notes @@ -0,0 +1 @@ +r \ No newline at end of file From 89438aa4fa9dfa335a830ab93ea354ffa66feb33 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 20:41:29 -0500 Subject: [PATCH 06/17] adding changes --- montonic.py | 0 sys_design_notes | 1 - 2 files changed, 1 deletion(-) delete mode 100644 montonic.py delete mode 100644 sys_design_notes diff --git a/montonic.py b/montonic.py deleted file mode 100644 index e69de29b..00000000 diff --git a/sys_design_notes b/sys_design_notes deleted file mode 100644 index 1d2f0149..00000000 --- a/sys_design_notes +++ /dev/null @@ -1 +0,0 @@ -r \ No newline at end of file From 5fba3dcf71a7aa3afbdcadfa019818e1f32f1798 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 21:28:08 -0500 Subject: [PATCH 07/17] git conflicts edit --- backend/routes/partners.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 8d488aa9..8b3fe912 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -17,8 +17,6 @@ from ..dto import InviteUserDTO from flask_mail import Message from ..config import TestingConfig -from flask_sqlalchemy import Pagination - from ..schemas import ( CreatePartnerSchema, @@ -151,9 +149,9 @@ class Config: } } """ +# inviting anyone to NPDC -# inviting anyone to NPDC @bp.route("/invite", methods=["POST"]) @jwt_required() @min_role_required(MemberRole.ADMIN) @@ -167,8 +165,6 @@ def add_member_to_partner(): PartnerMember.user_id == current_user.id, PartnerMember.partner_id == body.partner_id, ).first() - - if ( association is None or not association.is_administrator() @@ -236,7 +232,6 @@ def add_member_to_partner(): "status": "error", "message": "Something went wrong! Please try again!" }, 500 - # user can join org they were invited to @@ -317,7 +312,6 @@ def leave_organization(): # admin can remove any member from a partner organization - @bp.route("/remove_member", methods=['DELETE']) @jwt_required() @min_role_required(MemberRole.ADMIN) @@ -444,4 +438,3 @@ def stagedinvitations(): ] return jsonify({'staged_invitations': invitations_data}) - From 9d90f989ed201491018416602d3cc2706ae65816 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 22:48:26 -0500 Subject: [PATCH 08/17] test edit --- backend/tests/test_partners.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 8634e607..a64fba1d 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -119,13 +119,12 @@ def example_members(client, db_session, example_partner, p_admin_access_token): req = { "partner_id": partner_obj.id, - "user_id": user_obj.id, + "email": user_obj.email, "role": mock["role"], - "is_active": mock["is_active"], } res = client.post( - f"/api/v1/partners/{partner_obj.id}/members/add", + "/api/v1/partners/invite", json=req, headers={ "Authorization": "Bearer {0}".format(p_admin_access_token) @@ -222,17 +221,21 @@ def test_partner_pagination(client, example_partners, access_token): def test_add_member_to_partner(db_session, example_members): - created = example_members["publisher"] - - partner_member_obj = ( - db_session.query(PartnerMember) - .filter(PartnerMember.id == created["id"]) - .first() - ) - - assert partner_member_obj.partner_id == created["partner_id"] - assert partner_member_obj.user_id == created["user_id"] - assert partner_member_obj.role == created["role"] + # created = example_members["publisher"] + + # partner_member_obj = ( + # db_session.query(PartnerMember) + # .filter(PartnerMember.id == created["id"]) + # .first() + # ) + + # assert partner_member_obj.partner_id == created["partner_id"] + # assert partner_member_obj.email == created["email"] + # assert partner_member_obj.role == created["role"] + """ + Write tests for inviting users/adding members to partners after + establishing permanent mail server + """ def test_get_partner_members( From d07dc99a02645e6ac404467af8202b7c683ecfee Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 22:53:11 -0500 Subject: [PATCH 09/17] test fix --- backend/tests/test_partners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index a64fba1d..9eb200a2 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -220,7 +220,7 @@ def test_partner_pagination(client, example_partners, access_token): assert res.status_code == 404 -def test_add_member_to_partner(db_session, example_members): +# def test_add_member_to_partner(db_session, example_members): # created = example_members["publisher"] # partner_member_obj = ( From e0e125678ac5df6a7b294e9f2710c25363584e5a Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 5 Feb 2024 23:14:04 -0500 Subject: [PATCH 10/17] test fixes --- backend/tests/test_partners.py | 48 +--------------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 9eb200a2..adbc7d35 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -2,7 +2,7 @@ from backend.auth import user_manager from backend.database import Partner, PartnerMember, MemberRole from backend.database.models.user import User, UserRole -from typing import Any + publisher_email = "pub@partner.com" inactive_email = "lurker@partner.com" @@ -276,49 +276,3 @@ def test_get_partner_members( assert res.status_code == 200 assert res.json["results"].__len__() == users.__len__() # assert res.json["results"][0]["user"]["email"] == member_obj.email - - -def test_get_partner_users( - client: Any, - example_partner: Partner, - example_members: PartnerMember, - access_token: str, -) -> None: - # Test that we can get partner users - res: Any = client.get( - f"/api/v1/partners/{example_partner.id}/users", - headers={"Authorization": "Bearer {0}".format(access_token)}, - ) - assert res.status_code == 200 - data = res.get_json() - - # Verify the response structure - assert "results" in data - assert "page" in data - assert "totalPages" in data - assert "totalResults" in data - - # Verify the results - assert len(data["results"]) == len(mock_users) + 1 - - # Verify the page number - assert data["page"] == 1 - - # Verify the total pages - assert data["totalPages"] == 1 - - # Verify the total results - assert data["totalResults"] == len(mock_users) + 1 - - -def test_get_partner_users_error( - client: Any, - access_token: str, -) -> None: - # Test that we can get partner users - res: Any = client.get( - f"/api/v1/partners/{1234}/users", - headers={"Authorization": "Bearer {0}".format(access_token)}, - ) - assert res.status_code == 404 - assert res.get_json()["message"] == "Partner not found" From 8e8ff12c75c73d315857499f91aa32745855a783 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Wed, 14 Feb 2024 17:17:51 -0500 Subject: [PATCH 11/17] added /join org endpoint tests --- backend/routes/partners.py | 71 ++++++++++++++++++++++- backend/tests/test_partners.py | 100 +++++++++++++++++++++++++++++++-- 2 files changed, 166 insertions(+), 5 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 8b3fe912..82dafe8e 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -24,6 +24,8 @@ partner_member_orm_to_json, partner_to_orm, validate, + AddMemberSchema, + partner_member_to_orm ) bp = Blueprint("partner_routes", __name__, url_prefix="/api/v1/partners") @@ -323,7 +325,7 @@ def remove_member(): partner_id=body["partner_id"] ).delete() db.session.commit() - if user_found > 0: + if user_found > 0 and user_found.role != "Admin": return { "status" : "ok", "message" : "Member successfully deleted from Organization" @@ -438,3 +440,70 @@ def stagedinvitations(): ] return jsonify({'staged_invitations': invitations_data}) + + +@bp.route("//members/add", methods=["POST"]) +@jwt_required() +@min_role_required(UserRole.PUBLIC) +@validate(json=AddMemberSchema) +def add_member_to_partner_testing(partner_id: int): + """Add a member to a partner. + + TODO: Allow the API to accept a user email instad of a user id + TODO: Use the partner ID from the API path instead of the request body + The `partner_member_to_orm` function seems very picky about the input. + I wasn't able to get it to accept a dict or a PartnerMember object. + + Cannot be called in production environments + """ + if current_app.env == "production": + abort(418) + + # Ensure that the user has premission to add a member to this partner. + jwt_decoded = get_jwt() + + current_user = User.get(jwt_decoded["sub"]) + association = ( + db.session.query(PartnerMember) + .filter( + PartnerMember.user_id == current_user.id, + PartnerMember.partner_id == partner_id, + ) + .first() + ) + + if ( + association is None + or not association.is_administrator() + or not association.partner_id == partner_id + ): + abort(403) + + # TODO: Allow the API to accept a user email instad of a user id + # user_obj = User.get_by_email(request.context.json.user_email) + # if user_obj is None: + # abort(400) + + # new_member = PartnerMember( + # partner_id=partner_id, + # user_id=user_obj.id, + # role=request.context.json.role, + # ) + + try: + partner_member = partner_member_to_orm(request.context.json) + except Exception: + abort(400) + + created = partner_member.create() + + track_to_mp( + request, + "add_partner_member", + { + "partner_id": partner_id, + "user_id": partner_member.user_id, + "role": partner_member.role, + }, + ) + return partner_member_orm_to_json(created) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index adbc7d35..cfa8a29a 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -1,7 +1,8 @@ import pytest from backend.auth import user_manager -from backend.database import Partner, PartnerMember, MemberRole +from backend.database import Partner, PartnerMember, MemberRole, Invitation from backend.database.models.user import User, UserRole +from datetime import datetime publisher_email = "pub@partner.com" @@ -116,15 +117,15 @@ def example_members(client, db_session, example_partner, p_admin_access_token): .filter(User.email == mock["user_email"]) .first() ) - req = { "partner_id": partner_obj.id, - "email": user_obj.email, + "user_id": user_obj.id, "role": mock["role"], + "is_active": mock["is_active"], } res = client.post( - "/api/v1/partners/invite", + f"/api/v1/partners/{partner_obj.id}/members/add", json=req, headers={ "Authorization": "Bearer {0}".format(p_admin_access_token) @@ -132,6 +133,7 @@ def example_members(client, db_session, example_partner, p_admin_access_token): ) assert res.status_code == 200 created[id] = res.json + print(users["publisher"].id) return created @@ -276,3 +278,93 @@ def test_get_partner_members( assert res.status_code == 200 assert res.json["results"].__len__() == users.__len__() # assert res.json["results"][0]["user"]["email"] == member_obj.email + + +def test_join_organization( + client, + partner_publisher: User, + example_partner: Partner, + example_members, + db_session +): + """ + Two test scenarios + User already in the organization + User not in the organization + """ + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_publisher.email, + "password": example_password + }, + ).json["access_token"] + print(example_members["publisher"]) + """ + Join Endpoint requires the Invitation + Table to populated using the /invite endpoint + Adding a record to the Invitation Table manually + """ + invite = Invitation( + partner_id=example_partner.id, + user_id=example_members["publisher"]["user_id"], + role="Member" + + ) + db_session.add(invite) + db_session.commit() + + """ + Deleting existing PartnerMember record + for "user_id=example_members["publisher"]["user_id"], + partner_id=example_partner.id" as it + has already been added to the PartnerMember + Table using the "example_members function above + + In theory, records should only be added to + PartnerMember table using the /invite endpoint, + and after users have accepted their invites. + """ + db_session.query(PartnerMember).filter_by( + user_id=example_members["publisher"]["user_id"], + partner_id=example_partner.id + ).delete() + db_session.commit() + res = client.post( + "/api/v1/partners/join", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["publisher"]["user_id"], + "partner_id": example_partner.id, + "role": "Member", + "date_joined": datetime.now(), + "is_active" : True + } + ) + + # verify status code + assert res.status_code == 200 + + """ + Verify record has been added to + Partner Member table after /join endpoint + """ + partner_member_obj = PartnerMember.query.filter_by( + user_id=example_members["publisher"]["user_id"], + partner_id=example_partner.id + ).first() + + assert partner_member_obj.user_id == example_members["publisher"]["user_id"] + assert partner_member_obj.partner_id == example_partner.id + + """ + Record in Invitation Table has to + be deleted after /join endpoint + Verifying that this is happening correctly + """ + invitation_check = Invitation.query.filter_by( + partner_id=example_partner.id, + user_id=example_members["publisher"]["user_id"] + ).first() + + assert invitation_check is None From 98a54d6d3450b9d3bd38de28a95ef109fb3e9b66 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Wed, 14 Feb 2024 17:29:31 -0500 Subject: [PATCH 12/17] added second test case for /join endpoint test --- backend/tests/test_partners.py | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index cfa8a29a..88358211 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -299,7 +299,6 @@ def test_join_organization( "password": example_password }, ).json["access_token"] - print(example_members["publisher"]) """ Join Endpoint requires the Invitation Table to populated using the /invite endpoint @@ -368,3 +367,57 @@ def test_join_organization( ).first() assert invitation_check is None + + +""" +Test for when a user is trying to +join an organization but they are already +added to the organization +""" + + +def test_join_organization_user_exists( + client, + partner_publisher: User, + example_partner: Partner, + example_members, + db_session +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_publisher.email, + "password": example_password + }, + ).json["access_token"] + + res = client.post( + "/api/v1/partners/join", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["publisher"]["user_id"], + "partner_id": example_partner.id, + "role": "Member", + "date_joined": datetime.now(), + "is_active" : True + } + ) + + # verify status code + assert res.status_code == 400 + + +def test_leave_endpoint(): + pass + + +def test_remove_member(): + pass + + +def test_withdraw_invitation(): + pass + + +def test_role_change(): + pass From e54b9d6e1577aac9e36cd5785cf588869717e4af Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Wed, 14 Feb 2024 18:06:47 -0500 Subject: [PATCH 13/17] added /leave org endpoint tests --- backend/tests/test_partners.py | 44 ++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 88358211..1e9764af 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -407,8 +407,48 @@ def test_join_organization_user_exists( assert res.status_code == 400 -def test_leave_endpoint(): - pass +def test_leave_endpoint( + client, + partner_publisher: User, + example_partner: Partner, + example_members, + db_session +): + """ + Can leave org user is already part + of + """ + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_publisher.email, + "password": example_password + }, + ).json["access_token"] + + res = client.delete( + "/api/v1/partners/leave", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["publisher"]["user_id"], + "partner_id": example_partner.id, + } + ) + assert res.status_code == 200 + + """ + Cannot leave org one hasnot joined + """ + res = client.delete( + "/api/v1/partners/leave", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["publisher"]["user_id"], + "partner_id": example_partner.id, + } + ) + + assert res.status_code == 400 def test_remove_member(): From 33118ac73a429e1ffa02363125f9d3e0c3ca8f80 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 19 Feb 2024 20:31:11 -0500 Subject: [PATCH 14/17] added /remove_member endpoint tests --- backend/database/models/partner.py | 7 ++ backend/routes/partners.py | 9 +- backend/tests/test_partners.py | 161 ++++++++++++++++++++++++++++- 3 files changed, 171 insertions(+), 6 deletions(-) diff --git a/backend/database/models/partner.py b/backend/database/models/partner.py index 144cb497..5416f154 100644 --- a/backend/database/models/partner.py +++ b/backend/database/models/partner.py @@ -82,6 +82,13 @@ def create(self, refresh: bool = True): self.date_joined = datetime.now() return super().create(refresh) + def __repr__(self): + """Represent instance as a unique string.""" + return f"" + class Partner(db.Model, CrudMixin): id = db.Column(db.Integer, primary_key=True, autoincrement=True) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 82dafe8e..cc467cc4 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -323,9 +323,12 @@ def remove_member(): user_found = PartnerMember.query.filter_by( user_id=body["user_id"], partner_id=body["partner_id"] - ).delete() - db.session.commit() - if user_found > 0 and user_found.role != "Admin": + ).first() + if user_found and user_found.role != MemberRole.ADMIN: + PartnerMember.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"]).delete() + db.session.commit() return { "status" : "ok", "message" : "Member successfully deleted from Organization" diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 1e9764af..3a7768f0 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -8,7 +8,9 @@ publisher_email = "pub@partner.com" inactive_email = "lurker@partner.com" admin_email = "leader@partner.com" +admin2_email = "leader2@partner.com" member_email = "joe@partner.com" +member2_email = "jack@partner.com" example_password = "my_password" mock_partners = { @@ -46,6 +48,14 @@ "email": member_email, "password": example_password, }, + "admin2" : { + "email" : admin2_email, + "password" : example_password + }, + "member2" : { + "email" : member2_email, + "password" : example_password + } } mock_members = { @@ -69,6 +79,16 @@ "role": MemberRole.MEMBER, "is_active": True, }, + "admin2" : { + "user_email": admin_email, + "role": MemberRole.ADMIN, + "is_active": True, + }, + "member2" : { + "user_email": member2_email, + "role" : MemberRole.MEMBER, + "is_active" : True + } } @@ -133,7 +153,6 @@ def example_members(client, db_session, example_partner, p_admin_access_token): ) assert res.status_code == 200 created[id] = res.json - print(users["publisher"].id) return created @@ -435,6 +454,12 @@ def test_leave_endpoint( } ) assert res.status_code == 200 + # verify item has been deleted using endpoint + deleted = PartnerMember.query.filter_by( + user_id=example_members["publisher"]["user_id"], + partner_id=example_partner.id + ).first() + assert deleted is None """ Cannot leave org one hasnot joined @@ -450,14 +475,144 @@ def test_leave_endpoint( assert res.status_code == 400 +# test:only admin can remove members -def test_remove_member(): - pass + +def test_remove_member_admin( + client, + example_members, + example_partner, + partner_admin, + db_session +): + """ + Test cases: + 1)Only Admins can remove members + 2)Handle Members in the Partner Org + assert DB changes + 3)Handle Members not in the Parter Org + assert DB changes + + """ + # log in as admin + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + # use remove_member endpoint as admin + res = client.delete( + "/api/v1/partners/remove_member", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["publisher"]["user_id"], + "partner_id": example_partner.id, + } + ) + assert res.status_code == 200 + removed = PartnerMember.query.filter_by( + user_id=example_members["publisher"]["user_id"], + partner_id=example_partner.id + ).first() + assert removed is None + +# test admins cannot remove other admins + + +def test_remove_member_admin2( + client, + example_members, + example_partner, + partner_admin, + db_session +): + # log in as admin + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + # use remove_member endpoint as admin\ + # trying to remove admin as well + res = client.delete( + "/api/v1/partners/remove_member", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["admin2"]["user_id"], + "partner_id": example_partner.id, + } + ) + assert res.status_code == 400 + removed = PartnerMember.query.filter_by( + user_id=example_members["admin2"]["user_id"], + partner_id=example_partner.id, + ).first() + assert removed is not None + +# admins trying to remove records that don't exist + + +def test_remove_member_admin3( + client, + example_members, + example_partner, + partner_admin, + + +): + # log in as admin + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + # use remove_member endpoint as admin\ + # trying to remove record that does not\ + # exist + res = client.delete( + "/api/v1/partners/remove_member", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : 99999999, + "partner_id": 9999999, + } + ) + assert res.status_code == 400 + removed = PartnerMember.query.filter_by( + user_id=99999999, + partner_id=99999999, + ).first() + assert removed is None def test_withdraw_invitation(): pass + """ + 1)Only Admins can withdraw invitation + 2)Handle withdrawing invitations for users + already invited to the org + Assert DB changes + 3)Handle withdrawing invitations for users + not already invited to the org + Assert Db changes + """ def test_role_change(): pass + """ + Admins can only role change + Admins cannot change role of other admins + Role change for members already in the org + Role change for members not already in the + org + """ From 59f6486a26d99af202f9514cdd397e5a3634f645 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Mon, 19 Feb 2024 23:09:02 -0500 Subject: [PATCH 15/17] added /role_change endpoint tests --- backend/routes/partners.py | 2 +- backend/tests/test_partners.py | 182 ++++++++++++++++++++++++++++++--- 2 files changed, 167 insertions(+), 17 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index cc467cc4..23c4bf35 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -387,7 +387,7 @@ def role_change(): user_id=body["user_id"], partner_id=body["partner_id"] ).first() - if user_found and user_found.role != "Admin": + if user_found and user_found.role != "Administrator": user_found.role = body["role"] db.session.commit() return { diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 3a7768f0..549c94f8 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -560,11 +560,7 @@ def test_remove_member_admin2( def test_remove_member_admin3( client, - example_members, - example_partner, partner_admin, - - ): # log in as admin access_token = res = client.post( @@ -597,22 +593,176 @@ def test_remove_member_admin3( def test_withdraw_invitation(): pass """ - 1)Only Admins can withdraw invitation - 2)Handle withdrawing invitations for users + 1)Handle withdrawing invitations for users already invited to the org Assert DB changes - 3)Handle withdrawing invitations for users + 2)Handle withdrawing invitations for users not already invited to the org Assert Db changes """ +# normal:all conditions met -def test_role_change(): - pass - """ - Admins can only role change - Admins cannot change role of other admins - Role change for members already in the org - Role change for members not already in the - org - """ + +def test_role_change( + client, + partner_admin, + example_partner, + example_members +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["member2"]["user_id"], + "partner_id": example_partner.id, + "role": "Publisher" + } + ) + assert res.status_code == 200 + role_change = PartnerMember.query.filter_by( + user_id=example_members["member2"]["user_id"], + partner_id=example_partner.id, + ).first() + assert role_change.role == "Publisher" and role_change is not None + + +""" +admin cannot change the role +of another admin +""" + + +def test_role_change5( + client, + partner_admin, + example_partner, + example_members +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["admin2"]["user_id"], + "partner_id": example_partner.id, + "role": "Publisher" + } + ) + assert res.status_code == 400 + role_change = PartnerMember.query.filter_by( + user_id=example_members["admin2"]["user_id"], + partner_id=example_partner.id, + ).first() + assert role_change.role != "Publisher" and role_change is not None + + +""" +Rest of the role change tests +are for requests where the partner_id/ +user_id is not found +""" + + +def test_role_change1( + client, + partner_admin, + example_partner, +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : float("inf"), + "partner_id": example_partner.id, + "role": "Publisher" + } + ) + assert res.status_code == 400 + role_change_instance = PartnerMember.query.filter_by( + user_id=float("inf"), + partner_id=example_partner.id, + ).first() + assert role_change_instance is None + + +def test_role_change2( + client, + partner_admin, + example_members +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["member2"]["user_id"], + "partner_id": -1, + "role": "Publisher" + } + ) + assert res.status_code == 400 + role_change_instance = PartnerMember.query.filter_by( + user_id=example_members["member2"]["user_id"], + partner_id=-1, + ).first() + assert role_change_instance is None + + +def test_role_change3( + client, + partner_admin, +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.patch( + "/api/v1/partners/role_change", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : -1, + "partner_id": -1, + "role": "Publisher" + } + ) + assert res.status_code == 400 + role_change_instance = PartnerMember.query.filter_by( + user_id=-1, + partner_id=-1, + ).first() + assert role_change_instance is None From 2ae6763de82d3494230e1d7961591c57d9eb01d0 Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Tue, 20 Feb 2024 16:48:34 -0500 Subject: [PATCH 16/17] adding /withdra_invitations endpoints --- backend/routes/partners.py | 8 ++- backend/tests/test_partners.py | 89 ++++++++++++++++++++++++++++++---- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 23c4bf35..a9e019e0 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -356,9 +356,13 @@ def withdraw_invitation(): user_found = Invitation.query.filter_by( user_id=body["user_id"], partner_id=body["partner_id"] + ).first() + if user_found: + Invitation.query.filter_by( + user_id=body["user_id"], + partner_id=body["partner_id"] ).delete() - db.session.commit() - if user_found > 0: + db.session.commit() return { "status" : "ok", "message" : "Member's invitation withdrawn from Organization" diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 549c94f8..16aa35a5 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -590,16 +590,85 @@ def test_remove_member_admin3( assert removed is None -def test_withdraw_invitation(): - pass - """ - 1)Handle withdrawing invitations for users - already invited to the org - Assert DB changes - 2)Handle withdrawing invitations for users - not already invited to the org - Assert Db changes - """ +""" +withdrawing invitations that exist +""" + + +def test_withdraw_invitation( + client, + partner_admin, + db_session, + example_partner, + example_members, +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + invite = Invitation( + partner_id=example_partner.id, + user_id=example_members["member2"]["user_id"], + role="Member" + + ) + db_session.add(invite) + db_session.commit() + + res = client.delete( + "/api/v1/partners/withdraw_invitation", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["member2"]["user_id"], + "partner_id": example_partner.id, + } + ) + assert res.status_code == 200 + query = db_session.query(Invitation).filter_by( + user_id=example_members["member2"]["user_id"], + partner_id=example_partner.id + ).first() + assert query is None + + +""" +withdrawing invitations that don't exist +""" + + +def test_withdraw_invitation1( + client, + partner_admin, + db_session, + example_members, + example_partner, +): + access_token = res = client.post( + "api/v1/auth/login", + json={ + "email": partner_admin.email, + "password": example_password + }, + ).json["access_token"] + + res = client.delete( + "/api/v1/partners/withdraw_invitation", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "user_id" : example_members["member2"]["user_id"], + "partner_id": example_partner.id, + } + ) + assert res.status_code == 400 + query = db_session.query(Invitation).filter_by( + user_id=example_members["member2"]["user_id"], + partner_id=example_partner.id + ).first() + assert query is None # normal:all conditions met From e812d117476c6cdf56970f629e3c8f8bb276086a Mon Sep 17 00:00:00 2001 From: Harsha Rauniyar Date: Thu, 29 Feb 2024 23:32:15 -0500 Subject: [PATCH 17/17] style fix --- backend/routes/partners.py | 6 ++---- backend/tests/test_partners.py | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/routes/partners.py b/backend/routes/partners.py index 4b78a862..78745437 100644 --- a/backend/routes/partners.py +++ b/backend/routes/partners.py @@ -244,7 +244,6 @@ def add_member_to_partner(): @bp.route("/join", methods=["POST"]) @jwt_required() @min_role_required(UserRole.PUBLIC) - def join_organization(): try: body = request.get_json() @@ -283,10 +282,9 @@ def join_organization(): finally: db.session.close() +# user can leave org they already joined - -# user can leave org they already joined @bp.route("/leave", methods=["DELETE"]) @jwt_required() @min_role_required(UserRole.PUBLIC) @@ -434,8 +432,8 @@ def get_invitations(): return str(e) - # view staged invitations table + @bp.route("/stagedinvitations", methods=["GET"]) @jwt_required() @validate() diff --git a/backend/tests/test_partners.py b/backend/tests/test_partners.py index 7a825d60..3687fef2 100644 --- a/backend/tests/test_partners.py +++ b/backend/tests/test_partners.py @@ -836,4 +836,3 @@ def test_role_change3( partner_id=-1, ).first() assert role_change_instance is None -