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 5 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
5 changes: 2 additions & 3 deletions app/database/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

from .database import Base
from app.database.database import Base


class User(Base):
Expand All @@ -27,8 +27,7 @@ class Event(Base):
id = Column(Integer, primary_key=True, index=True)
title = Column(String)
content = Column(String)
start = Column(DateTime, nullable=False)
end = Column(DateTime, nullable=False)
date = Column(DateTime, nullable=False)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

owner_id = Column(Integer, ForeignKey("users.id"))

owner = relationship("User", back_populates="events")
178 changes: 178 additions & 0 deletions app/internal/import_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import datetime
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:
file_size = os.stat(file).st_size / 1048576 # convert bytes to MB.
return file_size <= max_size


def check_file_extension(file: str,
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


def check_date_in_range(date1: 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(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 \
check_date.year < now_year - valid_dates:
return False
return True


def check_validity_of_txt(row: str) -> bool:
"""Check if the row contains valid data"""
get_values = re.findall(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})$", row)
if get_values:
Copy link
Member

Choose a reason for hiding this comment

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

return get_values and len(get_values[0]) == NUM_OF_VALUES

if len(get_values[0]) == NUM_OF_VALUES:
return True
return False


def before_import_checking(file: str) -> bool:
"""
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):
Copy link
Member

Choose a reason for hiding this comment

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

Prefer:
return is_file_exists(file) and ... and ...

Or even:

validations = (is_file_exists, is_file_extension_valid, is_file_size_valid)
return all(validation(file) for validation in validations)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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]]],
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 = {}
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:
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:
events = text.readlines()
for event in events:
if not check_validity_of_txt(event):
return list()
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]]]:
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]]],
user_id: int,
session: SessionLocal = SessionLocal()) -> None:
"""insert the events into Event table"""
for event in events:
event = Event(
title=event["Head"],
content=event["Content"],
date=event["Date"],
owner_id=user_id
)
session.add(event)
session.commit()
session.close()


def user_click_import(file: str, user_id: int,
Copy link
Contributor

@Gonzom Gonzom Jan 23, 2021

Choose a reason for hiding this comment

The 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]):
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:
if after_import_checking(import_file):
move_events_to_db(import_file, user_id, session)
return "Import success"
return "Import failed"
Binary file modified requirements.txt
Binary file not shown.
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ def session():
Base.metadata.drop_all(bind=engine)


@pytest.fixture
def test_session():
Base.metadata.create_all(bind=test_engine)
test_session = TestingSessionLocal()
yield test_session
test_session.close()
Base.metadata.drop_all(bind=test_engine)


@pytest.fixture
def user(session):
faker = Faker()
Expand Down
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
Loading