Skip to content

feat: adding holidays to calendar as events #165

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 34 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
307e584
feat: adding holidays to calendar as events
Jan 30, 2021
f8f4d5d
fix: unused import and long lines
Jan 30, 2021
a064a89
fix: add os to get folder location for test
Jan 30, 2021
468d18b
fix: add line break
Jan 30, 2021
a1fec2b
fix: another try for pytest relative folder for my test
Jan 30, 2021
af814ad
fix: another try for pytest relative folder for my test
Jan 30, 2021
94f5ede
fix: another try for pytest relative folder for my test
Jan 30, 2021
6597f7a
fix: fixed merge request comments
Jan 31, 2021
bcc34a2
fix: fixed merge request comments
Jan 31, 2021
d67a4e3
fix: fixed merge request comments
Jan 31, 2021
d2d1d52
fix: fixed awaited test failure
Jan 31, 2021
e2ff48e
fix: fixed blank line
Jan 31, 2021
1d7b2b7
fix: removed unused import
Jan 31, 2021
c38afb1
fix: renamed 128 into variable
Jan 31, 2021
ec85764
fix: fix line too long
Jan 31, 2021
dfbd98c
fix: removed not needed code
Jan 31, 2021
6292b0c
fix: fixed mr comments - added one more test and minor fixes
Feb 1, 2021
53c3b00
fix: fixed new line at end of file
Feb 1, 2021
3388982
fix: added type annotations
Feb 7, 2021
b79af9c
Merge remote-tracking branch 'upstream/develop' into holiday
Feb 7, 2021
edfe81e
fix: solve merge conflict
Feb 7, 2021
b042297
fix: added type annotations
Feb 9, 2021
410aae1
Merge branch 'develop' into holiday
Feb 9, 2021
018f82d
fix: merge conflict
Feb 9, 2021
e42f6be
fix: fixed type annotation for test
Feb 9, 2021
65e8507
fix: fixed mr comments
Feb 10, 2021
c4d6b30
Merge branch 'develop' into holiday
Feb 10, 2021
43d1344
fix: merge conflict
Feb 10, 2021
b712b4b
fix: solve mr comments
Feb 10, 2021
b29f6f9
fix: solve mr comments
Feb 11, 2021
191d8e7
Merge branch 'develop' into holiday
Feb 11, 2021
3d1ad20
fix: solve mr comments
Feb 11, 2021
b9c6402
fix: moved import holidays to internal folder
Feb 11, 2021
995d423
fix: mr comments
Feb 11, 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
64 changes: 64 additions & 0 deletions app/internal/import_holidays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import re
from datetime import datetime, timedelta

from app.database.models import User, Event, UserEvent
from sqlalchemy.orm import Session
from typing import List, Match

REGEX_EXTRACT_HOLIDAYS = re.compile(
r'SUMMARY:(?P<title>.*)(\n.*){1,8}DTSTAMP:(?P<date>\w{8})',
re.MULTILINE)


def get_holidays_from_file(file: List[Event], session: Session) -> List[Event]:
"""
This function using regex to extract holiday title
and date from standrd ics file
:param file:standard ics file
:param session:current connection
:return:list of holidays events
"""
parsed_holidays = REGEX_EXTRACT_HOLIDAYS.finditer(file)
holidays = []
for holiday in parsed_holidays:
holiday_event = create_holiday_event(
holiday, session.query(User).filter_by(id=1).first().id)
holidays.append(holiday_event)
return holidays


def create_holiday_event(holiday: Match[str], owner_id: int) -> Event:
valid_ascii_chars_range = 128
title = holiday.groupdict()['title'].strip()
title_to_save = ''.join(i if ord(i) < valid_ascii_chars_range
else '' for i in title)
date = holiday.groupdict()['date'].strip()
format_string = '%Y%m%d'
holiday = Event(
title=title_to_save,
start=datetime.strptime(date, format_string),
end=datetime.strptime(date, format_string) + timedelta(days=1),
content='holiday',
owner_id=owner_id
)
return holiday


def save_holidays_to_db(holidays: List[Event], session: Session):
"""
this function saves holiday list into database.
:param holidays: list of holidays events
:param session: current connection
"""
session.add_all(holidays)
session.commit()
session.flush(holidays)
userevents = []
for holiday in holidays:
userevent = UserEvent(
user_id=holiday.owner_id,
event_id=holiday.id
)
userevents.append(userevent)
session.add_all(userevents)
session.commit()
25 changes: 25 additions & 0 deletions app/routers/profile.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import io

from loguru import logger
from fastapi import APIRouter, Depends, File, Request, UploadFile
from PIL import Image
from starlette.responses import RedirectResponse
from starlette.status import HTTP_302_FOUND
from sqlalchemy.exc import SQLAlchemyError

from app import config
from app.database.models import User
from app.dependencies import get_db, MEDIA_PATH, templates
from app.internal.on_this_day_events import get_on_this_day_events
from app.internal.import_holidays import (get_holidays_from_file,
save_holidays_to_db)

PICTURE_EXTENSION = config.PICTURE_EXTENSION
PICTURE_SIZE = config.AVATAR_SIZE
Expand Down Expand Up @@ -135,6 +139,13 @@ async def update_telegram_id(
return RedirectResponse(url=url, status_code=HTTP_302_FOUND)


@router.get("/holidays/import")
def import_holidays(request: Request):
return templates.TemplateResponse("import_holidays.html", {
"request": request,
})


async def process_image(image, user):
img = Image.open(io.BytesIO(image))
width, height = img.size
Expand All @@ -152,3 +163,17 @@ def get_image_crop_area(width, height):
return delta, 0, width - delta, height
delta = (height - width) // 2
return 0, delta, width, width + delta


@router.post("/holidays/update")
async def update_holidays(
file: UploadFile = File(...), session=Depends(get_db)):
icsfile = await file.read()
holidays = get_holidays_from_file(icsfile.decode(), session)
try:
save_holidays_to_db(holidays, session)
except SQLAlchemyError as ex:
logger.exception(ex)
finally:
url = router.url_path_for("profile")
return RedirectResponse(url=url, status_code=HTTP_302_FOUND)
4 changes: 4 additions & 0 deletions app/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ body {
border: none;
background-color: whitesmoke;
}

.subtitle {
font-size: 20px;
}
19 changes: 19 additions & 0 deletions app/templates/import_holidays.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% extends "base.html" %}


{% block content %}

<form class="m-3" action="update_holidays" method="post" enctype="multipart/form-data">
<h1 class="title">Import holidays using ics file</h1>
<p class="subtitle">Use the upload button to choose ics file</p>
<a href="https://www.feiertagskalender.ch/export.php?geo=3542&hl=en#" target="_blank">Check this website to export holidays by country</a>
<div class="input-group">
<label for="file"></label>
<input type="file" class="form-control-sm" accept=".ics" id="file">
</div>
<div class="mt-4">
<button type="submit" class="btn btn-sm btn-success">Add events from file to calender</button>
</div>
</form>

{% endblock %}
3 changes: 3 additions & 0 deletions app/templates/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ <h6 class="card-title text-center mb-1">{{ user.full_name }}</h6>
<li class="list-group-item list-group-item-action no-border">
<a class="text-decoration-none text-secondary" href="#">Export my calendar</a>
</li>
<li class="list-group-item list-group-item-action no-border">
<a class="text-decoration-none text-secondary" href="{{ url_for('import_holidays') }}">Add holidays to calendar</a>
</li>
<li class="list-group-item list-group-item-action no-border">
<a class="text-decoration-none text-secondary" href="#">Your feature</a>
</li>
Expand Down
36 changes: 36 additions & 0 deletions tests/resources/ics_example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
STATUS:CONFIRMED
DTSTAMP:20210225T000000
DTSTART;VALUE=DATE:20210225
UID:feiertag2021-0323-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:Purim
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
STATUS:CONFIRMED
DTSTAMP:20210226T000000
DTSTART;VALUE=DATE:20210226
UID:feiertag2021-0324-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:Pesach
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
STATUS:CONFIRMED
DTSTAMP:20210328T000000
DTSTART;VALUE=DATE:20210328
UID:feiertag2021-0336-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:Pesach
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
STATUS:CONFIRMED
DTSTAMP:20210329T010000
DTSTART;VALUE=DATE:20210329
UID:feiertag2021-0336-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:One More Holiday For Test
DTSTAMP:20210101T010000
25 changes: 25 additions & 0 deletions tests/resources/wrong_ics_example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
STATUS:CONFIRMED
DTSTAMP:20210225T000000
DTSTART;VALUE=DATE:20210225
UID:feiertag2021-0323-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
STATUS:CONFIRMED
DTSTAMP:20210226T000000
DTSTART;VALUE=DATE:20210226
UID:feiertag2021-0324-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:Pesach
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
STATUS:CONFIRMED
DTSTART;VALUE=DATE:20210328
UID:feiertag2021-0336-3542-fcal.ch
CATEGORIES:Public holidays
END:VEVENT
BEGIN:VEVENT
SUMMARY:Pesach
DESCRIPTION:Feiertagskalender.ch - free data for private /internal use only.
34 changes: 34 additions & 0 deletions tests/test_holidays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os
from app.database.models import Event, User
from app.routers import profile
from sqlalchemy.orm import Session


class TestHolidaysImport:
HOLIDAYS = '/profile/holidays/import'

@staticmethod
def test_import_holidays_page_exists(client):
resp = client.get(TestHolidaysImport.HOLIDAYS)
assert resp.ok
assert b'Import holidays using ics file' in resp.content

def test_get_holidays(self, session: Session, user: User):
current_folder = os.path.dirname(os.path.realpath(__file__))
resource_folder = os.path.join(current_folder, 'resources')
test_file = os.path.join(resource_folder, 'ics_example.txt')
with open(test_file) as file:
ics_content = file.read()
holidays = profile.get_holidays_from_file(ics_content, session)
profile.save_holidays_to_db(holidays, session)
assert len(session.query(Event).all()) == 4

def test_wrong_file_get_holidays(self, session: Session, user: User):
current_folder = os.path.dirname(os.path.realpath(__file__))
resource_folder = os.path.join(current_folder, 'resources')
test_file = os.path.join(resource_folder, 'wrong_ics_example.txt')
with open(test_file) as file:
ics_content = file.read()
holidays = profile.get_holidays_from_file(ics_content, session)
profile.save_holidays_to_db(holidays, session)
assert len(session.query(Event).all()) == 0