Skip to content

Feature/import to calendar #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Feb 1, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
eff5b49
feat: import file to calendar
Nir-P Jan 22, 2021
521625b
feat: import file to calendar
Nir-P Jan 22, 2021
7d8816b
fix: import ics fix invalid date
Nir-P Jan 22, 2021
87f3ded
fix: import ics fix invalid date
Nir-P Jan 22, 2021
6758901
fix: line to long E501
Nir-P Jan 23, 2021
ed5704d
fix: change functions to support start and end dates plus code readab…
Nir-P Jan 23, 2021
3bd5f6f
fix: support start and end dates
Nir-P Jan 23, 2021
387c4a4
fix: remove unnecessary test files
Nir-P Jan 23, 2021
fd9d0b4
added files from develop
Nir-P Jan 24, 2021
8f0fa28
fix: added globals to confix and some minor changes
Nir-P Jan 25, 2021
36d3765
added from develop and fix requirements
Nir-P Jan 25, 2021
b1c7a99
fix: conftest
Nir-P Jan 25, 2021
7939c5e
fix: up coverage tests to 100%
Nir-P Jan 25, 2021
12719b9
fix: up coverage tests to 100% and removed dup fixture
Nir-P Jan 25, 2021
5142ab5
fix: some minor fixes and more readibility
Nir-P Jan 27, 2021
826417f
fix: some minor fixes and more readibility
Nir-P Jan 27, 2021
1b045bd
fix: requirements fix
Nir-P Jan 27, 2021
cb35056
fix: small readibility fix
Nir-P Jan 27, 2021
fcf0ad6
fix: small readibility
Nir-P Jan 27, 2021
a90f824
fix: small changes
Nir-P Jan 28, 2021
9bc8b51
fix: requirements
Nir-P Jan 28, 2021
208ef69
fix: using [] literals
Nir-P Jan 30, 2021
71b8ff5
fix: requirements
Nir-P Jan 30, 2021
8775ae3
fix: using [] literals
Nir-P Jan 30, 2021
3de6b39
fix: small changes
Nir-P Jan 31, 2021
7afb542
fix: small changes
Nir-P Jan 31, 2021
3923d2d
fix: small changes
Nir-P Jan 31, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ email_conf = ConnectionConfig(
USE_CREDENTIALS=True,
)

# import
NUM_OF_VALUES = 4 # Event contains head, content, start_date and end_date.
MAX_FILE_SIZE_MB = 5 # 5MB
VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files.
VALID_YEARS = 20 # Events must be within 20 years range from the current year.
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.
MAX_EVENTS_START_DATE = 10 # Max Events with the same start date.
# PATHS
STATIC_ABS_PATH = os.path.abspath("static")

Expand Down
197 changes: 197 additions & 0 deletions app/internal/import_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
from collections import defaultdict
import datetime
import os
from pathlib import Path
import re
from typing import Any, Dict, List, Tuple, Union

from icalendar import Calendar

from app.config import (
EVENT_CONTENT_LIMIT,
EVENT_HEADER_LIMIT,
EVENT_HEADER_NOT_EMPTY,
MAX_EVENTS_START_DATE,
MAX_FILE_SIZE_MB,
VALID_FILE_EXTENSION,
VALID_YEARS
)
from app.database.models import Event
from app.database.database import SessionLocal


DATE_FORMAT = "%m-%d-%Y"
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})$")


def is_file_size_valid(file: str, max_size: int = MAX_FILE_SIZE_MB) -> bool:
file_size = os.stat(file).st_size / 1048576 # convert bytes to MB.
return file_size <= max_size


def is_file_extension_valid(file: str,
extension: Union[str, Tuple[str, ...]]
= VALID_FILE_EXTENSION) -> bool:
return file.lower().endswith(extension)


def is_file_exist(file: str) -> bool:
return Path(file).is_file()


def is_date_in_range(date: Union[str, datetime.datetime],
valid_dates: int = VALID_YEARS) -> bool:
"""
check if date is valid and in the range according to the rule we have set
"""
now_year = datetime.datetime.now().year
if isinstance(date, str):
try:
check_date = datetime.datetime.strptime(date, DATE_FORMAT)
except ValueError:
return False
else:
check_date = date
return now_year - valid_dates < check_date.year < now_year + valid_dates


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


def is_file_valid_to_import(file: str) -> bool:
"""
checking before importing that the file exist, the file extension and
the size meet the rules we have set.
"""
return (is_file_exist(file) and is_file_extension_valid(file) and
is_file_size_valid(file))


def is_file_valid_to_save_to_database(events: List[Dict[str, Union[str, Any]]],
max_event_start_date: int
= MAX_EVENTS_START_DATE) -> bool:
"""
checking after importing that there is no larger quantity of events
with the same date according to the rule we have set.
"""
same_date_counter = 1
date_n_count = defaultdict(int)
for event in events:
date_n_count[event["S_Date"]] += 1
if date_n_count[event["S_Date"]] > same_date_counter:
same_date_counter = date_n_count[event["S_Date"]]
return same_date_counter <= max_event_start_date


def open_txt_file(txt_file: str) -> List[str]:
with open(txt_file, "r") as text:
return text.readlines()


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 (not is_date_in_range(start_date) or
not is_date_in_range(end_date.replace("\n", ""))):
return False
start_date = datetime.datetime.strptime(start_date, DATE_FORMAT)
end_date = datetime.datetime.strptime(end_date.replace("\n", ""),
DATE_FORMAT)
calendar_content.append({"Head": head,
"Content": content,
"S_Date": start_date,
"E_Date": end_date})
return True


def import_txt_file(txt_file: str) -> List[Dict[str, Union[str, Any]]]:
calendar_content = []
events = open_txt_file(txt_file)
for event in events:
if (not is_event_text_valid(event) or
not save_calendar_content_txt(event, calendar_content)):
return list()
return calendar_content


def open_ics(ics_file: str) -> Union[List, Calendar]:
with open(ics_file, "r") as ics:
try:
calendar_read = Calendar.from_ical(ics.read())
except (IndexError, ValueError):
return list()
return calendar_read


def is_valid_data_event_ics(component) -> bool:
"""check if ics event data content is valid"""
return not (str(component.get('summary')) is None or
component.get('dtstart') is None or
component.get('dtend') is None or
not is_date_in_range(component.get('dtstart').dt) or
not is_date_in_range(component.get('dtend').dt))


def save_calendar_content_ics(component, calendar_content) -> None:
calendar_content.append({
"Head": str(component.get('summary')),
"Content": str(component.get('description')),
"S_Date": component.get('dtstart').dt
.replace(tzinfo=None),
"E_Date": component.get('dtend').dt
.replace(tzinfo=None)
})


def import_ics_file(ics_file: str) -> List[Dict[str, Union[str, Any]]]:
calendar_content = []
calendar_read = open_ics(ics_file)
if not calendar_read:
return list()
for component in calendar_read.walk():
if component.name == DESC_EVENT:
if is_valid_data_event_ics(component):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flip the condition to return early

Copy link
Contributor Author

@Nir-P Nir-P Jan 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the first if checks if the event is "VEVENT". other events like "VTODO" and "VJOURNAL" not interest us and we skip them.
the nested if checks if the "VEVENT" fields are valid. so I'm thinking to keep it in this order if it is ok.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention was to rewrite the code like that:

            if not is_valid_data_event_ics(component):
                return []
            save_calendar_content_ics(component, calendar_content)

Is there any logical difference that I miss here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought you want to change the if's order my bad :)
I changed it.
In addition, after you accept this ticket could I open a new one after I characterize all the new changes required and tests?
(I want to add location support and more start and end date tests for our code)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah sure why not :)

save_calendar_content_ics(component, calendar_content)
else:
return list()
return calendar_content


def save_events_to_database(events: List[Dict[str, Union[str, Any]]],
user_id: int,
session: SessionLocal) -> None:
"""insert the events into Event table"""
for event in events:
event = Event(
title=event["Head"],
content=event["Content"],
start=event["S_Date"],
end=event["E_Date"],
owner_id=user_id
)
session.add(event)
session.commit()


def user_click_import(file: str, user_id: int, session: SessionLocal) -> str:
"""
when user choose a file and click import, we are checking the file
and if everything is ok we will insert the data to DB
"""
if is_file_valid_to_import(file):
if is_file_extension_valid(file, ".ics"):
import_file = import_ics_file(file)
else:
import_file = import_txt_file(file)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the function for txt and csv files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, import_txt_file(file) handles txt and csv files

if import_file and is_file_valid_to_save_to_database(import_file):
save_events_to_database(import_file, user_id, session)
return True
return False
Binary file modified requirements.txt
Binary file not shown.
32 changes: 32 additions & 0 deletions tests/files_for_import_file_tests/sample.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT
SUMMARY:HeadA
DTSTART;TZID=America/New_York:20190802T103400
DTEND;TZID=America/New_York:20190802T110400
LOCATION:1000 Broadway Ave.\, Brooklyn
DESCRIPTION:Content1
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_1
ACTION:DISPLAY
END:VALARM
END:VEVENT
BEGIN:VEVENT
SUMMARY:HeadB
DTSTART;TZID=America/New_York:20190802T200000
DTEND;TZID=America/New_York:20190802T203000
LOCATION:900 Jay St.\, Brooklyn
DESCRIPTION:Content2
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_2
ACTION:DISPLAY
END:VALARM
END:VEVENT
END:VCALENDAR
Empty file.
32 changes: 32 additions & 0 deletions tests/files_for_import_file_tests/sample2.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN

SUMMARY:HeadA
DTSTART;TZID=America/New_York:20190802T103400
DTEND;TZID=America/New_York:20190802T110400
LOCATION:1000 Broadway Ave.\, Brooklyn
DESCRIPTION:Content1
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_1
ACTION:DISPLAY
END:VALARM
END:VEVENT
BEGIN:VEVENT
SUMMARY:HeadB
DTSTART;TZID=America/New_York:20190802T200000
DTEND;TZID=America/New_York:20190802T203000
LOCATION:900 Jay St.\, Brooklyn
DESCRIPTION:Content2
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_2
ACTION:DISPLAY
END:VALARM
END:VEVENT
END:VCALENDAR
32 changes: 32 additions & 0 deletions tests/files_for_import_file_tests/sample3.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
BEGIN:VEVENT



LOCATION:1000 Broadway Ave.\, Brooklyn
DESCRIPTION:Content1
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_1
ACTION:DISPLAY
END:VALARM
END:VEVENT
BEGIN:VEVENT
SUMMARY:HeadB
DTSTART;TZID=America/New_York:20190802T200000
DTEND;TZID=America/New_York:20190802T203000
LOCATION:900 Jay St.\, Brooklyn
DESCRIPTION:Content2
STATUS:CONFIRMED
SEQUENCE:3
BEGIN:VALARM
TRIGGER:-PT10M
DESCRIPTION:desc_2
ACTION:DISPLAY
END:VALARM
END:VEVENT
END:VCALENDAR
Loading