diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..5368018c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,26 @@ +name: Continuous Integration + +on: [push, pull_request] + +jobs: + CI: + name: Run core tests + runs-on: ubuntu-latest + + steps: + - name: Init job + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.10.12 + cache: 'pip' + + - name: Install Python dependencies + run: pip install -r requirements.txt + + - name: Run core tests + run: python -m pytest src/app/core + + diff --git a/pyproject.toml b/pyproject.toml index 15e069ce..83875998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,9 @@ [tool.ruff] ignore = [ "F403" # undefined-names, +] + +[tool.pytest.ini_options] +env = [ + "ENVIRONMENT=test" ] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index de955934..75f08d01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,8 @@ cowsay==6.1 dnspython==2.6.1 email_validator==2.1.1 et-xmlfile==1.1.0 +exceptiongroup==1.2.1 +Faker==26.0.0 Flask==3.0.2 Flask-APScheduler==1.13.1 Flask-Bcrypt==1.0.1 @@ -24,6 +26,7 @@ greenlet==3.0.3 gunicorn==22.0.0 httplib2==0.22.0 idna==3.7 +iniconfig==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.3 MarkupSafe==2.1.5 @@ -32,17 +35,22 @@ numpy==1.26.4 openpyxl==3.1.2 packaging==24.0 pandas==2.2.1 +pluggy==1.5.0 proto-plus==1.23.0 protobuf==4.25.3 pyasn1==0.6.0 pyasn1_modules==0.4.0 pyparsing==3.1.2 +pytest==8.2.2 +pytest-describe==2.2.0 +pytest-env==1.1.3 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 pytz==2024.1 requests==2.32.3 rsa==4.9 six==1.16.0 +tomli==2.0.1 tzdata==2024.1 tzlocal==5.2 uritemplate==4.1.1 diff --git a/src/app/core/__init__.py b/src/app/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/commons/csv_file.py b/src/app/core/commons/csv_file.py index 2d394d6d..cedb1871 100644 --- a/src/app/core/commons/csv_file.py +++ b/src/app/core/commons/csv_file.py @@ -1,28 +1,34 @@ from typing import Dict, List from werkzeug.datastructures import FileStorage -from core.commons import Error - -from infra.providers.data_analyser_provider import DataAnalyserProvider +from core.interfaces.providers import DataAnalyserProviderInterface +from core.errors.validation import CSVFileNotValidError, CSVColumnsNotValidError class CsvFile: - def __init__(self, csv_file: FileStorage) -> None: - self.csv_file = csv_file - self.data_analyser_provider = DataAnalyserProvider() + def __init__( + self, + file: FileStorage, + data_analyser_provider: DataAnalyserProviderInterface, + ): + if not isinstance(file, FileStorage): + raise CSVFileNotValidError() + + self._file = file + self._data_analyser_provider = data_analyser_provider def read(self): - extension = self.get_extension() + extension = self.__get_extension() if extension in ["csv", "txt"]: - self.data_analyser_provider.read_csv(self.csv_file) + self._data_analyser_provider.read_csv(self._file) elif extension == "xlsx": - self.data_analyser_provider.read_excel(self.csv_file) + self._data_analyser_provider.read_excel(self._file) else: - raise Error("Arquivo CSV inválido") + raise CSVFileNotValidError() def get_records(self) -> List[Dict]: - records = self.data_analyser_provider.convert_to_list_of_records() + records = self._data_analyser_provider.convert_to_list_of_records() records_list = [] for record in records: @@ -30,15 +36,15 @@ def get_records(self) -> List[Dict]: return records_list - def validate_columns(self, columns: List[str]) -> bool: - csv_columns = self.data_analyser_provider.get_columns() + def validate_columns(self, columns: List[str]): + csv_columns = self._data_analyser_provider.get_columns() has_valid_columns = set(map(lambda x: x.lower(), csv_columns)) == set( map(lambda x: x.lower(), columns) ) if not has_valid_columns: - raise Error("As colunas do arquivo CSV não estão corretas") + raise CSVColumnsNotValidError() - def get_extension(self) -> str: - return self.csv_file.filename.split(".")[1] + def __get_extension(self) -> str: + return self._file.filename.split(".")[1] diff --git a/src/app/core/commons/date.py b/src/app/core/commons/date.py index e54e28d5..a9d4e4ad 100644 --- a/src/app/core/commons/date.py +++ b/src/app/core/commons/date.py @@ -1,6 +1,6 @@ from datetime import date, datetime -from core.commons import Error +from core.errors.validation import DateNotValidError class Date: @@ -14,10 +14,12 @@ def __init__(self, value: date): self.value = value except Exception: - raise Error("Valor de data inválido") + raise DateNotValidError def format_value(self): - self.value = self.value.strftime("%d/%m/%Y") + if isinstance(self.value, date): + self.value = self.value.strftime("%d/%m/%Y") + return self def get_value(self, is_date: bool = False) -> str: diff --git a/src/app/core/commons/datetime.py b/src/app/core/commons/datetime.py index 5e3bf3bf..c6142e63 100644 --- a/src/app/core/commons/datetime.py +++ b/src/app/core/commons/datetime.py @@ -1,23 +1,33 @@ from datetime import datetime, time +from core.errors.validation import DatetimeNotValidError + class Datetime: value: datetime def __init__(self, value: datetime): + if not isinstance(value, datetime): + raise DatetimeNotValidError() + self.value = value def format_value(self): - self.value = self.value.strftime("%d/%m/%Y %H:%M") + if isinstance(self.value, datetime): + self.value = self.value.strftime("%d/%m/%Y %H:%M") + return self - def get_time(self) -> time: + def get_time(self): + if isinstance(self.value, str): + return datetime.strptime(self.value, "%d/%m/%Y %H:%M").time() + return time( hour=self.value.hour, minute=self.value.minute, ) - def get_value(self, is_datetime: bool = False) -> str: + def get_value(self, is_datetime: bool = False): if is_datetime: return self.value diff --git a/src/app/core/commons/error.py b/src/app/core/commons/error.py index c9002595..7380328e 100644 --- a/src/app/core/commons/error.py +++ b/src/app/core/commons/error.py @@ -12,5 +12,7 @@ def __init__( self.internal_message = internal_message self.status_code = status_code + super().__init__(ui_message) + cow_say(ui_message) cow_say(internal_message) diff --git a/src/app/core/commons/line_chart.py b/src/app/core/commons/line_chart.py index 8687022c..63bf6e41 100644 --- a/src/app/core/commons/line_chart.py +++ b/src/app/core/commons/line_chart.py @@ -1,44 +1,14 @@ from datetime import timedelta +from dataclasses import asdict -from core.commons.error import Error -from core.entities import Plant +from core.entities.plant import Plant +from core.entities.line_chart_record import LineChartRecord from core.constants import DAYS_RANGES class LineChart: - def __init__(self, records: list[dict], attribute: str): - self.records = [] - - for record in records: - for required_attribute in ["date", "plant_id", attribute]: - if required_attribute not in record: - raise Error("Required attribute not found in record") - - self.records.append( - { - "date": record["date"], - "plant_id": record["plant_id"], - "value": record[attribute], - } - ) - - def filter_records_by_range_of_days(self, days_range: int, plant_id: str): - last_date = self.__get_last_record_date_by_plant(plant_id) - data = [] - - for day in range(days_range - 1, -1, -1): - current_date = last_date - timedelta(days=day) - - for record in self.records: - if record["date"] == current_date: - data.append( - { - **record, - "date": record["date"].strftime("%d/%m/%Y"), - } - ) - - return data + def __init__(self, records: list[LineChartRecord]): + self.records = [asdict(record) for record in records] def get_data(self, plants: list[Plant]): chart_data = { @@ -51,10 +21,13 @@ def get_data(self, plants: list[Plant]): for days_range in DAYS_RANGES: for plant in plants: - days_range_records = self.filter_records_by_range_of_days( + days_range_records = self.__filter_records_by_range_of_days( days_range, plant.id ) + if not len(days_range_records): + continue + values = [] dates = [] @@ -75,6 +48,28 @@ def get_data(self, plants: list[Plant]): return chart_data + def __filter_records_by_range_of_days(self, days_range: int, plant_id: str): + last_date = self.__get_last_record_date_by_plant(plant_id) + + if last_date is None: + return [] + + data = [] + + for day in range(days_range - 1, -1, -1): + current_date = last_date - timedelta(days=day) + + for record in self.records: + if record["date"] == current_date: + data.append( + { + **record, + "date": record["date"].strftime("%d/%m/%Y"), + } + ) + + return data + def __get_last_record_date_by_plant(self, plant_id: str): for index in range(1, len(self.records) + 1): record = self.records[-index] diff --git a/src/app/core/commons/ordered_plants.py b/src/app/core/commons/ordered_plants.py index f9078f99..f43187a1 100644 --- a/src/app/core/commons/ordered_plants.py +++ b/src/app/core/commons/ordered_plants.py @@ -1,14 +1,15 @@ -from core.entities import Plant +from core.entities.plant import Plant class OrderedPlants: - value: list[Plant] + value: list[Plant] = [] - def __init__(self, plants: list[Plant], active_plant_id: str) -> None: + def __init__(self, plants: list[Plant], active_plant_id: str): self.value = self.__order(plants, active_plant_id) def __order(self, plants: list[Plant], active_plant_id: str): - if len(plants) == 1: + plants_count = len(plants) + if plants_count == 0 or plants_count == 1: return plants filtered_plants_by_id = filter( diff --git a/src/app/core/commons/pagination.py b/src/app/core/commons/pagination.py index 33dcbb30..6cca5b74 100644 --- a/src/app/core/commons/pagination.py +++ b/src/app/core/commons/pagination.py @@ -1,10 +1,14 @@ from math import ceil from core.constants import PAGINATION +from core.errors.validation import PageNumberNotValidError class Pagination: - def __init__(self, page_number: int, records_count: int) -> None: + def __init__(self, page_number: int, records_count: int): + if not isinstance(page_number, int) or not isinstance(records_count, int): + raise PageNumberNotValidError() + self.page_number = page_number self.records_count = records_count diff --git a/src/app/core/commons/records_filters.py b/src/app/core/commons/records_filters.py index 8e79cfae..55fea3cf 100644 --- a/src/app/core/commons/records_filters.py +++ b/src/app/core/commons/records_filters.py @@ -1,4 +1,5 @@ from core.commons import Date +from core.errors.validation import DateNotValidError class RecordsFilters: @@ -18,11 +19,14 @@ def __handle_filters(self): if self.plant_id == "all": self.plant_id = None - if self.start_date != "" and isinstance(self.start_date, str): - self.start_date = Date(self.start_date).get_value() + try: + if self.start_date != "" and isinstance(self.start_date, str): + self.start_date = Date(self.start_date).get_value(is_date=True) - if self.end_date != "" and isinstance(self.end_date, str): - self.end_date = Date(self.end_date).get_value() + if self.end_date != "" and isinstance(self.end_date, str): + self.end_date = Date(self.end_date).get_value(is_date=True) + except Exception: + raise DateNotValidError() if self.start_date and (self.end_date is None or self.end_date == ""): self.end_date = self.start_date diff --git a/src/app/core/commons/tests/__init__.py b/src/app/core/commons/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/commons/tests/csv_file_test.py b/src/app/core/commons/tests/csv_file_test.py new file mode 100644 index 00000000..b65acb42 --- /dev/null +++ b/src/app/core/commons/tests/csv_file_test.py @@ -0,0 +1,103 @@ +from pathlib import Path +from werkzeug.datastructures import FileStorage + +from pytest import fixture, raises, mark + +from core.commons.csv_file import CsvFile +from core.errors.validation import CSVFileNotValidError, CSVColumnsNotValidError +from core.use_cases.tests.mocks.providers import DataAnalyserProviderMock + + +def describe_csv_file_common(): + @fixture + def file(tmp_path: Path): + return FileStorage(tmp_path / "fake_csv_.xlsx") + + @fixture + def data_analyser_provider(): + return DataAnalyserProviderMock() + + @fixture + def csv_file(file, data_analyser_provider): + return CsvFile(file, data_analyser_provider) + + def it_should_throw_error_if_file_is_not_valid( + data_analyser_provider: DataAnalyserProviderMock, + ): + with raises(CSVFileNotValidError): + CsvFile(None, data_analyser_provider) + + def it_should_throw_error_if_file_does_not_have_valid_extension( + tmp_path: Path, + data_analyser_provider: DataAnalyserProviderMock, + ): + fake_file = FileStorage(tmp_path / "fake_csv.invalid_extension") + + csv_file = CsvFile(fake_file, data_analyser_provider) + + with raises(CSVFileNotValidError): + csv_file.read() + + def it_should_read_file_as_excel_file( + tmp_path, + data_analyser_provider: DataAnalyserProviderMock, + ): + fake_file = FileStorage(tmp_path / "fake_csv.xlsx") + + csv_file = CsvFile(fake_file, data_analyser_provider) + + csv_file.read() + + file_data = data_analyser_provider.get_data() + + assert file_data == "excel file data" + + @mark.parametrize("extension", ["csv", "txt"]) + def it_should_read_file_as_csv_text_file( + tmp_path: Path, + extension, + data_analyser_provider: DataAnalyserProviderMock, + ): + fake_file = FileStorage(tmp_path / f"fake_csv.{extension}") + + csv_file = CsvFile(fake_file, data_analyser_provider) + + csv_file.read() + + file_data = data_analyser_provider.get_data() + + assert file_data == "csv text file data" + + def it_should_throw_error_if_at_least_one_invalid_column_is_found( + csv_file: CsvFile, + data_analyser_provider: DataAnalyserProviderMock, + ): + csv_file.read() + + fake_columns = ["column 1", "column 2", "column 3"] + + data_analyser_provider.get_columns = lambda: fake_columns + + csv_file.validate_columns(fake_columns) + + with raises(CSVColumnsNotValidError): + csv_file.validate_columns(["column 1", "column 2", "column 4"]) + + def it_should_get_records_with_key_of_each_of_them_as_lowercase( + csv_file: CsvFile, + data_analyser_provider: DataAnalyserProviderMock, + ): + csv_file.read() + + data_analyser_provider.convert_to_list_of_records = lambda: [ + {"KEY 1": "value", "KEY 2": "value", "KEY 3": "value"} + ] + + records = csv_file.get_records() + + keys = [] + + for record in records: + keys.extend(record.keys()) + + assert keys == ["key 1", "key 2", "key 3"] diff --git a/src/app/core/commons/tests/date_test.py b/src/app/core/commons/tests/date_test.py new file mode 100644 index 00000000..4c81474a --- /dev/null +++ b/src/app/core/commons/tests/date_test.py @@ -0,0 +1,29 @@ +from datetime import date + +from pytest import raises + + +from core.commons.date import Date +from core.errors.validation import DateNotValidError + + +def describe_date_common(): + + def it_should_throw_error_if_it_could_not_convert_value_to_date(): + with raises(DateNotValidError): + Date("invalid date value") + + def it_should_convert_value_to_date_if_it_is_not_date(): + assert Date("2024-12-10").get_value(is_date=True) == date( + year=2024, month=12, day=10 + ) + + def it_should_get_value_as_date(): + today = date.today() + + assert today == Date(today).get_value(is_date=True) + + def it_should_get_value_as_string(): + today = date.today() + + assert str(today) == Date(today).get_value(is_date=False) diff --git a/src/app/core/commons/tests/datetime_test.py b/src/app/core/commons/tests/datetime_test.py new file mode 100644 index 00000000..9bd30a32 --- /dev/null +++ b/src/app/core/commons/tests/datetime_test.py @@ -0,0 +1,53 @@ +from datetime import datetime, time + +from pytest import raises + + +from core.commons.datetime import Datetime +from core.errors.validation import DatetimeNotValidError + + +def describe_datetime_common(): + + def it_should_throw_error_if_value_is_not_valid(): + with raises(DatetimeNotValidError): + Datetime("data") + + def it_should_get_value_as_datetime(): + currente_datetime = datetime.now() + + assert currente_datetime == Datetime(currente_datetime).get_value( + is_datetime=True + ) + + def it_should_get_value_datetime_as_string(): + currente_datetime = datetime.now() + + assert str(currente_datetime) == Datetime(currente_datetime).get_value( + is_datetime=False + ) + + def it_should_get_value(): + hour = 12 + minute = 24 + year = 2024 + month = 12 + day = 16 + + currente_datetime = datetime( + hour=hour, minute=minute, year=year, month=month, day=day + ) + + value = Datetime(currente_datetime).format_value().get_value() + + assert f"{day}/{month}/{year} {hour}:{minute}" == value + + def it_should_get_time(): + hour = 12 + minute = 40 + + currente_datetime = datetime( + hour=hour, minute=minute, year=2024, month=3, day=16 + ) + + assert time(hour=hour, minute=minute) == Datetime(currente_datetime).get_time() diff --git a/src/app/core/commons/tests/days_range_test.py b/src/app/core/commons/tests/days_range_test.py new file mode 100644 index 00000000..4e215a52 --- /dev/null +++ b/src/app/core/commons/tests/days_range_test.py @@ -0,0 +1,12 @@ +from core.commons.days_ranges import DaysRange + + +def describe_days_range_common(): + def it_should_return_value(): + days_range_value = DaysRange().get_value() + + assert days_range_value == [ + ("7 days", "7 dias"), + ("30 days", "30 dias"), + ("90 days", "90 dias"), + ] diff --git a/src/app/core/commons/tests/line_chart_test.py b/src/app/core/commons/tests/line_chart_test.py new file mode 100644 index 00000000..80fdc258 --- /dev/null +++ b/src/app/core/commons/tests/line_chart_test.py @@ -0,0 +1,107 @@ +from datetime import date, timedelta + +from pytest import fixture + +from core.commons.line_chart import LineChart +from core.entities.tests.fakers import LineChartRecordsFaker, PlantsFaker + + +def fake_record(plant_id: str, value: int, days_range: date): + current_date = date(year=2024, month=7, day=1) + + return LineChartRecordsFaker.fake( + plant_id=plant_id, value=value, date=current_date - timedelta(days=days_range) + ) + + +def describe_line_chart_common(): + @fixture + def plant(): + return PlantsFaker.fake() + + @fixture + def records(plant): + fake_records_of_last_90_days = [ + fake_record(plant.id, value=90, days_range=90), + fake_record(plant.id, value=90, days_range=52), + fake_record(plant.id, value=90, days_range=40), + ] + fake_records_of_last_30_days = [ + fake_record(plant.id, value=30, days_range=30), + fake_record(plant.id, value=30, days_range=24), + fake_record(plant.id, value=30, days_range=12), + ] + fake_records_of_last_7_days = [ + fake_record(plant.id, value=7, days_range=7), + fake_record(plant.id, value=7, days_range=3), + fake_record(plant.id, value=7, days_range=1), + ] + + fake_records = [] + fake_records.extend(fake_records_of_last_90_days) + fake_records.extend(fake_records_of_last_30_days) + fake_records.extend(fake_records_of_last_7_days) + + return fake_records + + def it_should_return_dates_for_each_days_range(records, plant): + + line_chart = LineChart(records) + + data = line_chart.get_data(plants=[plant]) + + assert data[plant.id]["7 days"]["dates"] == [ + "24/06/2024", + "28/06/2024", + "30/06/2024", + ] + assert data[plant.id]["30 days"]["dates"] == [ + "01/06/2024", + "07/06/2024", + "19/06/2024", + "24/06/2024", + "28/06/2024", + "30/06/2024", + ] + assert data[plant.id]["90 days"]["dates"] == [ + "02/04/2024", + "10/05/2024", + "22/05/2024", + "01/06/2024", + "07/06/2024", + "19/06/2024", + "24/06/2024", + "28/06/2024", + "30/06/2024", + ] + + def it_should_return_values_for_each_days_range(records, plant): + + line_chart = LineChart(records) + + data = line_chart.get_data(plants=[plant]) + + assert data[plant.id]["7 days"]["values"] == [7, 7, 7] + assert data[plant.id]["30 days"]["values"] == [30, 30, 30, 7, 7, 7] + assert data[plant.id]["90 days"]["values"] == [90, 90, 90, 30, 30, 30, 7, 7, 7] + + def it_should_calculate_average_for_each_days_range(records, plant): + line_chart = LineChart(records) + + data = line_chart.get_data(plants=[plant]) + + assert round(data[plant.id]["7 days"]["average"], 2) == 7.00 + assert round(data[plant.id]["30 days"]["average"], 2) == 18.5 + assert round(data[plant.id]["90 days"]["average"], 2) == 42.33 + + def it_should_get_data_for_each_plant(records, plant): + fake_plants = [plant] + line_chart = LineChart(records) + + other_fake_plants = PlantsFaker.fake_many(3) + fake_plants.extend(other_fake_plants) + + data = line_chart.get_data(plants=fake_plants) + + for fake_plant in fake_plants: + assert fake_plant.id in data diff --git a/src/app/core/commons/tests/ordered_plants_test.py b/src/app/core/commons/tests/ordered_plants_test.py new file mode 100644 index 00000000..9611c8e4 --- /dev/null +++ b/src/app/core/commons/tests/ordered_plants_test.py @@ -0,0 +1,27 @@ +from core.commons.ordered_plants import OrderedPlants +from core.entities.tests.fakers import PlantsFaker + + +def describe_ordered_plants_common(): + + def it_should_not_order_if_has_only_one_plant_or_none(): + fake_plant = PlantsFaker.fake() + + ordered_plants = OrderedPlants([fake_plant], active_plant_id=fake_plant.id) + assert ordered_plants.get_value() == [fake_plant] + + ordered_plants = OrderedPlants([], active_plant_id=None) + assert ordered_plants.get_value() == [] + + def it_should_order_putting_the_active_plant_at_the_beginning_of_the_list(): + fake_plants = PlantsFaker.fake_many(5) + active_fake_plant = PlantsFaker.fake() + + fake_plants.append(active_fake_plant) + + ordered_plants = OrderedPlants( + plants=fake_plants, active_plant_id=active_fake_plant.id + ) + + assert len(ordered_plants.get_value()) == 6 + assert ordered_plants.get_value()[0] == active_fake_plant diff --git a/src/app/core/commons/tests/pagination_test.py b/src/app/core/commons/tests/pagination_test.py new file mode 100644 index 00000000..3dd7417a --- /dev/null +++ b/src/app/core/commons/tests/pagination_test.py @@ -0,0 +1,30 @@ +from pytest import raises + +from core.commons.pagination import Pagination +from core.errors.validation import PageNumberNotValidError + + +def describe_pagination_common(): + def it_should_throw_error_if_page_number_records_count_are_not_integers(): + with raises(PageNumberNotValidError): + Pagination(page_number=None, records_count=None) + + def it_should_get_the_current_and_last_page_numbers(): + pagination = Pagination(page_number=2, records_count=24) + + current_page_number, last_page_number = ( + pagination.get_current_and_last_page_numbers() + ) + + assert last_page_number == 4 + assert current_page_number == 2 + + def it_should_ensure_the_page_number_does_not_exceed_the_max_of_possible_pages(): + pagination = Pagination(page_number=100, records_count=12) + + current_page_number, last_page_number = ( + pagination.get_current_and_last_page_numbers() + ) + + assert last_page_number == 2 + assert current_page_number == 2 diff --git a/src/app/core/commons/tests/records_filters_test.py b/src/app/core/commons/tests/records_filters_test.py new file mode 100644 index 00000000..25d3fb27 --- /dev/null +++ b/src/app/core/commons/tests/records_filters_test.py @@ -0,0 +1,27 @@ +from pytest import raises + +from core.commons import RecordsFilters, Date +from core.errors.validation import DateNotValidError + + +def describe_records_filter_common(): + + def it_should_throw_error_if_it_could_not_convert_start_date_value_to_date(): + with raises(DateNotValidError): + RecordsFilters(plant_id=None, start_date="invalid date", end_date=None) + + def it_should_throw_error_if_it_could_not_convert_end_date_value_to_date(): + with raises(DateNotValidError): + RecordsFilters(plant_id=None, start_date=None, end_date="invalid date") + + def it_should_set_plant_id_to_none_if_it_is_equal_to_all(): + filters = RecordsFilters(plant_id="all", start_date=None, end_date=None) + + assert filters.plant_id is None + + def it_should_set_end_date_to_start_date_if_start_date_is_valid_but_end_date_is_not(): + start_date = "2024-12-12" + + filters = RecordsFilters(plant_id=None, start_date=start_date, end_date=None) + + assert filters.end_date == Date(start_date).get_value(is_date=True) diff --git a/src/app/core/commons/tests/weekday_test.py b/src/app/core/commons/tests/weekday_test.py new file mode 100644 index 00000000..1ea46762 --- /dev/null +++ b/src/app/core/commons/tests/weekday_test.py @@ -0,0 +1,71 @@ +from datetime import date + +from pytest import raises + +from core.commons.weekday import Weekday +from core.errors.validation import DateNotValidError + + +def describe_weekday_common(): + + def it_should_throw_error_if_date_value_is_not_valid(): + with raises(DateNotValidError): + Weekday("invalid date value") + + def it_should_get_value(): + sunday_date = date( + year=2024, + month=6, + day=30, + ) + monday_date = date( + year=2024, + month=7, + day=1, + ) + tuesday_date = date( + year=2024, + month=7, + day=2, + ) + wednesday_date = date( + year=2024, + month=7, + day=3, + ) + thursday_date = date( + year=2024, + month=7, + day=4, + ) + friday_date = date( + year=2024, + month=7, + day=5, + ) + saturday_date = date( + year=2024, + month=7, + day=6, + ) + + weekday = Weekday(sunday_date) + assert weekday.get_value() == "domingo" + + weekday = Weekday(monday_date) + assert weekday.get_value() == "segunda" + + weekday = Weekday(tuesday_date) + assert weekday.get_value() == "terça" + + weekday = Weekday(wednesday_date) + assert weekday.get_value() == "quarta" + + weekday = Weekday(thursday_date) + assert weekday.get_value() == "quinta" + + weekday = Weekday(friday_date) + assert weekday.get_value() == "sexta" + + weekday = Weekday(saturday_date) + assert weekday.get_value() == "sábado" diff --git a/src/app/core/commons/weekday.py b/src/app/core/commons/weekday.py index 19877eb2..7d8e531d 100644 --- a/src/app/core/commons/weekday.py +++ b/src/app/core/commons/weekday.py @@ -3,9 +3,14 @@ from core.constants import WEEKDAYS +from core.errors.validation import DateNotValidError + class Weekday: def __init__(self, value: date): + if not isinstance(value, date): + raise DateNotValidError() + self.value = WEEKDAYS[day_name[value.weekday()].lower()] def get_value(self): diff --git a/src/app/core/entities/__init__.py b/src/app/core/entities/__init__.py index 7b14a6e3..5f4b1234 100644 --- a/src/app/core/entities/__init__.py +++ b/src/app/core/entities/__init__.py @@ -1,4 +1,5 @@ from .checklist_record import * from .sensors_record import * +from .line_chart_record import * from .plant import * from .user import * diff --git a/src/app/core/entities/checklist_record.py b/src/app/core/entities/checklist_record.py index bfc30d72..e67a8cac 100644 --- a/src/app/core/entities/checklist_record.py +++ b/src/app/core/entities/checklist_record.py @@ -18,7 +18,7 @@ class CheckListRecord(Entity): leaf_appearance: str = None leaf_color: str = None plantation_type: str = None - fertilizer_expiration_date: Date = None report: str = None + fertilizer_expiration_date: Date = None created_at: Datetime = None plant: Plant = None diff --git a/src/app/core/entities/line_chart_record.py b/src/app/core/entities/line_chart_record.py new file mode 100644 index 00000000..d7569c8c --- /dev/null +++ b/src/app/core/entities/line_chart_record.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from datetime import date as date_value + +from .entity import Entity + + +@dataclass +class LineChartRecord(Entity): + date: date_value = None + value: int = None + plant_id: str = None diff --git a/src/app/core/entities/tests/fakers/__init__.py b/src/app/core/entities/tests/fakers/__init__.py new file mode 100644 index 00000000..b4e1a15e --- /dev/null +++ b/src/app/core/entities/tests/fakers/__init__.py @@ -0,0 +1,5 @@ +from .plants_faker import * +from .line_chart_records_faker import * +from .sensors_records_faker import * +from .checklist_records_faker import * +from .users_faker import * diff --git a/src/app/core/entities/tests/fakers/base_faker.py b/src/app/core/entities/tests/fakers/base_faker.py new file mode 100644 index 00000000..23d46a08 --- /dev/null +++ b/src/app/core/entities/tests/fakers/base_faker.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod + + +class BaseFaker(ABC): + _base_fake_data = None + + @classmethod + @abstractmethod + def fake(cls, **base_fake_data): ... + + @classmethod + @abstractmethod + def fake_many(cls, count: int = 10): + return [cls.fake() for _ in range(count)] + + @classmethod + @abstractmethod + def _fake_attribute(cls, attribute, fallback): + if cls._base_fake_data is None: + return + + if attribute in cls._base_fake_data: + return cls._base_fake_data[attribute] + else: + return fallback diff --git a/src/app/core/entities/tests/fakers/checklist_records_faker.py b/src/app/core/entities/tests/fakers/checklist_records_faker.py new file mode 100644 index 00000000..570457e7 --- /dev/null +++ b/src/app/core/entities/tests/fakers/checklist_records_faker.py @@ -0,0 +1,42 @@ +from faker import Faker + +from core.entities import CheckListRecord +from core.commons import Date, Datetime + +from .base_faker import BaseFaker +from .plants_faker import PlantsFaker + + +class ChecklistRecordsFaker(BaseFaker): + @classmethod + def fake(cls, **base_fake_data): + faker = Faker() + + if base_fake_data is not None: + cls._base_fake_data = base_fake_data + + return CheckListRecord( + id=cls._fake_attribute("id", faker.uuid4()), + soil_ph=cls._fake_attribute( + "soil_ph", faker.pyint(min_value=0, max_value=100) + ), + water_consumption=cls._fake_attribute( + "water_consumption", faker.pyint(min_value=0, max_value=100) + ), + lai=cls._fake_attribute("lai", faker.pyfloat(min_value=0.0)), + temperature=cls._fake_attribute( + "temperature", faker.pyfloat(min_value=-273) + ), + illuminance=cls._fake_attribute( + "illuminance", faker.pyfloat(min_value=0.0) + ), + leaf_appearance=cls._fake_attribute("leaf_appearance", faker.pystr()), + leaf_color=cls._fake_attribute("leaf_color", faker.pystr()), + plantation_type=cls._fake_attribute("plantation_type", faker.pystr()), + report=cls._fake_attribute("report", faker.pystr()), + fertilizer_expiration_date=cls._fake_attribute( + "fertilizer_expiration_date", Date(faker.date_this_year()) + ), + created_at=cls._fake_attribute("created_at", Datetime(faker.date_time())), + plant=cls._fake_attribute("plant", PlantsFaker.fake()), + ) diff --git a/src/app/core/entities/tests/fakers/line_chart_records_faker.py b/src/app/core/entities/tests/fakers/line_chart_records_faker.py new file mode 100644 index 00000000..3a472f5f --- /dev/null +++ b/src/app/core/entities/tests/fakers/line_chart_records_faker.py @@ -0,0 +1,21 @@ +from faker import Faker + +from core.entities import LineChartRecord + +from .base_faker import BaseFaker + + +class LineChartRecordsFaker(BaseFaker): + @classmethod + def fake(cls, **base_fake_data): + faker = Faker() + + if base_fake_data is not None: + cls._base_fake_data = base_fake_data + + return LineChartRecord( + id=cls._fake_attribute("id", faker.uuid4()), + date=cls._fake_attribute("date", faker.date_this_month()), + value=cls._fake_attribute("value", faker.pyint(min_value=0)), + plant_id=cls._fake_attribute("plant_id", faker.uuid4()), + ) diff --git a/src/app/core/entities/tests/fakers/plants_faker.py b/src/app/core/entities/tests/fakers/plants_faker.py new file mode 100644 index 00000000..1bb4ff04 --- /dev/null +++ b/src/app/core/entities/tests/fakers/plants_faker.py @@ -0,0 +1,20 @@ +from faker import Faker + +from core.entities import Plant + +from .base_faker import BaseFaker + + +class PlantsFaker(BaseFaker): + @classmethod + def fake(cls, **base_fake_data): + faker = Faker() + + if base_fake_data is not None: + cls._base_fake_data = base_fake_data + + return Plant( + id=cls._fake_attribute("id", faker.uuid4()), + name=cls._fake_attribute("name", faker.name_nonbinary()), + hex_color=cls._fake_attribute("hex_color", faker.hex_color()), + ) diff --git a/src/app/core/entities/tests/fakers/sensors_records_faker.py b/src/app/core/entities/tests/fakers/sensors_records_faker.py new file mode 100644 index 00000000..db44bb74 --- /dev/null +++ b/src/app/core/entities/tests/fakers/sensors_records_faker.py @@ -0,0 +1,35 @@ +from faker import Faker + +from core.entities import SensorsRecord +from core.commons import Datetime, Weekday + +from .base_faker import BaseFaker +from .plants_faker import PlantsFaker + + +class SensorsRecordsFaker(BaseFaker): + @classmethod + def fake(cls, **base_fake_data): + faker = Faker() + + if base_fake_data is not None: + cls._base_fake_data = base_fake_data + + return SensorsRecord( + id=cls._fake_attribute("id", faker.uuid4()), + soil_humidity=cls._fake_attribute( + "soil_humidity", faker.pyint(min_value=0, max_value=100) + ), + ambient_humidity=cls._fake_attribute( + "ambient_humidity", faker.pyint(min_value=0, max_value=100) + ), + water_volume=cls._fake_attribute( + "water_volume", faker.pyfloat(min_value=0.0) + ), + temperature=cls._fake_attribute( + "temperature", faker.pyfloat(min_value=-273) + ), + created_at=cls._fake_attribute("created_at", Datetime(faker.date_time())), + weekday=cls._fake_attribute("weekday", Weekday(faker.date_this_year())), + plant=cls._fake_attribute("plant", PlantsFaker.fake()), + ) diff --git a/src/app/core/entities/tests/fakers/users_faker.py b/src/app/core/entities/tests/fakers/users_faker.py new file mode 100644 index 00000000..19a47046 --- /dev/null +++ b/src/app/core/entities/tests/fakers/users_faker.py @@ -0,0 +1,21 @@ +from faker import Faker + +from core.entities import User + +from .base_faker import BaseFaker + + +class UsersFaker(BaseFaker): + @classmethod + def fake(cls, **base_fake_data): + faker = Faker() + + if base_fake_data is not None: + cls._base_fake_data = base_fake_data + + return User( + id=cls._fake_attribute("id", faker.uuid4()), + email=cls._fake_attribute("email", faker.email()), + password=cls._fake_attribute("password", faker.password()), + active_plant_id=cls._fake_attribute("active_plant_id", faker.uuid4()), + ) diff --git a/src/app/core/errors/authentication/__init__.py b/src/app/core/errors/authentication/__init__.py new file mode 100644 index 00000000..6f9a5a24 --- /dev/null +++ b/src/app/core/errors/authentication/__init__.py @@ -0,0 +1,12 @@ +from .user_email_not_valid_error import * +from .email_template_not_valid_error import * +from .sender_password_not_valid_error import * +from .admin_user_email_not_matched_error import * +from .new_password_not_valid_error import * +from .credentials_not_valid_error import * +from .user_not_found_error import * +from .cookie_expired_error import * +from .token_not_valid_error import * +from .user_email_not_valid_error import * +from .user_not_valid_error import * +from .logout_failed_error import * diff --git a/src/app/core/errors/authentication/admin_user_email_not_matched_error.py b/src/app/core/errors/authentication/admin_user_email_not_matched_error.py new file mode 100644 index 00000000..ce88e11e --- /dev/null +++ b/src/app/core/errors/authentication/admin_user_email_not_matched_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class AdminUserEmailNotMatchedError(BaseError): + ui_message = "E-mail fornecido não é o e-mail do administrador" + internal_message = "User email is not equal to admin user email" + status_code = 500 diff --git a/src/app/core/errors/authentication/cookie_expired_error.py b/src/app/core/errors/authentication/cookie_expired_error.py new file mode 100644 index 00000000..2a874650 --- /dev/null +++ b/src/app/core/errors/authentication/cookie_expired_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class CookieExpiredError(BaseError): + ui_message = "Seu token Expirou!, reenvie novamente para o email!" + internal_message = "Client token has expired" + status_code = 401 diff --git a/src/app/core/errors/authentication/credentials_not_valid_error.py b/src/app/core/errors/authentication/credentials_not_valid_error.py new file mode 100644 index 00000000..5b174c98 --- /dev/null +++ b/src/app/core/errors/authentication/credentials_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class CredentialsNotValidError(BaseError): + ui_message = "E-mail ou senha incorretos" + internal_message = "E-mail or password invalid" + status_code = 401 diff --git a/src/app/core/errors/authentication/email_template_not_valid_error.py b/src/app/core/errors/authentication/email_template_not_valid_error.py new file mode 100644 index 00000000..97936c39 --- /dev/null +++ b/src/app/core/errors/authentication/email_template_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class EmailTemplateNotValidError(BaseError): + ui_message = "Template de e-mail não fornecido" + internal_message = "Email template is not provided" + status_code = 500 diff --git a/src/app/core/errors/authentication/logout_failed_error.py b/src/app/core/errors/authentication/logout_failed_error.py new file mode 100644 index 00000000..b6e738bf --- /dev/null +++ b/src/app/core/errors/authentication/logout_failed_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class LogoutFailedError(BaseError): + ui_message = "Erro interno ao fazer logout" + internal_message = "Failed to logout" + status_code = 500 diff --git a/src/app/core/errors/authentication/new_password_not_valid_error.py b/src/app/core/errors/authentication/new_password_not_valid_error.py new file mode 100644 index 00000000..30611f5c --- /dev/null +++ b/src/app/core/errors/authentication/new_password_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class NewPasswordNotValidError(BaseError): + ui_message = "Nova senha não é válida" + internal_message = "New password is not valid" + status_code = 400 diff --git a/src/app/core/errors/authentication/sender_password_not_valid_error.py b/src/app/core/errors/authentication/sender_password_not_valid_error.py new file mode 100644 index 00000000..0794a9b7 --- /dev/null +++ b/src/app/core/errors/authentication/sender_password_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class SenderPasswordNotValidError(BaseError): + ui_message = "Senha necessária para enviar o email não fornecida" + internal_message = "Email sender password is not provided" + status_code = 500 diff --git a/src/app/core/errors/authentication/token_not_valid_error.py b/src/app/core/errors/authentication/token_not_valid_error.py new file mode 100644 index 00000000..47e18f13 --- /dev/null +++ b/src/app/core/errors/authentication/token_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class TokenNotValidError(BaseError): + ui_message = "Seu token Expirou!, reenvie novamente para o email!" + internal_message = "Token for authentication is invalid" + status_code = 401 diff --git a/src/app/core/errors/authentication/user_email_not_valid_error.py b/src/app/core/errors/authentication/user_email_not_valid_error.py new file mode 100644 index 00000000..e54f470e --- /dev/null +++ b/src/app/core/errors/authentication/user_email_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class UserEmailNotValidError(BaseError): + ui_message = "E-mail de usuário não fornecido" + internal_message = "User email is not provided" + status_code = 500 diff --git a/src/app/core/errors/authentication/user_not_found_error.py b/src/app/core/errors/authentication/user_not_found_error.py new file mode 100644 index 00000000..1721243f --- /dev/null +++ b/src/app/core/errors/authentication/user_not_found_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class UserNotFoundError(BaseError): + ui_message = "Usuário não encontrado" + internal_message = "User not found" + status_code = 404 diff --git a/src/app/core/errors/authentication/user_not_valid_error.py b/src/app/core/errors/authentication/user_not_valid_error.py new file mode 100644 index 00000000..43169a22 --- /dev/null +++ b/src/app/core/errors/authentication/user_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class UserNotValidError(BaseError): + ui_message = "Usuário não fornecido" + internal_message = "User not provided" + status_code = 500 diff --git a/src/app/core/errors/base_error.py b/src/app/core/errors/base_error.py new file mode 100644 index 00000000..544a7e5b --- /dev/null +++ b/src/app/core/errors/base_error.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from cowsay import func as cow_say + + +@dataclass +class BaseError(Exception, ABC): + ui_message: str = "Pultz, algo deu errado" + internal_message: str = "Internal Server Error" + status_code: int = 500 + + def __post_init__( + self, + ): + super().__init__(self.ui_message) + self.print_error() + + @abstractmethod + def print_error(self): + cow_say(self.internal_message) diff --git a/src/app/core/errors/checklist_records/__init__.py b/src/app/core/errors/checklist_records/__init__.py new file mode 100644 index 00000000..2703fc44 --- /dev/null +++ b/src/app/core/errors/checklist_records/__init__.py @@ -0,0 +1 @@ +from .checklist_record_not_found_error import * diff --git a/src/app/core/errors/checklist_records/checklist_record_not_found_error.py b/src/app/core/errors/checklist_records/checklist_record_not_found_error.py new file mode 100644 index 00000000..afc3c61c --- /dev/null +++ b/src/app/core/errors/checklist_records/checklist_record_not_found_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class ChecklistRecordNotFoundError(BaseError): + ui_message: str = "Registro check-list não encontrado" + internal_message: str = "Checklist record not found" + status_code: int = 404 diff --git a/src/app/core/errors/forms/__init__.py b/src/app/core/errors/forms/__init__.py new file mode 100644 index 00000000..a0e499ac --- /dev/null +++ b/src/app/core/errors/forms/__init__.py @@ -0,0 +1 @@ +from .invalid_form_data_error import * diff --git a/src/app/core/errors/forms/invalid_form_data_error.py b/src/app/core/errors/forms/invalid_form_data_error.py new file mode 100644 index 00000000..a2c478f0 --- /dev/null +++ b/src/app/core/errors/forms/invalid_form_data_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class InvalidFormDataError(BaseError): + ui_message: str = "Formulário inválido" + internal_message: str = "Invalid form data" + status_code: int = 400 diff --git a/src/app/core/errors/plants/__init__.py b/src/app/core/errors/plants/__init__.py new file mode 100644 index 00000000..6e36f63a --- /dev/null +++ b/src/app/core/errors/plants/__init__.py @@ -0,0 +1,4 @@ +from .plant_name_already_in_use_error import * +from .plant_id_not_valid_error import * +from .plant_not_found_error import * +from .plant_name_not_valid_error import * diff --git a/src/app/core/errors/plants/plant_id_not_valid_error.py b/src/app/core/errors/plants/plant_id_not_valid_error.py new file mode 100644 index 00000000..48f082e2 --- /dev/null +++ b/src/app/core/errors/plants/plant_id_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class PlantIdNotValidError(BaseError): + ui_message = "Planta inválida" + internal_message = "Plant id is not valid" + status_code = 400 diff --git a/src/app/core/errors/plants/plant_name_already_in_use_error.py b/src/app/core/errors/plants/plant_name_already_in_use_error.py new file mode 100644 index 00000000..2978acce --- /dev/null +++ b/src/app/core/errors/plants/plant_name_already_in_use_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class PlantNameAlreadyInUseError(BaseError): + ui_message = "Nome de planta já utilizada" + internal_message = "Plant name already in use" + status_code = 409 diff --git a/src/app/core/errors/plants/plant_name_not_valid_error.py b/src/app/core/errors/plants/plant_name_not_valid_error.py new file mode 100644 index 00000000..24299956 --- /dev/null +++ b/src/app/core/errors/plants/plant_name_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class PlantNameNotValidError(BaseError): + ui_message = "Nome de planta inválido" + internal_message = "Plant name is not valid" + status_code = 400 diff --git a/src/app/core/errors/plants/plant_not_found_error.py b/src/app/core/errors/plants/plant_not_found_error.py new file mode 100644 index 00000000..ed22e9fc --- /dev/null +++ b/src/app/core/errors/plants/plant_not_found_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class PlantNotFoundError(BaseError): + ui_message: str = "Planta não encontrada" + internal_message: str = "Plant is not found" + status_code: int = 404 diff --git a/src/app/core/errors/sensors_records/__init__.py b/src/app/core/errors/sensors_records/__init__.py new file mode 100644 index 00000000..ca7a941b --- /dev/null +++ b/src/app/core/errors/sensors_records/__init__.py @@ -0,0 +1 @@ +from .sensors_record_not_found_error import * diff --git a/src/app/core/errors/sensors_records/sensors_record_not_found_error.py b/src/app/core/errors/sensors_records/sensors_record_not_found_error.py new file mode 100644 index 00000000..798094ca --- /dev/null +++ b/src/app/core/errors/sensors_records/sensors_record_not_found_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class SensorsRecordNotFoundError(BaseError): + ui_message: str = "Registro dos sensores não encontrado" + internal_message: str = "Sensors record not found" + status_code: int = 404 diff --git a/src/app/core/errors/validation/__init__.py b/src/app/core/errors/validation/__init__.py new file mode 100644 index 00000000..bd75795a --- /dev/null +++ b/src/app/core/errors/validation/__init__.py @@ -0,0 +1,8 @@ +from .checklist_record_not_valid_error import * +from .csv_columns_not_valid_error import * +from .csv_file_not_valid_error import * +from .datetime_not_valid_error import * +from .date_not_valid_error import * +from .sensors_record_not_valid_error import * +from .page_number_not_valid_error import * +from .hour_not_valid_error import * diff --git a/src/app/core/errors/validation/checklist_record_not_valid_error.py b/src/app/core/errors/validation/checklist_record_not_valid_error.py new file mode 100644 index 00000000..9e156965 --- /dev/null +++ b/src/app/core/errors/validation/checklist_record_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class ChecklistRecordNotValidError(BaseError): + ui_message = "Registro check-list não válido" + internal_message = "Checklist Record is not valid" + status_code = 400 diff --git a/src/app/core/errors/validation/csv_columns_not_valid_error.py b/src/app/core/errors/validation/csv_columns_not_valid_error.py new file mode 100644 index 00000000..7ccd80f2 --- /dev/null +++ b/src/app/core/errors/validation/csv_columns_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class CSVColumnsNotValidError(BaseError): + ui_message = "As colunas do arquivo CSV não estão corretas" + internal_message = "CSV file columns are not valid" + status_code = 400 diff --git a/src/app/core/errors/validation/csv_file_not_valid_error.py b/src/app/core/errors/validation/csv_file_not_valid_error.py new file mode 100644 index 00000000..e8a8a476 --- /dev/null +++ b/src/app/core/errors/validation/csv_file_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class CSVFileNotValidError(BaseError): + ui_message = "Arquivo CSV inválido" + internal_message = "CSV file is not valid" + status_code = 400 diff --git a/src/app/core/errors/validation/date_not_valid_error.py b/src/app/core/errors/validation/date_not_valid_error.py new file mode 100644 index 00000000..f902ca41 --- /dev/null +++ b/src/app/core/errors/validation/date_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class DateNotValidError(BaseError): + ui_message: str = "Valor de data não válido" + internal_message: str = "Date value is not valid" + status_code: int = 400 diff --git a/src/app/core/errors/validation/datetime_not_valid_error.py b/src/app/core/errors/validation/datetime_not_valid_error.py new file mode 100644 index 00000000..2e7f2d9e --- /dev/null +++ b/src/app/core/errors/validation/datetime_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class DatetimeNotValidError(BaseError): + ui_message: str = "Valor de data ou hora mal formatado" + internal_message: str = "Datetime value is not valid" + status_code: int = 400 diff --git a/src/app/core/errors/validation/hour_not_valid_error.py b/src/app/core/errors/validation/hour_not_valid_error.py new file mode 100644 index 00000000..ef60ea56 --- /dev/null +++ b/src/app/core/errors/validation/hour_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class HourNotValidError(BaseError): + ui_message = "Hora não é válido" + internal_message = "Hour is not valid" + status_code = 400 diff --git a/src/app/core/errors/validation/page_number_not_valid_error.py b/src/app/core/errors/validation/page_number_not_valid_error.py new file mode 100644 index 00000000..108a22bf --- /dev/null +++ b/src/app/core/errors/validation/page_number_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class PageNumberNotValidError(BaseError): + ui_message = "Número de página inválido" + internal_message = "Page number value is not valid" + status_code = 400 diff --git a/src/app/core/errors/validation/sensors_record_not_valid_error.py b/src/app/core/errors/validation/sensors_record_not_valid_error.py new file mode 100644 index 00000000..cc6a4ef9 --- /dev/null +++ b/src/app/core/errors/validation/sensors_record_not_valid_error.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from ..base_error import BaseError + + +@dataclass +class SensorsRecordNotValidError(BaseError): + ui_message = "Registro dos sensores não válido" + internal_message = "Sensors Record is not valid" + status_code = 400 diff --git a/src/app/core/interfaces/authentication/__init__.py b/src/app/core/interfaces/authentication/__init__.py new file mode 100644 index 00000000..b960ece9 --- /dev/null +++ b/src/app/core/interfaces/authentication/__init__.py @@ -0,0 +1,2 @@ +from .auth_interface import * +from .auth_user_interface import * diff --git a/src/app/core/interfaces/authentication/auth_interface.py b/src/app/core/interfaces/authentication/auth_interface.py new file mode 100644 index 00000000..9d0c2161 --- /dev/null +++ b/src/app/core/interfaces/authentication/auth_interface.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod + +from core.entities import User + +from .auth_user_interface import AuthUserInterface + + +class AuthInterface(ABC): + @abstractmethod + def get_user(self) -> AuthUserInterface: ... + + @abstractmethod + def load_user(self, user_id: str) -> AuthUserInterface: ... + + @abstractmethod + def login(self, user: User, should_remember_user: bool) -> AuthUserInterface: ... + + @abstractmethod + def logout(self) -> None: ... + + @abstractmethod + def check_hash(self, hash: str, text: str) -> bool: ... + + @abstractmethod + def generate_hash(self, text: str) -> str: ... diff --git a/src/app/core/interfaces/authentication/auth_user_interface.py b/src/app/core/interfaces/authentication/auth_user_interface.py new file mode 100644 index 00000000..df986dc2 --- /dev/null +++ b/src/app/core/interfaces/authentication/auth_user_interface.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + +from core.entities import User + + +class AuthUserInterface(User, ABC): + is_active: bool = True + + @abstractmethod + def get_id(self) -> str: ... + + @property + def is_authenticated(self) -> bool: ... diff --git a/src/app/core/interfaces/providers/__init__.py b/src/app/core/interfaces/providers/__init__.py new file mode 100644 index 00000000..fd398bac --- /dev/null +++ b/src/app/core/interfaces/providers/__init__.py @@ -0,0 +1,2 @@ +from .email_provider_interface import * +from .data_analyser_provider_interface import * diff --git a/src/app/core/interfaces/providers/data_analyser_provider_interface.py b/src/app/core/interfaces/providers/data_analyser_provider_interface.py new file mode 100644 index 00000000..4d883ade --- /dev/null +++ b/src/app/core/interfaces/providers/data_analyser_provider_interface.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +from werkzeug.datastructures import FileStorage + + +class DataAnalyserProviderInterface(ABC): + @abstractmethod + def analyse(self, data) -> None: ... + + @abstractmethod + def read_excel(self, file: FileStorage) -> None: ... + + @abstractmethod + def read_csv(self, file: FileStorage) -> None: ... + + @abstractmethod + def get_columns(self) -> list[str] | None: ... + + @abstractmethod + def convert_to_excel(self, folder: str, filename: str) -> None: ... + + @abstractmethod + def convert_to_list_of_records(self) -> list[dict] | None: ... + + @abstractmethod + def get_data(self): ... diff --git a/src/app/core/interfaces/providers/email_provider_interface.py b/src/app/core/interfaces/providers/email_provider_interface.py new file mode 100644 index 00000000..538b191d --- /dev/null +++ b/src/app/core/interfaces/providers/email_provider_interface.py @@ -0,0 +1,8 @@ +from abc import ABC, abstractmethod + + +class EmailProvideInterface(ABC): + @abstractmethod + def send_email( + self, sender: str, receiver: str, template: str, password: str + ) -> None: ... diff --git a/src/app/core/interfaces/repositories/__init__.py b/src/app/core/interfaces/repositories/__init__.py new file mode 100644 index 00000000..fd035616 --- /dev/null +++ b/src/app/core/interfaces/repositories/__init__.py @@ -0,0 +1,4 @@ +from .plants_repository_interface import * +from .users_repository_interface import * +from .sensors_record_repository_interface import * +from .checklist_record_repository_interface import * diff --git a/src/app/core/interfaces/repositories/checklist_record_repository_interface.py b/src/app/core/interfaces/repositories/checklist_record_repository_interface.py new file mode 100644 index 00000000..67bb2a57 --- /dev/null +++ b/src/app/core/interfaces/repositories/checklist_record_repository_interface.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod +from datetime import date + +from core.entities import CheckListRecord, LineChartRecord + + +class ChecklistRecordsRepositoryInterface(ABC): + @abstractmethod + def create_checklist_record(self, checklist_record: CheckListRecord): ... + + @abstractmethod + def create_many_checklist_records( + self, checklist_records: list[CheckListRecord] + ) -> None: ... + + @abstractmethod + def delete_checklist_record_by_id(self, id: str) -> None: ... + + @abstractmethod + def delete_many_checklist_records_by_id(self, ids: list[str]) -> None: ... + + @abstractmethod + def get_filtered_checklist_records( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> list[CheckListRecord]: ... + + @abstractmethod + def get_leaf_appearances_and_leaf_colors_records(self) -> list[dict]: ... + + @abstractmethod + def get_lai_records_for_line_charts(self) -> list[LineChartRecord]: ... + + @abstractmethod + def get_checklist_records_count( + self, plant_id: str, start_date: date, end_date: date + ) -> int: ... + + @abstractmethod + def get_checklist_record_by_id(self, id: str) -> CheckListRecord | None: ... diff --git a/src/app/core/interfaces/repositories/plants_repository_interface.py b/src/app/core/interfaces/repositories/plants_repository_interface.py new file mode 100644 index 00000000..a9a804fd --- /dev/null +++ b/src/app/core/interfaces/repositories/plants_repository_interface.py @@ -0,0 +1,14 @@ +from abc import ABC + +from core.entities import Plant + + +class PlantsRepositoryInterface(ABC): + def create_plant(self, plants_record: Plant) -> Plant: ... + def get_plants(self) -> list[Plant]: ... + def get_plant_by_id(self, id: str) -> Plant | None: ... + def get_plant_by_name(self, name: str) -> Plant | None: ... + def get_last_plant(self) -> Plant | None: ... + def filter_plants_by_name(self, plant_name: str) -> list[Plant]: ... + def update_plant_by_id(self, plant: Plant) -> None: ... + def delete_plant_by_id(self, id: str) -> None: ... diff --git a/src/app/core/interfaces/repositories/sensors_record_repository_interface.py b/src/app/core/interfaces/repositories/sensors_record_repository_interface.py new file mode 100644 index 00000000..838adb06 --- /dev/null +++ b/src/app/core/interfaces/repositories/sensors_record_repository_interface.py @@ -0,0 +1,44 @@ +from abc import ABC, abstractmethod +from datetime import date + +from core.entities import SensorsRecord, LineChartRecord + + +class SensorRecordsRepositoryInterface(ABC): + @abstractmethod + def create_sensors_record(self, sensors_record: SensorsRecord) -> None: ... + + @abstractmethod + def create_many_sensors_records( + self, sensors_records: list[SensorsRecord] + ) -> None: ... + + @abstractmethod + def get_sensor_records_for_line_charts( + self, + ) -> list[dict[str, LineChartRecord]]: ... + + @abstractmethod + def get_last_sensors_records(self, count) -> list[SensorsRecord]: ... + + @abstractmethod + def get_sensors_record_by_id(self, id: str) -> SensorsRecord | None: ... + + @abstractmethod + def get_sensors_records_count( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> int: ... + + @abstractmethod + def update_sensors_record_by_id(self, sensors_record: SensorsRecord) -> None: ... + + @abstractmethod + def delete_sensors_record_by_id(self, id: str) -> None: ... + + @abstractmethod + def delete_many_sensors_records_by_id(self, ids: list[str]) -> None: ... + + @abstractmethod + def get_filtered_sensors_records( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> list[SensorsRecord]: ... diff --git a/src/app/core/interfaces/repositories/users_repository_interface.py b/src/app/core/interfaces/repositories/users_repository_interface.py new file mode 100644 index 00000000..ac4ddbfb --- /dev/null +++ b/src/app/core/interfaces/repositories/users_repository_interface.py @@ -0,0 +1,13 @@ +from abc import ABC + +from core.entities import User + + +class UsersRepositoryInterface(ABC): + def get_user_by_id( + self, id: str, should_include_password: bool = False + ) -> User | None: ... + def get_user_by_email(self, email: str) -> User | None: ... + def get_user_active_plant_id(self, email: str) -> User | None: ... + def update_password(self, email, new_password: str) -> None: ... + def update_active_plant(self, id: str, plant_id: str) -> None: ... diff --git a/src/app/core/use_cases/__init__.py b/src/app/core/use_cases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/use_cases/authentication/__init__.py b/src/app/core/use_cases/authentication/__init__.py index be1ca849..d374be60 100644 --- a/src/app/core/use_cases/authentication/__init__.py +++ b/src/app/core/use_cases/authentication/__init__.py @@ -1,7 +1,3 @@ -from .login_user import LoginUser -from .request_password_reset import RequestPasswordReset -from .reset_password import ResetPassword - -login_user = LoginUser() -request_password_reset = RequestPasswordReset() -reset_password = ResetPassword() +from .login_user import * +from .request_password_reset import * +from .reset_password import * diff --git a/src/app/core/use_cases/authentication/login_user.py b/src/app/core/use_cases/authentication/login_user.py index f8b4a5be..1df13554 100644 --- a/src/app/core/use_cases/authentication/login_user.py +++ b/src/app/core/use_cases/authentication/login_user.py @@ -1,32 +1,34 @@ -from core.commons import Error - from core.entities import User +from core.errors.authentication import CredentialsNotValidError, UserNotFoundError +from core.interfaces.repositories import UsersRepositoryInterface +from core.interfaces.authentication import AuthInterface from core.constants import ADMIN_USER_EMAIL -from infra.authentication import auth -from infra.repositories import users_repository - class LoginUser: - def execute(self, email: str, password: str, should_remember_user: bool): - try: - if email != ADMIN_USER_EMAIL: - raise Error("E-mail ou senha incorretos", status_code=400) + def __init__( + self, + repository: UsersRepositoryInterface, + auth: AuthInterface, + ): + self._repository = repository + self._auth = auth - user = users_repository.get_user_by_email(ADMIN_USER_EMAIL) + def execute(self, email: str, password: str, should_remember_user: bool): + if email != ADMIN_USER_EMAIL: + raise CredentialsNotValidError() - if not isinstance(user, User): - raise Error("Usuário não encontrado", status_code=500) + user = self._repository.get_user_by_email(ADMIN_USER_EMAIL) - is_password_correct = auth.check_hash(user.password, password) + if not isinstance(user, User): + raise UserNotFoundError() - if not is_password_correct: - raise Error("E-mail ou senha incorretos", status_code=400) + is_password_correct = self._auth.check_hash(user.password, password) - is_login_success = auth.login(user, should_remember_user) + if not is_password_correct: + raise CredentialsNotValidError() - if not is_login_success: - raise Error("E-mail ou senha incorretos", status_code=400) + is_login_success = self._auth.login(user, should_remember_user) - except Error as error: - raise error + if not is_login_success: + raise CredentialsNotValidError() diff --git a/src/app/core/use_cases/authentication/request_password_reset.py b/src/app/core/use_cases/authentication/request_password_reset.py index 4eddab5d..b9be75ca 100644 --- a/src/app/core/use_cases/authentication/request_password_reset.py +++ b/src/app/core/use_cases/authentication/request_password_reset.py @@ -1,21 +1,33 @@ -from os import getenv - -from infra.providers import EmailProvider -from core.commons import Error +from core.interfaces.providers import EmailProvideInterface from core.constants import ADMIN_USER_EMAIL, SUPPORT_USER_EMAIL +from core.errors.authentication import ( + UserEmailNotValidError, + EmailTemplateNotValidError, + SenderPasswordNotValidError, + AdminUserEmailNotMatchedError, +) class RequestPasswordReset: - def execute(self, user_email: str, template_email_string: str): - try: - if user_email != ADMIN_USER_EMAIL: - raise Error("E-mail fornecido não é o e-mail do administrador") - EmailSender = EmailProvider - app_password = getenv("SUPPORT_EMAIL_APP_PASSWORD") - support_email = SUPPORT_USER_EMAIL - EmailSender.send_email( - support_email, user_email, template_email_string, app_password - ) + def __init__(self, email_provider: EmailProvideInterface): + self._email_provider = email_provider + + def execute(self, user_email: str, email_template: str, sender_password: str): + if not isinstance(user_email, str): + raise UserEmailNotValidError() + + if not isinstance(email_template, str): + raise EmailTemplateNotValidError() + + if not isinstance(sender_password, str): + raise SenderPasswordNotValidError() + + if user_email != ADMIN_USER_EMAIL: + raise AdminUserEmailNotMatchedError() - except Error as error: - raise error + self._email_provider.send_email( + sender=SUPPORT_USER_EMAIL, + receiver=user_email, + template=email_template, + password=sender_password, + ) diff --git a/src/app/core/use_cases/authentication/reset_password.py b/src/app/core/use_cases/authentication/reset_password.py index 1765ff7b..61ec8d50 100644 --- a/src/app/core/use_cases/authentication/reset_password.py +++ b/src/app/core/use_cases/authentication/reset_password.py @@ -1,18 +1,24 @@ -from core.commons import Error +from core.errors.authentication import NewPasswordNotValidError +from core.interfaces.repositories import UsersRepositoryInterface +from core.interfaces.authentication import AuthInterface from core.constants import ADMIN_USER_EMAIL -from infra.repositories import users_repository -from infra.authentication import auth - class ResetPassword: + def __init__( + self, + repository: UsersRepositoryInterface, + auth: AuthInterface, + ): + self._repository = repository + self._auth = auth + def execute(self, new_password: str): - try: - password_hash = auth.generate_hash(new_password) + if not isinstance(new_password, str): + raise NewPasswordNotValidError() - users_repository.update_password( - email=ADMIN_USER_EMAIL, new_password=password_hash - ) + password_hash = self._auth.generate_hash(new_password) - except Error as error: - raise error + self._repository.update_password( + email=ADMIN_USER_EMAIL, new_password=password_hash + ) diff --git a/src/app/core/use_cases/authentication/tests/__init__.py b/src/app/core/use_cases/authentication/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/use_cases/authentication/tests/login_user_test.py b/src/app/core/use_cases/authentication/tests/login_user_test.py new file mode 100644 index 00000000..6da6499d --- /dev/null +++ b/src/app/core/use_cases/authentication/tests/login_user_test.py @@ -0,0 +1,118 @@ +from pytest import fixture, raises + +from core.use_cases.authentication import LoginUser +from core.use_cases.tests.mocks.repositories import UsersRepositoryMock +from core.use_cases.tests.mocks.authentication import AuthMock +from core.entities.tests.fakers import UsersFaker +from core.errors.authentication import CredentialsNotValidError, UserNotFoundError +from core.constants import ADMIN_USER_EMAIL + + +def describe_login_user_use_case(): + @fixture + def repository(): + return UsersRepositoryMock() + + @fixture + def auth(): + return AuthMock() + + @fixture + def use_case(repository: UsersRepositoryMock, auth: AuthMock): + repository.clear_users() + + return LoginUser(repository, auth) + + def it_should_throw_an_error_if_provided_email_is_not_equal_to_admin_email( + use_case: LoginUser, + ): + fake_user = UsersFaker.fake() + + with raises(CredentialsNotValidError): + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=False, + ) + + def it_should_throw_an_error_if_no_user_is_found( + use_case: LoginUser, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + + with raises(UserNotFoundError): + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=False, + ) + + def it_should_throw_an_error_if_password_is_incorrect( + repository: UsersRepositoryMock, + auth: AuthMock, + use_case: LoginUser, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + repository.create_user(fake_user) + + auth.check_hash = lambda user, should_remember_user: False + + with raises(CredentialsNotValidError): + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=False, + ) + + def it_should_throw_an_error_if_it_was_impossible_to_login_for_any_reason( + repository: UsersRepositoryMock, + auth: AuthMock, + use_case: LoginUser, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + repository.create_user(fake_user) + + auth.login = lambda user, should_remember_user: False + + with raises(CredentialsNotValidError): + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=False, + ) + + def it_should_set_should_remember_me( + repository: UsersRepositoryMock, + auth: AuthMock, + use_case: LoginUser, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + repository.create_user(fake_user) + + should_remember_user = True + + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=should_remember_user, + ) + + assert auth.should_remember_user == should_remember_user + + def it_should_login_user( + repository: UsersRepositoryMock, + auth: AuthMock, + use_case: LoginUser, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + repository.create_user(fake_user) + + use_case.execute( + email=fake_user.email, + password=fake_user.password, + should_remember_user=False, + ) + + logged_in_user = auth.get_user() + + assert logged_in_user == fake_user diff --git a/src/app/core/use_cases/authentication/tests/request_passord_reset_test.py b/src/app/core/use_cases/authentication/tests/request_passord_reset_test.py new file mode 100644 index 00000000..b71bb768 --- /dev/null +++ b/src/app/core/use_cases/authentication/tests/request_passord_reset_test.py @@ -0,0 +1,84 @@ +from pytest import fixture, raises + +from core.use_cases.authentication import RequestPasswordReset +from core.use_cases.tests.mocks.providers import EmailProviderMock +from core.entities.tests.fakers import UsersFaker +from core.errors.authentication import ( + UserEmailNotValidError, + EmailTemplateNotValidError, + SenderPasswordNotValidError, + AdminUserEmailNotMatchedError, +) + +from core.constants import ADMIN_USER_EMAIL, SUPPORT_USER_EMAIL + + +def describe_request_password_reset_use_case(): + @fixture + def email_provider(): + return EmailProviderMock() + + @fixture + def use_case(email_provider: EmailProviderMock): + return RequestPasswordReset(email_provider) + + def it_should_throw_an_error_if_user_email_is_not_string( + use_case: RequestPasswordReset, + ): + with raises(UserEmailNotValidError): + use_case.execute(user_email=None, email_template=None, sender_password=None) + + def it_should_throw_an_error_if_template_email_is_not_string( + use_case: RequestPasswordReset, + ): + fake_user = UsersFaker.fake() + + with raises(EmailTemplateNotValidError): + use_case.execute( + user_email=fake_user.email, + email_template=None, + sender_password=fake_user.password, + ) + + def it_should_throw_an_error_if_sender_password_is_not_string( + use_case: RequestPasswordReset, + ): + fake_user = UsersFaker.fake() + + with raises(SenderPasswordNotValidError): + use_case.execute( + user_email=fake_user.email, + email_template="fake template email string", + sender_password=444, + ) + + def it_should_throw_an_error_if_user_email_is_not_equal_to_admin_email( + use_case: RequestPasswordReset, + ): + fake_user = UsersFaker.fake() + + with raises(AdminUserEmailNotMatchedError): + use_case.execute( + user_email=fake_user.email, + email_template="fake template email string", + sender_password=fake_user.password, + ) + + def it_should_send_request_password_email( + use_case: RequestPasswordReset, + email_provider: EmailProviderMock, + ): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + + fake_email_template = "fake template email string" + + use_case.execute( + user_email=fake_user.email, + email_template=fake_email_template, + sender_password=fake_user.password, + ) + + assert ( + email_provider.email + == f"sender: {SUPPORT_USER_EMAIL}; receiver: {fake_user.email}; template: {fake_email_template}; password: {fake_user.password}" + ) diff --git a/src/app/core/use_cases/authentication/tests/reset_password_test.py b/src/app/core/use_cases/authentication/tests/reset_password_test.py new file mode 100644 index 00000000..0b316526 --- /dev/null +++ b/src/app/core/use_cases/authentication/tests/reset_password_test.py @@ -0,0 +1,46 @@ +from pytest import fixture, raises + +from core.use_cases.authentication import ResetPassword +from core.use_cases.tests.mocks.repositories import UsersRepositoryMock +from core.use_cases.tests.mocks.authentication import AuthMock +from core.entities.tests.fakers import UsersFaker +from core.errors.authentication import NewPasswordNotValidError +from core.constants import ADMIN_USER_EMAIL + + +def describe_reset_password_use_case(): + @fixture + def repository(): + return UsersRepositoryMock() + + @fixture + def auth(): + return AuthMock() + + @fixture + def use_case(repository: UsersRepositoryMock, auth: AuthMock): + return ResetPassword(repository, auth) + + def it_should_throw_an_error_if_new_password_is_not_string( + use_case: ResetPassword, + ): + with raises(NewPasswordNotValidError): + use_case.execute( + new_password=42, + ) + + def it_should_reset_password( + repository: UsersRepositoryMock, + use_case: ResetPassword, + ): + + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + repository.create_user(fake_user) + + new_password = "fake new password" + + use_case.execute(new_password=new_password) + + user = repository.get_user_by_id(fake_user.id) + + assert user.password == new_password diff --git a/src/app/core/use_cases/checklist_records/__init__.py b/src/app/core/use_cases/checklist_records/__init__.py index bbcd84ef..c60af996 100644 --- a/src/app/core/use_cases/checklist_records/__init__.py +++ b/src/app/core/use_cases/checklist_records/__init__.py @@ -1,19 +1,7 @@ -from .create_checklist_record_by_csv_file import CreateChecklistRecordsByCsvFile -from .create_checklist_record_by_form import CreateChecklistRecordByForm -from .get_checklist_records_csv_file import GetChecklistRecordsCsvFile -from .update_checklist_record import UpdateChecklistRecord -from .delete_checklist_records import DeleteChecklistRecords -from .get_checklist_records_table_page_data import GetChecklistRecordsTablePageData -from .get_checklist_records_dashboard_page_data import ( - GetChecklistRecordsDashboardPageData, -) -from .filter_checklist_records import FilterChecklistRecords - -get_checklist_records_csv_file = GetChecklistRecordsCsvFile() -create_checklist_records_by_csv_file = CreateChecklistRecordsByCsvFile() -create_checklist_record_by_form = CreateChecklistRecordByForm() -update_checklist_record = UpdateChecklistRecord() -delete_checklist_records = DeleteChecklistRecords() -get_checklist_records_table_page_data = GetChecklistRecordsTablePageData() -get_checklist_dashboard_page_data = GetChecklistRecordsDashboardPageData() -filter_checklist_records = FilterChecklistRecords() +from .create_checklist_record_by_csv_file import * +from .create_checklist_record_by_form import * +from .get_checklist_records_csv_file import * +from .update_checklist_record import * +from .delete_checklist_records import * +from .get_checklist_records_table_page_data import * +from .get_checklist_records_dashboard_page_data import * diff --git a/src/app/core/use_cases/checklist_records/create_checklist_record_by_csv_file.py b/src/app/core/use_cases/checklist_records/create_checklist_record_by_csv_file.py index 10b43e58..fb4fdd01 100644 --- a/src/app/core/use_cases/checklist_records/create_checklist_record_by_csv_file.py +++ b/src/app/core/use_cases/checklist_records/create_checklist_record_by_csv_file.py @@ -2,35 +2,47 @@ from datetime import datetime, date from werkzeug.datastructures import FileStorage -from core.commons import CsvFile, Error, Datetime +from core.commons import CsvFile, Datetime from core.entities import CheckListRecord +from core.interfaces.repositories import ( + ChecklistRecordsRepositoryInterface, + PlantsRepositoryInterface, +) +from core.errors.validation import DatetimeNotValidError, HourNotValidError +from core.errors.plants import PlantNotFoundError +from core.interfaces.providers import DataAnalyserProviderInterface from core.constants import CSV_FILE_COLUMNS -from infra.repositories import checklist_records_repository, plants_repository - class CreateChecklistRecordsByCsvFile: - def execute(self, file: FileStorage) -> None: - try: - csv_file = CsvFile(file) - csv_file.read() + def __init__( + self, + checklist_records_repository: ChecklistRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + data_analyser_provider: DataAnalyserProviderInterface, + ): + self._checklist_records_repository = checklist_records_repository + self._plants_repository = plants_repository + self._data_analyser_provider = data_analyser_provider - csv_file.validate_columns(CSV_FILE_COLUMNS["checklist_records"]) + def execute(self, file: FileStorage) -> None: + csv_file = CsvFile(file, self._data_analyser_provider) + csv_file.read() - records = csv_file.get_records() + csv_file.validate_columns(CSV_FILE_COLUMNS["checklist_records"]) - converted_records = self.__convert_csv_records_to_checklist_records(records) + records = csv_file.get_records() - for checklist_record in converted_records: - checklist_records_repository.create_checklist_record(checklist_record) + converted_records = self.__convert_csv_records_to_checklist_records(records) - except Error as error: - raise error + self._checklist_records_repository.create_many_checklist_records( + converted_records + ) def __convert_csv_records_to_checklist_records( self, records: list[dict] ) -> Generator: - plants = plants_repository.get_plants() + plants = self._plants_repository.get_plants() for record in records: try: @@ -49,25 +61,26 @@ def __convert_csv_records_to_checklist_records( record_fertilizer_expiration_date = record[ "validade da adubação?" ].date() - except Exception as exception: - print(exception) - raise Error( - internal_message=exception, - ui_message="Valor de data mal formatado", - status_code=400, - ) + except Exception: + raise DatetimeNotValidError() record_hour = record["hora da coleta (inserir valor de 0 a 23)"] try: - if not isinstance(record_hour, int): + if ( + not isinstance(record_hour, int) + or record_hour < 0 + or record_hour > 23 + ): record_hour = int(record_hour) - except Exception as exception: - print(exception) - raise Error( - internal_message=exception, - ui_message="Hora da coleta precisa ser um inteiro", - status_code=400, + except Exception: + raise HourNotValidError( + ui_message="Valor da hora precisa ser um número entre 0 e 23" + ) + + if record_hour < 0 or record_hour > 23: + raise HourNotValidError( + ui_message="Valor da hora precisa ser um número entre 0 e 23" ) record_plant_name = record["planta"] @@ -96,6 +109,11 @@ def __convert_csv_records_to_checklist_records( plant = current_plant break + if plant is None: + raise PlantNotFoundError( + f"Planta não encontrada para o registro da data {created_at.format_value().get_value()}" + ) + yield CheckListRecord( soil_ph=record["ph do solo?"], soil_humidity=record["umidade do solo?"], diff --git a/src/app/core/use_cases/checklist_records/create_checklist_record_by_form.py b/src/app/core/use_cases/checklist_records/create_checklist_record_by_form.py index e972fcb8..eeb217ba 100644 --- a/src/app/core/use_cases/checklist_records/create_checklist_record_by_form.py +++ b/src/app/core/use_cases/checklist_records/create_checklist_record_by_form.py @@ -1,48 +1,62 @@ from datetime import datetime, date from core.entities.checklist_record import CheckListRecord, Plant -from core.commons import Error, Date, Datetime - -from infra.repositories import checklist_records_repository +from core.commons import Date, Datetime +from core.errors.validation import DateNotValidError +from core.errors.plants import PlantNotFoundError +from core.interfaces.repositories import ( + ChecklistRecordsRepositoryInterface, + PlantsRepositoryInterface, +) class CreateChecklistRecordByForm: + def __init__( + self, + checklist_records_repository: ChecklistRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + ): + self._checklist_records_repository = checklist_records_repository + self._plants_repository = plants_repository + def execute(self, request: dict) -> None: if not isinstance(request["date"], date): - raise Error(ui_message="Data de registro não informado") - - try: - created_at = Datetime( - datetime( - hour=request["hour"], - year=request["date"].year, - month=request["date"].month, - day=request["date"].day, - ) - ) + raise DateNotValidError(ui_message="Data de inserção não válido") - fertilizer_expiration_date = Date(request["fertilizer_expiration_date"]) - - plant = Plant(id=request["plant_id"]) - - checklist_record = CheckListRecord( - plantation_type=request["plantation_type"], - illuminance=request["illuminance"], - lai=request["lai"], - soil_humidity=request["soil_humidity"], - temperature=request["temperature"], - water_consumption=request["water_consumption"], - soil_ph=request["soil_ph"], - report=request["report"], - air_humidity=request["air_humidity"], - leaf_color=request["leaf_color"], - leaf_appearance=request["leaf_appearance"], - fertilizer_expiration_date=fertilizer_expiration_date, - created_at=created_at, - plant=plant, - ) - - checklist_records_repository.create_checklist_record(checklist_record) + if not isinstance(request["fertilizer_expiration_date"], date): + raise DateNotValidError(ui_message="Data de validade de adubo não válido") - except Error as error: - raise error + created_at = Datetime( + datetime( + hour=request["hour"], + year=request["date"].year, + month=request["date"].month, + day=request["date"].day, + ) + ) + + fertilizer_expiration_date = Date(request["fertilizer_expiration_date"]) + + plant = self._plants_repository.get_plant_by_id(request["plant_id"]) + + if not isinstance(plant, Plant): + raise PlantNotFoundError() + + checklist_record = CheckListRecord( + plantation_type=request["plantation_type"], + illuminance=request["illuminance"], + lai=request["lai"], + soil_humidity=request["soil_humidity"], + temperature=request["temperature"], + water_consumption=request["water_consumption"], + soil_ph=request["soil_ph"], + report=request["report"], + air_humidity=request["air_humidity"], + leaf_color=request["leaf_color"], + leaf_appearance=request["leaf_appearance"], + fertilizer_expiration_date=fertilizer_expiration_date, + created_at=created_at, + plant=plant, + ) + + self._checklist_records_repository.create_checklist_record(checklist_record) diff --git a/src/app/core/use_cases/checklist_records/delete_checklist_records.py b/src/app/core/use_cases/checklist_records/delete_checklist_records.py index cda67d9a..1ebb73b9 100644 --- a/src/app/core/use_cases/checklist_records/delete_checklist_records.py +++ b/src/app/core/use_cases/checklist_records/delete_checklist_records.py @@ -1,24 +1,26 @@ -from core.commons import Error - -from infra.repositories import checklist_records_repository +from core.interfaces.repositories import ChecklistRecordsRepositoryInterface +from core.entities import CheckListRecord +from core.errors.validation import ChecklistRecordNotValidError +from core.errors.checklist_records import ChecklistRecordNotFoundError class DeleteChecklistRecords: - def execute(self, checklist_records_ids: list[str]) -> None: - try: - for id in checklist_records_ids: - if id and isinstance(id, str): - has_checklist_record = bool( - checklist_records_repository.get_checklist_record_by_id(id) - ) + def __init__( + self, + checklist_records_repository: ChecklistRecordsRepositoryInterface, + ): + self._checklist_records_repository = checklist_records_repository + + def execute(self, checklist_records_ids: list[str]): + for id in checklist_records_ids: + if not isinstance(id, str): + raise ChecklistRecordNotValidError() - if not has_checklist_record: - raise Error( - ui_message="Registro check-list não encontrado", - internal_message="Checklist record not found", - ) + record = self._checklist_records_repository.get_checklist_record_by_id(id) - checklist_records_repository.delete_checklist_record_by_id(id) + if not isinstance(record, CheckListRecord): + raise ChecklistRecordNotFoundError() - except Error as error: - raise error + self._checklist_records_repository.delete_many_checklist_records_by_id( + checklist_records_ids + ) diff --git a/src/app/core/use_cases/checklist_records/filter_checklist_records.py b/src/app/core/use_cases/checklist_records/filter_checklist_records.py deleted file mode 100644 index 3ac31236..00000000 --- a/src/app/core/use_cases/checklist_records/filter_checklist_records.py +++ /dev/null @@ -1,24 +0,0 @@ -from datetime import date - -from infra.repositories import checklist_records_repository - -from core.commons import Error - - -class FilterChecklistRecords: - def execute(self, plant_id: str, start_date: date, end_date: date): - if plant_id == "all": - plant_id = None - - try: - records = checklist_records_repository.get_filtered_checklist_records( - page_number=1, - start_date=start_date, - end_date=end_date, - plant_id=plant_id, - ) - - return records - - except Error as error: - raise error diff --git a/src/app/core/use_cases/checklist_records/get_checklist_records_csv_file.py b/src/app/core/use_cases/checklist_records/get_checklist_records_csv_file.py index 26a376f9..e4ddbc79 100644 --- a/src/app/core/use_cases/checklist_records/get_checklist_records_csv_file.py +++ b/src/app/core/use_cases/checklist_records/get_checklist_records_csv_file.py @@ -1,42 +1,42 @@ -from datetime import date, datetime +from datetime import date -from core.commons import RecordsFilters, Error +from core.commons import RecordsFilters +from core.interfaces.repositories import SensorRecordsRepositoryInterface +from core.interfaces.providers import DataAnalyserProviderInterface from core.constants import CSV_FILE_COLUMNS -from infra.repositories import checklist_records_repository -from infra.constants import FOLDERS -from infra.providers.data_analyser_provider import DataAnalyserProvider - class GetChecklistRecordsCsvFile: - def execute(self, plant_id: str, start_date: date, end_date: date): - try: - filters = RecordsFilters( - plant_id=plant_id, start_date=start_date, end_date=end_date - ) + def __init__( + self, + checklist_records_repository: SensorRecordsRepositoryInterface, + data_analyser_provider: DataAnalyserProviderInterface, + ): + self._data_analyser_provider = data_analyser_provider + self._checklist_records_repository = checklist_records_repository + + def execute(self, plant_id: str, start_date: date, end_date: date, folder: str): + filters = RecordsFilters( + plant_id=plant_id, start_date=start_date, end_date=end_date + ) - data = self.__get_data(filters) + data = self.__get_data(filters) - csv_name = "registros-checklist.xlsx" - tmp_folder = FOLDERS["tmp"] + csv_filename = "registros-checklist.xlsx" - data_analyser_provider = DataAnalyserProvider() - data_analyser_provider.analyse(data) - data_analyser_provider.convert_to_excel(tmp_folder, csv_name) + self._data_analyser_provider.analyse(data) + self._data_analyser_provider.convert_to_excel(folder, csv_filename) - return { - "folder": tmp_folder, - "filename": csv_name, - } - except Error as error: - raise error + return csv_filename def __get_data(self, filters: RecordsFilters): - checklist_records = checklist_records_repository.get_filtered_checklist_records( - page_number="all", - plant_id=filters.plant_id, - start_date=filters.start_date, - end_date=filters.end_date, + checklist_records = ( + self._checklist_records_repository.get_filtered_checklist_records( + page_number="all", + plant_id=filters.plant_id, + start_date=filters.start_date, + end_date=filters.end_date, + ) ) columns = CSV_FILE_COLUMNS["checklist_records"] @@ -82,8 +82,6 @@ def __get_data(self, filters: RecordsFilters): data["iaf (índice de área foliar)?"].append(value) case "leaf_appearance": data["qual o aspecto das folhas?"].append(value) - case "leaf_appearance": - data["qual o aspecto das folhas?"].append(value) case "leaf_color": data["qual a coloração das folhas?"].append(value) case "plantation_type": diff --git a/src/app/core/use_cases/checklist_records/get_checklist_records_dashboard_page_data.py b/src/app/core/use_cases/checklist_records/get_checklist_records_dashboard_page_data.py index bcabf781..d4b79350 100644 --- a/src/app/core/use_cases/checklist_records/get_checklist_records_dashboard_page_data.py +++ b/src/app/core/use_cases/checklist_records/get_checklist_records_dashboard_page_data.py @@ -1,57 +1,65 @@ from core.constants import LEAF_APPEARANCES, LEAF_COLORS, ADMIN_USER_EMAIL -from core.commons import Error, LineChart, OrderedPlants - -from infra.repositories import ( - checklist_records_repository, - plants_repository, - users_repository, +from core.commons import LineChart, OrderedPlants +from core.errors.plants import PlantNotFoundError +from core.errors.checklist_records import ChecklistRecordNotFoundError +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + UsersRepositoryInterface, + ChecklistRecordsRepositoryInterface, ) class GetChecklistRecordsDashboardPageData: + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + users_repository: UsersRepositoryInterface, + checklist_records_repository: ChecklistRecordsRepositoryInterface, + ): + self._plants_repository = plants_repository + self._users_repository = users_repository + self._checklist_records_repository = checklist_records_repository + def execute(self): - try: - plants = plants_repository.get_plants() + plants = self._plants_repository.get_plants() + + if len(plants) == 0: + raise PlantNotFoundError() - if len(plants) == 0: - raise Error("Nenhuma planta encontrada", status_code=404) + active_plant_id = self._users_repository.get_user_active_plant_id( + ADMIN_USER_EMAIL + ) - active_plant_id = users_repository.get_user_active_plant_id( - ADMIN_USER_EMAIL - ) + ordered_plants = OrderedPlants(plants, active_plant_id) - ordered_plants = OrderedPlants(plants, active_plant_id) + leaf_records = ( + self._checklist_records_repository.get_leaf_appearances_and_leaf_colors_records() + ) - leaf_records = ( - checklist_records_repository.get_leaf_appearances_and_leaf_colors_records() - ) + if len(leaf_records) == 0: + raise ChecklistRecordNotFoundError() - if len(leaf_records) == 0: - raise Error("Nenhum registro de check-list encontrado", status_code=404) + plants = ordered_plants.get_value() - leaf_charts_data = self.__get_leaf_charts_data( - leaf_records, ordered_plants.get_value() - ) + leaf_charts_data = self.__get_leaf_charts_data(leaf_records, plants) - lai_records = checklist_records_repository.get_lai_records() + lai_records = ( + self._checklist_records_repository.get_lai_records_for_line_charts() + ) - if len(lai_records) == 0: - raise Error("Nenhum registro de check-list encontrado", status_code=404) + if len(lai_records) == 0: + raise ChecklistRecordNotFoundError() - plant_growth_chart = LineChart(lai_records, "lai") + plant_growth_chart = LineChart(lai_records) - plant_growth_chart_data = plant_growth_chart.get_data( - ordered_plants.get_value() - ) + plant_growth_chart_data = plant_growth_chart.get_data(plants) - return { - **leaf_charts_data, - "plant_growth_chart_data": plant_growth_chart_data, - "plants": plants, - "active_plant_id": active_plant_id, - } - except Error as error: - raise error + return { + **leaf_charts_data, + "plant_growth_chart_data": plant_growth_chart_data, + "plants": plants, + "active_plant_id": active_plant_id, + } def __get_leaf_charts_data(self, records, plants): days_count_by_leaf_appearance_and_plant = { diff --git a/src/app/core/use_cases/checklist_records/get_checklist_records_table_page_data.py b/src/app/core/use_cases/checklist_records/get_checklist_records_table_page_data.py index d6d5e0cd..e6f58fdd 100644 --- a/src/app/core/use_cases/checklist_records/get_checklist_records_table_page_data.py +++ b/src/app/core/use_cases/checklist_records/get_checklist_records_table_page_data.py @@ -1,10 +1,20 @@ from core.entities import Plant, CheckListRecord -from core.commons import Pagination, RecordsFilters, Error - -from infra.repositories import plants_repository, checklist_records_repository +from core.commons import Pagination, RecordsFilters +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + SensorRecordsRepositoryInterface, +) class GetChecklistRecordsTablePageData: + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + checklist_records_repository: SensorRecordsRepositoryInterface, + ): + self._plants_repository = plants_repository + self._checklist_records_repository = checklist_records_repository + def execute( self, start_date: str, @@ -13,44 +23,42 @@ def execute( page_number: int = 1, should_get_plants: bool = False, ) -> tuple[list[CheckListRecord], int, list[Plant]]: - try: - plants = [] - if should_get_plants: - plants = plants_repository.get_plants() + plants = [] + if should_get_plants: + plants = self._plants_repository.get_plants() - filters = RecordsFilters( - plant_id=plant_id, start_date=start_date, end_date=end_date - ) + filters = RecordsFilters( + plant_id=plant_id, start_date=start_date, end_date=end_date + ) - checklist_records_count = ( - checklist_records_repository.get_checklist_records_count( - plant_id=filters.plant_id, - start_date=filters.start_date, - end_date=filters.end_date, - ) + checklist_records_count = ( + self._checklist_records_repository.get_checklist_records_count( + plant_id=filters.plant_id, + start_date=filters.start_date, + end_date=filters.end_date, ) + ) - pagination = Pagination(page_number, checklist_records_count) + pagination = Pagination(page_number, checklist_records_count) - current_page_number, last_page_number = ( - pagination.get_current_and_last_page_numbers() - ) + current_page_number, last_page_number = ( + pagination.get_current_and_last_page_numbers() + ) - checklist_records = ( - checklist_records_repository.get_filtered_checklist_records( - page_number=current_page_number, - plant_id=filters.plant_id, - start_date=filters.start_date, - end_date=filters.end_date, - ) - ) + print(checklist_records_count, flush=True) - return { - "checklist_records": checklist_records, - "plants": plants, - "last_page_number": last_page_number, - "current_page_number": current_page_number, - } + checklist_records = ( + self._checklist_records_repository.get_filtered_checklist_records( + page_number=current_page_number, + plant_id=filters.plant_id, + start_date=filters.start_date, + end_date=filters.end_date, + ) + ) - except Error as error: - raise error + return { + "checklist_records": checklist_records, + "plants": plants, + "last_page_number": last_page_number, + "current_page_number": current_page_number, + } diff --git a/src/app/core/use_cases/checklist_records/tests/__init__.py b/src/app/core/use_cases/checklist_records/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_csv_file_test.py b/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_csv_file_test.py new file mode 100644 index 00000000..c86817ba --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_csv_file_test.py @@ -0,0 +1,172 @@ +from pathlib import Path + +from pytest import fixture, raises +from werkzeug.datastructures import FileStorage + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + ChecklistRecordsRepositoryMock, +) +from core.use_cases.tests.mocks.providers import DataAnalyserProviderMock +from core.entities import Plant +from core.errors.plants import PlantNotFoundError +from core.entities.tests.fakers import PlantsFaker +from core.errors.validation import DatetimeNotValidError, HourNotValidError +from core.constants import CSV_FILE_COLUMNS + + +from ..create_checklist_record_by_csv_file import CreateChecklistRecordsByCsvFile + + +def fake_record(base_fake_record: dict = {}): + return { + "em qual plantio você quer coletar os dados?": "interno", + "data da coleta": "15/12/2023", + "hora da coleta (inserir valor de 0 a 23)": 12, + "temperatura ambiente?": 24.7, + "ph do solo?": 7, + "umidade do solo?": 72, + "umidade do ar?": 55, + "validade da adubação?": "30/12/2023", + "consumo de água (mililitros)?": 0, + "luminosidade (lux)?": 2, + "iaf (índice de área foliar)?": 0, + "qual o aspecto das folhas?": "viscoca", + "qual a coloração das folhas?": "verde", + "algum desvio detectado durante o processo?": "não", + "planta": "alface", + **base_fake_record, + } + + +def describe_create_checklist_records_by_csv_file_use_case(): + @fixture + def fake_plant(): + return PlantsFaker.fake(name="alface") + + @fixture + def plants_repository(fake_plant): + plants_repository = PlantsRepositoryMock() + plants_repository.clear_plants() + plants_repository.create_plant(fake_plant) + + return plants_repository + + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def data_analyser_provider(): + data_analyser_provider = DataAnalyserProviderMock() + data_analyser_provider.get_columns = lambda: CSV_FILE_COLUMNS[ + "checklist_records" + ] + + return data_analyser_provider + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + data_analyser_provider: DataAnalyserProviderMock, + ): + checklist_records_repository.clear_records() + return CreateChecklistRecordsByCsvFile( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + data_analyser_provider=data_analyser_provider, + ) + + @fixture + def file(tmp_path: Path): + return FileStorage(tmp_path / "fake_csv_.xlsx") + + def it_should_throw_error_if_datetime_from_any_record_is_not_valid( + file: FileStorage, + data_analyser_provider: DataAnalyserProviderMock, + use_case: CreateChecklistRecordsByCsvFile, + ): + + data_analyser_provider.convert_to_list_of_records = lambda: [ + fake_record({"data da coleta": "12/12-2024"}) + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + data_analyser_provider.convert_to_list_of_records = lambda: [ + fake_record({"validade da adubação?": "12/99/2024"}) + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + data_analyser_provider.convert_to_list_of_records = lambda: [ + fake_record( + { + "hora da coleta (inserir valor de 0 a 23)": 99, + } + ) + ] + + with raises(HourNotValidError) as error: + use_case.execute(file) + + assert str(error.value) == "Valor da hora precisa ser um número entre 0 e 23" + + def it_should_create_checklist_records( + file: FileStorage, + fake_plant: Plant, + data_analyser_provider: DataAnalyserProviderMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: CreateChecklistRecordsByCsvFile, + ): + record = fake_record() + + data_analyser_provider.convert_to_list_of_records = lambda: [record] + + use_case.execute(file) + + last_record = checklist_records_repository.get_last_checklist_records(count=1)[ + 0 + ] + + assert last_record.soil_humidity == record["umidade do solo?"] + assert last_record.soil_ph == record["ph do solo?"] + assert ( + last_record.plantation_type + == record["em qual plantio você quer coletar os dados?"] + ) + assert last_record.lai == record["iaf (índice de área foliar)?"] + assert last_record.illuminance == record["luminosidade (lux)?"] + assert last_record.leaf_appearance == record["qual o aspecto das folhas?"] + assert last_record.leaf_color == record["qual a coloração das folhas?"] + assert ( + last_record.report == record["algum desvio detectado durante o processo?"] + ) + assert last_record.air_humidity == record["umidade do ar?"] + assert last_record.temperature == record["temperatura ambiente?"] + assert last_record.water_consumption == record["consumo de água (mililitros)?"] + assert ( + last_record.fertilizer_expiration_date.get_value() == "2023-12-30 00:00:00" + ) + assert last_record.created_at.get_value() == "2023-12-15 12:00:00" + assert last_record.plant == fake_plant + + def it_should_throw_error_if_there_is_no_plant_in_repository( + file: FileStorage, + data_analyser_provider: DataAnalyserProviderMock, + use_case: CreateChecklistRecordsByCsvFile, + ): + data_analyser_provider.convert_to_list_of_records = lambda: [ + fake_record({"planta": "beterraba"}), + ] + + with raises(PlantNotFoundError) as error: + use_case.execute(file) + + assert ( + str(error.value) + == "Planta não encontrada para o registro da data 15/12/2023 12:00" + ) diff --git a/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_form_test.py b/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_form_test.py new file mode 100644 index 00000000..1a5dd631 --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/create_checklist_record_by_form_test.py @@ -0,0 +1,119 @@ +from datetime import date + +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + ChecklistRecordsRepositoryMock, + PlantsRepositoryMock, +) +from core.errors.validation import DateNotValidError +from core.errors.plants import PlantNotFoundError +from core.entities.tests.fakers import PlantsFaker + + +from ..create_checklist_record_by_form import CreateChecklistRecordByForm + + +def fake_request(base_fake_request: dict): + return { + "fertilizer_expiration_date": date(year=2024, month=3, day=16), + "date": date(year=2024, month=12, day=12), + "hour": 12, + "illuminance": 12, + "plantation_type": "interno", + "lai": 55, + "soil_humidity": 20, + "air_humidity": 50, + "temperature": 24.7, + "water_consumption": 0, + "soil_ph": 7, + "report": "não", + "leaf_color": "avermelhada", + "leaf_appearance": "um pouco murcha", + **base_fake_request, + } + + +def describe_create_checklist_record_by_form_use_case(): + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + checklist_records_repository.clear_records() + return CreateChecklistRecordByForm( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + ) + + def it_should_throw_error_if_date_from_request_is_not_valid( + use_case: CreateChecklistRecordByForm, + ): + request = fake_request({"date": "not valid date"}) + + with raises(DateNotValidError): + use_case.execute(request) + + def it_should_throw_error_if_fertilizer_expiration_date_from_request_is_not_valid( + use_case: CreateChecklistRecordByForm, + ): + request = fake_request( + {"fertilizer_expiration_date": "not valid fertilizer expiration date"} + ) + + with raises(DateNotValidError): + use_case.execute(request) + + def it_should_throw_error_if_there_is_no_plant_in_repository( + use_case: CreateChecklistRecordByForm, + ): + fake_plant = PlantsFaker.fake() + + request = fake_request({"plant_id": fake_plant.id}) + + with raises(PlantNotFoundError): + use_case.execute(request) + + def it_should_create_sensors_record( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: CreateChecklistRecordByForm, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + request = fake_request({"plant_id": fake_plant.id}) + + use_case.execute(request) + + last_record = checklist_records_repository.get_last_checklist_records(count=1)[ + 0 + ] + + assert last_record.soil_humidity == request["soil_humidity"] + assert last_record.soil_ph == request["soil_ph"] + assert last_record.plantation_type == request["plantation_type"] + assert last_record.lai == request["lai"] + assert last_record.illuminance == request["illuminance"] + assert last_record.leaf_appearance == request["leaf_appearance"] + assert last_record.leaf_color == request["leaf_color"] + assert last_record.report == request["report"] + assert last_record.air_humidity == request["air_humidity"] + assert last_record.temperature == request["temperature"] + assert last_record.water_consumption == request["water_consumption"] + assert ( + last_record.fertilizer_expiration_date.get_value(is_date=True) + == request["fertilizer_expiration_date"] + ) + assert ( + last_record.created_at.get_value(is_datetime=True).date() == request["date"] + ) + assert last_record.plant == fake_plant diff --git a/src/app/core/use_cases/checklist_records/tests/delete_checklist_records_test.py b/src/app/core/use_cases/checklist_records/tests/delete_checklist_records_test.py new file mode 100644 index 00000000..8320c0be --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/delete_checklist_records_test.py @@ -0,0 +1,62 @@ +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ChecklistRecordsRepositoryMock +from core.errors.checklist_records import ChecklistRecordNotFoundError +from core.errors.validation import ChecklistRecordNotValidError +from core.entities.tests.fakers import ChecklistRecordsFaker + +from core.use_cases.checklist_records import DeleteChecklistRecords + + +def describe_delete_checklist_records_use_case(): + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + ): + checklist_records_repository.clear_records() + return DeleteChecklistRecords( + checklist_records_repository=checklist_records_repository, + ) + + def it_should_throw_error_if_at_least_one_checklist_record_id_is_not_valid( + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: DeleteChecklistRecords, + ): + fake_records = ChecklistRecordsFaker.fake_many(3) + checklist_records_repository.create_many_checklist_records(fake_records) + + ids = [fake_record.id for fake_record in fake_records] + + ids.append(42) + + with raises(ChecklistRecordNotValidError): + use_case.execute(ids) + + def it_should_throw_error_if_at_least_one_checklist_record_is_not_found( + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: DeleteChecklistRecords, + ): + fake_records = ChecklistRecordsFaker.fake_many(3) + checklist_records_repository.create_many_checklist_records(fake_records[:2]) + + with raises(ChecklistRecordNotFoundError): + use_case.execute([fake_record.id for fake_record in fake_records]) + + def it_should_delete_many_records( + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: DeleteChecklistRecords, + ): + count = 3 + fake_records = ChecklistRecordsFaker.fake_many(count) + + checklist_records_repository.create_many_checklist_records(fake_records) + + use_case.execute([fake_record.id for fake_record in fake_records]) + + records = checklist_records_repository.get_last_checklist_records(count=count) + + assert len(records) == 0 diff --git a/src/app/core/use_cases/checklist_records/tests/get_checklist_records_csv_file_test.py b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_csv_file_test.py new file mode 100644 index 00000000..c2afd57d --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_csv_file_test.py @@ -0,0 +1,204 @@ +from datetime import datetime + +from pytest import fixture + +from core.use_cases.tests.mocks.repositories import ChecklistRecordsRepositoryMock +from core.use_cases.tests.mocks.providers import DataAnalyserProviderMock +from core.entities.tests.fakers import ChecklistRecordsFaker, PlantsFaker +from core.commons import Datetime +from core.constants import CSV_FILE_COLUMNS + +from ..get_checklist_records_csv_file import GetChecklistRecordsCsvFile + + +def describe_get_checklist_records_csv_file_use_case(): + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def data_analyser_provider(): + data_analyser_provider = DataAnalyserProviderMock() + data_analyser_provider.get_columns = lambda: CSV_FILE_COLUMNS[ + "checklist_records" + ] + + return data_analyser_provider + + @fixture + def use_case( + data_analyser_provider: DataAnalyserProviderMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + ): + checklist_records_repository.clear_records() + return GetChecklistRecordsCsvFile( + checklist_records_repository=checklist_records_repository, + data_analyser_provider=data_analyser_provider, + ) + + @fixture + def folder(): + return "fake_csv_folder" + + def it_should_create_csv_file_in_a_specific_path( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + use_case: GetChecklistRecordsCsvFile, + ): + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + + assert csv_file["path"] == f"{folder}/registros-checklist.xlsx" + + def it_should_create_csv_file_with_empty_data_if_there_is_no_any_checklist_record_in_repository( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + use_case: GetChecklistRecordsCsvFile, + ): + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + + for data in csv_file["data"].values(): + assert data == [] + + def it_should_create_csv_file_containing_checklist_records( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: GetChecklistRecordsCsvFile, + ): + fake_records = ChecklistRecordsFaker.fake_many() + + fake_records.sort( + key=lambda record: record.created_at.get_value(is_datetime=True) + ) + + checklist_records_repository.create_many_checklist_records(fake_records) + + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert data["data da coleta"] == [ + fake_record.created_at.get_value()[:10] for fake_record in fake_records + ] + assert data["validade da adubação?"] == [ + fake_record.fertilizer_expiration_date.get_value()[:10] + for fake_record in fake_records + ] + assert data["hora da coleta (inserir valor de 0 a 23)"] == [ + fake_record.created_at.get_time().hour for fake_record in fake_records + ] + assert data["umidade do ar?"] == [ + fake_record.air_humidity for fake_record in fake_records + ] + assert data["umidade do solo?"] == [ + fake_record.soil_humidity for fake_record in fake_records + ] + assert data["temperatura ambiente?"] == [ + fake_record.temperature for fake_record in fake_records + ] + assert data["consumo de água (mililitros)?"] == [ + fake_record.water_consumption for fake_record in fake_records + ] + assert data["luminosidade (lux)?"] == [ + fake_record.illuminance for fake_record in fake_records + ] + assert data["iaf (índice de área foliar)?"] == [ + fake_record.lai for fake_record in fake_records + ] + assert data["qual o aspecto das folhas?"] == [ + fake_record.leaf_appearance for fake_record in fake_records + ] + assert data["qual a coloração das folhas?"] == [ + fake_record.leaf_color for fake_record in fake_records + ] + assert data["em qual plantio você quer coletar os dados?"] == [ + fake_record.plantation_type for fake_record in fake_records + ] + assert data["algum desvio detectado durante o processo?"] == [ + fake_record.report for fake_record in fake_records + ] + assert data["planta"] == [ + fake_record.plant.name for fake_record in fake_records + ] + + def it_should_create_csv_file_with_checklist_records_filtered_by_plant( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: GetChecklistRecordsCsvFile, + ): + fake_records = ChecklistRecordsFaker.fake_many(10) + fake_plant = PlantsFaker.fake() + + fake_records.append(ChecklistRecordsFaker.fake(plant=fake_plant)) + fake_records.append(ChecklistRecordsFaker.fake(plant=fake_plant)) + fake_records.append(ChecklistRecordsFaker.fake(plant=fake_plant)) + + checklist_records_repository.create_many_checklist_records(fake_records) + + use_case.execute( + plant_id=fake_plant.id, start_date=None, end_date=None, folder=folder + ) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert len(data["planta"]) == 3 + assert data["planta"] == [fake_plant.name for _ in range(3)] + + def it_should_create_csv_file_with_checklist_records_filtered_by_date( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: GetChecklistRecordsCsvFile, + ): + fake_records = ChecklistRecordsFaker.fake_many(10) + + fake_records.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=12, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=25, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=30, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=11, day=12, hour=0, minute=0, second=0) + ) + ) + ) + + checklist_records_repository.create_many_checklist_records(fake_records) + + use_case.execute( + plant_id=None, + start_date="2024-12-12", + end_date="2024-12-30", + folder=folder, + ) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert len(data["data da coleta"]) == 3 + assert data["data da coleta"] == ["12/12/2024", "25/12/2024", "30/12/2024"] diff --git a/src/app/core/use_cases/checklist_records/tests/get_checklist_records_dashboard_page_data_test.py b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_dashboard_page_data_test.py new file mode 100644 index 00000000..3a8a2d45 --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_dashboard_page_data_test.py @@ -0,0 +1,188 @@ +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + UsersRepositoryMock, + ChecklistRecordsRepositoryMock, +) +from core.entities import User, Plant +from core.entities.tests.fakers import PlantsFaker, UsersFaker, ChecklistRecordsFaker +from core.commons import OrderedPlants +from core.errors.checklist_records import ChecklistRecordNotFoundError +from core.errors.plants import PlantNotFoundError +from core.constants import ADMIN_USER_EMAIL + +from ..get_checklist_records_dashboard_page_data import ( + GetChecklistRecordsDashboardPageData, +) + + +def describe_get_checklist_records_dashboard_page_data_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def users_repository(): + return UsersRepositoryMock() + + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + ): + plants_repository.clear_plants() + users_repository.clear_users() + return GetChecklistRecordsDashboardPageData( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + users_repository=users_repository, + ) + + @fixture + def fake_plant(): + return PlantsFaker.fake() + + @fixture + def fake_user(fake_plant): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + fake_user.active_plant_id = fake_plant.id + return fake_user + + def it_should_throw_error_if_there_is_no_plant_in_repository( + use_case: GetChecklistRecordsDashboardPageData, + ): + with raises(PlantNotFoundError): + use_case.execute() + + def it_should_throw_error_if_there_is_no_checklist_record_in_repository( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetChecklistRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + checklist_records_repository.get_sensor_records_for_line_charts = lambda: [] + + with raises(ChecklistRecordNotFoundError): + use_case.execute() + + def it_should_get_data_for_leaf_apprearance_and_plant_chart( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetChecklistRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + fake_records = [ + ChecklistRecordsFaker.fake(leaf_appearance="SAUDAVEL", plant=fake_plant), + ChecklistRecordsFaker.fake(leaf_appearance="SAUDAVEL", plant=fake_plant), + ChecklistRecordsFaker.fake(leaf_appearance="MURCHA", plant=fake_plant), + ] + + checklist_records_repository.create_many_checklist_records(fake_records) + + data = use_case.execute() + + chart_data = data["days_count_by_leaf_appearance_and_plant"][fake_plant.id] + + assert chart_data["SAUDAVEL"] == 2 + assert chart_data["MURCHA"] == 1 + assert chart_data["NÃO REGISTRADO"] == 0 + + def it_should_get_data_for_leaf_color_and_plant_chart( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + checklist_records_repository: ChecklistRecordsRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetChecklistRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + fake_records = [ + ChecklistRecordsFaker.fake( + leaf_color="VERDE CLARO PREDOMINANTE", plant=fake_plant + ), + ChecklistRecordsFaker.fake( + leaf_color="VERDE ESCURO PREDOMINANTE", plant=fake_plant + ), + ChecklistRecordsFaker.fake( + leaf_color="VERDE ESCURO PREDOMINANTE", plant=fake_plant + ), + ChecklistRecordsFaker.fake(leaf_color="NÃO REGISTRADO", plant=fake_plant), + ] + + checklist_records_repository.create_many_checklist_records(fake_records) + + data = use_case.execute() + + chart_data = data["days_count_by_leaf_color_and_plant"][fake_plant.id] + + assert chart_data["VERDE CLARO PREDOMINANTE"] == 1 + assert chart_data["VERDE ESCURO PREDOMINANTE"] == 2 + assert chart_data["NÃO REGISTRADO"] == 1 + assert chart_data["VERDE CLARO COM ALGUMAS MANCHAS CLARAS"] == 0 + assert chart_data["VERDE CLARO COM VARIAS MANCHAS CLARAS"] == 0 + assert chart_data["VERDE CLARO COM ALGUMAS MANCHAS ESCURAS"] == 0 + assert chart_data["VERDE CLARO COM VARIAS MANCHAS ESCURAS"] == 0 + assert chart_data["VERDE ESCURO COM ALGUMAS MANCHAS CLARAS"] == 0 + assert chart_data["VERDE ESCURO COM VARIAS MANCHAS CLARAS"] == 0 + assert chart_data["VERDE ESCURO COM ALGUMAS MANCHAS ESCURAS"] == 0 + assert chart_data["VERDE ESCURO COM VARIAS MANCHAS ESCURAS"] == 0 + assert chart_data["OPACO PREDOMINANTE"] == 0 + + def it_should_get_ordered_plants( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetChecklistRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + + fake_plants = PlantsFaker.fake_many(2) + fake_plants.append(fake_plant) + + for plant in fake_plants: + plants_repository.create_plant(plant) + + data = use_case.execute() + + assert ( + data["plants"] + == OrderedPlants(fake_plants, fake_user.active_plant_id).get_value() + ) + + def it_should_get_active_plant_id( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetChecklistRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + + fake_plants = PlantsFaker.fake_many(2) + fake_plants.append(fake_plant) + + for plant in fake_plants: + plants_repository.create_plant(plant) + + data = use_case.execute() + + assert data["active_plant_id"] == fake_user.active_plant_id diff --git a/src/app/core/use_cases/checklist_records/tests/get_checklist_records_table_page_data_test.py b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_table_page_data_test.py new file mode 100644 index 00000000..4ac32a9f --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/get_checklist_records_table_page_data_test.py @@ -0,0 +1,177 @@ +from datetime import datetime + +from pytest import fixture + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + ChecklistRecordsRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker, ChecklistRecordsFaker +from core.commons import Datetime +from core.constants import PAGINATION + +from ..get_checklist_records_table_page_data import GetChecklistRecordsTablePageData + + +def describe_get_checklist_records_dashboard_page_data_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + plants_repository.clear_plants() + checklist_records_repository.clear_records() + return GetChecklistRecordsTablePageData( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + ) + + def it_should_get_plants_only_when_is_required( + plants_repository: PlantsRepositoryMock, + use_case: GetChecklistRecordsTablePageData, + ): + fake_plants = PlantsFaker.fake_many() + + for fake_plant in fake_plants: + plants_repository.create_plant(fake_plant) + + data = use_case.execute( + should_get_plants=True, + plant_id=None, + start_date=None, + end_date=None, + page_number=1, + ) + + assert data["plants"] == fake_plants + + data = use_case.execute( + should_get_plants=False, + plant_id=None, + start_date=None, + end_date=None, + page_number=1, + ) + + assert data["plants"] == [] + + def it_should_get_checklist_records_filtered_by_plant_id( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetChecklistRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + records = ChecklistRecordsFaker.fake_many(5) + record_in_filter = ChecklistRecordsFaker.fake(plant=fake_plant) + + records.append(record_in_filter) + + checklist_records_repository.create_many_checklist_records(records) + + data = use_case.execute( + should_get_plants=True, + plant_id=fake_plant.id, + start_date=None, + end_date=None, + page_number=1, + ) + + data["checklist_records"] == [record_in_filter] + + def it_should_get_checklist_records_filtered_by_date( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetChecklistRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + fake_records = ChecklistRecordsFaker.fake_many(10) + + fake_records.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=11, day=12, hour=0, minute=0, second=0) + ) + ) + ) + + fake_records_in_filter = [] + + fake_records_in_filter.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=12, hour=0, minute=0, second=0) + ) + ) + ) + fake_records_in_filter.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=25, hour=0, minute=0, second=0) + ) + ) + ) + fake_records_in_filter.append( + ChecklistRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=30, hour=0, minute=0, second=0) + ) + ) + ) + + fake_records.extend(fake_records_in_filter) + + checklist_records_repository.create_many_checklist_records(fake_records) + + data = use_case.execute( + should_get_plants=True, + plant_id=None, + start_date="2024-12-12", + end_date="2024-12-30", + page_number=1, + ) + + assert data["checklist_records"] == fake_records_in_filter + + def it_should_get_checklist_records_filtered_by_page( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetChecklistRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + fake_records = ChecklistRecordsFaker.fake_many(24) + + fake_records.sort( + key=lambda record: record.created_at.get_value(is_datetime=True) + ) + + checklist_records_repository.create_many_checklist_records(fake_records) + + page_number = 4 + + data = use_case.execute( + should_get_plants=False, + plant_id=None, + start_date=None, + end_date=None, + page_number=page_number, + ) + + records_per_page = PAGINATION["records_per_page"] + records_slice = (page_number - 1) * records_per_page + + assert len(data["checklist_records"]) == records_per_page + assert data["checklist_records"] == fake_records[records_slice:] diff --git a/src/app/core/use_cases/checklist_records/tests/update_checklist_record_test.py b/src/app/core/use_cases/checklist_records/tests/update_checklist_record_test.py new file mode 100644 index 00000000..b82bce10 --- /dev/null +++ b/src/app/core/use_cases/checklist_records/tests/update_checklist_record_test.py @@ -0,0 +1,151 @@ +from datetime import datetime, date + +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + ChecklistRecordsRepositoryMock, +) +from core.entities.tests.fakers import ChecklistRecordsFaker, PlantsFaker +from core.errors.validation import ChecklistRecordNotValidError, DateNotValidError +from core.errors.checklist_records import ChecklistRecordNotFoundError +from core.errors.plants import PlantNotFoundError + +from ..update_checklist_record import UpdateChecklistRecord + + +def fake_request(base_fake_request: dict): + return { + "fertilizer_expiration_date": datetime( + year=2024, month=3, day=16, hour=12, minute=0 + ), + "date": date(year=2024, month=12, day=12), + "hour": 12, + "illuminance": 12, + "plantation_type": "interno", + "lai": 55, + "soil_humidity": 20, + "air_humidity": 50, + "temperature": 24.7, + "water_consumption": 0, + "soil_ph": 7, + "report": "não", + "leaf_color": "avermelhada", + "leaf_appearance": "um pouco murcha", + **base_fake_request, + } + + +def describe_update_checklist_record_use_case(): + @fixture + def checklist_records_repository(): + return ChecklistRecordsRepositoryMock() + + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def use_case( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + plants_repository.clear_plants() + checklist_records_repository.clear_records() + return UpdateChecklistRecord( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + ) + + def it_should_throw_error_if_id_from_request_is_not_valid( + use_case: UpdateChecklistRecord, + ): + + with raises(ChecklistRecordNotValidError): + use_case.execute(request=fake_request({"checklist_record_id": None})) + + def it_should_throw_error_if_no_checklist_record_is_found_in_repository( + use_case: UpdateChecklistRecord, + ): + fake_record = ChecklistRecordsFaker.fake() + + with raises(ChecklistRecordNotFoundError): + use_case.execute( + request=fake_request({"checklist_record_id": fake_record.id}) + ) + + def it_should_throw_error_if_date_from_request_is_not_valid( + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: UpdateChecklistRecord, + ): + fake_record = ChecklistRecordsFaker.fake() + checklist_records_repository.create_checklist_record(fake_record) + + with raises(DateNotValidError): + use_case.execute( + request=fake_request( + {"checklist_record_id": fake_record.id, "date": None} + ) + ) + + def it_should_throw_error_if_no_plant_is_found_in_repository( + checklist_records_repository: ChecklistRecordsRepositoryMock, + use_case: UpdateChecklistRecord, + ): + fake_record = ChecklistRecordsFaker.fake() + checklist_records_repository.create_checklist_record(fake_record) + + fake_plant = PlantsFaker.fake() + + with raises(PlantNotFoundError): + use_case.execute( + request=fake_request( + {"checklist_record_id": fake_record.id, "plant_id": fake_plant.id} + ) + ) + + def it_should_update_checklist_record( + checklist_records_repository: ChecklistRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: UpdateChecklistRecord, + ): + fake_record = ChecklistRecordsFaker.fake( + soil_humidity=25, + ambient_humidity=25, + water_volume=25, + temperature=25, + ) + checklist_records_repository.create_checklist_record(fake_record) + + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + request = fake_request( + {"plant_id": fake_plant.id, "checklist_record_id": fake_record.id} + ) + + use_case.execute(request=request) + + last_record = checklist_records_repository.get_last_checklist_records(count=1)[ + 0 + ] + + assert last_record.soil_humidity == request["soil_humidity"] + assert last_record.soil_ph == request["soil_ph"] + assert last_record.plantation_type == request["plantation_type"] + assert last_record.lai == request["lai"] + assert last_record.illuminance == request["illuminance"] + assert last_record.leaf_appearance == request["leaf_appearance"] + assert last_record.leaf_color == request["leaf_color"] + assert last_record.report == request["report"] + assert last_record.air_humidity == request["air_humidity"] + assert last_record.temperature == request["temperature"] + assert last_record.water_consumption == request["water_consumption"] + assert ( + last_record.fertilizer_expiration_date.get_value(is_date=True) + == request["fertilizer_expiration_date"] + ) + assert ( + last_record.created_at.get_value(is_datetime=True).date() == request["date"] + ) + assert last_record.plant == fake_plant diff --git a/src/app/core/use_cases/checklist_records/update_checklist_record.py b/src/app/core/use_cases/checklist_records/update_checklist_record.py index c4e27698..f1f1e151 100644 --- a/src/app/core/use_cases/checklist_records/update_checklist_record.py +++ b/src/app/core/use_cases/checklist_records/update_checklist_record.py @@ -1,74 +1,79 @@ from datetime import datetime, date from core.entities import CheckListRecord -from core.commons import Error, Date, Datetime - -from infra.repositories import checklist_records_repository, plants_repository +from core.commons import Date, Datetime +from core.errors.validation import ChecklistRecordNotValidError, DateNotValidError +from core.errors.checklist_records import ChecklistRecordNotFoundError +from core.errors.plants import PlantNotFoundError +from core.interfaces.repositories import ( + SensorRecordsRepositoryInterface, + PlantsRepositoryInterface, +) class UpdateChecklistRecord: - def execute(self, request: dict) -> None: - try: - checklist_record_id = request["checklist_record_id"] - - if not checklist_record_id or not isinstance(checklist_record_id, str): - raise Error( - ui_message="Registro check-list não encontrado", - internal_message="Checklist record id not found", - ) - - has_checklist_record = bool( - checklist_records_repository.get_checklist_record_by_id( - checklist_record_id - ) - ) + def __init__( + self, + checklist_records_repository: SensorRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + ): + self._checklist_records_repository = checklist_records_repository + self._plants_repository = plants_repository - if not has_checklist_record: - raise Error( - ui_message="Registro check-list não encontrado", - internal_message="Checklist record id not found", - ) + def execute(self, request: dict) -> None: + checklist_record_id = request["checklist_record_id"] - if not isinstance(request["date"], date): - raise Error(ui_message="Data de registro não informado") + if not checklist_record_id or not isinstance(checklist_record_id, str): + raise ChecklistRecordNotValidError() - created_at = Datetime( - datetime( - hour=request["hour"], - year=request["date"].year, - month=request["date"].month, - day=request["date"].day, - ) + has_checklist_record = bool( + self._checklist_records_repository.get_checklist_record_by_id( + checklist_record_id ) + ) - fertilizer_expiration_date = Date(request["fertilizer_expiration_date"]) - - plant = plants_repository.get_plant_by_id(request["plant_id"]) + if not has_checklist_record: + raise ChecklistRecordNotFoundError() - if not plant: - raise Error(ui_message="Planta não encontrada para esse registro") + if not isinstance(request["date"], date): + raise DateNotValidError() - checklist_record = CheckListRecord( - id=checklist_record_id, - plantation_type=request["plantation_type"], - illuminance=request["illuminance"], - lai=request["lai"], - soil_humidity=request["soil_humidity"], - temperature=request["temperature"], - water_consumption=request["water_consumption"], - soil_ph=request["soil_ph"], - report=request["report"], - air_humidity=request["air_humidity"], - leaf_color=request["leaf_color"], - leaf_appearance=request["leaf_appearance"], - fertilizer_expiration_date=fertilizer_expiration_date, - created_at=created_at, - plant=plant, + created_at = Datetime( + datetime( + hour=request["hour"], + year=request["date"].year, + month=request["date"].month, + day=request["date"].day, ) - - checklist_records_repository.update_checklist_record_by_id(checklist_record) - - return checklist_record - - except Error as error: - raise error + ) + + fertilizer_expiration_date = Date(request["fertilizer_expiration_date"]) + + plant = self._plants_repository.get_plant_by_id(request["plant_id"]) + + if not plant: + raise PlantNotFoundError() + + checklist_record = CheckListRecord( + id=checklist_record_id, + plantation_type=request["plantation_type"], + illuminance=request["illuminance"], + lai=request["lai"], + soil_humidity=request["soil_humidity"], + temperature=request["temperature"], + water_consumption=request["water_consumption"], + soil_ph=request["soil_ph"], + report=request["report"], + air_humidity=request["air_humidity"], + leaf_color=request["leaf_color"], + leaf_appearance=request["leaf_appearance"], + fertilizer_expiration_date=fertilizer_expiration_date, + created_at=created_at, + plant=plant, + ) + + self._checklist_records_repository.update_checklist_record_by_id( + checklist_record + ) + + return checklist_record diff --git a/src/app/core/use_cases/plants/__init__.py b/src/app/core/use_cases/plants/__init__.py index 9207926c..c78c45ed 100644 --- a/src/app/core/use_cases/plants/__init__.py +++ b/src/app/core/use_cases/plants/__init__.py @@ -1,16 +1,8 @@ -from .create_plant_by_form import CreatePlantByForm -from .get_plants_page_data import GetPlantsPageData -from .filter_plants import FilterPlants -from .update_plant import UpdatePlant -from .get_plant_by_id import GetPlantById -from .delete_plant import DeletePlant -from .update_active_plant import UpdateActivePlant - - -create_plant_by_form = CreatePlantByForm() -get_plants_page_data = GetPlantsPageData() -filter_plants = FilterPlants() -update_plant = UpdatePlant() -get_plant_by_id = GetPlantById() -delete_plant = DeletePlant() -update_active_plant = UpdateActivePlant() +from .create_plant_by_form import * +from .get_plants_page_data import * +from .filter_plants import * +from .update_plant import * +from .get_plant_by_id import * +from .delete_plant import * +from .update_active_plant import * +from .update_active_plant import * diff --git a/src/app/core/use_cases/plants/create_plant_by_form.py b/src/app/core/use_cases/plants/create_plant_by_form.py index afd2bc54..ef3d2c9e 100644 --- a/src/app/core/use_cases/plants/create_plant_by_form.py +++ b/src/app/core/use_cases/plants/create_plant_by_form.py @@ -1,19 +1,18 @@ from core.entities.plant import Plant -from core.commons import Error -from infra.repositories import plants_repository +from core.interfaces.repositories import PlantsRepositoryInterface +from core.errors.plants import PlantNameAlreadyInUseError class CreatePlantByForm: - def execute(self, request: dict): - try: - has_plant = plants_repository.get_plant_by_name(request["name"]) + def __init__(self, plants_repository: PlantsRepositoryInterface): + self.plants_repository = plants_repository - if has_plant: - raise Error("Nome de planta já utilizada") + def execute(self, request: dict): + has_plant = self.plants_repository.get_plant_by_name(request["name"]) - plant = Plant(name=request["name"], hex_color=request["hex_color"]) + if has_plant: + raise PlantNameAlreadyInUseError() - plants_repository.create_plant_record(plant) + plant = Plant(name=request["name"], hex_color=request["hex_color"]) - except Error as error: - raise error + self.plants_repository.create_plant(plant) diff --git a/src/app/core/use_cases/plants/delete_plant.py b/src/app/core/use_cases/plants/delete_plant.py index c9f224b9..a2e89358 100644 --- a/src/app/core/use_cases/plants/delete_plant.py +++ b/src/app/core/use_cases/plants/delete_plant.py @@ -1,41 +1,36 @@ -from core.commons import Error from core.entities import User - -from infra.repositories import plants_repository, users_repository +from core.errors.plants import PlantIdNotValidError, PlantNotFoundError +from core.errors.authentication import UserNotValidError +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + UsersRepositoryInterface, +) class DeletePlant: - def execute(self, user: User, plant_id: str) -> None: - try: - if not isinstance(plant_id, str): - raise Error( - ui_message="Planta não encontrada", - internal_message="Plant id is not provided", - status_code=404, - ) - - if not isinstance(user, User): - raise Error( - ui_message="Usuário não encontrada", - internal_message="User is not provided", - status_code=404, - ) - - has_plant = bool(plants_repository.get_plant_by_id(plant_id)) - - if not has_plant: - raise Error( - ui_message="Planta não encontrado", - internal_message="Plant not found", - status_code=404, - ) - - plants_repository.delete_plant_by_id(plant_id) - - if user.active_plant_id == plant_id: - last_plant = plants_repository.get_last_plant() - if last_plant: - users_repository.update_active_plant(user.id, last_plant.id) - - except Error as error: - raise error + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + users_repository: UsersRepositoryInterface, + ): + self.plants_repository = plants_repository + self.users_repository = users_repository + + def execute(self, user: User, plant_id: str): + if not isinstance(plant_id, str): + raise PlantIdNotValidError() + + if not isinstance(user, User): + raise UserNotValidError() + + has_plant = bool(self.plants_repository.get_plant_by_id(plant_id)) + + if not has_plant: + raise PlantNotFoundError() + + self.plants_repository.delete_plant_by_id(plant_id) + + if user.active_plant_id == plant_id: + last_plant = self.plants_repository.get_last_plant() + if last_plant: + self.users_repository.update_active_plant(user.id, last_plant.id) diff --git a/src/app/core/use_cases/plants/filter_plants.py b/src/app/core/use_cases/plants/filter_plants.py index 41827fdb..3b00ce6e 100644 --- a/src/app/core/use_cases/plants/filter_plants.py +++ b/src/app/core/use_cases/plants/filter_plants.py @@ -1,10 +1,23 @@ -from infra.repositories import plants_repository +from core.interfaces.repositories import ( + PlantsRepositoryInterface, +) +from core.errors.plants import PlantNameNotValidError class FilterPlants: + def __init__( + self, + repository: PlantsRepositoryInterface, + ): + self.repository = repository + def execute(self, plant_name: str | None): + if not isinstance(plant_name, str): + raise PlantNameNotValidError() + plant_name = plant_name.strip() - if isinstance(plant_name, str) and len(plant_name) != 0: - return plants_repository.filter_plants_by_name(plant_name) - return plants_repository.get_plants() + if len(plant_name) != 0: + return self.repository.filter_plants_by_name(plant_name) + + return self.repository.get_plants() diff --git a/src/app/core/use_cases/plants/get_plant_by_id.py b/src/app/core/use_cases/plants/get_plant_by_id.py index 399b66f5..9e7034cc 100644 --- a/src/app/core/use_cases/plants/get_plant_by_id.py +++ b/src/app/core/use_cases/plants/get_plant_by_id.py @@ -1,15 +1,18 @@ -from core.commons import Error -from infra.repositories import plants_repository +from core.interfaces.repositories import ( + PlantsRepositoryInterface, +) +from core.errors.plants import PlantIdNotValidError class GetPlantById: - def execute(self, id: str) -> None: - try: - if not isinstance(id, str): - raise Error("Planta não econtrada", status_code=404) + def __init__( + self, + repository: PlantsRepositoryInterface, + ): + self.repository = repository - plant = plants_repository.get_plant_by_id(id) - return plant + def execute(self, id: str): + if not isinstance(id, str): + raise PlantIdNotValidError() - except Error as error: - raise error + return self.repository.get_plant_by_id(id) diff --git a/src/app/core/use_cases/plants/get_plants_page_data.py b/src/app/core/use_cases/plants/get_plants_page_data.py index 96c9440b..d95ecb82 100644 --- a/src/app/core/use_cases/plants/get_plants_page_data.py +++ b/src/app/core/use_cases/plants/get_plants_page_data.py @@ -1,8 +1,16 @@ -from infra.repositories import plants_repository +from core.interfaces.repositories import ( + PlantsRepositoryInterface, +) class GetPlantsPageData: + def __init__( + self, + repository: PlantsRepositoryInterface, + ): + self.repository = repository + def execute(self): - plants = plants_repository.get_plants() + plants = self.repository.get_plants() return plants diff --git a/src/app/core/use_cases/plants/tests/__init__.py b/src/app/core/use_cases/plants/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/use_cases/plants/tests/create_plant_by_form_test.py b/src/app/core/use_cases/plants/tests/create_plant_by_form_test.py new file mode 100644 index 00000000..b8d08382 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/create_plant_by_form_test.py @@ -0,0 +1,50 @@ +from dataclasses import asdict + +from pytest import fixture, raises + +from core.use_cases.plants.create_plant_by_form import CreatePlantByForm +from core.errors.plants import PlantNameAlreadyInUseError + + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker + + +def describe_create_plant_by_form_use_case(): + @fixture + def repository(): + repository = PlantsRepositoryMock() + + yield repository + + repository.clear_plants() + + @fixture + def use_case(repository): + return CreatePlantByForm(repository) + + def it_should_throw_an_error_if_the_new_plant_name_is_already_in_use( + repository: PlantsRepositoryMock, + use_case: CreatePlantByForm, + ): + fake_plant = PlantsFaker.fake() + + repository.create_plant(fake_plant) + + with raises(PlantNameAlreadyInUseError): + use_case.execute(request=asdict(fake_plant)) + + def it_should_create_a_plant( + repository: PlantsRepositoryMock, + use_case: CreatePlantByForm, + ): + fake_plant = PlantsFaker.fake() + + use_case.execute(request=asdict(fake_plant)) + + created_plant = repository.get_last_plant() + + assert created_plant.name == fake_plant.name + assert created_plant.hex_color == fake_plant.hex_color diff --git a/src/app/core/use_cases/plants/tests/delete_plant_test.py b/src/app/core/use_cases/plants/tests/delete_plant_test.py new file mode 100644 index 00000000..e6fc3073 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/delete_plant_test.py @@ -0,0 +1,91 @@ +from pytest import fixture, raises + +from core.use_cases.plants import DeletePlant +from core.errors.plants import PlantIdNotValidError, PlantNotFoundError +from core.errors.authentication import UserNotValidError +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + UsersRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker, UsersFaker + + +def describe_delete_plant_use_case(): + @fixture + def plants_repository(): + plants_repository = PlantsRepositoryMock() + + yield plants_repository + + plants_repository.clear_plants() + + @fixture + def users_repository(): + return UsersRepositoryMock() + + @fixture + def use_case(plants_repository, users_repository): + plants_repository.clear_plants() + + return DeletePlant( + plants_repository=plants_repository, users_repository=users_repository + ) + + def it_should_throw_an_error_if_any_plant_id_is_not_valid( + use_case: DeletePlant, + ): + with raises(PlantIdNotValidError): + use_case.execute(plant_id=None, user=None) + + def it_should_throw_an_error_if_user_is_not_valid( + use_case: DeletePlant, + ): + fake_plant = PlantsFaker.fake() + + with raises(UserNotValidError): + use_case.execute(plant_id=fake_plant.id, user=None) + + def it_should_throw_an_error_if_no_plant_is_found( + use_case: DeletePlant, + ): + fake_plant = PlantsFaker.fake() + fake_user = UsersFaker.fake() + + with raises(PlantNotFoundError): + use_case.execute(plant_id=fake_plant.id, user=fake_user) + + def it_should_delete_plant( + plants_repository: PlantsRepositoryMock, + use_case: DeletePlant, + ): + fake_plant = PlantsFaker.fake() + fake_user = UsersFaker.fake() + + plants_repository.create_plant(fake_plant) + + use_case.execute(plant_id=fake_plant.id, user=fake_user) + + plants = plants_repository.get_plants() + + assert len(plants) == 0 + + def it_should_set_user_active_plant_id_to_last_plant_id_if_the_deleted_plant_id_was_the_previous_active_plant_of_user( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + use_case: DeletePlant, + ): + fake_plant = PlantsFaker.fake() + fake_last_plant = PlantsFaker.fake() + + fake_user = UsersFaker.fake() + + users_repository.create_user(fake_user) + + fake_user.active_plant_id = fake_plant.id + + plants_repository.create_plant(fake_last_plant) + plants_repository.create_plant(fake_plant) + + use_case.execute(plant_id=fake_plant.id, user=fake_user) + + assert fake_user.active_plant_id == fake_last_plant.id diff --git a/src/app/core/use_cases/plants/tests/filter_plants_test.py b/src/app/core/use_cases/plants/tests/filter_plants_test.py new file mode 100644 index 00000000..bba705c0 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/filter_plants_test.py @@ -0,0 +1,47 @@ +from pytest import fixture, raises + +from core.use_cases.plants import FilterPlants +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, +) +from core.errors.plants import PlantNameNotValidError +from core.entities.tests.fakers import PlantsFaker + + +def describe_filter_plants_use_case(): + @fixture + def repository(): + return PlantsRepositoryMock() + + @fixture + def use_case(repository): + repository.clear_plants() + + return FilterPlants(repository) + + def it_should_throw_an_error_if_plant_name_is_not_valid( + use_case: FilterPlants, + ): + with raises(PlantNameNotValidError): + use_case.execute(plant_name=42) + + def it_should_not_filter_plants_if_provided_name_is_empty( + use_case: FilterPlants, + ): + filtered_plants = use_case.execute(plant_name="") + + assert len(filtered_plants) == 0 + + def it_should_filter_plants( + use_case: FilterPlants, repository: PlantsRepositoryMock + ): + fake_name = "doce" + + repository.create_plant(PlantsFaker.fake(name="batata doce")) + repository.create_plant(PlantsFaker.fake(name="alface")) + repository.create_plant(PlantsFaker.fake(name="doce")) + + filtered_plants = use_case.execute(plant_name=fake_name) + + assert len(filtered_plants) == 2 + assert all([fake_name in plant.name for plant in filtered_plants]) diff --git a/src/app/core/use_cases/plants/tests/get_plant_by_id_test.py b/src/app/core/use_cases/plants/tests/get_plant_by_id_test.py new file mode 100644 index 00000000..499dc429 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/get_plant_by_id_test.py @@ -0,0 +1,36 @@ +from pytest import fixture, raises + +from core.use_cases.plants import GetPlantById +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker +from core.errors.plants import PlantIdNotValidError + + +def describe_get_plant_by_id_use_case(): + @fixture + def repository(): + return PlantsRepositoryMock() + + @fixture + def use_case(repository): + return GetPlantById(repository) + + def it_should_throw_an_error_if_plant_id_is_not_valid( + use_case: GetPlantById, + ): + with raises(PlantIdNotValidError): + use_case.execute(42) + + def it_should_get_plant( + repository: PlantsRepositoryMock, + use_case: GetPlantById, + ): + fake_plant = PlantsFaker.fake() + + repository.create_plant(fake_plant) + + plant = use_case.execute(fake_plant.id) + + assert plant == fake_plant diff --git a/src/app/core/use_cases/plants/tests/get_plants_page_data_test.py b/src/app/core/use_cases/plants/tests/get_plants_page_data_test.py new file mode 100644 index 00000000..f17e3313 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/get_plants_page_data_test.py @@ -0,0 +1,32 @@ +from pytest import fixture + +from core.use_cases.plants import GetPlantsPageData +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker + + +def describe_get_plants_page_data_use_case(): + @fixture + def repository(): + return PlantsRepositoryMock() + + @fixture + def use_case(repository): + repository.clear_plants() + + return GetPlantsPageData(repository) + + def it_should_get_plants( + repository: PlantsRepositoryMock, + use_case: GetPlantsPageData, + ): + fake_plants = PlantsFaker.fake_many() + + for fake_plant in fake_plants: + repository.create_plant(fake_plant) + + plants = use_case.execute() + + assert plants == fake_plants diff --git a/src/app/core/use_cases/plants/tests/update_active_plant_test.py b/src/app/core/use_cases/plants/tests/update_active_plant_test.py new file mode 100644 index 00000000..7c6e7ab8 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/update_active_plant_test.py @@ -0,0 +1,64 @@ +from pytest import fixture, raises + +from core.use_cases.plants import UpdateActivePlant +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + UsersRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker, UsersFaker +from core.errors.plants import PlantNotFoundError, PlantIdNotValidError +from core.errors.authentication import UserNotValidError + + +def describe_update_active_plant_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def users_repository(): + return UsersRepositoryMock() + + @fixture + def use_case(plants_repository, users_repository): + return UpdateActivePlant( + plants_repository=plants_repository, users_repository=users_repository + ) + + def it_should_throw_an_error_if_user_id_is_not_valid( + use_case: UpdateActivePlant, + ): + with raises(UserNotValidError): + use_case.execute(user_id=42, plant_id=None) + + def it_should_throw_an_error_if_plant_id_is_not_valid( + use_case: UpdateActivePlant, + ): + fake_user = UsersFaker.fake() + + with raises(PlantIdNotValidError): + use_case.execute(user_id=fake_user.id, plant_id=None) + + def it_should_throw_an_error_if_no_plant_is_found( + use_case: UpdateActivePlant, + ): + fake_user = UsersFaker.fake() + fake_plant = PlantsFaker.fake() + + with raises(PlantNotFoundError): + use_case.execute(user_id=fake_user.id, plant_id=fake_plant.id) + + def it_should_update_active_plant( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + use_case: UpdateActivePlant, + ): + fake_user = UsersFaker.fake() + fake_plant = PlantsFaker.fake() + + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + use_case.execute(user_id=fake_user.id, plant_id=fake_plant.id) + + assert fake_user.active_plant_id == fake_plant.id diff --git a/src/app/core/use_cases/plants/tests/update_plant_test .py b/src/app/core/use_cases/plants/tests/update_plant_test .py new file mode 100644 index 00000000..4fbfafe7 --- /dev/null +++ b/src/app/core/use_cases/plants/tests/update_plant_test .py @@ -0,0 +1,49 @@ +from pytest import fixture, raises + +from core.use_cases.plants import UpdatePlant +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, +) +from core.errors.plants import PlantIdNotValidError, PlantNotFoundError + +from core.entities.tests.fakers import PlantsFaker + + +def describe_update_plant_use_case(): + @fixture + def repository(): + return PlantsRepositoryMock() + + @fixture + def use_case(repository): + return UpdatePlant(repository) + + def it_should_throw_an_error_if_id_is_not_string( + use_case: UpdatePlant, + ): + with raises(PlantIdNotValidError): + use_case.execute(id=42, request=None) + + def it_should_throw_an_error_if_plant_does_not_exist( + use_case: UpdatePlant, + ): + fake_plant = PlantsFaker.fake() + + with raises(PlantNotFoundError): + use_case.execute(id=fake_plant.id, request=None) + + def it_should_update_plant( + repository: PlantsRepositoryMock, + use_case: UpdatePlant, + ): + fake_plant = PlantsFaker.fake() + repository.create_plant(fake_plant) + + request = {"name": "Updated plant name", "hex_color": "Updated plant hex color"} + + use_case.execute(id=fake_plant.id, request=request) + + created_plant = repository.get_last_plant() + + assert created_plant.name == request["name"] + assert created_plant.hex_color == request["hex_color"] diff --git a/src/app/core/use_cases/plants/update_active_plant.py b/src/app/core/use_cases/plants/update_active_plant.py index 95f0a582..032a460c 100644 --- a/src/app/core/use_cases/plants/update_active_plant.py +++ b/src/app/core/use_cases/plants/update_active_plant.py @@ -1,22 +1,31 @@ -from core.commons import Error, Plant - -from infra.repositories import users_repository, plants_repository +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + UsersRepositoryInterface, +) +from core.entities import Plant +from core.errors.authentication import UserNotValidError +from core.errors.plants import PlantIdNotValidError, PlantNotFoundError class UpdateActivePlant: + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + users_repository: UsersRepositoryInterface, + ): + self.plants_repository = plants_repository + self.users_repository = users_repository + def execute(self, user_id: str, plant_id: str): - try: - if not isinstance(user_id, str): - raise Error(ui_message="Usuário inválido", status_code=400) + if not isinstance(user_id, str): + raise UserNotValidError() - if not isinstance(plant_id, str): - raise Error(ui_message="Planta inválida", status_code=400) + if not isinstance(plant_id, str): + raise PlantIdNotValidError() - plant = plants_repository.get_plant_by_id(plant_id) + plant = self.plants_repository.get_plant_by_id(plant_id) - if not isinstance(plant, Plant): - raise Error(ui_message="Planta não encontrada", status_code=404) + if not isinstance(plant, Plant): + raise PlantNotFoundError - users_repository.update_active_plant(user_id, plant_id) - except Error as error: - raise error + self.users_repository.update_active_plant(user_id, plant_id) diff --git a/src/app/core/use_cases/plants/update_plant.py b/src/app/core/use_cases/plants/update_plant.py index 8e62ef4b..25fc14ad 100644 --- a/src/app/core/use_cases/plants/update_plant.py +++ b/src/app/core/use_cases/plants/update_plant.py @@ -1,19 +1,27 @@ from core.commons import Error from core.entities import Plant -from infra.repositories import plants_repository +from core.interfaces.repositories import ( + PlantsRepositoryInterface, +) class UpdatePlant: + def __init__( + self, + repository: PlantsRepositoryInterface, + ): + self.repository = repository + def execute(self, request: dict, id: str) -> None: try: - if not id or not isinstance(id, str): + if not isinstance(id, str): raise Error( - ui_message="Planta não encontrada", + ui_message="Planta inválida", internal_message="Plant id not provided", status_code=404, ) - has_plant = bool(plants_repository.get_plant_by_id(id)) + has_plant = bool(self.repository.get_plant_by_id(id)) if not has_plant: raise Error( @@ -28,7 +36,7 @@ def execute(self, request: dict, id: str) -> None: hex_color=request["hex_color"], ) - plants_repository.update_plant_by_id(plant) + self.repository.update_plant_by_id(plant) return plant diff --git a/src/app/core/use_cases/sensors_records/__init__.py b/src/app/core/use_cases/sensors_records/__init__.py index 5d6f815b..a748d321 100644 --- a/src/app/core/use_cases/sensors_records/__init__.py +++ b/src/app/core/use_cases/sensors_records/__init__.py @@ -1,22 +1,9 @@ -from .create_sensors_records_by_csv_file import CreateSensorsRecordsByCsvFile -from .create_sensors_record_by_api import CreateSensorsRecordByApi -from .create_sensors_records_by_form import CreateSensorsRecordByForm -from .get_sensors_dashboard_page_data import GetSensorDashboardPageData -from .get_last_sensors_record_page_data import GetLastSensorsRecordPageData -from .get_sensors_records_table_page_data import GetSensorsRecordsTablePageData -from .update_sensors_records import UpdateSensorsRecord -from .delete_sensors_records import DeleteSensorsRecord - -from .filter_sensors_records import FilterSensorsRecords -from .get_sensors_records_csv_file import GetSensorsRecordsCsvFile - -create_sensors_records_by_csv_file = CreateSensorsRecordsByCsvFile() -create_sensors_record_by_api = CreateSensorsRecordByApi() -create_sensors_records_by_form = CreateSensorsRecordByForm() -get_sensors_dashboard_page_data = GetSensorDashboardPageData() -get_last_sensors_record_page_data = GetLastSensorsRecordPageData() -get_sensors_records_table_page_data = GetSensorsRecordsTablePageData() -update_sensors_records = UpdateSensorsRecord() -delete_sensors_records = DeleteSensorsRecord() -filter_sensors_records = FilterSensorsRecords() -get_sensors_records_csv_file = GetSensorsRecordsCsvFile() +from .create_sensors_records_by_csv_file import * +from .create_sensors_record_by_api import * +from .create_sensors_records_by_form import * +from .get_sensors_records_dashboard_page_data import * +from .get_last_sensors_record_page_data import * +from .get_sensors_records_table_page_data import * +from .get_sensors_records_csv_file import * +from .update_sensors_record import * +from .delete_sensors_records import * diff --git a/src/app/core/use_cases/sensors_records/create_sensors_record_by_api.py b/src/app/core/use_cases/sensors_records/create_sensors_record_by_api.py index 66adc231..70bd6771 100644 --- a/src/app/core/use_cases/sensors_records/create_sensors_record_by_api.py +++ b/src/app/core/use_cases/sensors_records/create_sensors_record_by_api.py @@ -1,37 +1,48 @@ from datetime import datetime from core.entities import SensorsRecord, Plant -from core.commons import Error, Datetime +from core.commons import Datetime +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + UsersRepositoryInterface, + SensorRecordsRepositoryInterface, +) +from core.errors.validation import SensorsRecordNotValidError +from core.errors.plants import PlantNotFoundError from core.constants import ADMIN_USER_EMAIL -from infra.repositories import sensors_records_repository, users_repository - class CreateSensorsRecordByApi: - def execute(self, request: dict) -> None: - try: - if not request: - raise Error("Nenhum dado recebido", 400) + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + users_repository: UsersRepositoryInterface, + sensors_records_repository: SensorRecordsRepositoryInterface, + ): + self._plants_repository = plants_repository + self._users_repository = users_repository + self._sensors_records_repository = sensors_records_repository - created_at = Datetime(datetime.now()) + def execute(self, request: dict) -> None: + if not request: + raise SensorsRecordNotValidError() - active_plant_id = users_repository.get_user_active_plant_id( - ADMIN_USER_EMAIL - ) + created_at = Datetime(datetime.now()) - if not active_plant_id: - raise Error("Nenhuma planta cadastrada no sistema", 500) + active_plant_id = self._users_repository.get_user_active_plant_id( + ADMIN_USER_EMAIL + ) - sensors_record = SensorsRecord( - soil_humidity=request["soil_humidity"], - ambient_humidity=request["ambient_humidity"], - temperature=request["temperature"], - water_volume=request["water_volume"], - created_at=created_at, - plant=Plant(id=active_plant_id), - ) + if not active_plant_id: + raise PlantNotFoundError() - sensors_records_repository.create_sensors_record(sensors_record) + sensors_record = SensorsRecord( + soil_humidity=request["soil_humidity"], + ambient_humidity=request["ambient_humidity"], + temperature=request["temperature"], + water_volume=request["water_volume"], + created_at=created_at, + plant=Plant(id=active_plant_id), + ) - except Error as error: - raise error + self._sensors_records_repository.create_sensors_record(sensors_record) diff --git a/src/app/core/use_cases/sensors_records/create_sensors_records_by_csv_file.py b/src/app/core/use_cases/sensors_records/create_sensors_records_by_csv_file.py index 73b17b3b..2d18ef5b 100644 --- a/src/app/core/use_cases/sensors_records/create_sensors_records_by_csv_file.py +++ b/src/app/core/use_cases/sensors_records/create_sensors_records_by_csv_file.py @@ -4,16 +4,31 @@ from werkzeug.datastructures import FileStorage from core.commons import CsvFile, Error, Datetime +from core.errors.validation import DatetimeNotValidError +from core.errors.plants import PlantNotFoundError from core.entities.sensors_record import SensorsRecord +from core.interfaces.repositories import ( + SensorRecordsRepositoryInterface, + PlantsRepositoryInterface, +) +from core.interfaces.providers import DataAnalyserProviderInterface from core.constants import CSV_FILE_COLUMNS -from infra.repositories import sensors_records_repository, plants_repository - class CreateSensorsRecordsByCsvFile: + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + data_analyser_provider: DataAnalyserProviderInterface, + ): + self._sensors_records_repository = sensors_records_repository + self._plants_repository = plants_repository + self._data_analyser_provider = data_analyser_provider + def execute(self, file: FileStorage): try: - csv_file = CsvFile(file) + csv_file = CsvFile(file, self._data_analyser_provider) csv_file.read() csv_file.validate_columns(CSV_FILE_COLUMNS["sensors_records"]) @@ -22,7 +37,9 @@ def execute(self, file: FileStorage): converted_records = self.__convert_csv_records_to_sensors_records(records) - sensors_records_repository.create_many_sensors_records(converted_records) + self._sensors_records_repository.create_many_sensors_records( + converted_records + ) except Error as error: raise error @@ -30,7 +47,7 @@ def execute(self, file: FileStorage): def __convert_csv_records_to_sensors_records( self, records: List[Dict] ) -> Generator: - plants = plants_repository.get_plants() + plants = self._plants_repository.get_plants() for record in records: try: @@ -46,12 +63,8 @@ def __convert_csv_records_to_sensors_records( else: record_time = record["hora"] - except Exception as exception: - raise Error( - internal_message=exception, - ui_message="Valor de data ou hora mal formatado", - status_code=400, - ) + except Exception: + raise DatetimeNotValidError() record_plant_name = record["planta"] @@ -72,6 +85,11 @@ def __convert_csv_records_to_sensors_records( plant = current_plant break + if plant is None: + raise PlantNotFoundError( + f"Planta não encontrada para o registro da data {created_at.format_value().get_value()}" + ) + yield SensorsRecord( ambient_humidity=record["umidade ambiente"], soil_humidity=record["umidade solo"], diff --git a/src/app/core/use_cases/sensors_records/create_sensors_records_by_form.py b/src/app/core/use_cases/sensors_records/create_sensors_records_by_form.py index 10f358c9..0080a24a 100644 --- a/src/app/core/use_cases/sensors_records/create_sensors_records_by_form.py +++ b/src/app/core/use_cases/sensors_records/create_sensors_records_by_form.py @@ -1,39 +1,53 @@ -from datetime import datetime, date +from datetime import datetime, date, time from core.entities.sensors_record import SensorsRecord, Plant -from core.commons import Error, Datetime - -from infra.repositories import sensors_records_repository +from core.interfaces.repositories import ( + SensorRecordsRepositoryInterface, + PlantsRepositoryInterface, +) +from core.errors.validation import DatetimeNotValidError, DateNotValidError +from core.errors.plants import PlantNotFoundError +from core.commons import Datetime class CreateSensorsRecordByForm: - def execute(self, request: dict) -> None: - if not isinstance(request["date"], date): - raise Error(ui_message="Data de registro não informado") - - try: - created_at = Datetime( - datetime( - hour=request["time"].hour, - minute=request["time"].minute, - year=request["date"].year, - month=request["date"].month, - day=request["date"].day, - ) + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + ): + self._sensors_records_repository = sensors_records_repository + self._plants_repository = plants_repository + + def execute(self, request: dict): + if "time" not in request or not isinstance(request["time"], time): + raise DatetimeNotValidError() + + if "date" not in request or not isinstance(request["date"], date): + raise DateNotValidError() + + created_at = Datetime( + datetime( + hour=request["time"].hour, + minute=request["time"].minute, + year=request["date"].year, + month=request["date"].month, + day=request["date"].day, ) + ) - plant = Plant(id=request["plant_id"]) + plant = self._plants_repository.get_plant_by_id(request["plant_id"]) - sensors_records = SensorsRecord( - soil_humidity=request["soil_humidity"], - ambient_humidity=request["ambient_humidity"], - temperature=request["temperature"], - water_volume=request["water_volume"], - created_at=created_at, - plant=plant, - ) + if not isinstance(plant, Plant): + raise PlantNotFoundError() - sensors_records_repository.create_sensors_record(sensors_records) + sensors_records = SensorsRecord( + soil_humidity=request["soil_humidity"], + ambient_humidity=request["ambient_humidity"], + temperature=request["temperature"], + water_volume=request["water_volume"], + created_at=created_at, + plant=plant, + ) - except Error as error: - raise error + self._sensors_records_repository.create_sensors_record(sensors_records) diff --git a/src/app/core/use_cases/sensors_records/delete_sensors_records.py b/src/app/core/use_cases/sensors_records/delete_sensors_records.py index 7c109523..6772e80a 100644 --- a/src/app/core/use_cases/sensors_records/delete_sensors_records.py +++ b/src/app/core/use_cases/sensors_records/delete_sensors_records.py @@ -1,21 +1,26 @@ -from core.commons import Error +from core.interfaces.repositories import SensorRecordsRepositoryInterface +from core.errors.validation import SensorsRecordNotValidError +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.entities import SensorsRecord -from infra.repositories import sensors_records_repository class DeleteSensorsRecord: - def execute(self,sensors_records_ids: list[str]) -> None: - try: - for id in sensors_records_ids: - if id and isinstance(id,str): - has_sensors_record = bool( - sensors_records_repository.get_sensors_record_by_id(id) - ) - - if not has_sensors_record: - raise Error( - ui_message="Registro dos sensors não encontrado", - internal_message="Sensors record not found", - ) - sensors_records_repository.delete_sensors_record_by_id(id) - except Error as error: - raise error \ No newline at end of file + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + ): + self._sensors_records_repository = sensors_records_repository + + def execute(self, sensors_records_ids: list[str]) -> None: + for id in sensors_records_ids: + if not isinstance(id, str): + raise SensorsRecordNotValidError() + + record = self._sensors_records_repository.get_sensors_record_by_id(id) + + if not isinstance(record, SensorsRecord): + raise SensorsRecordNotFoundError() + + self._sensors_records_repository.delete_many_sensors_records_by_id( + sensors_records_ids + ) diff --git a/src/app/core/use_cases/sensors_records/filter_sensors_records.py b/src/app/core/use_cases/sensors_records/filter_sensors_records.py deleted file mode 100644 index 541e6b05..00000000 --- a/src/app/core/use_cases/sensors_records/filter_sensors_records.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import date - -from infra.repositories import sensors_records_repository - -from core.commons import Error - -class FilterSensorsRecords: - def execute(self,plant_id:str,start_date:date,end_date:date): - if plant_id == "all": - plant_id = None - try: - records = sensors_records_repository.get_filtered_sensors_records( - page_number=1, - start_date=start_date, - end_date=end_date, - plant_id=plant_id - ) - - return records - except Error as error: - raise error \ No newline at end of file diff --git a/src/app/core/use_cases/sensors_records/get_last_sensors_record.py b/src/app/core/use_cases/sensors_records/get_last_sensors_record.py deleted file mode 100644 index d317f04e..00000000 --- a/src/app/core/use_cases/sensors_records/get_last_sensors_record.py +++ /dev/null @@ -1,28 +0,0 @@ -from core.entities.sensors_record import SensorsRecord - -from core.commons import Error -from infra.repositories import sensors_records_repository - - -class GetLastSensorsRecord: - def execute(self) -> SensorsRecord: - try: - last_sensors_record = sensors_records_repository.get_last_sensors_records( - count=2 - ) - - if not last_sensors_record: - return self.__get_empty_sensors_record() - - return last_sensors_record - - except Error: - return self.__get_empty_sensors_record() - - def __get_empty_sensors_record(self) -> SensorsRecord: - return SensorsRecord( - soil_humidity=0, - ambient_humidity=0, - temperature=0, - water_volume=0, - ) diff --git a/src/app/core/use_cases/sensors_records/get_last_sensors_record_page_data.py b/src/app/core/use_cases/sensors_records/get_last_sensors_record_page_data.py index ba62dc89..f6b103b9 100644 --- a/src/app/core/use_cases/sensors_records/get_last_sensors_record_page_data.py +++ b/src/app/core/use_cases/sensors_records/get_last_sensors_record_page_data.py @@ -1,10 +1,13 @@ from core.entities.sensors_record import SensorsRecord - -from core.commons import Error -from infra.repositories import sensors_records_repository +from core.interfaces.repositories import SensorRecordsRepositoryInterface class GetLastSensorsRecordPageData: + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + ): + self._sensors_records_repository = sensors_records_repository def execute(self): variations = { @@ -13,13 +16,12 @@ def execute(self): "water_volume": 0, "temperature": 0, } - try: - last_sensors_records = sensors_records_repository.get_last_sensors_records( - count=2 + last_sensors_records = ( + self._sensors_records_repository.get_last_sensors_records(count=2) ) - if not len(last_sensors_records) >= 2: + if len(last_sensors_records) < 2: return { "last_sensors_record": ( last_sensors_records[0] @@ -49,7 +51,7 @@ def execute(self): "variations": variations, } - except Error: + except Exception: return { "last_sensors_record": self.__get_empty_sensors_record(), "variations": variations, @@ -65,7 +67,7 @@ def __get_variation( penultimate_record_value = getattr(penultimate_record, attribute) if last_record_atribute_value == 0 or penultimate_record_value == 0: - return 0 + return 0.0 difference = last_record_atribute_value - penultimate_record_value diff --git a/src/app/core/use_cases/sensors_records/get_sensors_dashboard_page_data.py b/src/app/core/use_cases/sensors_records/get_sensors_dashboard_page_data.py deleted file mode 100644 index 45fb6504..00000000 --- a/src/app/core/use_cases/sensors_records/get_sensors_dashboard_page_data.py +++ /dev/null @@ -1,49 +0,0 @@ -from core.commons import LineChart, Error, OrderedPlants -from core.constants import ADMIN_USER_EMAIL - -from infra.repositories import ( - sensors_records_repository, - plants_repository, - users_repository, -) - - -class GetSensorDashboardPageData: - def execute(self): - plants = plants_repository.get_plants() - - if len(plants) == 0: - raise Error("Nenhuma planta encontrada", status_code=404) - - active_plant_id = users_repository.get_user_active_plant_id(ADMIN_USER_EMAIL) - - ordered_plants = OrderedPlants(plants, active_plant_id) - - sensors_records = ( - sensors_records_repository.get_sensor_records_grouped_by_date() - ) - - if len(sensors_records) == 0: - raise Error("Nenhum registro dos sensores encontrado", status_code=404) - - soil_humidity_chart = LineChart(sensors_records, "soil_humidity") - ambient_humidity_chart = LineChart(sensors_records, "ambient_humidity") - temperature_chart = LineChart(sensors_records, "temperature") - water_volume_chart = LineChart(sensors_records, "water_volume") - - return { - "soil_humidity_chart_data": soil_humidity_chart.get_data( - ordered_plants.get_value() - ), - "ambient_humidity_chart_data": ambient_humidity_chart.get_data( - ordered_plants.get_value() - ), - "temperature_chart_data": temperature_chart.get_data( - ordered_plants.get_value() - ), - "water_volume_chart_data": water_volume_chart.get_data( - ordered_plants.get_value() - ), - "plants": plants, - "active_plant_id": active_plant_id, - } diff --git a/src/app/core/use_cases/sensors_records/get_sensors_records_csv_file.py b/src/app/core/use_cases/sensors_records/get_sensors_records_csv_file.py index a90274bc..5cb3c307 100644 --- a/src/app/core/use_cases/sensors_records/get_sensors_records_csv_file.py +++ b/src/app/core/use_cases/sensors_records/get_sensors_records_csv_file.py @@ -1,15 +1,22 @@ -from datetime import date, datetime +from datetime import date -from core.commons import RecordsFilters, Error +from core.commons import RecordsFilters from core.constants import CSV_FILE_COLUMNS -from infra.repositories import sensors_records_repository -from infra.constants import FOLDERS -from infra.providers.data_analyser_provider import DataAnalyserProvider +from core.interfaces.repositories import SensorRecordsRepositoryInterface +from core.interfaces.providers import DataAnalyserProviderInterface class GetSensorsRecordsCsvFile: - def execute(self, plant_id: str, start_date: date, end_date: date): + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + data_analyser_provider: DataAnalyserProviderInterface, + ): + self._data_analyser_provider = data_analyser_provider + self._sensors_records_repository = sensors_records_repository + + def execute(self, plant_id: str, start_date: date, end_date: date, folder: str): try: filters = RecordsFilters( plant_id=plant_id, start_date=start_date, end_date=end_date @@ -17,22 +24,17 @@ def execute(self, plant_id: str, start_date: date, end_date: date): data = self.__get_data(filters) - csv_name = "registros-dos-sensores.xlsx" - tmp_folder = FOLDERS["tmp"] + csv_filename = "registros-dos-sensores.xlsx" - data_analyser_provider = DataAnalyserProvider() - data_analyser_provider.analyse(data) - data_analyser_provider.convert_to_excel(tmp_folder, csv_name) + self._data_analyser_provider.analyse(data) + self._data_analyser_provider.convert_to_excel(folder, csv_filename) - return { - "folder": tmp_folder, - "filename": csv_name, - } - except Error as error: - raise error + return csv_filename + except Exception as error: + print(error, flush=True) def __get_data(self, filters: RecordsFilters): - sensors_records = sensors_records_repository.get_filtered_sensors_records( + sensors_records = self._sensors_records_repository.get_filtered_sensors_records( page_number="all", plant_id=filters.plant_id, start_date=filters.start_date, @@ -42,6 +44,8 @@ def __get_data(self, filters: RecordsFilters): columns = CSV_FILE_COLUMNS["sensors_records"] data = {column: [] for column in columns} + print(sensors_records, flush=True) + if len(sensors_records) == 0: return data diff --git a/src/app/core/use_cases/sensors_records/get_sensors_records_dashboard_page_data.py b/src/app/core/use_cases/sensors_records/get_sensors_records_dashboard_page_data.py new file mode 100644 index 00000000..cbff3810 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/get_sensors_records_dashboard_page_data.py @@ -0,0 +1,58 @@ +from core.commons import LineChart, OrderedPlants +from core.errors.plants import PlantNotFoundError +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + UsersRepositoryInterface, + SensorRecordsRepositoryInterface, +) +from core.constants import ADMIN_USER_EMAIL + + +class GetSensorsRecordsDashboardPageData: + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + users_repository: UsersRepositoryInterface, + sensors_records_repository: SensorRecordsRepositoryInterface, + ): + self._plants_repository = plants_repository + self._users_repository = users_repository + self._sensors_records_repository = sensors_records_repository + + def execute(self): + plants = self._plants_repository.get_plants() + + if len(plants) == 0: + raise PlantNotFoundError("Nenhuma planta encontrada") + + active_plant_id = self._users_repository.get_user_active_plant_id( + ADMIN_USER_EMAIL + ) + + ordered_plants = OrderedPlants(plants, active_plant_id) + + records = self._sensors_records_repository.get_sensor_records_for_line_charts() + + if len(records) == 0: + raise SensorsRecordNotFoundError( + ui_message="Nenhum registro dos sensores encontrado" + ) + + soil_humidity_chart = LineChart(records["soil_humidity_line_chart_records"]) + ambient_humidity_chart = LineChart( + records["ambient_humidity_line_chart_records"] + ) + temperature_chart = LineChart(records["temperature_line_chart_records"]) + water_volume_chart = LineChart(records["water_volume_line_chart_records"]) + + plants = ordered_plants.get_value() + + return { + "soil_humidity_chart_data": soil_humidity_chart.get_data(plants), + "ambient_humidity_chart_data": ambient_humidity_chart.get_data(plants), + "temperature_chart_data": temperature_chart.get_data(plants), + "water_volume_chart_data": water_volume_chart.get_data(plants), + "plants": plants, + "active_plant_id": active_plant_id, + } diff --git a/src/app/core/use_cases/sensors_records/get_sensors_records_table_page_data.py b/src/app/core/use_cases/sensors_records/get_sensors_records_table_page_data.py index 3528f363..fbafc0f0 100644 --- a/src/app/core/use_cases/sensors_records/get_sensors_records_table_page_data.py +++ b/src/app/core/use_cases/sensors_records/get_sensors_records_table_page_data.py @@ -1,11 +1,20 @@ -from core.commons import Pagination, Date, Error - +from core.commons import Pagination, RecordsFilters +from core.interfaces.repositories import ( + PlantsRepositoryInterface, + SensorRecordsRepositoryInterface, +) from core.entities import SensorsRecord, Plant -from infra.repositories import plants_repository, sensors_records_repository - class GetSensorsRecordsTablePageData: + def __init__( + self, + plants_repository: PlantsRepositoryInterface, + sensors_records_repository: SensorRecordsRepositoryInterface, + ): + self._plants_repository = plants_repository + self._sensors_records_repository = sensors_records_repository + def execute( self, start_date: str, @@ -14,45 +23,38 @@ def execute( page_number: int = 1, should_get_plants: bool = False, ) -> tuple[list[SensorsRecord], int, list[Plant]]: - try: - plants = [] - if should_get_plants: - plants = plants_repository.get_plants() - filters = self.__handle_filters( - plant_id=plant_id, start_date=start_date, end_date=end_date - ) - - sensors_count = sensors_records_repository.get_sensors_records_count() - - pagination = Pagination(page_number, sensors_count) - - current_page_number, last_page_number = ( - pagination.get_current_and_last_page_numbers() - ) - - sensors_records = sensors_records_repository.get_filtered_sensors_records( - page_number=current_page_number, - plant_id=filters["plant_id"], - start_date=filters["start_date"], - end_date=filters["end_date"], - ) - - return { - "sensors_records": sensors_records, - "plants": plants, - "last_page_number": last_page_number, - "current_page_number": current_page_number, - } - - except Error as error: - return error - - def __handle_filters(self, start_date: str, end_date: str, plant_id: str): - if plant_id == "all": - plant_id = None - if start_date != "" and isinstance(start_date, str): - start_date = Date(start_date).get_value() - if end_date != "" and isinstance(end_date, str): - end_date = Date(end_date).get_value() - - return {"plant_id": plant_id, "start_date": start_date, "end_date": end_date} + plants = [] + if should_get_plants: + plants = self._plants_repository.get_plants() + + filters = RecordsFilters( + plant_id=plant_id, start_date=start_date, end_date=end_date + ) + + records_count = self._sensors_records_repository.get_sensors_records_count( + plant_id=filters.plant_id, + start_date=filters.start_date, + end_date=filters.end_date, + ) + + pagination = Pagination(page_number, records_count) + + current_page_number, last_page_number = ( + pagination.get_current_and_last_page_numbers() + ) + + print(current_page_number, flush=True) + + sensors_records = self._sensors_records_repository.get_filtered_sensors_records( + page_number=current_page_number, + plant_id=filters.plant_id, + start_date=filters.start_date, + end_date=filters.end_date, + ) + + return { + "sensors_records": sensors_records, + "plants": plants, + "last_page_number": last_page_number, + "current_page_number": current_page_number, + } diff --git a/src/app/core/use_cases/sensors_records/tests/__init__.py b/src/app/core/use_cases/sensors_records/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/app/core/use_cases/sensors_records/tests/create_sensors_record_by_api_test.py b/src/app/core/use_cases/sensors_records/tests/create_sensors_record_by_api_test.py new file mode 100644 index 00000000..a5c72258 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/create_sensors_record_by_api_test.py @@ -0,0 +1,83 @@ +from dataclasses import asdict + +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + UsersRepositoryMock, + SensorRecordsRepositoryMock, +) +from core.errors.validation import SensorsRecordNotValidError +from core.errors.plants import PlantNotFoundError +from core.entities.tests.fakers import SensorsRecordsFaker, UsersFaker, PlantsFaker +from core.constants import ADMIN_USER_EMAIL + +from ..create_sensors_record_by_api import CreateSensorsRecordByApi + + +def describe_create_sensors_record_by_api_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def users_repository(): + return UsersRepositoryMock() + + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + ): + users_repository.clear_users() + plants_repository.clear_plants() + sensors_records_repository.clear_records() + return CreateSensorsRecordByApi( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + users_repository=users_repository, + ) + + @fixture + def fake_request(): + return asdict(SensorsRecordsFaker.fake()) + + def it_should_throw_error_if_request_is_invalid( + use_case: CreateSensorsRecordByApi, + ): + with raises(SensorsRecordNotValidError): + use_case.execute(None) + + def it_should_throw_error_if_no_active_plant_is_found( + fake_request, + use_case: CreateSensorsRecordByApi, + ): + with raises(PlantNotFoundError): + use_case.execute(fake_request) + + def it_should_throw_error_no_active_plant_is_found( + fake_request, + users_repository: UsersRepositoryMock, + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: CreateSensorsRecordByApi, + ): + fake_user = UsersFaker.fake() + fake_plant = PlantsFaker.fake() + fake_user.email = ADMIN_USER_EMAIL + fake_user.active_plant_id = fake_plant.id + + users_repository.create_user(fake_user) + + use_case.execute(fake_request) + + last_record = sensors_records_repository.get_last_sensors_records(count=1)[0] + + assert fake_request["soil_humidity"] == last_record.soil_humidity + assert fake_request["ambient_humidity"] == last_record.ambient_humidity + assert fake_request["temperature"] == last_record.temperature + assert fake_request["water_volume"] == last_record.water_volume diff --git a/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_csv_file_test.py b/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_csv_file_test.py new file mode 100644 index 00000000..7c1736da --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_csv_file_test.py @@ -0,0 +1,187 @@ +from pathlib import Path + +from pytest import fixture, raises +from werkzeug.datastructures import FileStorage + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + SensorRecordsRepositoryMock, +) +from core.use_cases.tests.mocks.providers import DataAnalyserProviderMock +from core.entities import Plant +from core.errors.plants import PlantNotFoundError +from core.entities.tests.fakers import PlantsFaker +from core.errors.validation import DatetimeNotValidError +from core.constants import CSV_FILE_COLUMNS + + +from ..create_sensors_records_by_csv_file import CreateSensorsRecordsByCsvFile + + +def describe_create_sensors_records_by_csv_file_use_case(): + @fixture + def fake_plant(): + return PlantsFaker.fake(name="alface") + + @fixture + def plants_repository(fake_plant): + plants_repository = PlantsRepositoryMock() + plants_repository.clear_plants() + plants_repository.create_plant(fake_plant) + + return plants_repository + + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def data_analyser_provider(): + data_analyser_provider = DataAnalyserProviderMock() + data_analyser_provider.get_columns = lambda: CSV_FILE_COLUMNS["sensors_records"] + + return data_analyser_provider + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + data_analyser_provider: DataAnalyserProviderMock, + ): + sensors_records_repository.clear_records() + return CreateSensorsRecordsByCsvFile( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + data_analyser_provider=data_analyser_provider, + ) + + @fixture + def file(tmp_path: Path): + return FileStorage(tmp_path / "fake_csv_.xlsx") + + def it_should_throw_error_if_datetime_from_any_record_is_not_valid( + file: FileStorage, + data_analyser_provider: DataAnalyserProviderMock, + use_case: CreateSensorsRecordsByCsvFile, + ): + + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "15/09/2023", + "hora": "12:56", # invalid time format + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "alface", + } + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "15-09-2023", # invalid date format + "hora": "12:56", + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "alface", + } + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "15/42/2023", # invalid date + "hora": "12:56:00", + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "alface", + } + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "15/09/2023", + "hora": "12:99:00", # invalid time + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "alface", + } + ] + + with raises(DatetimeNotValidError): + use_case.execute(file) + + def it_should_create_sensors_records( + file: FileStorage, + fake_plant: Plant, + data_analyser_provider: DataAnalyserProviderMock, + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: CreateSensorsRecordsByCsvFile, + ): + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "17/09/2023", + "hora": "12:09:00", + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "alface", + }, + ] + + use_case.execute(file) + + last_record = sensors_records_repository.get_last_sensors_records(count=1)[0] + + assert last_record.soil_humidity == 72 + assert last_record.ambient_humidity == 55 + assert last_record.temperature == 24.7 + assert last_record.water_volume == 0 + assert last_record.created_at.get_value() == "2023-09-17 12:09:00" + assert last_record.plant == fake_plant + + def it_should_throw_error_if_there_is_no_plant_in_repository( + file: FileStorage, + data_analyser_provider: DataAnalyserProviderMock, + use_case: CreateSensorsRecordsByCsvFile, + ): + data_analyser_provider.convert_to_list_of_records = lambda: [ + { + "data": "17/09/2023", + "hora": "12:09:00", + "dia da semana": "quarta", + "umidade solo": 72, + "umidade Ambiente": 55, + "temperatura": 24.7, + "volume de Água (ml)": 0, + "planta": "beterraba", # Non-existing plant + }, + ] + + with raises(PlantNotFoundError) as error: + use_case.execute(file) + + assert ( + str(error.value) + == "Planta não encontrada para o registro da data 17/09/2023 12:09" + ) diff --git a/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_form_test.py b/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_form_test.py new file mode 100644 index 00000000..82b5d3cb --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/create_sensors_records_by_form_test.py @@ -0,0 +1,93 @@ +from datetime import date, time + +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + SensorRecordsRepositoryMock, + PlantsRepositoryMock, +) +from core.errors.validation import DatetimeNotValidError, DateNotValidError +from core.errors.plants import PlantNotFoundError +from core.entities.tests.fakers import PlantsFaker + + +from ..create_sensors_records_by_form import CreateSensorsRecordByForm + + +def fake_request(base_fake_request: dict): + return { + "time": time(hour=12, minute=52), + "date": date(year=2024, month=12, day=12), + "soil_humidity": 32, + "ambient_humidity": 55, + "temperature": 20, + "water_volume": 0, + **base_fake_request, + } + + +def describe_create_sensors_record_by_api_use_case(): + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + sensors_records_repository.clear_records() + return CreateSensorsRecordByForm( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + ) + + def it_should_throw_error_if_time_from_request_is_not_valid( + use_case: CreateSensorsRecordByForm, + ): + request = fake_request({"time": "not valid time"}) + + with raises(DatetimeNotValidError): + use_case.execute(request) + + def it_should_throw_error_if_date_from_request_is_not_valid( + use_case: CreateSensorsRecordByForm, + ): + request = fake_request({"date": "not valid date"}) + + with raises(DateNotValidError): + use_case.execute(request) + + def it_should_throw_error_if_there_is_no_plant_in_repository( + use_case: CreateSensorsRecordByForm, + ): + fake_plant = PlantsFaker.fake() + + request = fake_request({"plant_id": fake_plant.id}) + + with raises(PlantNotFoundError): + use_case.execute(request) + + def it_should_create_sensors_record( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: CreateSensorsRecordByForm, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + request = fake_request({"plant_id": fake_plant.id}) + + use_case.execute(request) + + last_record = sensors_records_repository.get_last_sensors_records(count=1)[0] + + assert last_record.ambient_humidity == request["ambient_humidity"] + assert last_record.soil_humidity == request["soil_humidity"] + assert last_record.temperature == request["temperature"] + assert last_record.water_volume == request["water_volume"] + assert last_record.plant == fake_plant diff --git a/src/app/core/use_cases/sensors_records/tests/delete_sensors_records_test.py b/src/app/core/use_cases/sensors_records/tests/delete_sensors_records_test.py new file mode 100644 index 00000000..025288a7 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/delete_sensors_records_test.py @@ -0,0 +1,62 @@ +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import SensorRecordsRepositoryMock +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.errors.validation import SensorsRecordNotValidError +from core.entities.tests.fakers import SensorsRecordsFaker + +from ..delete_sensors_records import DeleteSensorsRecord + + +def describe_delete_sensors_records_use_case(): + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + ): + sensors_records_repository.clear_records() + return DeleteSensorsRecord( + sensors_records_repository=sensors_records_repository, + ) + + def it_should_throw_error_if_at_least_one_sensors_record_id_is_not_valid( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: DeleteSensorsRecord, + ): + fake_records = SensorsRecordsFaker.fake_many(3) + sensors_records_repository.create_many_sensors_records(fake_records) + + ids = [fake_record.id for fake_record in fake_records] + + ids.append(42) + + with raises(SensorsRecordNotValidError): + use_case.execute(ids) + + def it_should_throw_error_if_at_least_one_sensors_record_is_not_found( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: DeleteSensorsRecord, + ): + fake_records = SensorsRecordsFaker.fake_many(3) + sensors_records_repository.create_many_sensors_records(fake_records[:2]) + + with raises(SensorsRecordNotFoundError): + use_case.execute([fake_record.id for fake_record in fake_records]) + + def it_should_delete_many_records( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: DeleteSensorsRecord, + ): + count = 3 + fake_records = SensorsRecordsFaker.fake_many(count) + + sensors_records_repository.create_many_sensors_records(fake_records) + + use_case.execute([fake_record.id for fake_record in fake_records]) + + records = sensors_records_repository.get_last_sensors_records(count=count) + + assert len(records) == 0 diff --git a/src/app/core/use_cases/sensors_records/tests/get_last_sensors_record_page_data_test.py b/src/app/core/use_cases/sensors_records/tests/get_last_sensors_record_page_data_test.py new file mode 100644 index 00000000..1aff8754 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/get_last_sensors_record_page_data_test.py @@ -0,0 +1,89 @@ +from pytest import fixture + +from core.use_cases.tests.mocks.repositories import SensorRecordsRepositoryMock +from core.entities.tests.fakers import SensorsRecordsFaker +from core.entities import SensorsRecord + +from ..get_last_sensors_record_page_data import GetLastSensorsRecordPageData + + +def describe_get_last_sensors_record_page_data_use_case(): + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + ): + sensors_records_repository.clear_records() + return GetLastSensorsRecordPageData( + sensors_records_repository=sensors_records_repository, + ) + + def it_should_get_empty_sensors_record_if_there_is_no_record_in_repository( + use_case: GetLastSensorsRecordPageData, + ): + data = use_case.execute() + + assert data["last_sensors_record"] == SensorsRecord( + soil_humidity=0, + ambient_humidity=0, + temperature=0, + water_volume=0, + ) + + def it_should_get_empty_variations_if_there_is_no_record_in_repository_or_only_one_record( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: GetLastSensorsRecordPageData, + ): + data = use_case.execute() + + assert data["variations"] == { + "soil_humidity": 0, + "ambient_humidity": 0, + "water_volume": 0, + "temperature": 0, + } + + fake_record = SensorsRecordsFaker.fake() + sensors_records_repository.create_sensors_record(fake_record) + + data = use_case.execute() + + assert data["variations"] == { + "soil_humidity": 0, + "ambient_humidity": 0, + "water_volume": 0, + "temperature": 0, + } + + def it_should_get_variations_between_the_two_last_records( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: GetLastSensorsRecordPageData, + ): + last_fake_record = SensorsRecordsFaker.fake( + soil_humidity=25, + ambient_humidity=50, + temperature=100, + water_volume=0, + ) + penultimate_fake_record = SensorsRecordsFaker.fake( + soil_humidity=50, + ambient_humidity=25, + temperature=100, + water_volume=25, + ) + + sensors_records_repository.create_many_sensors_records( + [last_fake_record, penultimate_fake_record] + ) + + data = use_case.execute() + + data["variations"] = { + "soil_humidity": -50.0, + "ambient_humidity": 100.0, + "water_volume": 0.0, + "temperature": 0.0, + } diff --git a/src/app/core/use_cases/sensors_records/tests/get_sensors_records_csv_file_test.py b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_csv_file_test.py new file mode 100644 index 00000000..d6273305 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_csv_file_test.py @@ -0,0 +1,179 @@ +from datetime import datetime + +from pytest import fixture + +from core.use_cases.tests.mocks.repositories import SensorRecordsRepositoryMock +from core.use_cases.tests.mocks.providers import DataAnalyserProviderMock +from core.entities.tests.fakers import SensorsRecordsFaker, PlantsFaker +from core.commons import Datetime +from core.constants import CSV_FILE_COLUMNS + +from ..get_sensors_records_csv_file import GetSensorsRecordsCsvFile + + +def describe_get_sensors_records_csv_file_use_case(): + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def data_analyser_provider(): + data_analyser_provider = DataAnalyserProviderMock() + data_analyser_provider.get_columns = lambda: CSV_FILE_COLUMNS["sensors_records"] + + return data_analyser_provider + + @fixture + def use_case( + data_analyser_provider: DataAnalyserProviderMock, + sensors_records_repository: SensorRecordsRepositoryMock, + ): + sensors_records_repository.clear_records() + return GetSensorsRecordsCsvFile( + sensors_records_repository=sensors_records_repository, + data_analyser_provider=data_analyser_provider, + ) + + @fixture + def folder(): + return "fake_csv_folder" + + def it_should_create_csv_file_in_a_specific_path( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + use_case: GetSensorsRecordsCsvFile, + ): + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + + assert csv_file["path"] == f"{folder}/registros-dos-sensores.xlsx" + + def it_should_create_csv_file_with_empty_data_if_there_is_no_any_sensors_record_in_repository( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + use_case: GetSensorsRecordsCsvFile, + ): + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + + for data in csv_file["data"].values(): + assert data == [] + + def it_should_create_csv_file_containing_sensors_records( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: GetSensorsRecordsCsvFile, + ): + fake_records = SensorsRecordsFaker.fake_many() + + sensors_records_repository.create_many_sensors_records(fake_records) + + use_case.execute(plant_id=None, start_date=None, end_date=None, folder=folder) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert data["data"] == [ + fake_record.created_at.get_value()[:10] for fake_record in fake_records + ] + assert data["hora"] == [ + fake_record.created_at.get_time() for fake_record in fake_records + ] + assert data["umidade Ambiente"] == [ + fake_record.ambient_humidity for fake_record in fake_records + ] + assert data["umidade solo"] == [ + fake_record.soil_humidity for fake_record in fake_records + ] + assert data["temperatura"] == [ + fake_record.temperature for fake_record in fake_records + ] + assert data["volume de Água (ml)"] == [ + fake_record.water_volume for fake_record in fake_records + ] + assert data["dia da semana"] == [ + fake_record.weekday.get_value() for fake_record in fake_records + ] + assert data["planta"] == [ + fake_record.plant.name for fake_record in fake_records + ] + + def it_should_create_csv_file_with_sensors_records_filtered_by_plant( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: GetSensorsRecordsCsvFile, + ): + fake_records = SensorsRecordsFaker.fake_many(10) + fake_plant = PlantsFaker.fake() + + fake_records.append(SensorsRecordsFaker.fake(plant=fake_plant)) + fake_records.append(SensorsRecordsFaker.fake(plant=fake_plant)) + fake_records.append(SensorsRecordsFaker.fake(plant=fake_plant)) + + sensors_records_repository.create_many_sensors_records(fake_records) + + use_case.execute( + plant_id=fake_plant.id, start_date=None, end_date=None, folder=folder + ) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert len(data["planta"]) == 3 + assert data["planta"] == [fake_plant.name for _ in range(3)] + + def it_should_create_csv_file_with_sensors_records_filtered_by_date( + folder: str, + data_analyser_provider: DataAnalyserProviderMock, + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: GetSensorsRecordsCsvFile, + ): + fake_records = SensorsRecordsFaker.fake_many(10) + + fake_records.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=12, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=25, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=30, hour=0, minute=0, second=0) + ) + ) + ) + fake_records.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=11, day=12, hour=0, minute=0, second=0) + ) + ) + ) + + sensors_records_repository.create_many_sensors_records(fake_records) + + use_case.execute( + plant_id=None, + start_date="2024-12-12", + end_date="2024-12-30", + folder=folder, + ) + + csv_file = data_analyser_provider.csv_file + data = csv_file["data"] + + assert len(data["data"]) == 3 + assert data["data"] == ["12/12/2024", "25/12/2024", "30/12/2024"] diff --git a/src/app/core/use_cases/sensors_records/tests/get_sensors_records_dashboard_page_data_test.py b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_dashboard_page_data_test.py new file mode 100644 index 00000000..f7300cc6 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_dashboard_page_data_test.py @@ -0,0 +1,137 @@ +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + UsersRepositoryMock, + SensorRecordsRepositoryMock, +) +from core.entities import User, Plant +from core.entities.tests.fakers import PlantsFaker, UsersFaker +from core.commons import OrderedPlants +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.errors.plants import PlantNotFoundError +from core.constants import ADMIN_USER_EMAIL + +from ..get_sensors_records_dashboard_page_data import GetSensorsRecordsDashboardPageData + + +def describe_get_sensors_records_dashboard_page_data_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def users_repository(): + return UsersRepositoryMock() + + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + ): + plants_repository.clear_plants() + users_repository.clear_users() + return GetSensorsRecordsDashboardPageData( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + users_repository=users_repository, + ) + + @fixture + def fake_plant(): + return PlantsFaker.fake() + + @fixture + def fake_user(fake_plant): + fake_user = UsersFaker.fake(email=ADMIN_USER_EMAIL) + fake_user.active_plant_id = fake_plant.id + return fake_user + + def it_should_throw_error_if_there_is_no_plant_in_repository( + use_case: GetSensorsRecordsDashboardPageData, + ): + with raises(PlantNotFoundError) as error: + use_case.execute() + + assert str(error.value) == "Nenhuma planta encontrada" + + def it_should_throw_error_if_there_is_no_sensors_record_in_repository( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + sensors_records_repository: SensorRecordsRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetSensorsRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + sensors_records_repository.get_sensor_records_for_line_charts = lambda: [] + + with raises(SensorsRecordNotFoundError) as error: + use_case.execute() + + assert str(error.value) == "Nenhum registro dos sensores encontrado" + + def it_should_get_data_for_each_line_chart( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetSensorsRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + plants_repository.create_plant(fake_plant) + + data = use_case.execute() + + assert isinstance(data["soil_humidity_chart_data"], dict) + assert isinstance(data["ambient_humidity_chart_data"], dict) + assert isinstance(data["temperature_chart_data"], dict) + assert isinstance(data["water_volume_chart_data"], dict) + + def it_should_get_ordered_plants( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetSensorsRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + + fake_plants = PlantsFaker.fake_many(2) + fake_plants.append(fake_plant) + + for plant in fake_plants: + plants_repository.create_plant(plant) + + data = use_case.execute() + + assert ( + data["plants"] + == OrderedPlants(fake_plants, fake_user.active_plant_id).get_value() + ) + + def it_should_get_active_plant_id( + plants_repository: PlantsRepositoryMock, + users_repository: UsersRepositoryMock, + fake_user: User, + fake_plant: Plant, + use_case: GetSensorsRecordsDashboardPageData, + ): + users_repository.create_user(fake_user) + + fake_plants = PlantsFaker.fake_many(2) + fake_plants.append(fake_plant) + + for plant in fake_plants: + plants_repository.create_plant(plant) + + data = use_case.execute() + + assert data["active_plant_id"] == fake_user.active_plant_id diff --git a/src/app/core/use_cases/sensors_records/tests/get_sensors_records_table_page_data_test.py b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_table_page_data_test.py new file mode 100644 index 00000000..d101a934 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/get_sensors_records_table_page_data_test.py @@ -0,0 +1,174 @@ +from pprint import pprint +from datetime import datetime + +from pytest import fixture + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + SensorRecordsRepositoryMock, +) +from core.entities.tests.fakers import PlantsFaker, SensorsRecordsFaker +from core.commons import Datetime +from core.constants import PAGINATION + +from ..get_sensors_records_table_page_data import GetSensorsRecordsTablePageData + + +def describe_get_sensors_records_dashboard_page_data_use_case(): + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + plants_repository.clear_plants() + sensors_records_repository.clear_records() + return GetSensorsRecordsTablePageData( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + ) + + def it_should_get_plants_only_when_is_required( + plants_repository: PlantsRepositoryMock, + use_case: GetSensorsRecordsTablePageData, + ): + fake_plants = PlantsFaker.fake_many() + + for fake_plant in fake_plants: + plants_repository.create_plant(fake_plant) + + data = use_case.execute( + should_get_plants=True, + plant_id=None, + start_date=None, + end_date=None, + page_number=1, + ) + + assert data["plants"] == fake_plants + + data = use_case.execute( + should_get_plants=False, + plant_id=None, + start_date=None, + end_date=None, + page_number=1, + ) + + assert data["plants"] == [] + + def it_should_get_sensors_records_filtered_by_plant_id( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetSensorsRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + records = SensorsRecordsFaker.fake_many(5) + record_in_filter = SensorsRecordsFaker.fake(plant=fake_plant) + + records.append(record_in_filter) + + sensors_records_repository.create_many_sensors_records(records) + + data = use_case.execute( + should_get_plants=True, + plant_id=fake_plant.id, + start_date=None, + end_date=None, + page_number=1, + ) + + data["sensors_records"] == [record_in_filter] + + def it_should_get_sensors_records_filtered_by_date( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetSensorsRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + fake_records = SensorsRecordsFaker.fake_many(10) + + fake_records.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=11, day=12, hour=0, minute=0, second=0) + ) + ) + ) + + fake_records_in_filter = [] + + fake_records_in_filter.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=12, hour=0, minute=0, second=0) + ) + ) + ) + fake_records_in_filter.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=25, hour=0, minute=0, second=0) + ) + ) + ) + fake_records_in_filter.append( + SensorsRecordsFaker.fake( + created_at=Datetime( + datetime(year=2024, month=12, day=30, hour=0, minute=0, second=0) + ) + ) + ) + + fake_records.extend(fake_records_in_filter) + + sensors_records_repository.create_many_sensors_records(fake_records) + + data = use_case.execute( + should_get_plants=True, + plant_id=None, + start_date="2024-12-12", + end_date="2024-12-30", + page_number=1, + ) + + assert data["sensors_records"] == fake_records_in_filter + + def it_should_get_sensors_records_filtered_by_page( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: GetSensorsRecordsTablePageData, + ): + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + fake_records = SensorsRecordsFaker.fake_many(24) + + sensors_records_repository.create_many_sensors_records(fake_records) + + page_number = 4 + + data = use_case.execute( + should_get_plants=False, + plant_id=None, + start_date=None, + end_date=None, + page_number=page_number, + ) + + records_per_page = PAGINATION["records_per_page"] + records_slice = (page_number - 1) * records_per_page + + assert len(data["sensors_records"]) == records_per_page + assert data["sensors_records"] == fake_records[records_slice:] diff --git a/src/app/core/use_cases/sensors_records/tests/update_sensors_record_test.py b/src/app/core/use_cases/sensors_records/tests/update_sensors_record_test.py new file mode 100644 index 00000000..ee2b163b --- /dev/null +++ b/src/app/core/use_cases/sensors_records/tests/update_sensors_record_test.py @@ -0,0 +1,149 @@ +from datetime import time, date + +from pytest import fixture, raises + +from core.use_cases.tests.mocks.repositories import ( + PlantsRepositoryMock, + SensorRecordsRepositoryMock, +) +from core.entities.tests.fakers import SensorsRecordsFaker, PlantsFaker +from core.errors.validation import ( + SensorsRecordNotValidError, + DatetimeNotValidError, + DateNotValidError, +) +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.errors.plants import PlantNotFoundError + +from ..update_sensors_record import UpdateSensorsRecord + + +def fake_request(base_fake_request: dict): + return { + "time": time(hour=12, minute=52), + "date": date(year=2024, month=12, day=12), + "soil_humidity": 32, + "ambient_humidity": 55, + "temperature": 20, + "water_volume": 0, + **base_fake_request, + } + + +def describe_update_sensors_record_use_case(): + @fixture + def sensors_records_repository(): + return SensorRecordsRepositoryMock() + + @fixture + def plants_repository(): + return PlantsRepositoryMock() + + @fixture + def use_case( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + ): + plants_repository.clear_plants() + sensors_records_repository.clear_records() + return UpdateSensorsRecord( + sensors_records_repository=sensors_records_repository, + plants_repository=plants_repository, + ) + + def it_should_throw_error_if_id_from_request_is_not_valid( + use_case: UpdateSensorsRecord, + ): + + with raises(SensorsRecordNotValidError): + use_case.execute(request=fake_request({"sensors_record_id": None})) + + def it_should_throw_error_if_no_sensors_record_is_found_in_repository( + use_case: UpdateSensorsRecord, + ): + fake_record = SensorsRecordsFaker.fake() + + with raises(SensorsRecordNotFoundError): + use_case.execute( + request=fake_request({"sensors_record_id": fake_record.id}) + ) + + def it_should_throw_error_if_time_from_request_is_not_valid( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: UpdateSensorsRecord, + ): + fake_record = SensorsRecordsFaker.fake() + sensors_records_repository.create_sensors_record(fake_record) + request = fake_request( + {"time": "not valid time", "sensors_record_id": fake_record.id} + ) + + with raises(DatetimeNotValidError): + use_case.execute(request) + + def it_should_throw_error_if_date_from_request_is_not_valid( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: UpdateSensorsRecord, + ): + fake_record = SensorsRecordsFaker.fake() + sensors_records_repository.create_sensors_record(fake_record) + + with raises(DateNotValidError): + use_case.execute( + request=fake_request( + {"sensors_record_id": fake_record.id, "date": None} + ) + ) + + def it_should_throw_error_if_no_plant_is_found_in_repository( + sensors_records_repository: SensorRecordsRepositoryMock, + use_case: UpdateSensorsRecord, + ): + fake_record = SensorsRecordsFaker.fake() + sensors_records_repository.create_sensors_record(fake_record) + + fake_plant = PlantsFaker.fake() + + with raises(PlantNotFoundError): + use_case.execute( + request=fake_request( + {"sensors_record_id": fake_record.id, "plant_id": fake_plant.id} + ) + ) + + def it_should_update_sensors_record( + sensors_records_repository: SensorRecordsRepositoryMock, + plants_repository: PlantsRepositoryMock, + use_case: UpdateSensorsRecord, + ): + fake_record = SensorsRecordsFaker.fake( + soil_humidity=25, + ambient_humidity=25, + water_volume=25, + temperature=25, + ) + sensors_records_repository.create_sensors_record(fake_record) + + fake_plant = PlantsFaker.fake() + plants_repository.create_plant(fake_plant) + + request = fake_request( + { + "sensors_record_id": fake_record.id, + "soil_humidity": 0, + "ambient_humidity": 0, + "temperature": 0, + "water_volume": 0, + "plant_id": fake_plant.id, + } + ) + + use_case.execute(request=request) + + last_record = sensors_records_repository.get_last_sensors_records(count=1)[0] + + assert last_record.ambient_humidity == request["ambient_humidity"] + assert last_record.soil_humidity == request["soil_humidity"] + assert last_record.temperature == request["temperature"] + assert last_record.water_volume == request["water_volume"] + assert last_record.plant.id == fake_plant.id diff --git a/src/app/core/use_cases/sensors_records/update_sensors_record.py b/src/app/core/use_cases/sensors_records/update_sensors_record.py new file mode 100644 index 00000000..428517b4 --- /dev/null +++ b/src/app/core/use_cases/sensors_records/update_sensors_record.py @@ -0,0 +1,78 @@ +from datetime import datetime, date, time + +from core.entities import SensorsRecord +from core.commons import Datetime, Weekday +from core.errors.validation import ( + SensorsRecordNotValidError, + DatetimeNotValidError, + DateNotValidError, +) +from core.errors.sensors_records import SensorsRecordNotFoundError +from core.errors.plants import PlantNotFoundError +from core.interfaces.repositories import ( + SensorRecordsRepositoryInterface, + PlantsRepositoryInterface, +) + + +class UpdateSensorsRecord: + def __init__( + self, + sensors_records_repository: SensorRecordsRepositoryInterface, + plants_repository: PlantsRepositoryInterface, + ): + self._sensors_records_repository = sensors_records_repository + self._plants_repository = plants_repository + + def execute(self, request: dict) -> None: + sensors_records_id = request["sensors_record_id"] + + if not sensors_records_id or not isinstance(sensors_records_id, str): + raise SensorsRecordNotValidError() + + has_sensors_record = bool( + self._sensors_records_repository.get_sensors_record_by_id( + sensors_records_id + ) + ) + + if not has_sensors_record: + raise SensorsRecordNotFoundError() + + if "time" not in request or not isinstance(request["time"], time): + raise DatetimeNotValidError() + + if "date" not in request or not isinstance(request["date"], date): + raise DateNotValidError() + + created_at = Datetime( + datetime( + hour=request["time"].hour, + minute=request["time"].minute, + year=request["date"].year, + month=request["date"].month, + day=request["date"].day, + ) + ) + + weekday = Weekday(created_at.get_value(is_datetime=True)) + + plant = self._plants_repository.get_plant_by_id(request["plant_id"]) + + if not plant: + raise PlantNotFoundError() + + sensors_record = SensorsRecord( + id=sensors_records_id, + soil_humidity=request["soil_humidity"], + ambient_humidity=request["ambient_humidity"], + temperature=request["temperature"], + water_volume=request["water_volume"], + plant=plant, + weekday=weekday, + created_at=created_at, + ) + + self._sensors_records_repository.update_sensors_record_by_id(sensors_record) + + return sensors_record diff --git a/src/app/core/use_cases/sensors_records/update_sensors_records.py b/src/app/core/use_cases/sensors_records/update_sensors_records.py deleted file mode 100644 index dead02f6..00000000 --- a/src/app/core/use_cases/sensors_records/update_sensors_records.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime, date - -from core.entities import SensorsRecord -from core.commons import Error, Datetime, Weekday - -from infra.repositories import sensors_records_repository, plants_repository - - -class UpdateSensorsRecord: - def execute(self, request: dict) -> None: - try: - sensors_records_id = request["sensors_record_id"] - - if not sensors_records_id or not isinstance(sensors_records_id, str): - raise Error( - ui_message="Registro dos sensores não encontrado", - internal_message="Sensors record in not found", - ) - has_sensors_record = bool( - sensors_records_repository.get_sensors_record_by_id(sensors_records_id) - ) - - if not has_sensors_record: - raise Error( - ui_message="Registro dos sensores não encontrado", - internal_message="Sensors record id not found", - ) - if not isinstance(request["date"], date): - raise Error(ui_message="Data de registro não informado") - - created_at = Datetime( - datetime( - hour=request["time"].hour, - minute=request["time"].minute, - year=request["date"].year, - month=request["date"].month, - day=request["date"].day, - ) - ) - - weekday = Weekday(created_at.get_value(is_datetime=True)) - - plant = plants_repository.get_plant_by_id(request["plant_id"]) - - if not plant: - raise Error(ui_message="Planta não encontrada para esse registro") - - sensors_record = SensorsRecord( - id=sensors_records_id, - soil_humidity=request["soil_humidity"], - ambient_humidity=request["ambient_humidity"], - temperature=request["temperature"], - water_volume=request["water_volume"], - plant=plant, - weekday=weekday, - created_at=created_at, - ) - - sensors_records_repository.update_sensors_record_by_id(sensors_record) - - return sensors_record - - except Error as error: - raise error diff --git a/src/app/core/use_cases/tests/mocks/authentication/__init__.py b/src/app/core/use_cases/tests/mocks/authentication/__init__.py new file mode 100644 index 00000000..cf9257c4 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/authentication/__init__.py @@ -0,0 +1 @@ +from .auth_mock import * diff --git a/src/app/core/use_cases/tests/mocks/authentication/auth_mock.py b/src/app/core/use_cases/tests/mocks/authentication/auth_mock.py new file mode 100644 index 00000000..fe06ae25 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/authentication/auth_mock.py @@ -0,0 +1,28 @@ +from core.entities import User + + +class AuthMock: + _user = None + _should_remember_user: bool = False + + def get_user(self): + return self._user + + def login(self, user: User, should_remember_user: bool): + self._user = user + self._should_remember_user = should_remember_user + + return True + + def logout(self): + self._user = None + + def check_hash(self, hash: str, text: str) -> bool: + return True + + def generate_hash(self, text: str) -> str: + return text + + @property + def should_remember_user(self): + return self._should_remember_user diff --git a/src/app/core/use_cases/tests/mocks/providers/__init__.py b/src/app/core/use_cases/tests/mocks/providers/__init__.py new file mode 100644 index 00000000..62332a74 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/providers/__init__.py @@ -0,0 +1,2 @@ +from .data_analyser_provider_mock import * +from .email_provider_mock import * diff --git a/src/app/core/use_cases/tests/mocks/providers/data_analyser_provider_mock.py b/src/app/core/use_cases/tests/mocks/providers/data_analyser_provider_mock.py new file mode 100644 index 00000000..bac6efef --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/providers/data_analyser_provider_mock.py @@ -0,0 +1,31 @@ +from werkzeug.datastructures import FileStorage + +from core.interfaces.providers import DataAnalyserProviderInterface + + +class DataAnalyserProviderMock(DataAnalyserProviderInterface): + _data = None + _csv_file = None + + def analyse(self, data): + self._data = data + + def read_excel(self, file: FileStorage): + self._data = "excel file data" + + def read_csv(self, file: FileStorage): + self._data = "csv text file data" + + def get_columns(self): ... + + def convert_to_excel(self, folder: str, filename: str): + self._csv_file = {"path": f"{folder}/{filename}", "data": self._data} + + def convert_to_list_of_records(self): ... + + def get_data(self): + return self._data + + @property + def csv_file(self): + return self._csv_file diff --git a/src/app/core/use_cases/tests/mocks/providers/email_provider_mock.py b/src/app/core/use_cases/tests/mocks/providers/email_provider_mock.py new file mode 100644 index 00000000..3e594b1b --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/providers/email_provider_mock.py @@ -0,0 +1,12 @@ +from core.interfaces.providers import EmailProvideInterface + + +class EmailProviderMock(EmailProvideInterface): + _email = None + + def send_email(self, sender: str, receiver: str, template: str, password: str): + self._email = f"sender: {sender}; receiver: {receiver}; template: {template}; password: {password}" + + @property + def email(self): + return self._email diff --git a/src/app/core/use_cases/tests/mocks/repositories/__init__.py b/src/app/core/use_cases/tests/mocks/repositories/__init__.py new file mode 100644 index 00000000..8a527b3a --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/repositories/__init__.py @@ -0,0 +1,4 @@ +from .plants_repository_mock import * +from .sensors_records_repository_mock import * +from .checklist_records_repository_mock import * +from .users_repository_mock import * diff --git a/src/app/core/use_cases/tests/mocks/repositories/checklist_records_repository_mock.py b/src/app/core/use_cases/tests/mocks/repositories/checklist_records_repository_mock.py new file mode 100644 index 00000000..2a51e290 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/repositories/checklist_records_repository_mock.py @@ -0,0 +1,113 @@ +from datetime import date + +from core.entities import CheckListRecord +from core.interfaces.repositories import ChecklistRecordsRepositoryInterface +from core.entities.tests.fakers import LineChartRecordsFaker + +from core.constants import PAGINATION + + +class ChecklistRecordsRepositoryMock(ChecklistRecordsRepositoryInterface): + _checklist_records = [] + + def create_checklist_record(self, checklist_record: CheckListRecord) -> None: + self._checklist_records.append(checklist_record) + + def create_many_checklist_records(self, checklist_records: list[CheckListRecord]): + for record in checklist_records: + self._checklist_records.append(record) + + def get_lai_records_for_line_charts(self): + return LineChartRecordsFaker.fake_many() + + def get_checklist_record_by_id(self, id: str) -> CheckListRecord | None: + checklist_records = list( + filter(lambda plant: plant.id == id, self._checklist_records) + ) + + if len(checklist_records): + return checklist_records[0] + + return None + + def get_checklist_records_count( + self, plant_id: str, start_date: date, end_date: date + ): + return len(self._checklist_records) + + def get_last_checklist_records(self, count) -> list[CheckListRecord]: + records = self._checklist_records.copy() + + return records[:count] + + def get_leaf_appearances_and_leaf_colors_records(self): + records = [ + { + "leaf_appearance": record.leaf_appearance, + "leaf_color": record.leaf_color, + "created_at": record.created_at, + "plant_id": record.plant.id, + } + for record in self._checklist_records + ] + + records.sort( + key=lambda record: record["created_at"].get_value(is_datetime=True) + ) + + return records + + def update_checklist_record_by_id(self, checklist_record: CheckListRecord): + self._checklist_records = [ + ( + checklist_record + if checklist_record.id == current_checklist_record.id + else current_checklist_record + ) + for current_checklist_record in self._checklist_records + ] + + def delete_checklist_record_by_id(self, id: str): + self._checklist_records = [ + checklist_record + for checklist_record in self._checklist_records + if checklist_record.id != id + ] + + def delete_many_checklist_records_by_id(self, ids: str): + self._checklist_records = [ + checklist_record + for checklist_record in self._checklist_records + if checklist_record.id not in ids + ] + + def get_filtered_checklist_records( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> list[CheckListRecord]: + records = self._checklist_records + + if plant_id: + records = [record for record in records if record.plant.id == plant_id] + + if start_date and end_date: + records = [ + record + for record in records + if record.created_at.get_value(is_datetime=True).date() >= start_date + and record.created_at.get_value(is_datetime=True).date() <= end_date + ] + + if page_number != "all": + records_per_page = PAGINATION["records_per_page"] + + slice_start = (page_number - 1) * records_per_page + records = records[slice_start : slice_start + records_per_page] + else: + records = records + + records.sort(key=lambda record: record.created_at.get_value(is_datetime=True)) + + return records + + def clear_records(self): + self._checklist_records = [] diff --git a/src/app/core/use_cases/tests/mocks/repositories/plants_repository_mock.py b/src/app/core/use_cases/tests/mocks/repositories/plants_repository_mock.py new file mode 100644 index 00000000..602123d0 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/repositories/plants_repository_mock.py @@ -0,0 +1,51 @@ +from core.entities.plant import Plant +from core.interfaces.repositories import PlantsRepositoryInterface + + +class PlantsRepositoryMock(PlantsRepositoryInterface): + _plants: list[Plant] = [] + + def create_plant(self, plant: Plant): + self._plants.append(plant) + + def get_plants(self) -> list[Plant]: + return self._plants + + def get_plant_by_id(self, id: str) -> Plant | None: + plants = list(filter(lambda plant: plant.id == id, self._plants)) + + if len(plants): + return plants[0] + + return None + + def get_plant_by_name(self, name: str): + plants = list(filter(lambda plant: plant.name == name, self._plants)) + + if len(plants): + return plants[0] + + return None + + def get_last_plant(self) -> Plant | None: + if len(self._plants): + return self._plants[-1] + + return None + + def filter_plants_by_name(self, plant_name: str) -> list[Plant]: + plants = list(filter(lambda plant: plant_name in plant.name, self._plants)) + + return plants + + def update_plant_by_id(self, plant: Plant) -> None: + self._plants = [ + plant if plant.id == current_plant.id else current_plant + for current_plant in self._plants + ] + + def delete_plant_by_id(self, id: str): + self._plants = [plant for plant in self._plants if plant.id != id] + + def clear_plants(self): + self._plants = [] diff --git a/src/app/core/use_cases/tests/mocks/repositories/sensors_records_repository_mock.py b/src/app/core/use_cases/tests/mocks/repositories/sensors_records_repository_mock.py new file mode 100644 index 00000000..516791d6 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/repositories/sensors_records_repository_mock.py @@ -0,0 +1,99 @@ +from datetime import date + +from core.entities import SensorsRecord +from core.interfaces.repositories import SensorRecordsRepositoryInterface +from core.entities.tests.fakers import LineChartRecordsFaker + +from core.constants import PAGINATION + + +class SensorRecordsRepositoryMock(SensorRecordsRepositoryInterface): + _sensors_records = [] + + def create_sensors_record(self, sensors_record: SensorsRecord) -> None: + self._sensors_records.append(sensors_record) + + def create_many_sensors_records(self, sensors_records: list[SensorsRecord]): + for record in sensors_records: + self._sensors_records.append(record) + + def get_sensor_records_for_line_charts(self): + return { + "soil_humidity_line_chart_records": LineChartRecordsFaker.fake_many(), + "ambient_humidity_line_chart_records": LineChartRecordsFaker.fake_many(), + "temperature_line_chart_records": LineChartRecordsFaker.fake_many(), + "water_volume_line_chart_records": LineChartRecordsFaker.fake_many(), + } + + def get_last_sensors_records(self, count) -> list[SensorsRecord]: + records = self._sensors_records.copy() + + return records[:count] + + def get_sensors_record_by_id(self, id: str) -> SensorsRecord | None: + sensors_records = list( + filter(lambda plant: plant.id == id, self._sensors_records) + ) + + if len(sensors_records): + return sensors_records[0] + + return None + + def get_sensors_records_count( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> int: + return len(self._sensors_records) + + def update_sensors_record_by_id(self, sensors_record: SensorsRecord): + self._sensors_records = [ + ( + sensors_record + if sensors_record.id == current_sensors_record.id + else current_sensors_record + ) + for current_sensors_record in self._sensors_records + ] + + def delete_sensors_record_by_id(self, id: str): + self._sensors_records = [ + sensors_record + for sensors_record in self._sensors_records + if sensors_record.id != id + ] + + def delete_many_sensors_records_by_id(self, ids: str): + self._sensors_records = [ + sensors_record + for sensors_record in self._sensors_records + if sensors_record.id not in ids + ] + + def get_filtered_sensors_records( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> list[SensorsRecord]: + records = self._sensors_records + + if plant_id: + records = [record for record in records if record.plant.id == plant_id] + + if start_date and end_date: + records = [ + record + for record in records + if record.created_at.get_value(is_datetime=True).date() >= start_date + and record.created_at.get_value(is_datetime=True).date() <= end_date + ] + + if page_number != "all": + records_per_page = PAGINATION["records_per_page"] + + slice_start = (page_number - 1) * records_per_page + records = records[slice_start : slice_start + records_per_page] + else: + records = records + + return records + + def clear_records(self): + self._sensors_records = [] diff --git a/src/app/core/use_cases/tests/mocks/repositories/users_repository_mock.py b/src/app/core/use_cases/tests/mocks/repositories/users_repository_mock.py new file mode 100644 index 00000000..796564e8 --- /dev/null +++ b/src/app/core/use_cases/tests/mocks/repositories/users_repository_mock.py @@ -0,0 +1,53 @@ +from core.entities import User +from core.interfaces.repositories import UsersRepositoryInterface + + +class UsersRepositoryMock(UsersRepositoryInterface): + _users: list[User] = [] + + def get_user_by_id(self, id: str) -> User | None: + users = list(filter(lambda user: user.id == id, self._users)) + + if len(users): + return users[0] + + return None + + def get_user_by_email(self, email: str) -> User | None: + users = list(filter(lambda user: user.email == email, self._users)) + + if len(users): + return users[0] + + return None + + def get_user_active_plant_id(self, email: str) -> User | None: + user = self.get_user_by_email(email) + + return user.active_plant_id if user else None + + def update_password(self, email, new_password: str): + user = self.get_user_by_email(email) + + user.password = new_password + + self.__update_user(user) + + def update_active_plant(self, id: str, plant_id: str): + user = self.get_user_by_id(id) + + user.active_plant_id = plant_id + + self.__update_user(user) + + def create_user(self, user: User): + self._users.append(user) + + def clear_users(self): + self._users = [] + + def __update_user(self, user): + self._users = [ + current_user if current_user.id != user.id else user + for current_user in self._users + ] diff --git a/src/app/infra/database/mysql.py b/src/app/infra/database/mysql.py index 3df93da2..ff591292 100644 --- a/src/app/infra/database/mysql.py +++ b/src/app/infra/database/mysql.py @@ -15,9 +15,14 @@ MYSQL_DATABASE_NAME = getenv("MYSQL_DATABASE_NAME") MYSQL_DATABASE_HOST = getenv("MYSQL_DATABASE_HOST") +ENVIRONMENT = getenv("ENVIRONMENT") + class MySQL: def __init__(self) -> None: + if ENVIRONMENT == "test": + return + config = { "user": MYSQL_DATABASE_USER, "password": MYSQL_DATABASE_PASSWORD, diff --git a/src/app/infra/factories/use_cases/authentication/__init__.py b/src/app/infra/factories/use_cases/authentication/__init__.py new file mode 100644 index 00000000..d08c9928 --- /dev/null +++ b/src/app/infra/factories/use_cases/authentication/__init__.py @@ -0,0 +1,7 @@ +from .login_user_factory import LoginUserFactory +from .request_password_reset_factory import RequestPasswordResetFactory +from .reset_password_factory import ResetPasswordFactory + +login_user = LoginUserFactory.produce() +request_password_reset = RequestPasswordResetFactory.produce() +reset_password = ResetPasswordFactory.produce() diff --git a/src/app/infra/factories/use_cases/authentication/login_user_factory.py b/src/app/infra/factories/use_cases/authentication/login_user_factory.py new file mode 100644 index 00000000..1a6f5262 --- /dev/null +++ b/src/app/infra/factories/use_cases/authentication/login_user_factory.py @@ -0,0 +1,10 @@ +from core.use_cases.authentication import LoginUser + +from infra.repositories import users_repository +from infra.authentication import auth + + +class LoginUserFactory: + @staticmethod + def produce(): + return LoginUser(repository=users_repository, auth=auth) diff --git a/src/app/infra/factories/use_cases/authentication/request_password_reset_factory.py b/src/app/infra/factories/use_cases/authentication/request_password_reset_factory.py new file mode 100644 index 00000000..c7aa7fea --- /dev/null +++ b/src/app/infra/factories/use_cases/authentication/request_password_reset_factory.py @@ -0,0 +1,9 @@ +from core.use_cases.authentication import RequestPasswordReset + +from infra.providers import EmailProvider + + +class RequestPasswordResetFactory: + @staticmethod + def produce(): + return RequestPasswordReset(EmailProvider()) diff --git a/src/app/infra/factories/use_cases/authentication/reset_password_factory.py b/src/app/infra/factories/use_cases/authentication/reset_password_factory.py new file mode 100644 index 00000000..d982252c --- /dev/null +++ b/src/app/infra/factories/use_cases/authentication/reset_password_factory.py @@ -0,0 +1,10 @@ +from core.use_cases.authentication import ResetPassword + +from infra.repositories import users_repository +from infra.authentication import auth + + +class ResetPasswordFactory: + @staticmethod + def produce(): + return ResetPassword(repository=users_repository, auth=auth) diff --git a/src/app/infra/factories/use_cases/checklist_records/__init__.py b/src/app/infra/factories/use_cases/checklist_records/__init__.py new file mode 100644 index 00000000..1de10eb5 --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/__init__.py @@ -0,0 +1,26 @@ +from .create_checklist_records_by_csv_file_factory import ( + CreateChecklistRecordsByCsvFileFactory, +) +from .create_checklist_record_by_form_factory import CreateChecklistRecordByFormFactory +from .delete_checklist_records_factory import DeleteChecklistRecordsFactory +from .get_checklist_records_csv_file_factory import GetChecklistRecordsCsvFileFactory +from .get_checklist_records_dashboard_page_data_factory import ( + GetChecklistRecordsDashboardPageDataFactory, +) +from .get_checklist_records_table_page_data_factory import ( + GetChecklistRecordsTablePageDataFactory, +) +from .update_checklist_records_factory import UpdateChecklistRecordFactory + + +create_checklist_records_by_csv_file = CreateChecklistRecordsByCsvFileFactory.produce() +create_checklist_record_by_form = CreateChecklistRecordByFormFactory.produce() +delete_checklist_records = DeleteChecklistRecordsFactory.produce() +get_checklist_records_csv_file = GetChecklistRecordsCsvFileFactory.produce() +get_checklist_records_dashboard_page_data = ( + GetChecklistRecordsDashboardPageDataFactory.produce() +) +get_checklist_records_table_page_data = ( + GetChecklistRecordsTablePageDataFactory.produce() +) +update_checklist_record = UpdateChecklistRecordFactory.produce() diff --git a/src/app/infra/factories/use_cases/checklist_records/create_checklist_record_by_form_factory.py b/src/app/infra/factories/use_cases/checklist_records/create_checklist_record_by_form_factory.py new file mode 100644 index 00000000..d7d54bba --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/create_checklist_record_by_form_factory.py @@ -0,0 +1,12 @@ +from core.use_cases.checklist_records import CreateChecklistRecordByForm + +from infra.repositories import checklist_records_repository, plants_repository + + +class CreateChecklistRecordByFormFactory: + @staticmethod + def produce(): + return CreateChecklistRecordByForm( + checklist_records_repository=checklist_records_repository, + plants_repository=plants_repository, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/create_checklist_records_by_csv_file_factory.py b/src/app/infra/factories/use_cases/checklist_records/create_checklist_records_by_csv_file_factory.py new file mode 100644 index 00000000..ad43143c --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/create_checklist_records_by_csv_file_factory.py @@ -0,0 +1,16 @@ +from core.use_cases.checklist_records import CreateChecklistRecordsByCsvFile + +from infra.repositories import plants_repository, checklist_records_repository +from infra.providers import DataAnalyserProvider + + +class CreateChecklistRecordsByCsvFileFactory: + @staticmethod + def produce(): + data_analyser_provider = DataAnalyserProvider() + + return CreateChecklistRecordsByCsvFile( + plants_repository=plants_repository, + checklist_records_repository=checklist_records_repository, + data_analyser_provider=data_analyser_provider, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/delete_checklist_records_factory.py b/src/app/infra/factories/use_cases/checklist_records/delete_checklist_records_factory.py new file mode 100644 index 00000000..3115a75f --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/delete_checklist_records_factory.py @@ -0,0 +1,13 @@ +from core.use_cases.checklist_records import DeleteChecklistRecords + +from infra.repositories import ( + checklist_records_repository, +) + + +class DeleteChecklistRecordsFactory: + @staticmethod + def produce(): + return DeleteChecklistRecords( + checklist_records_repository=checklist_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_csv_file_factory.py b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_csv_file_factory.py new file mode 100644 index 00000000..137b2f27 --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_csv_file_factory.py @@ -0,0 +1,17 @@ +from core.use_cases.checklist_records import GetChecklistRecordsCsvFile + +from infra.repositories import ( + checklist_records_repository, +) +from infra.providers.data_analyser_provider import DataAnalyserProvider + + +class GetChecklistRecordsCsvFileFactory: + @staticmethod + def produce(): + data_analyser_provider = DataAnalyserProvider() + + return GetChecklistRecordsCsvFile( + checklist_records_repository=checklist_records_repository, + data_analyser_provider=data_analyser_provider, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_dashboard_page_data_factory.py b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_dashboard_page_data_factory.py new file mode 100644 index 00000000..7f705be8 --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_dashboard_page_data_factory.py @@ -0,0 +1,17 @@ +from core.use_cases.checklist_records import GetChecklistRecordsDashboardPageData + +from infra.repositories import ( + plants_repository, + users_repository, + checklist_records_repository, +) + + +class GetChecklistRecordsDashboardPageDataFactory: + @staticmethod + def produce(): + return GetChecklistRecordsDashboardPageData( + plants_repository=plants_repository, + users_repository=users_repository, + checklist_records_repository=checklist_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_table_page_data_factory.py b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_table_page_data_factory.py new file mode 100644 index 00000000..acb8fa2d --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/get_checklist_records_table_page_data_factory.py @@ -0,0 +1,15 @@ +from core.use_cases.checklist_records import GetChecklistRecordsTablePageData + +from infra.repositories import ( + plants_repository, + checklist_records_repository, +) + + +class GetChecklistRecordsTablePageDataFactory: + @staticmethod + def produce(): + return GetChecklistRecordsTablePageData( + plants_repository=plants_repository, + checklist_records_repository=checklist_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/checklist_records/update_checklist_records_factory.py b/src/app/infra/factories/use_cases/checklist_records/update_checklist_records_factory.py new file mode 100644 index 00000000..be694dbe --- /dev/null +++ b/src/app/infra/factories/use_cases/checklist_records/update_checklist_records_factory.py @@ -0,0 +1,12 @@ +from core.use_cases.checklist_records import UpdateChecklistRecord + +from infra.repositories import plants_repository, checklist_records_repository + + +class UpdateChecklistRecordFactory: + @staticmethod + def produce(): + return UpdateChecklistRecord( + plants_repository=plants_repository, + checklist_records_repository=checklist_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/plants/__init__.py b/src/app/infra/factories/use_cases/plants/__init__.py new file mode 100644 index 00000000..22017ee1 --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/__init__.py @@ -0,0 +1,15 @@ +from .create_plant_by_form_factory import CreatePlantByFormFactory +from .delete_plant_factory import DeletePlantFactory +from .filter_plants_factory import FilterPlantsFactory +from .get_plant_by_id_factory import GetPlantByIdFactory +from .get_plants_page_data_factory import GetPlantsPageDataFactory +from .update_plant_factory import UpdatePlantFactory +from .update_active_plant_factory import UpdateActivePlantFactory + +create_plant_by_form = CreatePlantByFormFactory.produce() +delete_plant = DeletePlantFactory.produce() +filter_plants = FilterPlantsFactory.produce() +get_plant_by_id = GetPlantByIdFactory.produce() +get_plants_page_data = GetPlantsPageDataFactory.produce() +update_plant = UpdatePlantFactory.produce() +update_active_plant = UpdateActivePlantFactory.produce() diff --git a/src/app/infra/factories/use_cases/plants/create_plant_by_form_factory.py b/src/app/infra/factories/use_cases/plants/create_plant_by_form_factory.py new file mode 100644 index 00000000..dc7e50c3 --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/create_plant_by_form_factory.py @@ -0,0 +1,8 @@ +from core.use_cases.plants import CreatePlantByForm +from infra.repositories import plants_repository + + +class CreatePlantByFormFactory: + @staticmethod + def produce(): + return CreatePlantByForm(plants_repository=plants_repository) diff --git a/src/app/infra/factories/use_cases/plants/delete_plant_factory.py b/src/app/infra/factories/use_cases/plants/delete_plant_factory.py new file mode 100644 index 00000000..3579fe5d --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/delete_plant_factory.py @@ -0,0 +1,10 @@ +from core.use_cases.plants import DeletePlant +from infra.repositories import plants_repository, users_repository + + +class DeletePlantFactory: + @staticmethod + def produce(): + return DeletePlant( + users_repository=users_repository, plants_repository=plants_repository + ) diff --git a/src/app/infra/factories/use_cases/plants/filter_plants_factory.py b/src/app/infra/factories/use_cases/plants/filter_plants_factory.py new file mode 100644 index 00000000..2131efd5 --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/filter_plants_factory.py @@ -0,0 +1,8 @@ +from core.use_cases.plants import FilterPlants +from infra.repositories import plants_repository + + +class FilterPlantsFactory: + @staticmethod + def produce(): + return FilterPlants(plants_repository) diff --git a/src/app/infra/factories/use_cases/plants/get_plant_by_id_factory.py b/src/app/infra/factories/use_cases/plants/get_plant_by_id_factory.py new file mode 100644 index 00000000..785cd5b6 --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/get_plant_by_id_factory.py @@ -0,0 +1,8 @@ +from core.use_cases.plants import GetPlantById +from infra.repositories import plants_repository + + +class GetPlantByIdFactory: + @staticmethod + def produce(): + return GetPlantById(plants_repository) diff --git a/src/app/infra/factories/use_cases/plants/get_plants_page_data_factory.py b/src/app/infra/factories/use_cases/plants/get_plants_page_data_factory.py new file mode 100644 index 00000000..baf707ca --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/get_plants_page_data_factory.py @@ -0,0 +1,8 @@ +from core.use_cases.plants import GetPlantsPageData +from infra.repositories import plants_repository + + +class GetPlantsPageDataFactory: + @staticmethod + def produce(): + return GetPlantsPageData(plants_repository) diff --git a/src/app/infra/factories/use_cases/plants/update_active_plant_factory.py b/src/app/infra/factories/use_cases/plants/update_active_plant_factory.py new file mode 100644 index 00000000..6fc4571a --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/update_active_plant_factory.py @@ -0,0 +1,10 @@ +from core.use_cases.plants import UpdateActivePlant +from infra.repositories import plants_repository, users_repository + + +class UpdateActivePlantFactory: + @staticmethod + def produce(): + return UpdateActivePlant( + plants_repository=plants_repository, users_repository=users_repository + ) diff --git a/src/app/infra/factories/use_cases/plants/update_plant_factory.py b/src/app/infra/factories/use_cases/plants/update_plant_factory.py new file mode 100644 index 00000000..6c510e89 --- /dev/null +++ b/src/app/infra/factories/use_cases/plants/update_plant_factory.py @@ -0,0 +1,8 @@ +from core.use_cases.plants import UpdatePlant +from infra.repositories import plants_repository + + +class UpdatePlantFactory: + @staticmethod + def produce(): + return UpdatePlant(plants_repository) diff --git a/src/app/infra/factories/use_cases/sensors_records/__init__.py b/src/app/infra/factories/use_cases/sensors_records/__init__.py new file mode 100644 index 00000000..aef86a4f --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/__init__.py @@ -0,0 +1,30 @@ +from .create_sensors_record_by_api_factory import CreateSensorsRecordByApiFactory +from .create_sensors_records_by_csv_file_factory import ( + CreateSensorsRecordsByCsvFileFactory, +) +from .create_sensors_records_by_form_factory import CreateSensorsRecordByFormFactory +from .delete_sensors_records_factory import DeleteSensorsRecordFactory +from .get_sensors_records_csv_file_factory import GetSensorsRecordsCsvFileFactory +from .get_sensors_records_dashboard_page_data_factory import ( + GetSensorsRecordsDashboardPageDataFactory, +) +from .get_last_sensors_record_page_data_factory import ( + GetLastSensorsRecordPageDataFactory, +) +from .get_sensors_records_table_page_data_factory import ( + GetSensorsRecordsTablePageDataFactory, +) +from .update_sensors_records_factory import UpdateSensorsRecordFactory + + +create_sensors_record_by_api = CreateSensorsRecordByApiFactory.produce() +create_sensors_records_by_csv_file = CreateSensorsRecordsByCsvFileFactory.produce() +create_sensors_records_by_form = CreateSensorsRecordByFormFactory.produce() +delete_sensors_records = DeleteSensorsRecordFactory.produce() +get_sensors_records_csv_file = GetSensorsRecordsCsvFileFactory.produce() +get_last_sensors_record_page_data = GetLastSensorsRecordPageDataFactory.produce() +get_sensors_records_dashboard_page_data = ( + GetSensorsRecordsDashboardPageDataFactory.produce() +) +get_sensors_records_table_page_data = GetSensorsRecordsTablePageDataFactory.produce() +update_sensors_record = UpdateSensorsRecordFactory.produce() diff --git a/src/app/infra/factories/use_cases/sensors_records/create_sensors_record_by_api_factory.py b/src/app/infra/factories/use_cases/sensors_records/create_sensors_record_by_api_factory.py new file mode 100644 index 00000000..bc9e6de9 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/create_sensors_record_by_api_factory.py @@ -0,0 +1,17 @@ +from core.use_cases.sensors_records import CreateSensorsRecordByApi + +from infra.repositories import ( + plants_repository, + users_repository, + sensors_records_repository, +) + + +class CreateSensorsRecordByApiFactory: + @staticmethod + def produce(): + return CreateSensorsRecordByApi( + plants_repository=plants_repository, + users_repository=users_repository, + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_csv_file_factory.py b/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_csv_file_factory.py new file mode 100644 index 00000000..ad73b178 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_csv_file_factory.py @@ -0,0 +1,19 @@ +from core.use_cases.sensors_records import CreateSensorsRecordsByCsvFile + +from infra.repositories import ( + plants_repository, + sensors_records_repository, +) +from infra.providers import DataAnalyserProvider + + +class CreateSensorsRecordsByCsvFileFactory: + @staticmethod + def produce(): + data_analyser_provider = DataAnalyserProvider() + + return CreateSensorsRecordsByCsvFile( + plants_repository=plants_repository, + sensors_records_repository=sensors_records_repository, + data_analyser_provider=data_analyser_provider, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_form_factory.py b/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_form_factory.py new file mode 100644 index 00000000..e1892786 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/create_sensors_records_by_form_factory.py @@ -0,0 +1,15 @@ +from core.use_cases.sensors_records import CreateSensorsRecordByForm + +from infra.repositories import ( + plants_repository, + sensors_records_repository, +) + + +class CreateSensorsRecordByFormFactory: + @staticmethod + def produce(): + return CreateSensorsRecordByForm( + plants_repository=plants_repository, + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/delete_sensors_records_factory.py b/src/app/infra/factories/use_cases/sensors_records/delete_sensors_records_factory.py new file mode 100644 index 00000000..cec9eb03 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/delete_sensors_records_factory.py @@ -0,0 +1,13 @@ +from core.use_cases.sensors_records import DeleteSensorsRecord + +from infra.repositories import ( + sensors_records_repository, +) + + +class DeleteSensorsRecordFactory: + @staticmethod + def produce(): + return DeleteSensorsRecord( + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/get_last_sensors_record_page_data_factory.py b/src/app/infra/factories/use_cases/sensors_records/get_last_sensors_record_page_data_factory.py new file mode 100644 index 00000000..dd57de1b --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/get_last_sensors_record_page_data_factory.py @@ -0,0 +1,11 @@ +from core.use_cases.sensors_records import GetLastSensorsRecordPageData + +from infra.repositories import sensors_records_repository + + +class GetLastSensorsRecordPageDataFactory: + @staticmethod + def produce(): + return GetLastSensorsRecordPageData( + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_csv_file_factory.py b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_csv_file_factory.py new file mode 100644 index 00000000..215b7acc --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_csv_file_factory.py @@ -0,0 +1,17 @@ +from core.use_cases.sensors_records import GetSensorsRecordsCsvFile + +from infra.repositories import ( + sensors_records_repository, +) +from infra.providers.data_analyser_provider import DataAnalyserProvider + + +class GetSensorsRecordsCsvFileFactory: + @staticmethod + def produce(): + data_analyser_provider = DataAnalyserProvider() + + return GetSensorsRecordsCsvFile( + sensors_records_repository=sensors_records_repository, + data_analyser_provider=data_analyser_provider, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_dashboard_page_data_factory.py b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_dashboard_page_data_factory.py new file mode 100644 index 00000000..97fb5847 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_dashboard_page_data_factory.py @@ -0,0 +1,17 @@ +from core.use_cases.sensors_records import GetSensorsRecordsDashboardPageData + +from infra.repositories import ( + plants_repository, + users_repository, + sensors_records_repository, +) + + +class GetSensorsRecordsDashboardPageDataFactory: + @staticmethod + def produce(): + return GetSensorsRecordsDashboardPageData( + plants_repository=plants_repository, + users_repository=users_repository, + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_table_page_data_factory.py b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_table_page_data_factory.py new file mode 100644 index 00000000..821e02c9 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/get_sensors_records_table_page_data_factory.py @@ -0,0 +1,15 @@ +from core.use_cases.sensors_records import GetSensorsRecordsTablePageData + +from infra.repositories import ( + plants_repository, + sensors_records_repository, +) + + +class GetSensorsRecordsTablePageDataFactory: + @staticmethod + def produce(): + return GetSensorsRecordsTablePageData( + plants_repository=plants_repository, + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/factories/use_cases/sensors_records/update_sensors_records_factory.py b/src/app/infra/factories/use_cases/sensors_records/update_sensors_records_factory.py new file mode 100644 index 00000000..c4018870 --- /dev/null +++ b/src/app/infra/factories/use_cases/sensors_records/update_sensors_records_factory.py @@ -0,0 +1,12 @@ +from core.use_cases.sensors_records import UpdateSensorsRecord + +from infra.repositories import plants_repository, sensors_records_repository + + +class UpdateSensorsRecordFactory: + @staticmethod + def produce(): + return UpdateSensorsRecord( + plants_repository=plants_repository, + sensors_records_repository=sensors_records_repository, + ) diff --git a/src/app/infra/providers/data_analyser_provider.py b/src/app/infra/providers/data_analyser_provider.py index 22e850c5..d8cfd135 100644 --- a/src/app/infra/providers/data_analyser_provider.py +++ b/src/app/infra/providers/data_analyser_provider.py @@ -4,8 +4,10 @@ from pandas import DataFrame, read_csv, read_excel +from core.interfaces.providers import DataAnalyserProviderInterface -class DataAnalyserProvider: + +class DataAnalyserProvider(DataAnalyserProviderInterface): dataframe: DataFrame def __init__(self) -> None: diff --git a/src/app/infra/providers/email_provider.py b/src/app/infra/providers/email_provider.py index 30504c39..3840b65a 100644 --- a/src/app/infra/providers/email_provider.py +++ b/src/app/infra/providers/email_provider.py @@ -1,10 +1,13 @@ from smtplib import SMTP, SMTPAuthenticationError + + +from core.interfaces.providers import EmailProvideInterface from core.commons import Error from email.message import Message -class EmailProvider: - def send_email(sender: str, receiver: str, template: str, password: str): +class EmailProvider(EmailProvideInterface): + def send_email(self, sender: str, receiver: str, template: str, password: str): try: email_body = template msg = Message() @@ -18,4 +21,6 @@ def send_email(sender: str, receiver: str, template: str, password: str): smtp.sendmail(sender, receiver, msg.as_string().encode("utf-8")) except SMTPAuthenticationError as error: - raise Error(ui_message="app password errada", internal_message=error) + raise Error( + ui_message="Erro ao tentar enviar e-mail", internal_message=error + ) diff --git a/src/app/infra/repositories/checklist_records_repository.py b/src/app/infra/repositories/checklist_records_repository.py index 875ddb7b..f11b6b72 100644 --- a/src/app/infra/repositories/checklist_records_repository.py +++ b/src/app/infra/repositories/checklist_records_repository.py @@ -1,13 +1,14 @@ from datetime import date -from core.entities.checklist_record import CheckListRecord, Plant +from core.interfaces.repositories import ChecklistRecordsRepositoryInterface +from core.entities import CheckListRecord, Plant, LineChartRecord from core.commons import Datetime, Date from core.constants import PAGINATION from infra.database import mysql -class ChecklistRecordsRepository: +class ChecklistRecordsRepository(ChecklistRecordsRepositoryInterface): def create_checklist_record(self, checklist_record: CheckListRecord) -> None: sql = """ INSERT INTO checklist_records @@ -146,40 +147,12 @@ def delete_checklist_record_by_id(self, checklist_record_id: str): ], ) - def get_filtered_checklist_records( - self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 - ) -> list[CheckListRecord]: - where = self.__get_where_with_filters(plant_id, start_date, end_date) - - limit = "" - if page_number != "all": - pagination_limit = PAGINATION["records_per_page"] - offset = (page_number - 1) * pagination_limit - limit = f"LIMIT {pagination_limit} OFFSET {offset}" - - rows = mysql.query( - sql=f""" - SELECT - CR.*, - P.id AS plant_id, - P.name AS plant_name, - P.hex_color as plant_color - FROM checklist_records AS CR - JOIN plants AS P ON P.id = CR.plant_id - {where} - ORDER BY CR.created_at DESC - {limit} - """, - is_single=False, + def delete_many_checklist_records_by_id(self, checklist_record_ids: list[str]): + mysql.mutate_many( + sql="DELETE FROM checklist_records WHERE id = %s", + params=[(id,) for id in checklist_record_ids], ) - sensors_records = [] - - if len(rows) > 0: - sensors_records = [self.__get_checklist_record_entity(row) for row in rows] - - return sensors_records - def get_leaf_appearances_and_leaf_colors_records(self): rows = mysql.query( sql=""" @@ -199,7 +172,7 @@ def get_leaf_appearances_and_leaf_colors_records(self): return rows - def get_lai_records(self): + def get_lai_records_for_line_charts(self): rows = mysql.query( sql=""" SELECT @@ -216,7 +189,14 @@ def get_lai_records(self): if len(rows) == 0: return [] - return rows + return [ + LineChartRecord( + date=row["date"], + value=row["lai"], + plant_id=row["plant_id"], + ) + for row in rows + ] def get_checklist_records_count( self, plant_id: str, start_date: date, end_date: date @@ -235,20 +215,39 @@ def get_checklist_records_count( return result["count"] - def get_ordered_by_date_leaf_appearance_and_leaf_color_records(self): + def get_filtered_checklist_records( + self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 + ) -> list[CheckListRecord]: + where = self.__get_where_with_filters(plant_id, start_date, end_date) + + limit = "" + if page_number != "all": + pagination_limit = PAGINATION["records_per_page"] + offset = (page_number - 1) * pagination_limit + limit = f"LIMIT {pagination_limit} OFFSET {offset}" + rows = mysql.query( - sql="SELECT leaf_appearance, leaf_color, created_at FROM checklist_records", + sql=f""" + SELECT + CR.*, + P.id AS plant_id, + P.name AS plant_name, + P.hex_color as plant_color + FROM checklist_records AS CR + JOIN plants AS P ON P.id = CR.plant_id + {where} + ORDER BY CR.created_at DESC + {limit} + """, is_single=False, ) - if len(rows) == 0: - return [] + sensors_records = [] - for row in rows: - row["date"] = row["created_at"].date() - del row["created_at"] + if len(rows) > 0: + sensors_records = [self.__get_checklist_record_entity(row) for row in rows] - return rows + return sensors_records def get_checklist_record_by_id(self, id: str) -> CheckListRecord | None: row = mysql.query( diff --git a/src/app/infra/repositories/plants_repository.py b/src/app/infra/repositories/plants_repository.py index 777b172d..85e079e8 100644 --- a/src/app/infra/repositories/plants_repository.py +++ b/src/app/infra/repositories/plants_repository.py @@ -1,9 +1,11 @@ from core.entities.plant import Plant +from core.interfaces.repositories import PlantsRepositoryInterface + from infra.database import mysql -class PlantsRepository: - def create_plant_record(self, plants_record: Plant): +class PlantsRepository(PlantsRepositoryInterface): + def create_plant(self, plants_record: Plant): sql = """ INSERT INTO plants (name, hex_color) @@ -62,7 +64,7 @@ def get_last_plant(self) -> Plant | None: return None - def filter_plants_by_name(self, plant_name: str): + def filter_plants_by_name(self, plant_name: str) -> list[Plant]: rows = mysql.query( sql=f""" SELECT * FROM plants diff --git a/src/app/infra/repositories/sensors_records_repository.py b/src/app/infra/repositories/sensors_records_repository.py index b2d3c1a8..bd546094 100644 --- a/src/app/infra/repositories/sensors_records_repository.py +++ b/src/app/infra/repositories/sensors_records_repository.py @@ -1,5 +1,6 @@ -from core.entities import SensorsRecord, Datetime, Plant +from core.entities import SensorsRecord, LineChartRecord, Datetime, Plant from core.commons import Weekday +from core.interfaces.repositories import SensorRecordsRepositoryInterface from core.constants import PAGINATION from infra.database import mysql @@ -7,7 +8,7 @@ from datetime import date -class SensorRecordsRepository: +class SensorRecordsRepository(SensorRecordsRepositoryInterface): def create_sensors_record(self, sensors_record: SensorsRecord) -> None: sql = """ INSERT INTO sensors_records @@ -50,7 +51,7 @@ def create_many_sensors_records(self, sensors_records: list[SensorsRecord]): params=params, ) - def get_sensor_records_grouped_by_date(self): + def get_sensor_records_for_line_charts(self): sql = """ SELECT DATE(created_at) AS date, @@ -62,11 +63,29 @@ def get_sensor_records_grouped_by_date(self): FROM sensors_records GROUP BY date, plant_id ORDER BY date ASC - LIMIT 500; + LIMIT 20000; """ rows = mysql.query(sql=sql, is_single=False) - return rows + if not len(rows): + return [] + + return { + "ambient_humidity_line_chart_records": [ + self.__get_line_chart_record_entity(row, "ambient_humidity") + for row in rows + ], + "soil_humidity_line_chart_records": [ + self.__get_line_chart_record_entity(row, "soil_humidity") + for row in rows + ], + "temperature_line_chart_records": [ + self.__get_line_chart_record_entity(row, "temperature") for row in rows + ], + "water_volume_line_chart_records": [ + self.__get_line_chart_record_entity(row, "water_volume") for row in rows + ], + } def get_last_sensors_records(self, count) -> list[SensorsRecord]: rows = mysql.query( @@ -106,43 +125,22 @@ def get_sensors_record_by_id(self, id: str) -> SensorsRecord | None: return None - def get_sensors_records_count(self) -> int: - result = mysql.query( - sql="SELECT COUNT(*) AS count FROM sensors_records", - is_single=True, + def get_sensors_records_count( + self, plant_id: str, start_date: date, end_date: date + ) -> int: + where = self.__get_where_with_filters( + plant_id=plant_id, start_date=start_date, end_date=end_date ) - return result["count"] - - def update_sensors_record_by_id(self, sensors_record: SensorsRecord): - mysql.mutate( - """ - UPDATE sensors_records - SET - soil_humidity = %s, - ambient_humidity = %s, - temperature = %s, - water_volume = %s, - created_at = %s, - plant_id = %s - WHERE id = %s + result = mysql.query( + sql=f""" + SELECT COUNT(id) AS count FROM sensors_records AS SR + {where} """, - params=[ - sensors_record.soil_humidity, - sensors_record.ambient_humidity, - sensors_record.temperature, - sensors_record.water_volume, - sensors_record.created_at.get_value(), - sensors_record.plant.id, - sensors_record.id, - ], + is_single=True, ) - def delete_sensors_record_by_id(self, sensors_record_id: str): - mysql.mutate( - "DELETE FROM sensors_records WHERE id = %s", - params=[sensors_record_id], - ) + return result["count"] def get_filtered_sensors_records( self, plant_id: str, start_date: date, end_date: date, page_number: int = 1 @@ -178,6 +176,42 @@ def get_filtered_sensors_records( return sensors_records + def update_sensors_record_by_id(self, sensors_record: SensorsRecord): + mysql.mutate( + """ + UPDATE sensors_records + SET + soil_humidity = %s, + ambient_humidity = %s, + temperature = %s, + water_volume = %s, + created_at = %s, + plant_id = %s + WHERE id = %s + """, + params=[ + sensors_record.soil_humidity, + sensors_record.ambient_humidity, + sensors_record.temperature, + sensors_record.water_volume, + sensors_record.created_at.get_value(), + sensors_record.plant.id, + sensors_record.id, + ], + ) + + def delete_sensors_record_by_id(self, sensors_record_id: str): + mysql.mutate( + "DELETE FROM sensors_records WHERE id = %s", + params=[sensors_record_id], + ) + + def delete_many_sensors_records_by_id(self, sensors_record_ids: list[str]): + mysql.mutate_many( + sql="DELETE FROM sensors_records WHERE id = %s", + params=[(id,) for id in sensors_record_ids], + ) + def __get_where_with_filters(self, plant_id: str, start_date: date, end_date: date): filters = [] @@ -215,3 +249,12 @@ def __get_sensors_record_entity(self, row: dict) -> SensorsRecord: ) else: return None + + def __get_line_chart_record_entity( + self, row: dict, attribute: str + ) -> LineChartRecord: + return LineChartRecord( + date=row["date"], + value=row[attribute], + plant_id=row["plant_id"], + ) diff --git a/src/app/infra/repositories/users_repository.py b/src/app/infra/repositories/users_repository.py index 34a42cd8..69a78299 100644 --- a/src/app/infra/repositories/users_repository.py +++ b/src/app/infra/repositories/users_repository.py @@ -1,8 +1,10 @@ from core.entities import User +from core.interfaces.repositories import UsersRepositoryInterface + from infra.database import mysql -class UsersRepository: +class UsersRepository(UsersRepositoryInterface): def get_user_by_id( self, id: str, should_include_password: bool = False ) -> User | None: diff --git a/src/app/infra/views/authentication_views/login_user_view.py b/src/app/infra/views/authentication_views/login_user_view.py index 53ba39e4..8e9d0ea3 100644 --- a/src/app/infra/views/authentication_views/login_user_view.py +++ b/src/app/infra/views/authentication_views/login_user_view.py @@ -1,8 +1,8 @@ from flask import request, render_template, url_for, make_response -from core.use_cases.authentication import login_user -from core.commons import Error +from core.errors.forms import InvalidFormDataError +from infra.factories.use_cases.authentication import login_user from infra.forms import LoginForm @@ -11,7 +11,7 @@ def login_user_view(): try: if not login_form.validate_on_submit(): - raise Error("Formulário inválido", status_code=400) + raise InvalidFormDataError() response = make_response() @@ -33,7 +33,7 @@ def login_user_view(): ) return response - except Error as error: + except Exception as error: return ( render_template( "pages/login/login_form.html", diff --git a/src/app/infra/views/authentication_views/logout_user_view.py b/src/app/infra/views/authentication_views/logout_user_view.py index de198549..301488a0 100644 --- a/src/app/infra/views/authentication_views/logout_user_view.py +++ b/src/app/infra/views/authentication_views/logout_user_view.py @@ -1,15 +1,19 @@ from flask import url_for, redirect +from core.errors.authentication import LogoutFailedError from infra.authentication import auth def logout_user_view(): try: - auth.logout() + has_logout = auth.logout() + + if not has_logout: + raise LogoutFailedError() url = url_for("authentication_views.login_page_view") return redirect(url) - except Exception: - return "Error ao tentar fazer logout", 500 + except Exception as error: + return error.ui_message, error.status_code diff --git a/src/app/infra/views/authentication_views/request_password_reset_page_view.py b/src/app/infra/views/authentication_views/request_password_reset_page_view.py index fba58ef8..1b786e45 100644 --- a/src/app/infra/views/authentication_views/request_password_reset_page_view.py +++ b/src/app/infra/views/authentication_views/request_password_reset_page_view.py @@ -1,14 +1,9 @@ from flask import render_template -from core.commons import Error - from infra.forms import RequestPasswordResetForm def request_password_reset_page_view(): form = RequestPasswordResetForm() - try: - return render_template("pages/request_password_reset/index.html", form=form) - except Error as error: - raise error + return render_template("pages/request_password_reset/index.html", form=form) diff --git a/src/app/infra/views/authentication_views/request_password_reset_view.py b/src/app/infra/views/authentication_views/request_password_reset_view.py index fa58bc79..9a39bf03 100644 --- a/src/app/infra/views/authentication_views/request_password_reset_view.py +++ b/src/app/infra/views/authentication_views/request_password_reset_view.py @@ -3,15 +3,15 @@ from flask import render_template, make_response, request -from core.use_cases.authentication import request_password_reset - -from core.commons import Error +from core.errors.forms import InvalidFormDataError +from infra.factories.use_cases.authentication import request_password_reset from infra.authentication import auth -from infra.constants import COOKIES from infra.forms import RequestPasswordResetForm +from infra.constants import COOKIES URL = getenv("URL") +SENDER_PASSWORD = getenv("SUPPORT_EMAIL_APP_PASSWORD") def request_password_reset_view(): @@ -19,7 +19,7 @@ def request_password_reset_view(): try: if not form.validate_on_submit(): - raise Error(ui_message="Formulário inválido", status_code=400) + raise InvalidFormDataError() password_reset_token = uuid4() token = auth.generate_hash(str(password_reset_token)) @@ -44,10 +44,14 @@ def request_password_reset_view(): max_age=900, # 15 minutes ) - request_password_reset.execute(user_email, email_template) + request_password_reset.execute( + user_email=user_email, + email_template=email_template, + sender_password=SENDER_PASSWORD, + ) return response - except Error as error: + except Exception as error: return ( render_template( "pages/request_password_reset/email_field.html", diff --git a/src/app/infra/views/authentication_views/reset_password_page_view.py b/src/app/infra/views/authentication_views/reset_password_page_view.py index 519cd6ed..5e4e1a85 100644 --- a/src/app/infra/views/authentication_views/reset_password_page_view.py +++ b/src/app/infra/views/authentication_views/reset_password_page_view.py @@ -1,7 +1,7 @@ from flask import render_template, flash, request, make_response, redirect -from core.commons import Error +from core.errors.authentication import CookieExpiredError, TokenNotValidError from infra.authentication import auth from infra.constants import COOKIES @@ -15,11 +15,7 @@ def reset_password_page_view(): form = ResetPasswordForm() if not cookie: - raise Error( - internal_message="Client token has expired", - ui_message="Seu token Expirou!, reenvie novamente para o email!", - status_code=401, - ) + raise CookieExpiredError() is_token_valid = auth.check_hash(email_token, cookie) @@ -31,13 +27,9 @@ def reset_password_page_view(): return response else: - raise Error( - internal_message="Token authentication error", - ui_message="Token de autenticação inválido", - status_code=401, - ) + raise TokenNotValidError() - except Error as error: + except Exception as error: flash(error.ui_message, "error") return redirect("/login") diff --git a/src/app/infra/views/authentication_views/reset_password_view.py b/src/app/infra/views/authentication_views/reset_password_view.py index 7aea16cf..d9ea785a 100644 --- a/src/app/infra/views/authentication_views/reset_password_view.py +++ b/src/app/infra/views/authentication_views/reset_password_view.py @@ -1,14 +1,10 @@ from flask import render_template, request, make_response -from core.commons import Error - -from infra.authentication import auth - -from core.use_cases.authentication import reset_password - +from core.errors.forms import InvalidFormDataError from infra.forms import ResetPasswordForm +from infra.factories.use_cases.authentication import reset_password from infra.constants import COOKIES @@ -17,10 +13,7 @@ def reset_password_view(): try: if not form.validate_on_submit(): - raise Error( - internal_message="Reset password form is not valid", - ui_message="Formulário inválido", - ) + raise InvalidFormDataError() new_password = form.password.data @@ -32,11 +25,8 @@ def reset_password_view(): respose.delete_cookie(COOKIES["keys"]["password_reset_token"]) return respose - except Error as error: + except Exception as error: return ( render_template("pages/reset_password/fields.html", form=form), error.status_code, ) - - ##TODO: altough password is changing in db is not allowing me to enter even with the correct password - ## diff --git a/src/app/infra/views/checklist_records_views/checklist_records_csv_file_view.py b/src/app/infra/views/checklist_records_views/checklist_records_csv_file_view.py index ca3ab5df..108c1f48 100644 --- a/src/app/infra/views/checklist_records_views/checklist_records_csv_file_view.py +++ b/src/app/infra/views/checklist_records_views/checklist_records_csv_file_view.py @@ -1,26 +1,32 @@ from flask import send_file, after_this_request, request -from core.use_cases.checklist_records import get_checklist_records_csv_file - +from infra.factories.use_cases.checklist_records import get_checklist_records_csv_file from infra.utils.file import File +from infra.constants import FOLDERS def checklist_records_csv_file_view(): - start_date = request.args.get("start-date", None) - end_date = request.args.get("end-date", None) - plant_id = request.args.get("plant", "all") - - csv_file = get_checklist_records_csv_file.execute( - start_date=start_date, end_date=end_date, plant_id=plant_id - ) - csv_folder = csv_file["folder"] - csv_filename = csv_file["filename"] - csv_path = f"{csv_folder}/{csv_filename}" - - @after_this_request - def _(response): - File(csv_file["folder"], csv_file["filename"]).delete() - - return response - - return send_file(csv_path, as_attachment=True) + try: + start_date = request.args.get("start-date", None) + end_date = request.args.get("end-date", None) + plant_id = request.args.get("plant", "all") + + csv_folder = FOLDERS["tmp"] + + csv_filename = get_checklist_records_csv_file.execute( + start_date=start_date, + end_date=end_date, + plant_id=plant_id, + folder=csv_folder, + ) + csv_path = f"{csv_folder}/{csv_filename}" + + @after_this_request + def _(response): + File(csv_folder, csv_filename).delete() + + return response + + return send_file(csv_path, as_attachment=True) + except Exception as error: + print(error, flush=True) diff --git a/src/app/infra/views/checklist_records_views/checklist_records_dashboard_page_view.py b/src/app/infra/views/checklist_records_views/checklist_records_dashboard_page_view.py index 04399e80..0f5c482b 100644 --- a/src/app/infra/views/checklist_records_views/checklist_records_dashboard_page_view.py +++ b/src/app/infra/views/checklist_records_views/checklist_records_dashboard_page_view.py @@ -1,13 +1,14 @@ from json import dumps from flask import render_template, flash, redirect, url_for -from core.use_cases.checklist_records import get_checklist_dashboard_page_data from core.constants import LEAF_COLORS -from core.commons import DaysRange, Error +from core.commons import DaysRange +from infra.factories.use_cases.checklist_records import ( + get_checklist_records_dashboard_page_data, +) from infra.constants import LEAF_COLORS_CHART_LEGEND_HEX_COLORS from infra.utils import JSONEncoder - from infra.authentication import auth @@ -15,7 +16,7 @@ def checklist_records_dashboard_page_view(): try: auth_user = auth.get_user() - data = get_checklist_dashboard_page_data.execute() + data = get_checklist_records_dashboard_page_data.execute() leaf_appearences_chart_data = dumps( data["days_count_by_leaf_appearance_and_plant"], ensure_ascii=False @@ -45,7 +46,7 @@ def checklist_records_dashboard_page_view(): active_plant_id=active_plant_id, auth_user=auth_user, ) - except Error as error: + except Exception as error: flash(error.ui_message, "error") return redirect( url_for( diff --git a/src/app/infra/views/checklist_records_views/checklist_records_table_page_view.py b/src/app/infra/views/checklist_records_views/checklist_records_table_page_view.py index 32ce4899..3f40e1cd 100644 --- a/src/app/infra/views/checklist_records_views/checklist_records_table_page_view.py +++ b/src/app/infra/views/checklist_records_views/checklist_records_table_page_view.py @@ -1,7 +1,8 @@ from flask import render_template, request -from core.use_cases.checklist_records import get_checklist_records_table_page_data -from core.commons import Error +from infra.factories.use_cases.checklist_records import ( + get_checklist_records_table_page_data, +) from core.constants import PAGINATION from infra.forms import ChecklistRecordForm, CsvForm @@ -52,6 +53,5 @@ def checklist_records_table_page_view(): page_buttons_limit=PAGINATION["page_buttons_siblings_count"], auth_user=auth_user, ) - except Error as error: - print(error, flush=True) - return "500 ERROR PAGE" + except Exception as error: + return error.ui_message, error.status_code diff --git a/src/app/infra/views/checklist_records_views/create_checklist_record_by_form_view.py b/src/app/infra/views/checklist_records_views/create_checklist_record_by_form_view.py index 377506e3..232ae7db 100644 --- a/src/app/infra/views/checklist_records_views/create_checklist_record_by_form_view.py +++ b/src/app/infra/views/checklist_records_views/create_checklist_record_by_form_view.py @@ -1,10 +1,10 @@ from flask import render_template, request -from core.use_cases.checklist_records import ( +from infra.factories.use_cases.checklist_records import ( create_checklist_record_by_form, get_checklist_records_table_page_data, ) -from core.commons import Error +from core.errors.validation import ChecklistRecordNotValidError from core.constants import PAGINATION from infra.forms import ChecklistRecordForm @@ -25,7 +25,7 @@ def create_checklist_record_by_form_view(): auth_user = auth.get_user() if not checklist_record_form.validate_on_submit(): - raise Error + raise ChecklistRecordNotValidError() create_checklist_record_by_form.execute( { @@ -71,7 +71,7 @@ def create_checklist_record_by_form_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: render_template( "pages/checklist_records_table/create_checklist_record_form/fields.html", checklist_record_form=checklist_record_form, diff --git a/src/app/infra/views/checklist_records_views/create_checklist_records_by_csv_file_view.py b/src/app/infra/views/checklist_records_views/create_checklist_records_by_csv_file_view.py index 9c7d12b3..ed1fde07 100644 --- a/src/app/infra/views/checklist_records_views/create_checklist_records_by_csv_file_view.py +++ b/src/app/infra/views/checklist_records_views/create_checklist_records_by_csv_file_view.py @@ -2,24 +2,21 @@ from flask import render_template, request -from core.use_cases.checklist_records import ( +from infra.factories.use_cases.checklist_records import ( create_checklist_records_by_csv_file, get_checklist_records_table_page_data, ) -from core.commons import Error from core.constants import PAGINATION +from core.errors.validation import ChecklistRecordNotValidError from infra.forms import CsvForm - from infra.authentication import auth @auth.login_middleware def create_checklist_records_by_csv_file_view(): - - - + form_data = request.form.to_dict() form_data["csv"] = request.files["csv"] @@ -33,9 +30,8 @@ def create_checklist_records_by_csv_file_view(): try: auth_user = auth.get_user() - if not csv_form.validate_on_submit(): - raise Error(ui_message="Arquivo CSV inválido", status_code=400) + raise ChecklistRecordNotValidError() create_checklist_records_by_csv_file.execute(request.files["csv"]) @@ -58,10 +54,10 @@ def create_checklist_records_by_csv_file_view(): current_page_number=current_page_number, page_buttons_limit=PAGINATION["page_buttons_siblings_count"], create_by_csv_message="Registros check-list por arquivo csv realizado com sucesso", - auth_user = auth_user + auth_user=auth_user, ) - except Error as error: + except Exception as error: return ( render_template( "components/csv_form_error.html", diff --git a/src/app/infra/views/checklist_records_views/delete_checklist_records_view.py b/src/app/infra/views/checklist_records_views/delete_checklist_records_view.py index 8f3fc788..f0ee0ead 100644 --- a/src/app/infra/views/checklist_records_views/delete_checklist_records_view.py +++ b/src/app/infra/views/checklist_records_views/delete_checklist_records_view.py @@ -1,19 +1,16 @@ from flask import request, render_template -from core.use_cases.checklist_records import ( +from core.constants import PAGINATION + +from infra.factories.use_cases.checklist_records import ( delete_checklist_records, get_checklist_records_table_page_data, ) -from core.commons import Error -from core.constants import PAGINATION - from infra.authentication import auth @auth.login_middleware def delete_checklist_records_view(): - - checklist_records_ids = request.form.getlist("checklist-records-ids[]") start_date = request.args.get("start-date", None) @@ -24,7 +21,6 @@ def delete_checklist_records_view(): try: auth_user = auth.get_user() - delete_checklist_records.execute(checklist_records_ids) data = get_checklist_records_table_page_data.execute( @@ -46,7 +42,7 @@ def delete_checklist_records_view(): current_page_number=current_page_number, page_buttons_limit=PAGINATION["page_buttons_siblings_count"], delete_message="Registro(s) check-list deletado(s) com sucesso", - auth_user=auth_user + auth_user=auth_user, ) - except Error as error: + except Exception as error: return "ERROR", error.status_code diff --git a/src/app/infra/views/checklist_records_views/filter_checklist_records_view.py b/src/app/infra/views/checklist_records_views/filter_checklist_records_view.py index 288c68ea..60c3c84a 100644 --- a/src/app/infra/views/checklist_records_views/filter_checklist_records_view.py +++ b/src/app/infra/views/checklist_records_views/filter_checklist_records_view.py @@ -1,9 +1,10 @@ from flask import render_template, request -from core.use_cases.checklist_records import get_checklist_records_table_page_data -from core.commons import Error from core.constants import PAGINATION +from infra.factories.use_cases.checklist_records import ( + get_checklist_records_table_page_data, +) from infra.authentication import auth @@ -37,5 +38,5 @@ def filter_checklist_records_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: return "ERROR", error.status_code diff --git a/src/app/infra/views/checklist_records_views/update_checklist_record_form_view.py b/src/app/infra/views/checklist_records_views/update_checklist_record_form_view.py index 6f022736..e8ce8ace 100644 --- a/src/app/infra/views/checklist_records_views/update_checklist_record_form_view.py +++ b/src/app/infra/views/checklist_records_views/update_checklist_record_form_view.py @@ -1,7 +1,5 @@ from flask import render_template -from core.commons import Error - from infra.forms import ChecklistRecordForm from infra.repositories import checklist_records_repository @@ -19,5 +17,5 @@ def update_checklist_record_form_view(id: str): checklist_record_id=checklist_record.id, update_checklist_record_form=update_checklist_record_form, ) - except Error as error: + except Exception as error: return "Registro não encontrado 😢", error.status_code diff --git a/src/app/infra/views/checklist_records_views/update_checklist_record_view.py b/src/app/infra/views/checklist_records_views/update_checklist_record_view.py index 0ea62bb9..0cb28d81 100644 --- a/src/app/infra/views/checklist_records_views/update_checklist_record_view.py +++ b/src/app/infra/views/checklist_records_views/update_checklist_record_view.py @@ -1,10 +1,9 @@ from flask import request, render_template -from core.use_cases.checklist_records import update_checklist_record -from core.commons import Error +from core.errors.validation import ChecklistRecordNotValidError from infra.forms import ChecklistRecordForm - +from infra.factories.use_cases.checklist_records import update_checklist_record from infra.authentication import auth @@ -17,7 +16,7 @@ def update_checklist_record_view(id: str): auth_user = auth.get_user() if not checklist_record_form.validate_on_submit(): - raise Error("Formulário inválido") + raise ChecklistRecordNotValidError() updated_checklist_record = update_checklist_record.execute( { @@ -47,7 +46,7 @@ def update_checklist_record_view(id: str): update_message="Registro check-list atualizado com sucesso", auth_user=auth_user, ) - except Error as error: + except Exception as error: print(checklist_record_form.errors, flush=True) return ( render_template( diff --git a/src/app/infra/views/error_views/__init__.py b/src/app/infra/views/error_views/__init__.py index 8c2ea397..aeb6feed 100644 --- a/src/app/infra/views/error_views/__init__.py +++ b/src/app/infra/views/error_views/__init__.py @@ -1,7 +1,5 @@ from flask import Flask, render_template -from core.commons import Error - def init_error_views(app: Flask): @@ -12,10 +10,8 @@ def page_not_found_error_page_view(_): ) @app.errorhandler(Exception) - @app.errorhandler(Error) @app.errorhandler(500) def internal_server_error_page_view(error): - print("error", error, flush=True) return render_template( "pages/error/index.html", status_code=500, diff --git a/src/app/infra/views/plants_views/create_plant_view.py b/src/app/infra/views/plants_views/create_plant_view.py index 5a31d36c..27691425 100644 --- a/src/app/infra/views/plants_views/create_plant_view.py +++ b/src/app/infra/views/plants_views/create_plant_view.py @@ -1,10 +1,9 @@ from flask import request, render_template -from core.use_cases.plants import create_plant_by_form, get_plants_page_data from core.commons import Error from infra.forms import PlantForm - +from infra.factories.use_cases.plants import get_plants_page_data, create_plant_by_form from infra.authentication import auth diff --git a/src/app/infra/views/plants_views/delete_plant_view.py b/src/app/infra/views/plants_views/delete_plant_view.py index 2929aab2..e2cc11f7 100644 --- a/src/app/infra/views/plants_views/delete_plant_view.py +++ b/src/app/infra/views/plants_views/delete_plant_view.py @@ -1,9 +1,9 @@ from flask import render_template -from core.use_cases.plants import delete_plant, get_plants_page_data - from core.commons import Error +from infra.factories.use_cases.plants import delete_plant, get_plants_page_data + from infra.authentication import auth @@ -23,5 +23,4 @@ def delete_plant_view(id: str): auth_user=auth_user, ) except Error as error: - print(error.ui_message) return "ERROR", error.status_code diff --git a/src/app/infra/views/plants_views/filter_plants_view.py b/src/app/infra/views/plants_views/filter_plants_view.py index d7aace3c..da29220c 100644 --- a/src/app/infra/views/plants_views/filter_plants_view.py +++ b/src/app/infra/views/plants_views/filter_plants_view.py @@ -1,6 +1,6 @@ from flask import request, render_template -from core.use_cases.plants import filter_plants +from infra.factories.use_cases.plants import filter_plants from core.commons import Error @@ -15,5 +15,4 @@ def filter_plants_view(): "pages/plants/plants_cards/index.html", plants=plants, auth_user=None ) except Error as error: - print(error) return "ERROR", error.status_code diff --git a/src/app/infra/views/plants_views/plants_page_view.py b/src/app/infra/views/plants_views/plants_page_view.py index 9360a5c4..f5a287f5 100644 --- a/src/app/infra/views/plants_views/plants_page_view.py +++ b/src/app/infra/views/plants_views/plants_page_view.py @@ -1,6 +1,6 @@ from flask import request, render_template -from core.use_cases.plants import filter_plants +from infra.factories.use_cases.plants import filter_plants from infra.forms import PlantForm from infra.authentication import auth diff --git a/src/app/infra/views/plants_views/update_active_plant_view.py b/src/app/infra/views/plants_views/update_active_plant_view.py index a5ba44b0..65a35948 100644 --- a/src/app/infra/views/plants_views/update_active_plant_view.py +++ b/src/app/infra/views/plants_views/update_active_plant_view.py @@ -1,6 +1,6 @@ from flask import request, render_template -from core.use_cases.plants import update_active_plant +from infra.factories.use_cases.plants import update_active_plant from core.commons import Error from infra.authentication import auth diff --git a/src/app/infra/views/plants_views/update_plant_form_view.py b/src/app/infra/views/plants_views/update_plant_form_view.py index dada5dcd..d643427a 100644 --- a/src/app/infra/views/plants_views/update_plant_form_view.py +++ b/src/app/infra/views/plants_views/update_plant_form_view.py @@ -1,6 +1,6 @@ from flask import render_template -from core.use_cases.plants import get_plant_by_id +from infra.factories.use_cases.plants import get_plant_by_id from core.commons import Error from infra.forms import PlantForm diff --git a/src/app/infra/views/plants_views/update_plant_view.py b/src/app/infra/views/plants_views/update_plant_view.py index 04921e64..b4359607 100644 --- a/src/app/infra/views/plants_views/update_plant_view.py +++ b/src/app/infra/views/plants_views/update_plant_view.py @@ -1,12 +1,13 @@ from flask import request, render_template -from core.use_cases.plants import update_plant +from infra.factories.use_cases.plants import update_plant from core.commons import Error from infra.forms import PlantForm from infra.authentication import auth + @auth.login_middleware def update_plant_view(id: str): plant_form = PlantForm(formdata=request.form) @@ -28,15 +29,14 @@ def update_plant_view(id: str): "pages/plants/plants_cards/card.html", plant=updated_plant, update_message="Planta atualizada com sucesso", - auth_user=auth_user + auth_user=auth_user, ) except Error as error: - print(plant_form.hex_color.default, flush=True) return ( render_template( "pages/plants/update_plant_form/fields.html", update_plant_form=plant_form, - auth_user=auth_user + auth_user=auth_user, ), error.status_code, ) diff --git a/src/app/infra/views/sensors_records_views/__init__.py b/src/app/infra/views/sensors_records_views/__init__.py index 3c5df9ce..4a04e6ea 100644 --- a/src/app/infra/views/sensors_records_views/__init__.py +++ b/src/app/infra/views/sensors_records_views/__init__.py @@ -12,7 +12,6 @@ from .create_sensors_records_by_form_view import create_sensors_record_by_form_view from .create_sensors_record_by_api_view import create_sensors_record_by_api_view from .filter_sensors_records_view import filter_sensors_records_view -from .create_sensors_records_by_csv_file_view import create_sensors_records_by_csv_file_view from .sensors_records_csv_file_view import sensors_records_csv_file_view sensors_records_views = Blueprint("sensors_records_views", __name__) diff --git a/src/app/infra/views/sensors_records_views/create_sensors_record_by_api_view.py b/src/app/infra/views/sensors_records_views/create_sensors_record_by_api_view.py index 88c4852e..99ada6ba 100644 --- a/src/app/infra/views/sensors_records_views/create_sensors_record_by_api_view.py +++ b/src/app/infra/views/sensors_records_views/create_sensors_record_by_api_view.py @@ -1,16 +1,14 @@ from flask import request -from core.use_cases.sensors_records import create_sensors_record_by_api -from core.commons import Error +from infra.factories.use_cases.sensors_records import create_sensors_record_by_api def create_sensors_record_by_api_view(): try: data = request.get_json() - print(data,flush=True) + print(data, flush=True) create_sensors_record_by_api.execute(data) - - return "Chupa Sky Fly", 200 ##scary!! - except Error as error: - print(error.ui_message, flush=True) + + return "Chupa Sky Fly", 200 ##scary!! + except Exception as error: return error.ui_message, error.status_code diff --git a/src/app/infra/views/sensors_records_views/create_sensors_records_by_csv_file_view.py b/src/app/infra/views/sensors_records_views/create_sensors_records_by_csv_file_view.py index 38b24d31..88c420d2 100644 --- a/src/app/infra/views/sensors_records_views/create_sensors_records_by_csv_file_view.py +++ b/src/app/infra/views/sensors_records_views/create_sensors_records_by_csv_file_view.py @@ -2,15 +2,14 @@ from werkzeug.datastructures import ImmutableMultiDict -from core.use_cases.sensors_records import ( +from core.errors.validation import CSVFileNotValidError +from core.constants import PAGINATION + +from infra.factories.use_cases.sensors_records import ( create_sensors_records_by_csv_file, get_sensors_records_table_page_data, ) -from core.commons import Error -from core.constants import PAGINATION - from infra.forms.csv_form import CsvForm - from infra.authentication import auth @@ -33,7 +32,7 @@ def create_sensors_records_by_csv_file_view(): auth_user = auth.get_user() if not csv_form.validate_on_submit(): - raise Error(ui_message="Arquivo CSV inválido", status_code=400) + raise CSVFileNotValidError() create_sensors_records_by_csv_file.execute(request.files["csv"]) @@ -57,7 +56,7 @@ def create_sensors_records_by_csv_file_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: return ( render_template( "components/csv_form_error.html", diff --git a/src/app/infra/views/sensors_records_views/create_sensors_records_by_form_view.py b/src/app/infra/views/sensors_records_views/create_sensors_records_by_form_view.py index 691f3b24..8a658081 100644 --- a/src/app/infra/views/sensors_records_views/create_sensors_records_by_form_view.py +++ b/src/app/infra/views/sensors_records_views/create_sensors_records_by_form_view.py @@ -1,14 +1,13 @@ from flask import render_template, request -from core.use_cases.sensors_records import ( +from infra.factories.use_cases.sensors_records import ( create_sensors_records_by_form, get_sensors_records_table_page_data, ) -from core.commons import Error +from core.errors.validation import SensorsRecordNotValidError from core.constants import PAGINATION from infra.forms import SensorsRecordForm - from infra.authentication import auth @@ -24,7 +23,21 @@ def create_sensors_record_by_form_view(): try: auth_user = auth.get_user() if not sensors_record_form.validate_on_submit(): - raise Error + raise SensorsRecordNotValidError() + + print( + { + "soil_humidity": sensors_record_form.soil_humidity.data, + "ambient_humidity": sensors_record_form.ambient_humidity.data, + "temperature": sensors_record_form.temperature.data, + "water_volume": sensors_record_form.water_volume.data, + "plant_id": sensors_record_form.plant_id.data, + "created_at": sensors_record_form.date.data, + "date": sensors_record_form.date.data, + "time": sensors_record_form.time.data, + }, + flush=True, + ) create_sensors_records_by_form.execute( { @@ -59,7 +72,8 @@ def create_sensors_record_by_form_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: + print(error, flush=True) return ( render_template( "pages/sensors_records_table/create_sensors_record_form/fields.html", diff --git a/src/app/infra/views/sensors_records_views/delete_sensors_records_view.py b/src/app/infra/views/sensors_records_views/delete_sensors_records_view.py index 8fd6f1a4..ffec2dde 100644 --- a/src/app/infra/views/sensors_records_views/delete_sensors_records_view.py +++ b/src/app/infra/views/sensors_records_views/delete_sensors_records_view.py @@ -1,14 +1,14 @@ from flask import request, render_template -from core.use_cases.sensors_records import ( +from core.constants import PAGINATION + +from infra.factories.use_cases.sensors_records import ( delete_sensors_records, get_sensors_records_table_page_data, ) -from core.commons import Error -from core.constants import PAGINATION - from infra.authentication import auth + @auth.login_middleware def delete_sensors_records_view(): sensors_records_ids = request.form.getlist("sensors-records-ids[]") @@ -19,9 +19,9 @@ def delete_sensors_records_view(): page_number = int(request.args.get("page", 1)) try: - + auth_user = auth.get_user() - + delete_sensors_records.execute(sensors_records_ids) data = get_sensors_records_table_page_data.execute( start_date=start_date, @@ -41,7 +41,7 @@ def delete_sensors_records_view(): current_page_number=current_page_number, page_buttons_limit=PAGINATION["page_buttons_siblings_count"], delete_message="Registro(s) deletado(s) com sucesso", - auth_user=auth_user + auth_user=auth_user, ) - except Error as error: + except Exception as error: return "ERROR", error.status_code diff --git a/src/app/infra/views/sensors_records_views/filter_sensors_records_view.py b/src/app/infra/views/sensors_records_views/filter_sensors_records_view.py index 9b7f53b1..8b0997e2 100644 --- a/src/app/infra/views/sensors_records_views/filter_sensors_records_view.py +++ b/src/app/infra/views/sensors_records_views/filter_sensors_records_view.py @@ -1,9 +1,10 @@ from flask import render_template, request -from core.use_cases.sensors_records import get_sensors_records_table_page_data -from core.commons import Error from core.constants import PAGINATION +from infra.factories.use_cases.sensors_records import ( + get_sensors_records_table_page_data, +) from infra.authentication import auth @@ -14,7 +15,6 @@ def filter_sensors_records_view(): page_number = int(request.args.get("page", 1)) try: - auth_user = auth.get_user() data = get_sensors_records_table_page_data.execute( @@ -38,5 +38,5 @@ def filter_sensors_records_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: return "ERROR", error.status_code ##sla pq é assim mas o petros fez assim diff --git a/src/app/infra/views/sensors_records_views/last_sensors_record_page_view.py b/src/app/infra/views/sensors_records_views/last_sensors_record_page_view.py index fde03a8c..8636076e 100644 --- a/src/app/infra/views/sensors_records_views/last_sensors_record_page_view.py +++ b/src/app/infra/views/sensors_records_views/last_sensors_record_page_view.py @@ -1,8 +1,7 @@ from flask import render_template -from core.use_cases.sensors_records import get_last_sensors_record_page_data +from infra.factories.use_cases.sensors_records import get_last_sensors_record_page_data from core.entities import SensorsRecord -from core.commons import Error from infra.authentication import auth @@ -15,7 +14,7 @@ def last_sensors_record_page_view(): variations = data["variations"] last_sensors_record = data["last_sensors_record"] - except Error: + except Exception: last_sensors_record = SensorsRecord( soil_humidity=0, ambient_humidity=0, diff --git a/src/app/infra/views/sensors_records_views/sensors_records_csv_file_view.py b/src/app/infra/views/sensors_records_views/sensors_records_csv_file_view.py index 9477adca..14655eb1 100644 --- a/src/app/infra/views/sensors_records_views/sensors_records_csv_file_view.py +++ b/src/app/infra/views/sensors_records_views/sensors_records_csv_file_view.py @@ -1,8 +1,10 @@ from flask import send_file, after_this_request, request -from core.use_cases.sensors_records import get_sensors_records_csv_file - +from infra.factories.use_cases.sensors_records import ( + get_sensors_records_csv_file, +) from infra.utils.file import File +from infra.constants import FOLDERS def sensors_records_csv_file_view(): @@ -10,16 +12,19 @@ def sensors_records_csv_file_view(): end_date = request.args.get("end-date", None) plant_id = request.args.get("plant", "all") - csv_file = get_sensors_records_csv_file.execute( - start_date=start_date, end_date=end_date, plant_id=plant_id + csv_folder = FOLDERS["tmp"] + + csv_filename = get_sensors_records_csv_file.execute( + start_date=start_date, + end_date=end_date, + plant_id=plant_id, + folder=csv_folder, ) - csv_folder = csv_file["folder"] - csv_filename = csv_file["filename"] csv_path = f"{csv_folder}/{csv_filename}" @after_this_request def _(response): - File(csv_file["folder"], csv_file["filename"]).delete() + File(csv_folder, csv_filename).delete() return response diff --git a/src/app/infra/views/sensors_records_views/sensors_records_dashboard_page_view.py b/src/app/infra/views/sensors_records_views/sensors_records_dashboard_page_view.py index 76fc22a8..c7f551ec 100644 --- a/src/app/infra/views/sensors_records_views/sensors_records_dashboard_page_view.py +++ b/src/app/infra/views/sensors_records_views/sensors_records_dashboard_page_view.py @@ -2,19 +2,20 @@ from flask import render_template, redirect, url_for, flash -from core.use_cases.sensors_records import get_sensors_dashboard_page_data -from core.commons import Error, DaysRange - -from infra.utils import JSONEncoder +from core.commons import DaysRange from infra.authentication import auth +from infra.factories.use_cases.sensors_records import ( + get_sensors_records_dashboard_page_data, +) +from infra.utils import JSONEncoder def sensors_records_dashboard_page_view(): try: auth_user = auth.get_user() - data = get_sensors_dashboard_page_data.execute() + data = get_sensors_records_dashboard_page_data.execute() soil_humidity_chart_data = dumps( data["soil_humidity_chart_data"], cls=JSONEncoder @@ -42,7 +43,7 @@ def sensors_records_dashboard_page_view(): days_ranges=daysRange.get_value(), auth_user=auth_user, ) - except Error as error: + except Exception as error: flash(error.ui_message, "error") return redirect( url_for( diff --git a/src/app/infra/views/sensors_records_views/sensors_records_table_page_view.py b/src/app/infra/views/sensors_records_views/sensors_records_table_page_view.py index c5d2d5e9..4bc8dd26 100644 --- a/src/app/infra/views/sensors_records_views/sensors_records_table_page_view.py +++ b/src/app/infra/views/sensors_records_views/sensors_records_table_page_view.py @@ -1,12 +1,12 @@ from flask import render_template, request -from core.use_cases.sensors_records import get_sensors_records_table_page_data -from core.commons import Error from core.constants import PAGINATION +from infra.factories.use_cases.sensors_records import ( + get_sensors_records_table_page_data, +) from infra.forms import SensorsRecordForm from infra.forms import CsvForm - from infra.authentication import auth @@ -55,5 +55,5 @@ def sensors_records_table_page_view(): auth_user=auth_user, ) - except Error as error: + except Exception as error: return error, 500 diff --git a/src/app/infra/views/sensors_records_views/update_sensors_record_form_view.py b/src/app/infra/views/sensors_records_views/update_sensors_record_form_view.py index 68a3af8d..ed7c4d37 100644 --- a/src/app/infra/views/sensors_records_views/update_sensors_record_form_view.py +++ b/src/app/infra/views/sensors_records_views/update_sensors_record_form_view.py @@ -1,7 +1,5 @@ from flask import render_template -from core.commons import Error - from infra.forms import SensorsRecordForm from infra.repositories import sensors_records_repository from infra.authentication import auth @@ -19,5 +17,5 @@ def update_sensors_record_form_view(id: str): sensors_record_id=sensors_record.id, update_sensors_record_form=update_sensors_record_form, ) - except Error as error: + except Exception as error: return "Registro não encontrado 😢", error.status_code diff --git a/src/app/infra/views/sensors_records_views/update_sensors_records_view.py b/src/app/infra/views/sensors_records_views/update_sensors_records_view.py index 3261cc5d..7621a597 100644 --- a/src/app/infra/views/sensors_records_views/update_sensors_records_view.py +++ b/src/app/infra/views/sensors_records_views/update_sensors_records_view.py @@ -1,23 +1,23 @@ from flask import render_template, request -from core.use_cases.sensors_records import update_sensors_records -from core.commons import Error +from core.errors.validation import SensorsRecordNotValidError from infra.forms import SensorsRecordForm - +from infra.factories.use_cases.sensors_records import update_sensors_record from infra.authentication import auth + @auth.login_middleware def update_sensors_record_view(id: str): sensors_record_form = SensorsRecordForm(request.form) try: - + auth_user = auth.get_user() - + if not sensors_record_form.validate_on_submit(): - raise Error("Formulário inválido") + raise SensorsRecordNotValidError() - updated_sensors_record = update_sensors_records.execute( + updated_sensors_record = update_sensors_record.execute( { "soil_humidity": sensors_record_form.soil_humidity.data, "ambient_humidity": sensors_record_form.ambient_humidity.data, @@ -34,15 +34,14 @@ def update_sensors_record_view(id: str): "pages/sensors_records_table/row.html", sensors_record=updated_sensors_record, update_message="Registro atualizado com sucesso", - auth_user=auth_user + auth_user=auth_user, ) - except Error as error: - print(sensors_record_form.errors, flush=True) + except Exception as error: return ( render_template( "pages/sensors_records_table/update_sensors_record_form/fields.html", update_sensors_record_form=sensors_record_form, - auth_user=auth_user + auth_user=auth_user, ), error.status_code, ) diff --git a/src/ui/templates/pages/sensors_records_table/records.html b/src/ui/templates/pages/sensors_records_table/records.html index 3bb7186a..e6abb956 100644 --- a/src/ui/templates/pages/sensors_records_table/records.html +++ b/src/ui/templates/pages/sensors_records_table/records.html @@ -7,7 +7,7 @@ {{ toast( -id="create-sensors-records-message", +id="create-by-csv-message", message=create_by_csv_message, category="success", on_load="remove then remove .hidden from #csv-output", @@ -20,7 +20,7 @@ {{ toast( -id="create-sensors-records-message", +id="create-message", message=create_message, category="success", on_load="trigger click on #create-sensors-records-modal-trigger @@ -35,7 +35,7 @@ {{ toast( -id="delete-sensors-records-message", +id="delete-message", message=delete_message, category="success", on_load="remove ", @@ -46,7 +46,7 @@
+ class="text-sm text-left text-gray-500 rtl:text-right dark:text-gray-400"> {% include "pages/sensors_records_table/head.html"%} {% include "pages/sensors_records_table/body.html"%}
@@ -54,14 +54,13 @@ {% else %}
+ class="absolute grid place-content-center w-full h-full"> {{ loading("w-24 h-24") }}