-
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 5 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,178 @@ | ||
import datetime | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import os | ||
import re | ||
from typing import Any, Dict, List, Tuple, Union | ||
|
||
from icalendar import Calendar | ||
|
||
from app.database.models import Event | ||
from app.database.database import SessionLocal | ||
|
||
|
||
NUM_OF_VALUES = 3 # Event contains head, content and 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. | ||
|
||
|
||
def check_file_size(file: str, max_size: int = MAX_FILE_SIZE_MB) -> bool: | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
file_size = os.stat(file).st_size / 1048576 # convert bytes to MB. | ||
return file_size <= max_size | ||
|
||
|
||
def check_file_extension(file: str, | ||
Nir-P 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: | ||
try: | ||
with open(file, "r"): | ||
pass | ||
return True | ||
except (FileNotFoundError, OSError): | ||
return False | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def check_date_in_range(date1: Union[str, datetime.datetime], | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(date1, str): | ||
try: | ||
check_date = datetime.datetime.strptime(date1, "%m-%d-%Y") | ||
except ValueError: | ||
return False | ||
else: | ||
check_date = date1 | ||
if check_date.year > now_year + valid_dates or \ | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
check_date.year < now_year - valid_dates: | ||
return False | ||
return True | ||
|
||
|
||
def check_validity_of_txt(row: str) -> bool: | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Check if the row contains valid data""" | ||
get_values = re.findall(r"^(\w{" + str(EVENT_HEADER_NOT_EMPTY) + "," + | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
str(EVENT_HEADER_LIMIT) + r"})\,\s(\w{0," + | ||
str(EVENT_CONTENT_LIMIT) + | ||
r"})\,\s(\d{2}\-\d{2}\-\d{4})$", row) | ||
if get_values: | ||
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.
|
||
if len(get_values[0]) == NUM_OF_VALUES: | ||
return True | ||
return False | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def before_import_checking(file: str) -> bool: | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
""" | ||
checking before importing that the file exist, the file extension and | ||
the size meet the rules we have set. | ||
""" | ||
if not is_file_exist(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. Prefer: Or even: validations = (is_file_exists, is_file_extension_valid, is_file_size_valid)
return all(validation(file) for validation in validations) 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. Done |
||
return False | ||
if not check_file_extension(file): | ||
return False | ||
if not check_file_size(file): | ||
return False | ||
return True | ||
|
||
|
||
def after_import_checking(calendar1: List[Dict[str, Union[str, Any]]], | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 = {} | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for event in calendar1: | ||
if event["Date"] in date_n_count: | ||
date_n_count[event["Date"]] += 1 | ||
if date_n_count[event["Date"]] > same_date_counter: | ||
same_date_counter = date_n_count[event["Date"]] | ||
else: | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
date_n_count[event["Date"]] = 1 | ||
return same_date_counter <= max_event_start_date | ||
|
||
|
||
def import_txt_file(txt_file: str) -> List[Dict[str, Union[str, Any]]]: | ||
calendar_content = [] | ||
with open(txt_file, "r") as text: | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
events = text.readlines() | ||
for event in events: | ||
if not check_validity_of_txt(event): | ||
return list() | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
head, content, event_date = event.split(", ") | ||
if not check_date_in_range(event_date.replace("\n", "")): | ||
return list() | ||
event_date = datetime.datetime.strptime(event_date.replace("\n", ""), | ||
"%m-%d-%Y") | ||
calendar_content.append({"Head": head, | ||
"Content": content, | ||
"Date": event_date}) | ||
return calendar_content | ||
|
||
|
||
def import_ics_file(ics_file: str) -> List[Dict[str, Union[str, Any]]]: | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
calendar_content = [] | ||
with open(ics_file, "r") as ics: | ||
try: | ||
calendar_read = Calendar.from_ical(ics.read()) | ||
except (IndexError, ValueError): | ||
return list() | ||
for component in calendar_read.walk(): | ||
if component.name == "VEVENT": | ||
if str(component.get('summary')) is None or \ | ||
component.get('dtstart') is None or \ | ||
not check_date_in_range(component.get('dtstart').dt): | ||
return list() | ||
else: | ||
calendar_content.append({ | ||
"Head": str(component.get('summary')), | ||
"Content": str(component.get('description')), | ||
"Date": component.get('dtstart').dt | ||
.replace(tzinfo=None) | ||
}) | ||
return calendar_content | ||
|
||
|
||
def move_events_to_db(events: List[Dict[str, Union[str, Any]]], | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
user_id: int, | ||
session: SessionLocal = 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"], | ||
date=event["Date"], | ||
owner_id=user_id | ||
) | ||
session.add(event) | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
session.commit() | ||
session.close() | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def user_click_import(file: str, user_id: int, | ||
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 might have missed it, but I couldn't find where this function is called other than in the tests. If this happens from a post request, this should move to /routers, possibly /routers/event. Maybe also change the name to import_event(). |
||
session: SessionLocal = 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 before_import_checking(file): | ||
if file.lower().endswith(VALID_FILE_EXTENSION[-1]): | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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: | ||
yammesicka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if after_import_checking(import_file): | ||
move_events_to_db(import_file, user_id, session) | ||
return "Import success" | ||
Nir-P marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return "Import failed" |
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are very valid reasons to set an end date for an event. Please change this back.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Gonzom, thank you for your review, I appreciate it.
when I started this ticket we didn't have a start date and end date.
I'm working now to integrate it into my code.