diff --git a/calendar_utils/migrations/0002_auto_20200523_0243.py b/calendar_utils/migrations/0002_auto_20200523_0243.py new file mode 100644 index 0000000000000000000000000000000000000000..adaa9a64bd2229daa8fcec337f7dd6efafc2cba0 --- /dev/null +++ b/calendar_utils/migrations/0002_auto_20200523_0243.py @@ -0,0 +1,16 @@ +# 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(), + ), + ] diff --git a/calendar_utils/models.py b/calendar_utils/models.py index 6caf230f352a2887a93dcea798ee7e170091770b..d1980877a420e01daf5b3cfdb97190b482940294 100644 --- a/calendar_utils/models.py +++ b/calendar_utils/models.py @@ -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): """ diff --git a/calendar_utils/parser.py b/calendar_utils/parser.py index fe2fff3e793af1e53ce45a6621e3455bf71af295..4693a4ef2f8536bc5ca5a7118a8a6548c70fabaf 100644 --- a/calendar_utils/parser.py +++ b/calendar_utils/parser.py @@ -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) diff --git a/tests/calendar_utils/snapshots/snap_test_models.py b/tests/calendar_utils/snapshots/snap_test_models.py index 2114165718b84c1fd6b2e99447221e1af00e12d6..a396ca4f2c0e6f75b798a0dc06bbd6ec298e9f5b 100644 --- a/tests/calendar_utils/snapshots/snap_test_models.py +++ b/tests/calendar_utils/snapshots/snap_test_models.py @@ -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' diff --git a/tests/calendar_utils/snapshots/snap_test_parser.py b/tests/calendar_utils/snapshots/snap_test_parser.py index e884ab98c4752c52a5d83bf398813cb8bfda51ac..0ba59b7d4e4f3a71454b863c7f2bea31e9f565f9 100644 --- a/tests/calendar_utils/snapshots/snap_test_parser.py +++ b/tests/calendar_utils/snapshots/snap_test_parser.py @@ -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' + } +] diff --git a/tests/calendar_utils/test_parser.py b/tests/calendar_utils/test_parser.py index 3ad885209f75d4cfad79bd43a7cdc13b94c4112c..f5ed99ed6f50bcb80fa8428ad4b3ab65d0ecd663 100644 --- a/tests/calendar_utils/test_parser.py +++ b/tests/calendar_utils/test_parser.py @@ -1,6 +1,12 @@ +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)