From 211dc99d07336ab7d6fcda0f645b4fe356f32bed Mon Sep 17 00:00:00 2001 From: Helen Guan <87948268+helenellyx@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:31:10 -0500 Subject: [PATCH] Create buildings table (#125) * Create buildings table * Set up buildings table to have prexisting building * Create script * Change building_id to building * move script to seeding dir * Update backend/app/services/implementations/log_records_service.py Co-authored-by: Safwaan Chowdhury <57375371+Safewaan@users.noreply.github.com> * Add JOIN statement * fix features to use building_id instead of building * format * chnage building to building id * fix csv * address PR comments * add default buildings on db creation * pr comments * final touchups * final touchups * remove redundant date check * run linter there's so many changes * add building filters when counting log records --------- Co-authored-by: Safewaan Co-authored-by: Safwaan Chowdhury <57375371+Safewaan@users.noreply.github.com> Co-authored-by: Kelly Pham Co-authored-by: Connor Bechthold --- backend/app/models/__init__.py | 1 + backend/app/models/buildings.py | 32 ++++++ backend/app/models/log_record_tags.py | 4 +- backend/app/models/log_records.py | 7 +- backend/app/models/residents.py | 7 +- backend/app/models/tags.py | 4 +- backend/app/rest/auth_routes.py | 4 +- .../services/implementations/auth_service.py | 4 +- .../implementations/log_records_service.py | 85 +++++++++------- .../implementations/residents_service.py | 38 +++++-- .../services/interfaces/residents_service.py | 8 +- .../exceptions/firebase_exceptions.py | 14 +-- backend/app/utilities/firebase_rest_client.py | 20 ++-- ...5ad7_create_junction_table_between_log_.py | 44 ++++++--- .../versions/8b5132609f1f_merging.py | 4 +- .../a5d22b31faab_add_resident_table.py | 6 +- .../c24644595836_adding_buildings_table.py | 43 ++++++++ .../fd734d591b67_added_log_records_to_db.py | 8 +- frontend/src/APIClients/AuthAPIClient.ts | 16 +-- frontend/src/APIClients/LogRecordAPIClient.ts | 16 +-- frontend/src/APIClients/ResidentAPIClient.ts | 11 ++- frontend/src/components/auth/Authy.tsx | 36 +++---- frontend/src/components/forms/CreateLog.tsx | 20 ++-- .../src/components/forms/CreateResident.tsx | 20 ++-- frontend/src/components/forms/EditLog.tsx | 24 ++--- .../src/components/forms/EditResident.tsx | 25 ++--- frontend/src/components/forms/Login.tsx | 98 +++++++++---------- frontend/src/components/forms/Signup.tsx | 17 ++-- .../components/pages/HomePage/HomePage.tsx | 20 ++-- .../pages/HomePage/LogRecordsTable.tsx | 4 +- .../pages/HomePage/SearchAndFilters.tsx | 34 ++++--- .../ResidentDirectoryTable.tsx | 2 +- frontend/src/helper/CSVConverter.tsx | 7 +- frontend/src/helper/authErrorMessage.ts | 12 +-- frontend/src/theme/common/textStyles.tsx | 4 +- frontend/src/types/AuthTypes.ts | 3 +- frontend/src/types/BuildingTypes.ts | 4 +- frontend/src/types/LogRecordTypes.ts | 15 ++- frontend/src/types/ResidentTypes.ts | 15 ++- seeding/create-log-records.sh | 4 +- seeding/create-residents.sh | 4 +- 41 files changed, 460 insertions(+), 284 deletions(-) create mode 100644 backend/app/models/buildings.py create mode 100644 backend/migrations/versions/c24644595836_adding_buildings_table.py diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index fc9cc63a..b40e490d 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -13,6 +13,7 @@ def init_app(app): from .tags import Tag from .log_record_tags import LogRecordTag from .residents import Residents + from .buildings import Buildings app.app_context().push() db.init_app(app) diff --git a/backend/app/models/buildings.py b/backend/app/models/buildings.py new file mode 100644 index 00000000..246931c4 --- /dev/null +++ b/backend/app/models/buildings.py @@ -0,0 +1,32 @@ +from . import db +from sqlalchemy import inspect, cast, String +from sqlalchemy.orm.properties import ColumnProperty + + +class Buildings(db.Model): + __tablename__ = "buildings" + id = db.Column(db.Integer, primary_key=True, nullable=False) + address = db.Column(db.String, nullable=False) + name = db.Column(db.String, nullable=False) + log_record = db.relationship("LogRecords", back_populates="building") + resident = db.relationship("Residents", back_populates="building") + + def to_dict(self, include_relationships=False): + # define the entities table + cls = type(self) + + mapper = inspect(cls) + formatted = {} + for column in mapper.attrs: + field = column.key + attr = getattr(self, field) + # if it's a regular column, extract the value + if isinstance(column, ColumnProperty): + formatted[field] = attr + # otherwise, it's a relationship field + # (currently not applicable, but may be useful for entity groups) + elif include_relationships: + # recursively format the relationship + # don't format the relationship's relationships + formatted[field] = [obj.to_dict() for obj in attr] + return formatted diff --git a/backend/app/models/log_record_tags.py b/backend/app/models/log_record_tags.py index 2096f0e3..e052e234 100644 --- a/backend/app/models/log_record_tags.py +++ b/backend/app/models/log_record_tags.py @@ -8,7 +8,9 @@ class LogRecordTag(db.Model): __tablename__ = "log_record_tag" log_record_tag_id = db.Column(db.Integer, primary_key=True, nullable=False) - log_record_id = db.Column(db.Integer, db.ForeignKey("log_records.log_id"), nullable=False) + log_record_id = db.Column( + db.Integer, db.ForeignKey("log_records.log_id"), nullable=False + ) tag_id = db.Column(db.Integer, db.ForeignKey("tags.tag_id"), nullable=False) def to_dict(self, include_relationships=False): diff --git a/backend/app/models/log_records.py b/backend/app/models/log_records.py index 2bf7c878..aab98794 100644 --- a/backend/app/models/log_records.py +++ b/backend/app/models/log_records.py @@ -13,8 +13,11 @@ class LogRecords(db.Model): attn_to = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) # TODO: replace open String fields with VarChar(NUM_CHARS) note = db.Column(db.String, nullable=False) - building = db.Column(db.String, nullable=False) - tags = db.relationship("Tag", secondary="log_record_tag", back_populates="log_records") + building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False) + tags = db.relationship( + "Tag", secondary="log_record_tag", back_populates="log_records" + ) + building = db.relationship("Buildings", back_populates="log_record") def to_dict(self, include_relationships=False): # define the entities table diff --git a/backend/app/models/residents.py b/backend/app/models/residents.py index 5b1b5466..8e2bbc3a 100644 --- a/backend/app/models/residents.py +++ b/backend/app/models/residents.py @@ -10,7 +10,8 @@ class Residents(db.Model): room_num = db.Column(db.Integer, nullable=False) date_joined = db.Column(db.Date, nullable=False) date_left = db.Column(db.Date, nullable=True) - building = db.Column(db.Enum("144", "402", "362", name="buildings"), nullable=False) + building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False) + building = db.relationship("Buildings", back_populates="resident") resident_id = db.column_property(initial + cast(room_num, String)) @@ -31,7 +32,9 @@ def to_dict(self, include_relationships=False): attr = getattr(self, field) # if it's a regular column, extract the value if isinstance(column, ColumnProperty): - if (field == "date_joined" or field == "date_left") and attr: + if field == "building_id": + formatted["building"] = {"id": attr} + elif (field == "date_joined" or field == "date_left") and attr: formatted[field] = attr.strftime("%Y-%m-%d") else: formatted[field] = attr diff --git a/backend/app/models/tags.py b/backend/app/models/tags.py index 7636d127..18a6194d 100644 --- a/backend/app/models/tags.py +++ b/backend/app/models/tags.py @@ -10,7 +10,9 @@ class Tag(db.Model): tag_id = db.Column(db.Integer, primary_key=True, nullable=False) name = db.Column(db.String, nullable=False) status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False) - log_records = db.relationship("LogRecords", secondary="log_record_tag", back_populates="tags") + log_records = db.relationship( + "LogRecords", secondary="log_record_tag", back_populates="tags" + ) def to_dict(self, include_relationships=False): # define the entities table diff --git a/backend/app/rest/auth_routes.py b/backend/app/rest/auth_routes.py index 587a6faa..cf6d78d2 100644 --- a/backend/app/rest/auth_routes.py +++ b/backend/app/rest/auth_routes.py @@ -1,7 +1,7 @@ import os from ..utilities.exceptions.firebase_exceptions import ( - InvalidPasswordException, - TooManyLoginAttemptsException + InvalidPasswordException, + TooManyLoginAttemptsException, ) from flask import Blueprint, current_app, jsonify, request diff --git a/backend/app/services/implementations/auth_service.py b/backend/app/services/implementations/auth_service.py index e2ee2a80..1b752008 100644 --- a/backend/app/services/implementations/auth_service.py +++ b/backend/app/services/implementations/auth_service.py @@ -1,8 +1,8 @@ import firebase_admin.auth from ...utilities.exceptions.firebase_exceptions import ( - InvalidPasswordException, - TooManyLoginAttemptsException + InvalidPasswordException, + TooManyLoginAttemptsException, ) from ..interfaces.auth_service import IAuthService from ...resources.auth_dto import AuthDTO diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index fe7397a7..2d0c9002 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -4,8 +4,7 @@ from ...models import db from datetime import datetime from pytz import timezone -from sqlalchemy import select, cast, Date, text -import os +from sqlalchemy import text class LogRecordsService(ILogRecordsService): @@ -37,12 +36,12 @@ def add_record(self, log_record): return log_record except Exception as postgres_error: raise postgres_error - + def construct_tags(self, log_record, tag_names): for tag_name in tag_names: tag = Tag.query.filter_by(name=tag_name).first() - if not tag: + if not tag: raise Exception(f"Tag with name {tag_name} does not exist") log_record.tags.append(tag) @@ -53,30 +52,39 @@ def to_json_list(self, logs): logs_list.append( { "log_id": log[0], - "resident_id": log[2], - "datetime": str(log[3].astimezone(timezone("US/Eastern"))), - "flagged": log[4], - "attn_to": { - "id": log[5], - "first_name": log[11], - "last_name": log[12] - }, "employee": { "id": log[1], - "first_name": log[9], - "last_name": log[10] + "first_name": log[2], + "last_name": log[3], }, - "note": log[6], - "tags ": log[7], - "building": log[8], + "resident_id": log[4], + "attn_to": { + "id": log[5], + "first_name": log[6], + "last_name": log[7], + } + if log[5] + else None, + "building": {"id": log[8], "name": log[9]}, + "tags": log[10] if log[10] else [], + "note": log[11], + "flagged": log[12], + "datetime": str(log[13].astimezone(timezone("US/Eastern"))), } ) return logs_list except Exception as postgres_error: raise postgres_error - def filter_by_building(self, building): - return f"\nlogs.building='{building}'" + def filter_by_building_id(self, building_id): + if type(building_id) == list: + sql_statement = f"\nlogs.building_id={building_id[0]}" + for i in range(1, len(building_id)): + sql_statement = ( + sql_statement + f"\nOR logs.building_id={building_id[i]}" + ) + return sql_statement + return f"\logs.building_id={building_id}" def filter_by_employee_id(self, employee_id): if type(employee_id) == list: @@ -140,7 +148,7 @@ def filter_log_records(self, filters=None): is_first_filter = True options = { - "building": self.filter_by_building, + "building_id": self.filter_by_building_id, "employee_id": self.filter_by_employee_id, "resident_id": self.filter_by_resident_id, "attn_to": self.filter_by_attn_to, @@ -157,7 +165,7 @@ def filter_log_records(self, filters=None): if filters.get(filter): sql = sql + "\nAND " + options[filter](filters.get(filter)) return sql - + def join_tag_attributes(self): return "\nLEFT JOIN\n \ (SELECT logs.log_id, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ @@ -165,7 +173,7 @@ def join_tag_attributes(self): JOIN tags ON lrt.tag_id = tags.tag_id\n \ GROUP BY logs.log_id \n \ ) t ON logs.log_id = t.log_id\n" - + def get_log_records( self, page_number, return_all, results_per_page=10, filters=None ): @@ -173,22 +181,24 @@ def get_log_records( sql = "SELECT\n \ logs.log_id,\n \ logs.employee_id,\n \ - CONCAT(residents.initial, residents.room_num) AS resident_id,\n \ - logs.datetime,\n \ - logs.flagged,\n \ - logs.attn_to,\n \ - logs.note,\n \ - t.tag_names, \n \ - logs.building,\n \ employees.first_name AS employee_first_name,\n \ employees.last_name AS employee_last_name,\n \ + CONCAT(residents.initial, residents.room_num) AS resident_id,\n \ + logs.attn_to,\n \ attn_tos.first_name AS attn_to_first_name,\n \ - attn_tos.last_name AS attn_to_last_name\n \ + attn_tos.last_name AS attn_to_last_name,\n \ + buildings.id AS building_id,\n \ + buildings.name AS building_name,\n \ + t.tag_names, \n \ + logs.note,\n \ + logs.flagged,\n \ + logs.datetime\n \ FROM log_records logs\n \ LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ JOIN users employees ON logs.employee_id = employees.id\n \ - JOIN residents ON logs.resident_id = residents.id" - + JOIN residents ON logs.resident_id = residents.id\n \ + JOIN buildings on logs.building_id = buildings.id" + sql += self.join_tag_attributes() sql += self.filter_log_records(filters) @@ -214,10 +224,11 @@ def count_log_records(self, filters=None): COUNT(*)\n \ FROM log_records logs\n \ LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ - JOIN users employees ON logs.employee_id = employees.id" - - sql += f"\n{self.join_tag_attributes()}" + JOIN users employees ON logs.employee_id = employees.id\n \ + JOIN residents ON logs.resident_id = residents.id\n \ + JOIN buildings on logs.building_id = buildings.id" + sql += f"\n{self.join_tag_attributes()}" sql += self.filter_log_records(filters) num_results = db.session.execute(text(sql)) @@ -254,7 +265,7 @@ def update_log_record(self, log_id, updated_log_record): ) if "tags" in updated_log_record: log_record = LogRecords.query.filter_by(log_id=log_id).first() - if (log_record): + if log_record: log_record.tags = [] self.construct_tags(log_record, updated_log_record["tags"]) else: @@ -268,7 +279,7 @@ def update_log_record(self, log_id, updated_log_record): LogRecords.employee_id: updated_log_record["employee_id"], LogRecords.resident_id: updated_log_record["resident_id"], LogRecords.flagged: updated_log_record["flagged"], - LogRecords.building: updated_log_record["building"], + LogRecords.building_id: updated_log_record["building_id"], LogRecords.note: updated_log_record["note"], LogRecords.datetime: updated_log_record["datetime"], } diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index 3fab5e71..b9d0a982 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -1,6 +1,7 @@ from ..interfaces.residents_service import IResidentsService from ...models.residents import Residents from ...models.log_records import LogRecords +from ...models.buildings import Buildings from ...models import db from datetime import datetime from sqlalchemy import select, cast, Date @@ -21,6 +22,16 @@ def __init__(self, logger): """ self.logger = logger + def to_residents_json_list(self, resident_results): + residents_json_list = [] + for result in resident_results: + resident, building = result[0], result[1] + + resident_dict = resident.to_dict() + resident_dict["building"]["name"] = building + residents_json_list.append(resident_dict) + return residents_json_list + def convert_to_date_obj(self, date): return datetime.strptime(date, "%Y-%m-%d") @@ -94,20 +105,33 @@ def get_residents( ): try: if resident_id: - residents_results = Residents.query.filter_by(resident_id=resident_id) + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .with_entities(Residents, Buildings.name.label("building")) + .filter_by(resident_id=resident_id) + ) elif return_all: - residents_results = Residents.query.all() + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .with_entities(Residents, Buildings.name.label("building")) + .all() + ) else: residents_results = ( - Residents.query.limit(results_per_page) + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .limit(results_per_page) .offset((page_number - 1) * results_per_page) + .with_entities(Residents, Buildings.name.label("building")) .all() ) - residents_results = list( - map(lambda resident: resident.to_dict(), residents_results) - ) - return {"residents": residents_results} + return {"residents": self.to_residents_json_list(residents_results)} except Exception as postgres_error: raise postgres_error diff --git a/backend/app/services/interfaces/residents_service.py b/backend/app/services/interfaces/residents_service.py index eb58d6f8..24796097 100644 --- a/backend/app/services/interfaces/residents_service.py +++ b/backend/app/services/interfaces/residents_service.py @@ -7,7 +7,7 @@ class IResidentsService(ABC): """ @abstractmethod - def add_resident(self, id, initial, room_num, date_joined, date_left, building): + def add_resident(self, id, initial, room_num, date_joined, date_left, building_id): """ Add a resident to the database :param id: autogenerated unique id of the resident @@ -15,13 +15,13 @@ def add_resident(self, id, initial, room_num, date_joined, date_left, building): :param initial: initial of resident :type initial: string :param room_num: room number which the resident resides in - :type room_num: integer + :type room_num: int :param date_joined: the date the resident joined :type date_joined: date :param date_left: the date the resident left, if exists :type date_left: date - :param building: the building in which the resident is staying - :type building: Enum("144", "402", "362) + :param building_id: the building_id in which the resident is staying + :type building_id: int :raises Exception: if resident creation fails """ pass diff --git a/backend/app/utilities/exceptions/firebase_exceptions.py b/backend/app/utilities/exceptions/firebase_exceptions.py index 8bd6fdf4..c57d7a0b 100644 --- a/backend/app/utilities/exceptions/firebase_exceptions.py +++ b/backend/app/utilities/exceptions/firebase_exceptions.py @@ -2,15 +2,17 @@ class InvalidPasswordException(Exception): """ Raised when an invalid password is entered """ + def __init__(self): - self.message = "Incorrect password. Please try again." - super().__init__(self.message) + self.message = "Incorrect password. Please try again." + super().__init__(self.message) + class TooManyLoginAttemptsException(Exception): """ - Raised when there have been too many failed attempts to login + Raised when there have been too many failed attempts to login """ - def __init__(self): - self.message = "Too many failed login attempts. Please try again later." - super().__init__(self.message) + def __init__(self): + self.message = "Too many failed login attempts. Please try again later." + super().__init__(self.message) diff --git a/backend/app/utilities/firebase_rest_client.py b/backend/app/utilities/firebase_rest_client.py index 21dc2d20..60b59cff 100644 --- a/backend/app/utilities/firebase_rest_client.py +++ b/backend/app/utilities/firebase_rest_client.py @@ -2,8 +2,8 @@ import requests from ..utilities.exceptions.firebase_exceptions import ( - InvalidPasswordException, - TooManyLoginAttemptsException + InvalidPasswordException, + TooManyLoginAttemptsException, ) from ..resources.token import Token @@ -57,13 +57,21 @@ def sign_in_with_password(self, email, password): # Raise an invalid password exception # The corresponding error message from Firebase is INVALID_PASSWORD - if ("error" in response_json and response_json["error"]["code"] == 400 and response_json["error"]["message"] == "INVALID_PASSWORD"): + if ( + "error" in response_json + and response_json["error"]["code"] == 400 + and response_json["error"]["message"] == "INVALID_PASSWORD" + ): raise InvalidPasswordException - # The corresponding error message from Firebase is + # The corresponding error message from Firebase is # 'TOO_MANY_ATTEMPTS_TRY_LATER : Access to this account has been temporarily disabled due to many failed login attempts. You can immediately restore it by resetting your password or you can try again later.' - elif ("error" in response_json and response_json["error"]["code"] == 400 and response_json["error"]["message"][:27] == "TOO_MANY_ATTEMPTS_TRY_LATER"): + elif ( + "error" in response_json + and response_json["error"]["code"] == 400 + and response_json["error"]["message"][:27] == "TOO_MANY_ATTEMPTS_TRY_LATER" + ): raise TooManyLoginAttemptsException - elif "error" in response_json and response.status_code != 200: + elif "error" in response_json and response.status_code != 200: error_message = [ "Failed to sign-in via Firebase REST API, status code =", str(response.status_code), diff --git a/backend/migrations/versions/65a56c245ad7_create_junction_table_between_log_.py b/backend/migrations/versions/65a56c245ad7_create_junction_table_between_log_.py index b9c70c29..40960948 100644 --- a/backend/migrations/versions/65a56c245ad7_create_junction_table_between_log_.py +++ b/backend/migrations/versions/65a56c245ad7_create_junction_table_between_log_.py @@ -10,32 +10,46 @@ from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '65a56c245ad7' -down_revision = '82f36cdf325f' +revision = "65a56c245ad7" +down_revision = "82f36cdf325f" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('log_record_tag', - sa.Column('log_record_tag_id', sa.Integer(), nullable=False), - sa.Column('log_record_id', sa.Integer(), nullable=False), - sa.Column('tag_id', sa.Integer(), nullable=False), - sa.ForeignKeyConstraint(['log_record_id'], ['log_records.log_id'], ), - sa.ForeignKeyConstraint(['tag_id'], ['tags.tag_id'], ), - sa.PrimaryKeyConstraint('log_record_tag_id') + op.create_table( + "log_record_tag", + sa.Column("log_record_tag_id", sa.Integer(), nullable=False), + sa.Column("log_record_id", sa.Integer(), nullable=False), + sa.Column("tag_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["log_record_id"], + ["log_records.log_id"], + ), + sa.ForeignKeyConstraint( + ["tag_id"], + ["tags.tag_id"], + ), + sa.PrimaryKeyConstraint("log_record_tag_id"), ) - with op.batch_alter_table('log_records', schema=None) as batch_op: - batch_op.drop_column('tags') + with op.batch_alter_table("log_records", schema=None) as batch_op: + batch_op.drop_column("tags") # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('log_records', schema=None) as batch_op: - batch_op.add_column(sa.Column('tags', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True)) - - op.drop_table('log_record_tag') + with op.batch_alter_table("log_records", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "tags", + postgresql.ARRAY(sa.VARCHAR()), + autoincrement=False, + nullable=True, + ) + ) + + op.drop_table("log_record_tag") # ### end Alembic commands ### diff --git a/backend/migrations/versions/8b5132609f1f_merging.py b/backend/migrations/versions/8b5132609f1f_merging.py index 99188538..10153b85 100644 --- a/backend/migrations/versions/8b5132609f1f_merging.py +++ b/backend/migrations/versions/8b5132609f1f_merging.py @@ -10,8 +10,8 @@ # revision identifiers, used by Alembic. -revision = '8b5132609f1f' -down_revision = ('24fad25f60e3', '65a56c245ad7') +revision = "8b5132609f1f" +down_revision = ("24fad25f60e3", "65a56c245ad7") branch_labels = None depends_on = None diff --git a/backend/migrations/versions/a5d22b31faab_add_resident_table.py b/backend/migrations/versions/a5d22b31faab_add_resident_table.py index 8f159904..a12c199c 100644 --- a/backend/migrations/versions/a5d22b31faab_add_resident_table.py +++ b/backend/migrations/versions/a5d22b31faab_add_resident_table.py @@ -25,8 +25,10 @@ def upgrade(): sa.Column("room_num", sa.Integer(), nullable=False), sa.Column("date_joined", sa.DateTime(timezone=True), nullable=False), sa.Column("date_left", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "building", sa.Enum("144", "402", "362", name="buildings"), nullable=False + sa.Column("building_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["building_id"], + ["buildings.id"], ), sa.PrimaryKeyConstraint("id"), ) diff --git a/backend/migrations/versions/c24644595836_adding_buildings_table.py b/backend/migrations/versions/c24644595836_adding_buildings_table.py new file mode 100644 index 00000000..eb7b4c50 --- /dev/null +++ b/backend/migrations/versions/c24644595836_adding_buildings_table.py @@ -0,0 +1,43 @@ +"""adding buildings table + +Revision ID: c24644595836 +Revises: a5d22b31faab +Create Date: 2023-06-26 23:12:08.724039 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "c24644595836" +down_revision = "58313513d17f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "buildings", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("address", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.PrimaryKeyConstraint("id"), + ) + + sql = "INSERT INTO buildings (address, name) VALUES\n \ + ('144 Erb St. W', '144'),\n \ + ('362 Erb St. W', '362'),\n \ + ('402 Erb St. W', '402')" + + op.execute(sql) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.execute("DELETE FROM buildings WHERE name IN ('144', '362', 402)") + + op.drop_table("buildings") + # ### end Alembic commands ### diff --git a/backend/migrations/versions/fd734d591b67_added_log_records_to_db.py b/backend/migrations/versions/fd734d591b67_added_log_records_to_db.py index a5f07ec3..9ea6cab9 100644 --- a/backend/migrations/versions/fd734d591b67_added_log_records_to_db.py +++ b/backend/migrations/versions/fd734d591b67_added_log_records_to_db.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "fd734d591b67" -down_revision = "58313513d17f" +down_revision = "c24644595836" branch_labels = None depends_on = None @@ -29,7 +29,11 @@ def upgrade(): sa.Column("attn_to", sa.Integer(), nullable=True), sa.Column("note", sa.String(), nullable=False), sa.Column("tags", sa.ARRAY(sa.String()), nullable=True), - sa.Column("building", sa.String(), nullable=False), + sa.Column("building_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["building_id"], + ["buildings.id"], + ), sa.ForeignKeyConstraint( ["attn_to"], ["users.id"], diff --git a/frontend/src/APIClients/AuthAPIClient.ts b/frontend/src/APIClients/AuthAPIClient.ts index c516a56b..bde196bc 100644 --- a/frontend/src/APIClients/AuthAPIClient.ts +++ b/frontend/src/APIClients/AuthAPIClient.ts @@ -4,9 +4,13 @@ import { OperationVariables, } from "@apollo/client"; import { AxiosError } from "axios"; -import getLoginErrMessage from '../helper/authErrorMessage' +import getLoginErrMessage from "../helper/authErrorMessage"; import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; -import { AuthenticatedUser, AuthTokenResponse, ErrorResponse } from "../types/AuthTypes"; +import { + AuthenticatedUser, + AuthTokenResponse, + ErrorResponse, +} from "../types/AuthTypes"; import baseAPIClient from "./BaseAPIClient"; import { getLocalStorageObjProperty, @@ -29,13 +33,13 @@ const login = async ( if (axiosErr.response && axiosErr.response.status === 401) { return { errCode: axiosErr.response.status, - errMessage: getLoginErrMessage(axiosErr.response) - } + errMessage: getLoginErrMessage(axiosErr.response), + }; } return { errCode: 500, - errMessage: "Error logging in. Please try again." - } + errMessage: "Error logging in. Please try again.", + }; } }; diff --git a/frontend/src/APIClients/LogRecordAPIClient.ts b/frontend/src/APIClients/LogRecordAPIClient.ts index 6db0555b..4dc23873 100644 --- a/frontend/src/APIClients/LogRecordAPIClient.ts +++ b/frontend/src/APIClients/LogRecordAPIClient.ts @@ -12,7 +12,7 @@ import { } from "../types/LogRecordTypes"; const countLogRecords = async ({ - building = "", + buildingId = [], employeeId = [], attnTo = [], dateRange = [], @@ -28,7 +28,7 @@ const countLogRecords = async ({ const { data } = await baseAPIClient.get(`/log_records/count`, { params: { filters: { - building, + buildingId, employeeId, attnTo, dateRange, @@ -47,7 +47,7 @@ const countLogRecords = async ({ }; const filterLogRecords = async ({ - building = "", + buildingId = [], employeeId = [], attnTo = [], dateRange = [], @@ -66,7 +66,7 @@ const filterLogRecords = async ({ const { data } = await baseAPIClient.get(`/log_records`, { params: { filters: { - building, + buildingId, employeeId, attnTo, dateRange, @@ -94,7 +94,7 @@ const createLog = async ({ flagged, note, tags, - building, + buildingId, attnTo, }: CreateLogRecordParams): Promise => { try { @@ -111,7 +111,7 @@ const createLog = async ({ flagged, note, tags, - building, + buildingId, attnTo, }, { headers: { Authorization: bearerToken } }, @@ -145,7 +145,7 @@ const editLogRecord = async ({ flagged, note, tags, - building, + buildingId, attnTo, }: EditLogRecordParams): Promise => { try { @@ -163,7 +163,7 @@ const editLogRecord = async ({ note, attnTo, tags, - building, + buildingId, }, { headers: { Authorization: bearerToken } }, ); diff --git a/frontend/src/APIClients/ResidentAPIClient.ts b/frontend/src/APIClients/ResidentAPIClient.ts index 069c2d6a..df95a686 100644 --- a/frontend/src/APIClients/ResidentAPIClient.ts +++ b/frontend/src/APIClients/ResidentAPIClient.ts @@ -5,6 +5,7 @@ import { GetResidentsReponse, CountResidentsResponse, CreateResidentParams, + EditResidentParams, } from "../types/ResidentTypes"; import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; import baseAPIClient from "./BaseAPIClient"; @@ -58,7 +59,7 @@ const createResident = async ({ initial, roomNum, dateJoined, - building, + buildingId, }: CreateResidentParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( @@ -67,7 +68,7 @@ const createResident = async ({ )}`; await baseAPIClient.post( "/residents", - { initial, roomNum, dateJoined, building }, + { initial, roomNum, dateJoined, buildingId }, { headers: { Authorization: bearerToken } }, ); return true; @@ -100,9 +101,9 @@ const editResident = async ({ initial, roomNum, dateJoined, - building, + buildingId, dateLeft, -}: Resident): Promise => { +}: EditResidentParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( AUTHENTICATED_USER_KEY, @@ -110,7 +111,7 @@ const editResident = async ({ )}`; await baseAPIClient.put( `/residents/${id}`, - { initial, roomNum, dateJoined, building, dateLeft }, + { initial, roomNum, dateJoined, buildingId, dateLeft }, { headers: { Authorization: bearerToken } }, ); return true; diff --git a/frontend/src/components/auth/Authy.tsx b/frontend/src/components/auth/Authy.tsx index 2bc5b52b..4c4805ba 100644 --- a/frontend/src/components/auth/Authy.tsx +++ b/frontend/src/components/auth/Authy.tsx @@ -1,13 +1,6 @@ import React, { useContext, useState, useRef } from "react"; import { Redirect } from "react-router-dom"; -import { - Box, - Button, - Flex, - Input, - Text, - VStack -} from "@chakra-ui/react"; +import { Box, Button, Flex, Input, Text, VStack } from "@chakra-ui/react"; import authAPIClient from "../../APIClients/AuthAPIClient"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import { HOME_PAGE } from "../../constants/Routes"; @@ -73,10 +66,19 @@ const Authy = ({ return ( <> - + One last step! - In order to protect your account, please enter the authorization code from the Twilio Authy application. + + In order to protect your account, please enter the authorization + code from the Twilio Authy application. + {boxIndexes.map((boxIndex) => { return ( @@ -93,19 +95,17 @@ const Authy = ({ {authCode.length > boxIndex ? authCode[boxIndex] : " "} - ) + ); })} diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 0ed53021..06317325 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -57,9 +57,9 @@ type AlertDataOptions = { // Ideally we should be storing this information in the database const BUILDINGS = [ - { label: "144 Erb St. West", value: "144" }, - { label: "362 Erb St. West", value: "362" }, - { label: "402 Erb St. West", value: "402" }, + { label: "144", value: 1 }, + { label: "362", value: 2 }, + { label: "402", value: 3 }, ]; const ALERT_DATA: AlertDataOptions = { @@ -119,7 +119,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { hour12: false, }), ); - const [building, setBuilding] = useState(""); + const [buildingId, setBuildingId] = useState(-1); const [resident, setResident] = useState(-1); const [tags, setTags] = useState([]); const [attnTo, setAttnTo] = useState(-1); @@ -163,10 +163,10 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }; const handleBuildingChange = ( - selectedOption: SingleValue<{ label: string; value: string }>, + selectedOption: SingleValue<{ label: string; value: number }>, ) => { if (selectedOption !== null) { - setBuilding(selectedOption.value); + setBuildingId(selectedOption.value); } setBuildingError(selectedOption === null); @@ -244,7 +244,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { hour12: false, }), ); - setBuilding(""); + setBuildingId(-1); setResident(-1); setTags([]); setAttnTo(-1); @@ -271,7 +271,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { setEmployeeError(!employee.label); setDateError(date === null); setTimeError(time === ""); - setBuildingError(building === ""); + setBuildingError(buildingId === -1); setResidentError(resident === -1); setNotesError(notes === ""); @@ -280,7 +280,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { !employee.label || date === null || time === "" || - building === "" || + buildingId === -1 || resident === -1 || notes === "" ) { @@ -299,7 +299,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { flagged, note: notes, tags, - building, + buildingId, attnTo: attentionTo, }); if (res != null) { diff --git a/frontend/src/components/forms/CreateResident.tsx b/frontend/src/components/forms/CreateResident.tsx index 10af11e9..361cc460 100644 --- a/frontend/src/components/forms/CreateResident.tsx +++ b/frontend/src/components/forms/CreateResident.tsx @@ -38,9 +38,9 @@ type Props = { // TODO: Connect to Buidings table const BUILDINGS = [ - { label: "144", value: "144" }, - { label: "362", value: "362" }, - { label: "402", value: "402" }, + { label: "144", value: 1 }, + { label: "362", value: 2 }, + { label: "402", value: 3 }, ]; const CreateResident = ({ @@ -51,7 +51,7 @@ const CreateResident = ({ const [initials, setInitials] = useState(""); const [roomNumber, setRoomNumber] = useState(""); const [moveInDate, setMoveInDate] = useState(new Date()); - const [building, setBuilding] = useState(""); + const [buildingId, setBuildingId] = useState(-1); const [initialsError, setInitialsError] = useState(false); const [roomNumberError, setRoomNumberError] = useState(false); @@ -67,7 +67,7 @@ const CreateResident = ({ initial: initials.toUpperCase(), roomNum: parseInt(roomNumber, 10), dateJoined: convertToString(moveInDate), - building, + buildingId, }); getRecords(1); countResidents(); @@ -98,10 +98,10 @@ const CreateResident = ({ }; const handleBuildingChange = ( - selectedOption: SingleValue<{ label: string; value: string }>, + selectedOption: SingleValue<{ label: string; value: number }>, ) => { if (selectedOption !== null) { - setBuilding(selectedOption.value); + setBuildingId(selectedOption.value); setBuildingError(false); } }; @@ -113,7 +113,7 @@ const CreateResident = ({ setInitials(""); setRoomNumber(""); setMoveInDate(new Date()); - setBuilding(""); + setBuildingId(-1); // Reset the error states setInitialsError(false); @@ -132,14 +132,14 @@ const CreateResident = ({ const handleSubmit = () => { setInitialsError(initials.length !== 2); setRoomNumberError(roomNumber.length !== 3); - setBuildingError(building === ""); + setBuildingError(buildingId === -1); // Prevents form submission if any required values are incorrect if ( initials.length !== 2 || roomNumber.length !== 3 || moveInDateError || - building === "" + buildingId === -1 ) { return; } diff --git a/frontend/src/components/forms/EditLog.tsx b/frontend/src/components/forms/EditLog.tsx index 1f5b079a..f7102166 100644 --- a/frontend/src/components/forms/EditLog.tsx +++ b/frontend/src/components/forms/EditLog.tsx @@ -60,9 +60,9 @@ type AlertDataOptions = { // Ideally we should be storing this information in the database const BUILDINGS = [ - { label: "144 Erb St. West", value: "144" }, - { label: "362 Erb St. West", value: "362" }, - { label: "402 Erb St. West", value: "402" }, + { label: "144", value: 1 }, + { label: "362", value: 2 }, + { label: "402", value: 3 }, ]; const ALERT_DATA: AlertDataOptions = { @@ -120,7 +120,7 @@ const EditLog = ({ hour12: false, }), ); - const [building, setBuilding] = useState(""); + const [buildingId, setBuildingId] = useState(-1); const [resident, setResident] = useState(-1); const [tags, setTags] = useState([]); const [attnTo, setAttnTo] = useState(-1); @@ -159,10 +159,10 @@ const EditLog = ({ }; const handleBuildingChange = ( - selectedOption: SingleValue<{ label: string; value: string }>, + selectedOption: SingleValue<{ label: string; value: number }>, ) => { if (selectedOption !== null) { - setBuilding(selectedOption.value); + setBuildingId(selectedOption.value); } setBuildingError(selectedOption === null); @@ -211,13 +211,13 @@ const EditLog = ({ hour12: false, }), ); - setBuilding(logRecord.building); + setBuildingId(logRecord.building.id); const residentId = residentOptions.find( (item) => item.label === logRecord.residentId, )?.value; setResident(residentId !== undefined ? residentId : -1); setTags(logRecord.tags); - setAttnTo(logRecord.attnTo !== undefined ? logRecord.attnTo.id : -1); + setAttnTo(logRecord.attnTo ? logRecord.attnTo.id : -1); setNotes(logRecord.note); setFlagged(logRecord.flagged); @@ -235,7 +235,7 @@ const EditLog = ({ setEmployeeError(!employee.label); setDateError(date === null); setTimeError(time === ""); - setBuildingError(building === ""); + setBuildingError(buildingId === -1); setResidentError(resident === -1); setNotesError(notes === ""); @@ -244,7 +244,7 @@ const EditLog = ({ !employee.label || date === null || time === "" || - building === "" || + buildingId === -1 || resident === -1 || notes === "" ) { @@ -259,7 +259,7 @@ const EditLog = ({ flagged, note: notes, tags, - building, + buildingId, attnTo: attnTo === -1 ? undefined : attnTo, }); if (res) { @@ -348,7 +348,7 @@ const EditLog = ({ onChange={handleBuildingChange} styles={selectStyle} defaultValue={BUILDINGS.find( - (item) => item.value === building, + (item) => item.value === buildingId, )} /> Building is required. diff --git a/frontend/src/components/forms/EditResident.tsx b/frontend/src/components/forms/EditResident.tsx index 0676cf79..6f1d10b9 100644 --- a/frontend/src/components/forms/EditResident.tsx +++ b/frontend/src/components/forms/EditResident.tsx @@ -37,9 +37,9 @@ import { convertToDate, convertToString } from "../../helper/dateHelpers"; // TODO: Connect to Buidings table const BUILDINGS = [ - { label: "144", value: "144" }, - { label: "362", value: "362" }, - { label: "402", value: "402" }, + { label: "144", value: 1 }, + { label: "362", value: 2 }, + { label: "402", value: 3 }, ]; type Props = { @@ -60,7 +60,7 @@ const EditResident = ({ const [initials, setInitials] = useState(""); const [roomNumber, setRoomNumber] = useState(-1); const [moveInDate, setMoveInDate] = useState(new Date()); - const [userBuilding, setUserBuilding] = useState(""); + const [buildingId, setBuildingId] = useState(-1); const [moveOutDate, setMoveOutDate] = useState(); const [initialsError, setInitialsError] = useState(false); @@ -73,11 +73,10 @@ const EditResident = ({ const editResident = async () => { const res = await ResidentAPIClient.editResident({ id: resident.id, - residentId: resident.residentId, initial: initials.toUpperCase(), roomNum: roomNumber, dateJoined: convertToString(moveInDate), - building: userBuilding, + buildingId, dateLeft: moveOutDate ? convertToString(moveOutDate) : undefined, }); @@ -132,10 +131,10 @@ const EditResident = ({ }; const handleBuildingChange = ( - selectedOption: SingleValue<{ label: string; value: string }>, + selectedOption: SingleValue<{ label: string; value: number }>, ) => { if (selectedOption !== null) { - setUserBuilding(selectedOption.value); + setBuildingId(selectedOption.value); setBuildingError(false); } }; @@ -146,7 +145,7 @@ const EditResident = ({ setInitials(resident.initial); setRoomNumber(resident.roomNum); setMoveInDate(convertToDate(resident.dateJoined)); - setUserBuilding(resident.building); + setBuildingId(resident.building.id); setMoveOutDate( resident.dateLeft ? convertToDate(resident.dateLeft) : undefined, ); @@ -170,7 +169,7 @@ const EditResident = ({ setMoveOutDateError(true); return; } - if (userBuilding === "") { + if (buildingId === -1) { setBuildingError(true); return; } @@ -187,7 +186,7 @@ const EditResident = ({ setInitials(resident.initial); setRoomNumber(resident.roomNum); setMoveInDate(convertToDate(resident.dateJoined)); - setUserBuilding(resident.building); + setBuildingId(resident.building.id); setMoveOutDate( resident.dateLeft ? convertToDate(resident.dateLeft) : undefined, ); @@ -249,7 +248,9 @@ const EditResident = ({ Building - Please enter a valid email. - - - - {passwordErrorStr} - + + + Please enter a valid email. + + + + {passwordErrorStr} + - + Not a member yet? @@ -182,4 +178,4 @@ const Login = ({ return <>; }; -export default Login \ No newline at end of file +export default Login; diff --git a/frontend/src/components/forms/Signup.tsx b/frontend/src/components/forms/Signup.tsx index 103680db..bc7cfd4d 100644 --- a/frontend/src/components/forms/Signup.tsx +++ b/frontend/src/components/forms/Signup.tsx @@ -1,7 +1,7 @@ import React, { useContext } from "react"; import { Redirect, useHistory } from "react-router-dom"; import { Box, Button, Flex, Input, Text } from "@chakra-ui/react"; -import authAPIClient from "../../APIClients/AuthAPIClient" +import authAPIClient from "../../APIClients/AuthAPIClient"; import { HOME_PAGE, LOGIN_PAGE } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; import commonApiClient from "../../APIClients/CommonAPIClient"; @@ -138,14 +138,19 @@ const Signup = ({