Skip to content

Commit 38d6088

Browse files
authored
Feature/import to calendar (#119)
* feat: import file to calendar
1 parent 238d859 commit 38d6088

15 files changed

+187818
-0
lines changed

app/config.py.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ email_conf = ConnectionConfig(
4545
USE_CREDENTIALS=True,
4646
)
4747

48+
# import
49+
MAX_FILE_SIZE_MB = 5 # 5MB
50+
VALID_FILE_EXTENSION = (".txt", ".csv", ".ics") # Can import only these files.
51+
# Events must be within 20 years range from the current year.
52+
EVENT_VALID_YEARS = 20
53+
EVENT_HEADER_NOT_EMPTY = 1 # 1- for not empty, 0- for empty.
54+
EVENT_HEADER_LIMIT = 50 # Max characters for event header.
55+
EVENT_CONTENT_LIMIT = 500 # Max characters for event characters.
56+
MAX_EVENTS_START_DATE = 10 # Max Events with the same start date.
4857
# PATHS
4958
STATIC_ABS_PATH = os.path.abspath("static")
5059

app/internal/import_file.py

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
from collections import defaultdict
2+
import datetime
3+
import os
4+
from pathlib import Path
5+
import re
6+
from typing import Any, Dict, Generator, List, Tuple, Union
7+
8+
from icalendar import Calendar
9+
10+
from app.config import (
11+
EVENT_CONTENT_LIMIT,
12+
EVENT_HEADER_LIMIT,
13+
EVENT_HEADER_NOT_EMPTY,
14+
EVENT_VALID_YEARS,
15+
MAX_EVENTS_START_DATE,
16+
MAX_FILE_SIZE_MB,
17+
VALID_FILE_EXTENSION
18+
)
19+
from app.database.database import SessionLocal
20+
from app.routers.event import create_event
21+
from loguru import logger
22+
23+
24+
DATE_FORMAT = "%m-%d-%Y"
25+
DESC_EVENT = "VEVENT"
26+
EVENT_PATTERN = re.compile(r"^(\w{" + str(EVENT_HEADER_NOT_EMPTY) + "," +
27+
str(EVENT_HEADER_LIMIT) + r"}),\s(\w{0," +
28+
str(EVENT_CONTENT_LIMIT) +
29+
r"}),\s(\d{2}-\d{2}-\d{4})," +
30+
r"\s(\d{2}-\d{2}-\d{4})$")
31+
32+
33+
def is_file_size_valid(file: str, max_size: int = MAX_FILE_SIZE_MB) -> bool:
34+
file_size = os.stat(file).st_size / 1048576 # convert bytes to MB.
35+
return file_size <= max_size
36+
37+
38+
def is_file_extension_valid(file: str,
39+
extension: Union[str, Tuple[str, ...]]
40+
= VALID_FILE_EXTENSION) -> bool:
41+
return file.lower().endswith(extension)
42+
43+
44+
def is_file_exist(file: str) -> bool:
45+
return Path(file).is_file()
46+
47+
48+
def is_date_in_range(date: Union[str, datetime.datetime],
49+
valid_dates: int = EVENT_VALID_YEARS) -> bool:
50+
"""
51+
check if date is valid and in the range according to the rule we have set
52+
"""
53+
now_year = datetime.datetime.now().year
54+
if isinstance(date, str):
55+
try:
56+
check_date = datetime.datetime.strptime(date, DATE_FORMAT)
57+
except ValueError:
58+
return False
59+
else:
60+
check_date = date
61+
return now_year - valid_dates < check_date.year < now_year + valid_dates
62+
63+
64+
def is_event_text_valid(row: str) -> bool:
65+
"""Check if the row contains valid data"""
66+
get_values = EVENT_PATTERN.search(row)
67+
return get_values is not None
68+
69+
70+
def is_file_valid_to_import(file: str) -> bool:
71+
"""
72+
checking before importing that the file exist, the file extension and
73+
the size meet the rules we have set.
74+
"""
75+
return (is_file_exist(file) and is_file_extension_valid(file) and
76+
is_file_size_valid(file))
77+
78+
79+
def is_file_valid_to_save_to_database(events: List[Dict[str, Union[str, Any]]],
80+
max_event_start_date: int
81+
= MAX_EVENTS_START_DATE) -> bool:
82+
"""
83+
checking after importing that there is no larger quantity of events
84+
with the same date according to the rule we have set.
85+
"""
86+
same_date_counter = 1
87+
date_n_count = defaultdict(int)
88+
for event in events:
89+
date_n_count[event["S_Date"]] += 1
90+
if date_n_count[event["S_Date"]] > same_date_counter:
91+
same_date_counter = date_n_count[event["S_Date"]]
92+
return same_date_counter <= max_event_start_date
93+
94+
95+
def open_txt_file(txt_file: str) -> Generator[str, None, None]:
96+
with open(txt_file, "r") as text:
97+
for row in text.readlines():
98+
yield row
99+
100+
101+
def save_calendar_content_txt(event: str, calendar_content: List) -> bool:
102+
"""populate calendar with event content"""
103+
head, content, start_date, end_date = event.split(", ")
104+
if (not is_date_in_range(start_date) or
105+
not is_date_in_range(end_date.replace("\n", ""))):
106+
return False
107+
start_date = datetime.datetime.strptime(start_date, DATE_FORMAT)
108+
end_date = datetime.datetime.strptime(end_date.replace("\n", ""),
109+
DATE_FORMAT)
110+
calendar_content.append({"Head": head,
111+
"Content": content,
112+
"S_Date": start_date,
113+
"E_Date": end_date})
114+
return True
115+
116+
117+
def import_txt_file(txt_file: str) -> List[Dict[str, Union[str, Any]]]:
118+
calendar_content = []
119+
for event in open_txt_file(txt_file):
120+
if (not is_event_text_valid(event) or
121+
not save_calendar_content_txt(event, calendar_content)):
122+
return []
123+
return calendar_content
124+
125+
126+
def open_ics(ics_file: str) -> Union[List, Calendar]:
127+
with open(ics_file, "r") as ics:
128+
try:
129+
calendar_read = Calendar.from_ical(ics.read())
130+
except (IndexError, ValueError) as e:
131+
logger.error(f"open_ics function failed error message: {e}")
132+
return []
133+
return calendar_read
134+
135+
136+
def is_valid_data_event_ics(component) -> bool:
137+
"""check if ics event data content is valid"""
138+
return not (str(component.get('summary')) is None or
139+
component.get('dtstart') is None or
140+
component.get('dtend') is None or
141+
not is_date_in_range(component.get('dtstart').dt) or
142+
not is_date_in_range(component.get('dtend').dt))
143+
144+
145+
def save_calendar_content_ics(component, calendar_content) -> None:
146+
calendar_content.append({
147+
"Head": str(component.get('summary')),
148+
"Content": str(component.get('description')),
149+
"S_Date": component.get('dtstart').dt
150+
.replace(tzinfo=None),
151+
"E_Date": component.get('dtend').dt
152+
.replace(tzinfo=None)
153+
})
154+
155+
156+
def import_ics_file(ics_file: str) -> List[Dict[str, Union[str, Any]]]:
157+
calendar_content = []
158+
calendar_read = open_ics(ics_file)
159+
if not calendar_read:
160+
return []
161+
for component in calendar_read.walk():
162+
if component.name == DESC_EVENT:
163+
if not is_valid_data_event_ics(component):
164+
return []
165+
save_calendar_content_ics(component, calendar_content)
166+
return calendar_content
167+
168+
169+
def save_events_to_database(events: List[Dict[str, Union[str, Any]]],
170+
user_id: int,
171+
session: SessionLocal) -> None:
172+
"""insert the events into Event table"""
173+
for event in events:
174+
title = event["Head"]
175+
content = event["Content"]
176+
start = event["S_Date"]
177+
end = event["E_Date"]
178+
owner_id = user_id
179+
create_event(db=session,
180+
title=title,
181+
content=content,
182+
start=start,
183+
end=end,
184+
owner_id=owner_id)
185+
186+
187+
def user_click_import(file: str, user_id: int, session: SessionLocal) -> bool:
188+
"""
189+
when user choose a file and click import, we are checking the file
190+
and if everything is ok we will insert the data to DB
191+
"""
192+
if is_file_valid_to_import(file):
193+
if is_file_extension_valid(file, ".ics"):
194+
import_file = import_ics_file(file)
195+
else:
196+
import_file = import_txt_file(file)
197+
if import_file and is_file_valid_to_save_to_database(import_file):
198+
save_events_to_database(import_file, user_id, session)
199+
return True
200+
return False

requirements.txt

1.6 KB
Binary file not shown.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
CALSCALE:GREGORIAN
4+
BEGIN:VEVENT
5+
SUMMARY:HeadA
6+
DTSTART;TZID=America/New_York:20190802T103400
7+
DTEND;TZID=America/New_York:20190802T110400
8+
LOCATION:1000 Broadway Ave.\, Brooklyn
9+
DESCRIPTION:Content1
10+
STATUS:CONFIRMED
11+
SEQUENCE:3
12+
BEGIN:VALARM
13+
TRIGGER:-PT10M
14+
DESCRIPTION:desc_1
15+
ACTION:DISPLAY
16+
END:VALARM
17+
END:VEVENT
18+
BEGIN:VEVENT
19+
SUMMARY:HeadB
20+
DTSTART;TZID=America/New_York:20190802T200000
21+
DTEND;TZID=America/New_York:20190802T203000
22+
LOCATION:900 Jay St.\, Brooklyn
23+
DESCRIPTION:Content2
24+
STATUS:CONFIRMED
25+
SEQUENCE:3
26+
BEGIN:VALARM
27+
TRIGGER:-PT10M
28+
DESCRIPTION:desc_2
29+
ACTION:DISPLAY
30+
END:VALARM
31+
END:VEVENT
32+
END:VCALENDAR

tests/files_for_import_file_tests/sample2.blabla

Whitespace-only changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
CALSCALE:GREGORIAN
4+
5+
SUMMARY:HeadA
6+
DTSTART;TZID=America/New_York:20190802T103400
7+
DTEND;TZID=America/New_York:20190802T110400
8+
LOCATION:1000 Broadway Ave.\, Brooklyn
9+
DESCRIPTION:Content1
10+
STATUS:CONFIRMED
11+
SEQUENCE:3
12+
BEGIN:VALARM
13+
TRIGGER:-PT10M
14+
DESCRIPTION:desc_1
15+
ACTION:DISPLAY
16+
END:VALARM
17+
END:VEVENT
18+
BEGIN:VEVENT
19+
SUMMARY:HeadB
20+
DTSTART;TZID=America/New_York:20190802T200000
21+
DTEND;TZID=America/New_York:20190802T203000
22+
LOCATION:900 Jay St.\, Brooklyn
23+
DESCRIPTION:Content2
24+
STATUS:CONFIRMED
25+
SEQUENCE:3
26+
BEGIN:VALARM
27+
TRIGGER:-PT10M
28+
DESCRIPTION:desc_2
29+
ACTION:DISPLAY
30+
END:VALARM
31+
END:VEVENT
32+
END:VCALENDAR
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
CALSCALE:GREGORIAN
4+
BEGIN:VEVENT
5+
6+
7+
8+
LOCATION:1000 Broadway Ave.\, Brooklyn
9+
DESCRIPTION:Content1
10+
STATUS:CONFIRMED
11+
SEQUENCE:3
12+
BEGIN:VALARM
13+
TRIGGER:-PT10M
14+
DESCRIPTION:desc_1
15+
ACTION:DISPLAY
16+
END:VALARM
17+
END:VEVENT
18+
BEGIN:VEVENT
19+
SUMMARY:HeadB
20+
DTSTART;TZID=America/New_York:20190802T200000
21+
DTEND;TZID=America/New_York:20190802T203000
22+
LOCATION:900 Jay St.\, Brooklyn
23+
DESCRIPTION:Content2
24+
STATUS:CONFIRMED
25+
SEQUENCE:3
26+
BEGIN:VALARM
27+
TRIGGER:-PT10M
28+
DESCRIPTION:desc_2
29+
ACTION:DISPLAY
30+
END:VALARM
31+
END:VEVENT
32+
END:VCALENDAR

0 commit comments

Comments
 (0)