diff --git a/app/config.py.example b/app/config.py.example index b6059ece..af8fafce 100644 --- a/app/config.py.example +++ b/app/config.py.example @@ -62,8 +62,10 @@ VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files. EVENT_VALID_YEARS = 20 EVENT_HEADER_NOT_EMPTY = 1 # 1- for not empty, 0- for empty. EVENT_HEADER_LIMIT = 50 # Max characters for event header. -EVENT_CONTENT_LIMIT = 500 # Max characters for event characters. +EVENT_CONTENT_LIMIT = 500 # Max characters for event content. MAX_EVENTS_START_DATE = 10 # Max Events with the same start date. +LOCATION_LIMIT = 50 # Max characters for Location. +EVENT_DURATION_LIMIT = 2 # the max duration in days for an event. # PATHS STATIC_ABS_PATH = os.path.abspath("static") diff --git a/app/internal/import_file.py b/app/internal/import_file.py index 1664638c..45387543 100644 --- a/app/internal/import_file.py +++ b/app/internal/import_file.py @@ -9,9 +9,11 @@ from app.config import ( EVENT_CONTENT_LIMIT, + EVENT_DURATION_LIMIT, EVENT_HEADER_LIMIT, EVENT_HEADER_NOT_EMPTY, EVENT_VALID_YEARS, + LOCATION_LIMIT, MAX_EVENTS_START_DATE, MAX_FILE_SIZE_MB, VALID_FILE_EXTENSION @@ -22,12 +24,22 @@ DATE_FORMAT = "%m-%d-%Y" +DATE_FORMAT2 = "%m-%d-%Y %H:%M" DESC_EVENT = "VEVENT" EVENT_PATTERN = re.compile(r"^(\w{" + str(EVENT_HEADER_NOT_EMPTY) + "," + str(EVENT_HEADER_LIMIT) + r"}),\s(\w{0," + str(EVENT_CONTENT_LIMIT) + r"}),\s(\d{2}-\d{2}-\d{4})," + - r"\s(\d{2}-\d{2}-\d{4})$") + r"\s(\d{2}-\d{2}-\d{4})(?:,\s([\w\s-]{0," + + str(LOCATION_LIMIT) + + r"}))?$") +EVENT_PATTERN2 = re.compile(r"^(\w{" + str(EVENT_HEADER_NOT_EMPTY) + "," + + str(EVENT_HEADER_LIMIT) + r"}),\s(\w{0," + + str(EVENT_CONTENT_LIMIT) + + r"}),\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})," + + r"\s(\d{2}-\d{2}-\d{4}\s\d{2}:\d{2})" + + r"(?:,\s([\w\s-]{0," + str(LOCATION_LIMIT) + + r"}))?$") def is_file_size_valid(file: str, max_size: int = MAX_FILE_SIZE_MB) -> bool: @@ -45,6 +57,17 @@ def is_file_exist(file: str) -> bool: return Path(file).is_file() +def change_string_to_date(date: str) -> Union[datetime.datetime, bool]: + try: + if ":" in date: + date1 = datetime.datetime.strptime(date, DATE_FORMAT2) + else: + date1 = datetime.datetime.strptime(date, DATE_FORMAT) + except ValueError: + return False + return date1 + + def is_date_in_range(date: Union[str, datetime.datetime], valid_dates: int = EVENT_VALID_YEARS) -> bool: """ @@ -52,19 +75,36 @@ def is_date_in_range(date: Union[str, datetime.datetime], """ now_year = datetime.datetime.now().year if isinstance(date, str): - try: - check_date = datetime.datetime.strptime(date, DATE_FORMAT) - except ValueError: + check_date = change_string_to_date(date) + if not check_date: return False else: check_date = date return now_year - valid_dates < check_date.year < now_year + valid_dates +def is_start_day_before_end_date(start: Union[datetime.datetime, str], + end: Union[datetime.datetime, str]) -> bool: + if isinstance(start, str): + start = change_string_to_date(start) + end = change_string_to_date(end) + return start <= end + + +def is_event_valid_duration(start: Union[datetime.datetime, str], + end: Union[datetime.datetime, str], + max_duration: int = EVENT_DURATION_LIMIT) -> bool: + if isinstance(start, str): + start = change_string_to_date(start) + end = change_string_to_date(end) + return (end - start).days < max_duration + + def is_event_text_valid(row: str) -> bool: """Check if the row contains valid data""" get_values = EVENT_PATTERN.search(row) - return get_values is not None + get_values2 = EVENT_PATTERN2.search(row) + return get_values is not None or get_values2 is not None def is_file_valid_to_import(file: str) -> bool: @@ -100,17 +140,29 @@ def open_txt_file(txt_file: str) -> Generator[str, None, None]: def save_calendar_content_txt(event: str, calendar_content: List) -> bool: """populate calendar with event content""" - head, content, start_date, end_date = event.split(", ") + if len(event.split(", ")) == 5: + head, content, start_date, end_date, location = event.split(", ") + location = location.replace("\n", "") + else: + head, content, start_date, end_date = event.split(", ") + end_date = end_date.replace("\n", "") + location = None if (not is_date_in_range(start_date) or - not is_date_in_range(end_date.replace("\n", ""))): + not is_date_in_range(end_date) or + not is_start_day_before_end_date(start_date, end_date) or + not is_event_valid_duration(start_date, end_date)): return False - start_date = datetime.datetime.strptime(start_date, DATE_FORMAT) - end_date = datetime.datetime.strptime(end_date.replace("\n", ""), - DATE_FORMAT) + if re.search(r":", start_date) and re.search(r":", start_date): + start_date = datetime.datetime.strptime(start_date, DATE_FORMAT2) + end_date = datetime.datetime.strptime(end_date, DATE_FORMAT2) + else: + start_date = datetime.datetime.strptime(start_date, DATE_FORMAT) + end_date = datetime.datetime.strptime(end_date, DATE_FORMAT) calendar_content.append({"Head": head, "Content": content, "S_Date": start_date, - "E_Date": end_date}) + "E_Date": end_date, + "Location": location}) return True @@ -149,7 +201,8 @@ def save_calendar_content_ics(component, calendar_content) -> None: "S_Date": component.get('dtstart').dt .replace(tzinfo=None), "E_Date": component.get('dtend').dt - .replace(tzinfo=None) + .replace(tzinfo=None), + "Location": str(component.get('location')) }) @@ -175,12 +228,14 @@ def save_events_to_database(events: List[Dict[str, Union[str, Any]]], content = event["Content"] start = event["S_Date"] end = event["E_Date"] + location = event["Location"] owner_id = user_id create_event(db=session, title=title, content=content, start=start, end=end, + location=location, owner_id=owner_id) diff --git a/tests/files_for_import_file_tests/sample.ics b/tests/files_for_import_file_tests/sample.ics index 0a79e005..355fc30a 100644 --- a/tests/files_for_import_file_tests/sample.ics +++ b/tests/files_for_import_file_tests/sample.ics @@ -5,7 +5,7 @@ BEGIN:VEVENT SUMMARY:HeadA DTSTART;TZID=America/New_York:20190802T103400 DTEND;TZID=America/New_York:20190802T110400 -LOCATION:1000 Broadway Ave.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content1 STATUS:CONFIRMED SEQUENCE:3 @@ -19,7 +19,7 @@ BEGIN:VEVENT SUMMARY:HeadB DTSTART;TZID=America/New_York:20190802T200000 DTEND;TZID=America/New_York:20190802T203000 -LOCATION:900 Jay St.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content2 STATUS:CONFIRMED SEQUENCE:3 diff --git a/tests/files_for_import_file_tests/sample2.ics b/tests/files_for_import_file_tests/sample2.ics index cde28de2..c3025d0d 100644 --- a/tests/files_for_import_file_tests/sample2.ics +++ b/tests/files_for_import_file_tests/sample2.ics @@ -5,7 +5,7 @@ CALSCALE:GREGORIAN SUMMARY:HeadA DTSTART;TZID=America/New_York:20190802T103400 DTEND;TZID=America/New_York:20190802T110400 -LOCATION:1000 Broadway Ave.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content1 STATUS:CONFIRMED SEQUENCE:3 @@ -19,7 +19,7 @@ BEGIN:VEVENT SUMMARY:HeadB DTSTART;TZID=America/New_York:20190802T200000 DTEND;TZID=America/New_York:20190802T203000 -LOCATION:900 Jay St.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content2 STATUS:CONFIRMED SEQUENCE:3 diff --git a/tests/files_for_import_file_tests/sample3.ics b/tests/files_for_import_file_tests/sample3.ics index e045f6f8..6403946a 100644 --- a/tests/files_for_import_file_tests/sample3.ics +++ b/tests/files_for_import_file_tests/sample3.ics @@ -5,7 +5,7 @@ BEGIN:VEVENT -LOCATION:1000 Broadway Ave.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content1 STATUS:CONFIRMED SEQUENCE:3 @@ -19,7 +19,7 @@ BEGIN:VEVENT SUMMARY:HeadB DTSTART;TZID=America/New_York:20190802T200000 DTEND;TZID=America/New_York:20190802T203000 -LOCATION:900 Jay St.\, Brooklyn +LOCATION:Tel-Aviv DESCRIPTION:Content2 STATUS:CONFIRMED SEQUENCE:3 diff --git a/tests/files_for_import_file_tests/sample_date2_ver.txt b/tests/files_for_import_file_tests/sample_date2_ver.txt new file mode 100644 index 00000000..7d1635fd --- /dev/null +++ b/tests/files_for_import_file_tests/sample_date2_ver.txt @@ -0,0 +1,3 @@ +Option1, Content1, 05-21-2019 10:30, 05-21-2019 11:30 +Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30 +Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05 \ No newline at end of file diff --git a/tests/files_for_import_file_tests/sample_date_mix.txt b/tests/files_for_import_file_tests/sample_date_mix.txt new file mode 100644 index 00000000..dd452c75 --- /dev/null +++ b/tests/files_for_import_file_tests/sample_date_mix.txt @@ -0,0 +1,3 @@ +Option1, Content1, 05-21-2019, 05-21-2019 11:30 +Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30 +Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05 \ No newline at end of file diff --git "a/tests/files_for_import_file_tests/\342\200\217\342\200\217sample_loc_ver.txt" "b/tests/files_for_import_file_tests/\342\200\217\342\200\217sample_loc_ver.txt" new file mode 100644 index 00000000..056e447d --- /dev/null +++ "b/tests/files_for_import_file_tests/\342\200\217\342\200\217sample_loc_ver.txt" @@ -0,0 +1,3 @@ +Option1, Content1, 05-21-2019 10:30, 05-21-2019 11:30, aaa +Option2, Content2, 01-11-2010 11:30, 01-11-2010 12:30, bbb +Option3, Content3, 02-02-2022 13:00, 02-02-2022 13:05, ccc \ No newline at end of file diff --git a/tests/test_import_file.py b/tests/test_import_file.py index 6dedfd3e..707552ce 100644 --- a/tests/test_import_file.py +++ b/tests/test_import_file.py @@ -3,9 +3,10 @@ import pytest from app.internal.import_file import ( - import_ics_file, import_txt_file, is_date_in_range, is_event_text_valid, - is_file_exist, is_file_extension_valid, is_file_size_valid, - is_file_valid_to_import, user_click_import) + change_string_to_date, import_ics_file, import_txt_file, is_date_in_range, + is_event_text_valid, is_event_valid_duration, is_file_exist, + is_file_extension_valid, is_file_size_valid, is_file_valid_to_import, + is_start_day_before_end_date, user_click_import) FILE_TXT_SAMPLE = r"tests/files_for_import_file_tests/sample_calendar_data.txt" @@ -21,18 +22,24 @@ FILE_ICS_INVALID_DATA2 = r"tests/files_for_import_file_tests/sample3.ics" NOT_EXIST_BLABLA_EXTENSION = r"tests/files_for_import_file_tests/sample.blabla" FILE_BLABLA_EXTENSION = r"tests/files_for_import_file_tests/sample2.blabla" +FILE_TXT_DATE_VER = r"tests/files_for_import_file_tests/sample_date2_ver.txt" +FILE_TXT_LOCATION = r"tests/files_for_import_file_tests/‏‏sample_loc_ver.txt" +FILE_TXT_MIX_DATE = r"tests/files_for_import_file_tests/‏‏sample_date_mix.txt" IMPORT_TXT_FILE_RESULT_DATA = [ {'Head': 'Head1', 'Content': 'Content1', 'S_Date': datetime.datetime(2019, 5, 21, 0, 0), - 'E_Date': datetime.datetime(2019, 5, 21, 0, 0)}, + 'E_Date': datetime.datetime(2019, 5, 21, 0, 0), + 'Location': None}, {'Head': 'Head2', 'Content': 'Content2', 'S_Date': datetime.datetime(2010, 1, 11, 0, 0), - 'E_Date': datetime.datetime(2010, 1, 11, 0, 0)}, + 'E_Date': datetime.datetime(2010, 1, 11, 0, 0), + 'Location': None}, {'Head': 'Head3', 'Content': 'Content3', 'S_Date': datetime.datetime(2022, 2, 2, 0, 0), - 'E_Date': datetime.datetime(2022, 2, 2, 0, 0)} + 'E_Date': datetime.datetime(2022, 2, 2, 0, 0), + 'Location': None}, ] @@ -54,6 +61,19 @@ (FILE_NOT_EXIST, False) ] +change_string_to_date_tests = [ + ("02-28-2022 13:05", datetime.datetime(2022, 2, 28, 13, 5)), + ("02-28-2022", datetime.datetime(2022, 2, 28)), + ("01-32-2022 20:30", False), # Invalid Date +] + +start_day_before_end_date_tests = [ + (datetime.datetime(1991, 12, 1, 10), + datetime.datetime(1991, 12, 1, 11), True), + (datetime.datetime(1991, 12, 1, 11), + datetime.datetime(1991, 12, 1, 10), False) +] + file_extension_tests = [ (FILE_TXT_BELOW_1MB, None, True), (FILE_CSV_BELOW_1MB, None, True), @@ -66,6 +86,8 @@ date_in_range_tests = [ ("01-31-2022", None, True), ("01-32-2022", None, False), + ("01-31-2022 10:30", None, True), + ("01-32-2022 10:30", None, False), ("20-02-2022", None, False), ("1-20-2011", None, True), # date above the constraint we set (20 years after today date) @@ -78,8 +100,22 @@ ("01-01-2017", 2, False) ] +is_event_valid_duration_tests = [ + (datetime.datetime(1991, 12, 1), + datetime.datetime(1991, 12, 2), None, True), + (datetime.datetime(1991, 12, 1, 10, 30), + datetime.datetime(1991, 12, 3, 10, 29), None, True), + (datetime.datetime(1991, 12, 1, 10, 30), + datetime.datetime(1991, 12, 3, 10, 30), None, False), + (datetime.datetime(1991, 12, 1, 10, 30), + datetime.datetime(1991, 12, 5, 10, 30), 5, True) +] + + check_validity_of_text_tests = [ (r"Head1, Content1, 05-21-2019, 05-21-2019", True), + (r"Head1, Content1, 05-21-2019 10:30, 05-21-2019 11:30", True), + (r"Head1, Content1, 05-21-2019 10:30, 05-21-2019 11:30, Tel-Aviv", True), (r"Head1Content1, 05-21-2019, 05-21-2019", False), # title can't be empty (r" , Content1, 05-21-2019, 05-21-2019", False), @@ -91,7 +127,13 @@ (r"Head1, Content1, 05-21-2019, ", False), # row cant have multiple events (r"""Head1, Content1, 05-21-2019, 05-21-2019, - Head2, Content2, 05-21-2019, 05-21-2019""", False) + Head2, Content2, 05-21-2019, 05-21-2019""", False), + # dates cant be from a different formats + (r"Head1, Content1, 05-21-2019 10:30, 06-21-2019, Tel-Aviv", False), + # location may be empty + (r"Head1, Content1, 05-21-2019 10:30, 06-21-2019 11:30, ", True), + # location may have space and dash + (r"Head1, Content1, 05-21-2019, 06-21-2019, New York-1", True) ] import_txt_file_tests = [ @@ -111,10 +153,12 @@ [ {'Head': 'HeadA', 'Content': 'Content1', 'S_Date': datetime.datetime(2019, 8, 2, 10, 34), - 'E_Date': datetime.datetime(2019, 8, 2, 11, 4)}, + 'E_Date': datetime.datetime(2019, 8, 2, 11, 4), + 'Location': 'Tel-Aviv'}, {'Head': 'HeadB', 'Content': 'Content2', 'S_Date': datetime.datetime(2019, 8, 2, 20, 0), - 'E_Date': datetime.datetime(2019, 8, 2, 20, 30) + 'E_Date': datetime.datetime(2019, 8, 2, 20, 30), + 'Location': 'Tel-Aviv' } ]), # ics invalid file @@ -125,17 +169,27 @@ is_file_valid_to_import_tests = [ (FILE_TXT_SAMPLE, True), + (FILE_TXT_DATE_VER, True), + (FILE_TXT_LOCATION, True), (FILE_TXT_ABOVE_5MB, False), (NOT_EXIST_BLABLA_EXTENSION, False), (FILE_BLABLA_EXTENSION, False) ] +save_calendar_content_txt_tests = [ + ("Head, Content, 05-21-2019 10:30, 05-21-2019 11:30, Tel-Aviv", [], True), + ("Head, Content, 05-21-2019 10:30, 05-21-2019, Tel-Aviv", [], False), +] + user_click_import_tests = [ (FILE_TXT_SAMPLE, 1, True), + (FILE_TXT_DATE_VER, 1, True), + (FILE_TXT_LOCATION, 1, True), (FILE_ICS, 1, True), (FILE_TXT_ABOVE_5MB, 1, False), (NOT_EXIST_BLABLA_EXTENSION, 1, False), - (FILE_TXT_BELOW_1MB, 1, False) + (FILE_TXT_BELOW_1MB, 1, False), + (FILE_TXT_MIX_DATE, 1, False) ] @@ -152,6 +206,16 @@ def test_is_file_exist(file1, result): assert is_file_exist(file1) == result +@pytest.mark.parametrize("str1, result", change_string_to_date_tests) +def test_change_string_to_date(str1, result): + assert change_string_to_date(str1) == result + + +@pytest.mark.parametrize("start, end, result", start_day_before_end_date_tests) +def test_is_start_day_before_end_date(start, end, result): + assert is_start_day_before_end_date(start, end) == result + + @pytest.mark.parametrize("file1, extension, result", file_extension_tests) def test_is_file_extension_valid(file1, extension, result): if extension is None: @@ -183,11 +247,26 @@ def test_import_ics_file(file1, result): assert import_ics_file(file1) == result +@pytest.mark.parametrize("start, end, max_duration, result", + is_event_valid_duration_tests) +def test_is_event_valid_duration(start, end, max_duration, result): + if max_duration is None: + assert is_event_valid_duration(start, end) == result + else: + assert is_event_valid_duration(start, end, max_duration) == result + + @pytest.mark.parametrize("file1, result", is_file_valid_to_import_tests) def test_is_file_valid_to_import(file1, result): assert is_file_valid_to_import(file1) == result +@pytest.mark.parametrize("event, cal_content, result", + save_calendar_content_txt_tests) +def save_calendar_content_txt(event, cal_content, result, session): + assert save_calendar_content_txt(event, cal_content, session) == result + + @pytest.mark.parametrize("file1, user_id, result", user_click_import_tests) def test_user_click_import(file1, user_id, result, session): assert user_click_import(file1, user_id, session) == result