diff --git a/README.md b/README.md index b143e67..0a35ccd 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ +# Project Setup + +## Setup the database +**Create all tables in the database** + +`docker-compose exec backend python3 run.py create_db` + +**Accessing the database** +`docker-compose exec db psql --username={your_username} --dbname={your_dbname}` + +## Access flask shell + +`docker-compose exec backend flask shell` + +## Running Tests **Running a single test from a test file** `docker-compose exec backend pytest api/tests/name_of_test_file.py::name_of_test -vv` @@ -7,4 +22,4 @@ `docker-compose exec backend pytest api/tests/name_of_test_file.py -vv` **Run all test files in a single directory** -`docker-compose exec backend pytest api/tests/ -vv` \ No newline at end of file +`docker-compose exec backend pytest api/tests/ -vv` diff --git a/backend/api/auth/controllers.py b/backend/api/auth/controllers.py index ec0f3f4..d38ca77 100644 --- a/backend/api/auth/controllers.py +++ b/backend/api/auth/controllers.py @@ -9,6 +9,7 @@ user_schema = UserSchema(dump_only=["id"], unknown="EXCLUDE") users_schema = UserSchema(many=True, dump_only=["id"], unknown="EXCLUDE") + @auth.get("/hello") def auth_hello(): return {"message": "Auth blueprint is working"}, 200 @@ -22,7 +23,7 @@ def auth_create_new_user(): new_user = user_schema.load(data) # Set the password for the user - new_user.password = data['password'] + new_user.password = data["password"] # Save data to the database db.session.add(new_user) @@ -30,28 +31,31 @@ def auth_create_new_user(): return {"message": f"New user {new_user.name} created"}, 200 + @auth.post("/login") def auth_login_user(): # Collect data from the json post body data = request.json # Check whether user with given email exists - user: User = User.find_by_email(data['email']) + user: User = User.find_by_email(data["email"]) if user: - password_correct = user.check_password(data['password']) + password_correct = user.check_password(data["password"]) if password_correct: token = create_access_token(identity=user.id) return {"token": token, "user_type": f"{user.type}"}, 200 return {"message": "Wrong user credentials"}, 400 return {"message": "A user with the given credentials does not exist"}, 404 + @auth.get("/users") @jwt_required() def auth_get_all_users(): all_users = User.query.all() return jsonify(users_schema.dump(all_users)), 200 + @auth.get("/users/") @jwt_required() def auth_get_user_by_id(id): diff --git a/backend/api/auth/models.py b/backend/api/auth/models.py index 0abed8e..3608c42 100644 --- a/backend/api/auth/models.py +++ b/backend/api/auth/models.py @@ -11,6 +11,9 @@ class User(db.Model): email = db.Column(db.String(50), nullable=False, unique=True) password_hash = db.Column(db.String(120), nullable=False) type = db.Column(db.String(10), nullable=False, default="normal") + contacts = db.relationship( + "Contact", backref="user", cascade="all, delete", passive_deletes=True + ) @property def password(self): @@ -21,7 +24,7 @@ def password(self, password) -> None: self.password_hash = generate_password_hash(password) def check_password(self, password) -> bool: - return check_password_hash(self.password_hash, password)\ + return check_password_hash(self.password_hash, password) @classmethod def find_by_id(cls, id): @@ -34,6 +37,7 @@ def find_by_email(cls, email): def __repr__(self): return f"" + class UserSchema(Schema): id = fields.Int(required=True) name = fields.Str(required=True) diff --git a/backend/api/config.py b/backend/api/config.py index b5f3803..5f626e3 100644 --- a/backend/api/config.py +++ b/backend/api/config.py @@ -2,7 +2,7 @@ basedir = os.path.abspath(os.path.dirname(__file__)) + class BaseConfig(object): SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite://") SQLALCHEMY_TRACK_MODIFICATIONS = False - diff --git a/backend/api/reminder/controllers.py b/backend/api/reminder/controllers.py index 7434c2b..4ec7066 100644 --- a/backend/api/reminder/controllers.py +++ b/backend/api/reminder/controllers.py @@ -1,7 +1,99 @@ -from flask import Blueprint +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from api import db +from api.reminder.models import Contact, ContactSchema +from marshmallow import ValidationError +from sqlalchemy import extract +import datetime reminder = Blueprint("reminder", __name__, url_prefix="/api/reminder") +# contact schema +contact_schema = ContactSchema(unknown="EXCLUDE") +contacts_schema = ContactSchema(many=True, unknown="EXCLUDE") + + @reminder.get("/hello") def reminder_hello(): - return {"message": "Reminder blueprint working"} \ No newline at end of file + return {"message": "Reminder blueprint working"} + + +@reminder.post("/contact") +@jwt_required() +def reminder_create_contact(): + + # Retrieve the data + data = request.json + + try: + # Create a new instance of the contact + new_contact = contact_schema.load(data) + new_contact.user_id = get_jwt_identity() + + # Save the contact + db.session.add(new_contact) + db.session.commit() + + return {"message": "Contact created successfully"}, 200 + + except ValidationError as err: + return {"message": "Bad data format"}, 400 + + +@reminder.get("/contact") +@jwt_required() +def reminder_get_all_contacts(): + # Get the user id + user_id = get_jwt_identity() + + # Get all user contacts + contacts = Contact.query.filter_by(user_id=user_id) + + # Serialize the data + return jsonify(contacts_schema.dump(contacts)), 200 + + +@reminder.get("/birthday/upcoming") +@jwt_required() +def reminder_get_all_upcoming_birthdays(): + # Get today's date + today = datetime.date.today() + + # Find all users who birthdays are after today for the rest of the year + upcoming = ( + db.session.query(Contact) + .filter( + extract("day", Contact.date_of_birth) >= today.day, + extract("month", Contact.date_of_birth) >= today.month, + user_id=get_jwt_identity(), + ) + .order_by( + extract("day", Contact.date_of_birth), + extract("month", Contact.date_of_birth), + extract("year", Contact.date_of_birth), + ) + .all() + ) + + return jsonify(contacts_schema.dump(upcoming)), 200 + + +@reminder.get("/birthday/current") +@jwt_required() +def reminder_get_all_birthdays_current_year(): + # Get today's date + today = datetime.date.today() + + # Find all contacts + upcoming = ( + db.session.query(Contact) + .filter(user_id=get_jwt_identity()) + .order_by( + extract("day", Contact.date_of_birth), + extract("month", Contact.date_of_birth), + extract("year", Contact.date_of_birth), + ) + .all() + ) + + return jsonify(contacts_schema.dump(upcoming)), 200 diff --git a/backend/api/reminder/models.py b/backend/api/reminder/models.py index 31cb249..a6803a4 100644 --- a/backend/api/reminder/models.py +++ b/backend/api/reminder/models.py @@ -1,6 +1,8 @@ from api import db -from sqlalchemy import Enum +from sqlalchemy import Enum, extract import enum +from marshmallow import Schema, fields, post_load + class ContactRelation(enum.Enum): father = 1 @@ -9,10 +11,26 @@ class ContactRelation(enum.Enum): sister = 4 friend = 5 + class Contact(db.Model): __tablename__ = "contact" id = db.Column(db.Integer, primary_key=True, autoincrement=True) + user_id = db.Column( + db.Integer, db.ForeignKey("custom_user.id", ondelete="cascade"), nullable=False + ) name = db.Column(db.String(100), nullable=False) date_of_birth = db.Column(db.Date(), nullable=False) - relation = db.Column(Enum(ContactRelation)) \ No newline at end of file + relation = db.Column(Enum(ContactRelation)) + + +class ContactSchema(Schema): + id = fields.Int(required=True, dump_only=True) + user_id = fields.Int(required=True, dump_only=True) + name = fields.Str(required=True) + date_of_birth = fields.Date(required=True) + relation = fields.Str(required=True) + + @post_load + def make_user(self, data, **kwargs): + return Contact(**data) diff --git a/backend/api/tests/conftest.py b/backend/api/tests/conftest.py index 8f728c1..00a4792 100644 --- a/backend/api/tests/conftest.py +++ b/backend/api/tests/conftest.py @@ -8,6 +8,7 @@ def app(): flask_app.app_context().push() yield flask_app + @pytest.fixture def client(app): - yield app.test_client() \ No newline at end of file + yield app.test_client() diff --git a/backend/api/tests/data.py b/backend/api/tests/data.py index 6ba22c1..3714c18 100644 --- a/backend/api/tests/data.py +++ b/backend/api/tests/data.py @@ -2,19 +2,34 @@ "name": "Super User", "email": "superu@email.com", "type": "super", - "password": "testing1234" + "password": "testing1234", } test_user_1 = { "name": "John Doe", "email": "johndoe@email.com", "type": "normal", - "password": "testing1234" + "password": "testing1234", } test_user_2 = { "name": "Jane Doe", "email": "janedoe@email.com", "type": "normal", - "password": "testing1234" + "password": "testing1234", } + +# Contacts +valid_contacts = [ + {"name": "Isabel Vasilevich", "date_of_birth": "2001-07-09", "relation": "sister"}, + {"name": "Marshall Yeatman", "date_of_birth": "2001-10-30", "relation": "father"}, + {"name": "Rogers McNirlin", "date_of_birth": "2001-02-13", "relation": "brother"}, + {"name": "Bobinette Tollady", "date_of_birth": "2001-01-13", "relation": "friend"}, + {"name": "Myca Berling", "date_of_birth": "2021-12-30", "relation": "friend"}, + {"name": "Andrej Rudall", "date_of_birth": "2021-12-10", "relation": "friend"}, + {"name": "Dalia Shannahan", "date_of_birth": "2021-12-16", "relation": "friend"}, + {"name": "Amil Garlett", "date_of_birth": "2021-12-22", "relation": "friend"}, + {"name": "Guy Haig", "date_of_birth": "2021-12-28", "relation": "friend"}, +] + +invalid_contact = [{"name": "Roselia Golda", "date_of_birth": "2001-10-01"}] diff --git a/backend/api/tests/setup.py b/backend/api/tests/setup.py index 7bae4ba..b037b8f 100644 --- a/backend/api/tests/setup.py +++ b/backend/api/tests/setup.py @@ -1,15 +1,12 @@ from api import db from api.auth.models import User, UserSchema -from api.reminder.models import Contact -from api.tests.data import ( - super_user, - test_user_1, - test_user_2 -) +from api.reminder.models import Contact, ContactSchema +from api.tests.data import super_user, test_user_1, test_user_2, valid_contacts # Setting up the schema for the models user_schema = UserSchema(dump_only=["id"], unknown="EXCLUDE") users_schema = UserSchema(many=True, dump_only=["id"], unknown="EXCLUDE") +contact_schema = ContactSchema(unknown="EXCLUDE") def reset_db(): @@ -17,49 +14,61 @@ def reset_db(): Contact.query.delete() db.session.commit() + +# Creating Users def create_super_user(client): # Create super user - client.post( - "/api/auth/users", - json=super_user - ) + client.post("/api/auth/users", json=super_user) - super = User.find_by_email(super_user['email']) + super = User.find_by_email(super_user["email"]) # Login the super user response = client.post( "/api/auth/login", - json={"email": super_user['email'], "password": super_user['password']} + json={"email": super_user["email"], "password": super_user["password"]}, ) return response.json - def create_one_test_user(client): # Create first user - client.post( - "/api/auth/users", - json=test_user_1 + client.post("/api/auth/users", json=test_user_1) + + # Login the user + response = client.post( + "/api/auth/login", + json={"email": test_user_1["email"], "password": test_user_1["password"]}, ) - user = User.find_by_email(test_user_1["email"]) + return response.json - return user_schema.dump(user) def create_two_test_users(client): # Create first user - client.post( - "/api/auth/users", - json=test_user_1 - ) + client.post("/api/auth/users", json=test_user_1) # Create the second user - client.post( - "/api/auth/users", - json=test_user_2 - ) + client.post("/api/auth/users", json=test_user_2) users = User.query.all() return users_schema.dump(users) + + +# Contacts +def create_one_contact(client, user): + response = client.post( + "api/reminder/contact", + json=valid_contacts[0], + headers={"Authorization": f"Bearer {user['token']}"}, + ) + + +def create_multiple_contacts(client, user): + for i in range(len(valid_contacts)): + response = client.post( + "api/reminder/contact", + json=valid_contacts[i], + headers={"Authorization": f"Bearer {user['token']}"}, + ) diff --git a/backend/api/tests/test_auth.py b/backend/api/tests/test_auth.py index 89c6e90..75e99eb 100644 --- a/backend/api/tests/test_auth.py +++ b/backend/api/tests/test_auth.py @@ -1,30 +1,24 @@ -from api.tests.data import ( - test_user_1, - test_user_2 -) +from api.tests.data import test_user_1, test_user_2 from api.tests.setup import ( reset_db, create_super_user, create_one_test_user, - create_two_test_users + create_two_test_users, ) + def test_auth_hello(client): - response = client.get( - "api/auth/hello" - ) + response = client.get("api/auth/hello") + + assert response.json["message"] == "Auth blueprint is working" - assert response.json['message'] == 'Auth blueprint is working' def test_auth_create_new_user(client): # Reset the database before the test begins reset_db() - response = client.post( - "/api/auth/users", - json=test_user_1 - ) + response = client.post("/api/auth/users", json=test_user_1) assert response.status_code == 200 assert response.json @@ -32,6 +26,7 @@ def test_auth_create_new_user(client): # Reset the database after running all tests reset_db() + def test_auth_login_user(client): # Reset the database before the test begins @@ -42,12 +37,13 @@ def test_auth_login_user(client): response = client.post( "/api/auth/login", - json={"email": test_user_1['email'], "password": test_user_1['password']} + json={"email": test_user_1["email"], "password": test_user_1["password"]}, ) assert response.status_code == 200 - assert response.json['token'] - assert response.json['user_type'] == 'normal' + assert response.json["token"] + assert response.json["user_type"] == "normal" + def test_auth_get_all_users(client): # Reset the database before the test begins @@ -59,14 +55,13 @@ def test_auth_get_all_users(client): create_two_test_users(client) response = client.get( - "/api/auth/users", - headers={"Authorization": f"Bearer {super['token']}"} + "/api/auth/users", headers={"Authorization": f"Bearer {super['token']}"} ) assert response.status_code == 200 assert len(response.json) == 3 - assert response.json[1]['email'] == test_user_1['email'] - assert response.json[2]['email'] == test_user_2['email'] + assert response.json[1]["email"] == test_user_1["email"] + assert response.json[2]["email"] == test_user_2["email"] # Reset the database before the test begins reset_db() @@ -83,13 +78,13 @@ def test_auth_get_user_by_id(client): response = client.get( f"/api/auth/users/{user['id']}", - headers={"Authorization": f"Bearer {super['token']}"} + headers={"Authorization": f"Bearer {super['token']}"}, ) assert response.status_code == 200 - assert response.json['id'] == user['id'] - assert response.json['email'] == user['email'] - assert response.json['name'] == user['name'] + assert response.json["id"] == user["id"] + assert response.json["email"] == user["email"] + assert response.json["name"] == user["name"] # Reset the database before the test begins reset_db() diff --git a/backend/api/tests/test_reminder.py b/backend/api/tests/test_reminder.py new file mode 100644 index 0000000..0ed6a4e --- /dev/null +++ b/backend/api/tests/test_reminder.py @@ -0,0 +1,82 @@ +from api.reminder.models import Contact +from api.tests.setup import reset_db, create_one_test_user, create_multiple_contacts +from api.tests.data import valid_contacts, invalid_contact + + +def test_reminder_create_contact(client): + # Reset the database + reset_db() + + user = create_one_test_user(client) + + response = client.post( + "api/reminder/contact", + json=valid_contacts[0], + headers={"Authorization": f"Bearer {user['token']}"}, + ) + + contact = Contact.query.filter_by(name=valid_contacts[0]["name"]) + + assert response.status_code == 200 + assert response.json["message"] == "Contact created successfully" + assert contact != None + + # Reset database + reset_db() + + +def test_reminder_create_contact_bad_data(client): + # Reset the database + reset_db() + + user = create_one_test_user(client) + + response = client.post( + "api/reminder/contact", + json=invalid_contact[0], + headers={"Authorization": f"Bearer {user['token']}"}, + ) + + assert response.status_code == 400 + assert response.json["message"] == "Bad data format" + + # Reset the database + reset_db() + + +def test_reminder_get_all_contacts(client): + # Reset the database + reset_db() + + user = create_one_test_user(client) + + create_multiple_contacts(client, user) + + response = client.get( + "/api/reminder/contact", headers={"Authorization": f"Bearer {user['token']}"} + ) + + assert response.status_code == 200 + assert len(response.json) == 4 + + # Reset the database + reset_db() + + +def test_reminder_get_all_upcoming_birthdays(client): + # Reset the database + reset_db() + + user = create_one_test_user(client) + + create_multiple_contacts(client, user) + + response = client.get( + "/api/reminder/birthday/upcoming", + headers={"Authorization": f"Bearer {user['token']}"}, + ) + print(response.json) + assert 1 == 2 + + # Reset the database + # reset_db() diff --git a/backend/api/tests/test_reminders.py b/backend/api/tests/test_reminders.py deleted file mode 100644 index e69de29..0000000