Skip to content
Snippets Groups Projects
Commit 969f4580 authored by jan.bednarik's avatar jan.bednarik
Browse files

calendar utils: Improved event preprocessing

parent c33c7e58
Branches
No related tags found
1 merge request!11Calendar
# Generated by Django 3.0.6 on 2020-05-23 00:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("calendar_utils", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="calendar", name="url", field=models.URLField(),
),
]
......@@ -4,7 +4,7 @@ from django.contrib.postgres.fields import JSONField
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from .parser import parse_ical, split_events
from .parser import process_ical
def _convert_arrow_to_datetime(event):
......@@ -28,7 +28,9 @@ class EventsJSONField(JSONField):
class Calendar(models.Model):
url = models.URLField(unique=True)
ACTUAL_NUM = 6
url = models.URLField()
source = models.TextField(null=True)
last_update = models.DateTimeField(null=True)
past_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True)
......@@ -38,12 +40,19 @@ class Calendar(models.Model):
source = requests.get(self.url).text
if self.source != source:
self.source = source
past, future = split_events(parse_ical(source))
past, future = process_ical(source)
self.past_events = list(map(_convert_arrow_to_datetime, past))
self.future_events = list(map(_convert_arrow_to_datetime, future))
self.last_update = arrow.utcnow().datetime
self.save()
def actual_events(self):
events = self.future_events[-self.ACTUAL_NUM :]
num = len(events)
if num < 6:
events += self.past_events[: self.ACTUAL_NUM - num]
return events
class CalendarMixin(models.Model):
"""
......
......@@ -7,7 +7,7 @@ EVENT_KEYS = ("begin", "end", "all_day", "name", "description", "location")
def parse_ical(source):
"""Parses iCalendar source and returns events as list of dicts"""
"""Parses iCalendar source and returns events as list of dicts."""
cal = Calendar(source)
events = []
for event in sorted(cal.events, key=attrgetter("begin"), reverse=True):
......@@ -16,8 +16,35 @@ def parse_ical(source):
def split_events(events):
"""Splits events and returns list of past events and future events"""
"""Splits events and returns list of past events and future events."""
now = arrow.utcnow()
past = [ev for ev in events if ev["begin"] < now]
future = [ev for ev in events if ev["begin"] > now]
return past, future
def set_event_duration(event):
"""Sets duration for event."""
if event["all_day"]:
event["duration"] = "celý den"
return event
delta = event["end"] - event["begin"]
if delta.days < 1:
begin = event["begin"].format("H:mm")
end = event["end"].format("H:mm")
event["duration"] = f"{begin} - {end}"
else:
begin = event["begin"].format("H:mm")
end = event["end"].format("H:mm (D.M.)")
event["duration"] = f"{begin} - {end}"
return event
def process_ical(source):
"""Parses iCalendar source and returns events as list of dicts. Returns
tuple of past and future events.
"""
events = parse_ical(source)
events = list(map(set_event_duration, events))
return split_events(events)
......@@ -12,6 +12,7 @@ snapshots['test_calendar__update_source 1'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 1, 23, 17, 0, tzinfo=tzutc())'),
'description': '',
'duration': '17:00 - 20:00',
'end': GenericRepr('FakeDatetime(2020, 1, 23, 20, 0, tzinfo=tzutc())'),
'location': 'The 27 Music Bar, Školní 24, 796 01 Prostějov, Česko',
'name': 'Veřejná schůze Prostějovských Pirátů'
......@@ -20,6 +21,7 @@ snapshots['test_calendar__update_source 1'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 1, 15, 18, 0, tzinfo=tzoffset(None, 3600))'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('FakeDatetime(2020, 1, 15, 21, 0, tzinfo=tzoffset(None, 3600))'),
'location': '',
'name': 'Veřejná schůze Pirátů MS Olomouc'
......@@ -28,6 +30,7 @@ snapshots['test_calendar__update_source 1'] = [
'all_day': True,
'begin': GenericRepr('FakeDatetime(2020, 1, 11, 0, 0, tzinfo=tzutc())'),
'description': '',
'duration': 'celý den',
'end': GenericRepr('FakeDatetime(2020, 1, 13, 0, 0, tzinfo=tzutc())'),
'location': '',
'name': 'CF Ostrava'
......@@ -36,6 +39,7 @@ snapshots['test_calendar__update_source 1'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 1, 7, 18, 0, tzinfo=tzoffset(None, 3600))'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('FakeDatetime(2020, 1, 7, 21, 0, tzinfo=tzoffset(None, 3600))'),
'location': 'PiCOlo, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Veřejná schůze Pirátů Olomouckého kraje'
......@@ -44,6 +48,7 @@ snapshots['test_calendar__update_source 1'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2019, 4, 10, 18, 0, tzinfo=tzoffset(None, 7200))'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('FakeDatetime(2019, 4, 10, 21, 0, tzinfo=tzoffset(None, 7200))'),
'location': 'Jeseník',
'name': 'Veřejná schůze Pirátů XOLK v Jeseníku'
......@@ -55,6 +60,7 @@ snapshots['test_calendar__update_source 2'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 4, 8, 16, 0, tzinfo=tzutc())'),
'description': '',
'duration': '16:00 - 17:30',
'end': GenericRepr('FakeDatetime(2020, 4, 8, 17, 30, tzinfo=tzutc())'),
'location': 'Picolo - Pirátské centrum Olomouc, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Setkání s místopředsedou Evropského parlamentu Marcelem Kolalokou'
......@@ -63,6 +69,7 @@ snapshots['test_calendar__update_source 2'] = [
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 3, 24, 17, 0, tzinfo=tzutc())'),
'description': 'Přednáší MUDr. Marti Moron',
'duration': '17:00 - 18:30',
'end': GenericRepr('FakeDatetime(2020, 3, 24, 18, 30, tzinfo=tzutc())'),
'location': '',
'name': 'Je kouření stejně škodlivé jako vapování?'
......@@ -73,6 +80,7 @@ snapshots['test_calendar__update_source 2'] = [
'description': '''Tato událost má videohovor.
Připojit se: https://meet.google.com/ahv-nrmw-kmp
+1 484-696-1205 PIN: 546973991#''',
'duration': '14:00 - 17:00',
'end': GenericRepr('FakeDatetime(2020, 3, 1, 17, 0, tzinfo=tzutc())'),
'location': 'Koliba & Pivovar U Tří králů, Finská 4592/8, 796 01 Prostějov,Česko',
'name': 'Veřejná schůze Prostějovských Pirátů 01.03.2020'
......@@ -81,6 +89,7 @@ Připojit se: https://meet.google.com/ahv-nrmw-kmp
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 2, 21, 17, 30, tzinfo=tzutc())'),
'description': 'Tradiční páteční deskohraní s Karlem Bezejmeným. K zapůjčení budou deskové hry všeho druhu – a to jak jednoduché, tak náročnější pro ty,kteří se nebojí u hry strávit delší dobu. Všechny pravidla Vám budou vysvětlena. Nemusíte se bát.',
'duration': '17:30 - 21:00',
'end': GenericRepr('FakeDatetime(2020, 2, 21, 21, 0, tzinfo=tzutc())'),
'location': 'Picolo - Pirátské centrum Olomouc, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Páteční deskovky v Picolu - Karel Bezejmenný'
......@@ -89,6 +98,7 @@ Připojit se: https://meet.google.com/ahv-nrmw-kmp
'all_day': False,
'begin': GenericRepr('FakeDatetime(2020, 2, 17, 17, 30, tzinfo=tzutc())'),
'description': '',
'duration': '17:30 - 20:30',
'end': GenericRepr('FakeDatetime(2020, 2, 17, 20, 30, tzinfo=tzutc())'),
'location': 'Nebe počká, 20, Kratochvílova 122, Město, 750 02 Přerov, Česko',
'name': 'Schůzka Piráti Přerov'
......
......@@ -179,3 +179,101 @@ Připojit se: https://meet.google.com/ahv-nrmw-kmp
'name': 'Schůzka Piráti Přerov'
}
]
snapshots['test_process_ical 1'] = [
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-01-23T17:00:00+00:00]>'),
'description': '',
'duration': '17:00 - 20:00',
'end': GenericRepr('<Arrow [2020-01-23T20:00:00+00:00]>'),
'location': 'The 27 Music Bar, Školní 24, 796 01 Prostějov, Česko',
'name': 'Veřejná schůze Prostějovských Pirátů'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-01-15T18:00:00+01:00]>'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('<Arrow [2020-01-15T21:00:00+01:00]>'),
'location': '',
'name': 'Veřejná schůze Pirátů MS Olomouc'
},
{
'all_day': True,
'begin': GenericRepr('<Arrow [2020-01-11T00:00:00+00:00]>'),
'description': '',
'duration': 'celý den',
'end': GenericRepr('<Arrow [2020-01-13T00:00:00+00:00]>'),
'location': '',
'name': 'CF Ostrava'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-01-07T18:00:00+01:00]>'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('<Arrow [2020-01-07T21:00:00+01:00]>'),
'location': 'PiCOlo, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Veřejná schůze Pirátů Olomouckého kraje'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2019-04-10T18:00:00+02:00]>'),
'description': '',
'duration': '18:00 - 21:00',
'end': GenericRepr('<Arrow [2019-04-10T21:00:00+02:00]>'),
'location': 'Jeseník',
'name': 'Veřejná schůze Pirátů XOLK v Jeseníku'
}
]
snapshots['test_process_ical 2'] = [
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-04-08T16:00:00+00:00]>'),
'description': '',
'duration': '16:00 - 17:30',
'end': GenericRepr('<Arrow [2020-04-08T17:30:00+00:00]>'),
'location': 'Picolo - Pirátské centrum Olomouc, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Setkání s místopředsedou Evropského parlamentu Marcelem Kolalokou'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-03-24T17:00:00+00:00]>'),
'description': 'Přednáší MUDr. Marti Moron',
'duration': '17:00 - 18:30',
'end': GenericRepr('<Arrow [2020-03-24T18:30:00+00:00]>'),
'location': '',
'name': 'Je kouření stejně škodlivé jako vapování?'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-03-01T14:00:00+00:00]>'),
'description': '''Tato událost má videohovor.
Připojit se: https://meet.google.com/ahv-nrmw-kmp
+1 484-696-1205 PIN: 546973991#''',
'duration': '14:00 - 17:00',
'end': GenericRepr('<Arrow [2020-03-01T17:00:00+00:00]>'),
'location': 'Koliba & Pivovar U Tří králů, Finská 4592/8, 796 01 Prostějov,Česko',
'name': 'Veřejná schůze Prostějovských Pirátů 01.03.2020'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-02-21T17:30:00+00:00]>'),
'description': 'Tradiční páteční deskohraní s Karlem Bezejmeným. K zapůjčení budou deskové hry všeho druhu – a to jak jednoduché, tak náročnější pro ty,kteří se nebojí u hry strávit delší dobu. Všechny pravidla Vám budou vysvětlena. Nemusíte se bát.',
'duration': '17:30 - 21:00',
'end': GenericRepr('<Arrow [2020-02-21T21:00:00+00:00]>'),
'location': 'Picolo - Pirátské centrum Olomouc, 8. května 522/5, 779 00 Olomouc, Česko',
'name': 'Páteční deskovky v Picolu - Karel Bezejmenný'
},
{
'all_day': False,
'begin': GenericRepr('<Arrow [2020-02-17T17:30:00+00:00]>'),
'description': '',
'duration': '17:30 - 20:30',
'end': GenericRepr('<Arrow [2020-02-17T20:30:00+00:00]>'),
'location': 'Nebe počká, 20, Kratochvílova 122, Město, 750 02 Přerov, Česko',
'name': 'Schůzka Piráti Přerov'
}
]
import arrow
import pytest
from calendar_utils.parser import parse_ical, split_events
from calendar_utils.parser import (
parse_ical,
process_ical,
set_event_duration,
split_events,
)
def test_parse_ical(sample, snapshot):
......@@ -13,3 +19,40 @@ def test_split_events(sample, snapshot):
past_events, future_events = split_events(parse_ical(sample))
snapshot.assert_match(past_events)
snapshot.assert_match(future_events)
def test_set_event_duration__all_day():
event = {
"begin": arrow.get("2020-02-03T00:00:00Z"),
"end": arrow.get("2020-02-04T00:00:00Z"),
"all_day": True,
}
out = set_event_duration(event)
assert out["duration"] == "celý den"
def test_set_event_duration__hours():
event = {
"begin": arrow.get("2020-02-03T08:00:00Z"),
"end": arrow.get("2020-02-03T12:25:00Z"),
"all_day": False,
}
out = set_event_duration(event)
assert out["duration"] == "8:00 - 12:25"
def test_set_event_duration__days():
event = {
"begin": arrow.get("2020-02-03T09:15:00Z"),
"end": arrow.get("2020-02-04T22:30:00Z"),
"all_day": False,
}
out = set_event_duration(event)
assert out["duration"] == "9:15 - 22:30 (4.2.)"
@pytest.mark.freeze_time("2020-02-02")
def test_process_ical(sample, snapshot):
past_events, future_events = process_ical(sample)
snapshot.assert_match(past_events)
snapshot.assert_match(future_events)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment