-
Notifications
You must be signed in to change notification settings - Fork 52
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
Changes from 19 commits
eff5b49
521625b
7d8816b
87f3ded
6758901
ed5704d
3bd5f6f
387c4a4
fd9d0b4
8f0fa28
36d3765
b1c7a99
7939c5e
12719b9
5142ab5
826417f
1b045bd
cb35056
fcf0ad6
a90f824
9bc8b51
208ef69
71b8ff5
8775ae3
3de6b39
7afb542
3923d2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
from collections import defaultdict | ||
import datetime | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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, | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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() | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Flip the condition to return early There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the first There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought you want to change the if's order my bad :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
title=event["Head"], | ||
content=event["Content"], | ||
start=event["S_Date"], | ||
end=event["E_Date"], | ||
owner_id=user_id | ||
) | ||
session.add(event) | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the function for txt and csv files? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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 |
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 |
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 |
Uh oh!
There was an error while loading. Please reload this page.