diff --git a/.isort.cfg b/.isort.cfg index 444fbf680ab7878dfe5970e09b36e8e9bb812360..1c35b69a72a98586a6b6d0ba165c5690f2d1ea5c 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -4,4 +4,4 @@ line_length = 88 multi_line_output = 3 default_sectiont = "THIRDPARTY" include_trailing_comma = true -known_third_party = django,environ,pirates,wagtail +known_third_party = arrow,django,environ,faker,ics,pirates,pytest,pytz,requests,snapshottest,wagtail diff --git a/README.md b/README.md index 9322a7fc317779fd6e5cd8b7f72169a086590b52..ec937158fe6c6d0a87e523e94c756b5102b72081 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,11 @@ jako přehled pluginů a rozšíření pro Wagtail. . ├── home = app na web úvodní stránky Majáku + ├── senat_campaign = app na weby kandidátů na senátory + ... ├── majak = Django projekt s konfigurací Majáku + ├── calendar_utils = app s modelem a utilitami na iCal kalendáře ├── search = app pro fulltext search (default, asi se k ničemu nepoužívá) - ├── senat_campaign = app na weby kandidátů na senátory └── users = app s custom user modelem a SSO, apod. Appky v sobě mají modely pro stránky a statické soubory a templaty. Momentálně se @@ -55,6 +57,14 @@ V produkci musí být navíc nastaveno: | `DJANGO_SECRET_KEY` | | tajný šifrovací klíč | | `DJANGO_ALLOWED_HOSTS` | | allowed hosts (více hodnot odděleno čárkami) | +### Management commands + +Přes CRON je třeba na pozadí spouštět Django `manage.py` commandy: + +* `clearsessions` - maže expirované sessions (denně až týdně) +* `publish_scheduled_pages` - publikuje naplánované stránky (každou hodinu) +* `update_callendars` - stáhne a aktualizuje kalendáře (několikrát denně) + ### Přidání nového webu Doména či subdoména se musí nakonfigurovat v: diff --git a/calendar_utils/__init__.py b/calendar_utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/calendar_utils/apps.py b/calendar_utils/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..bf508d1a3221c12fa8a38f245727aa2978772b09 --- /dev/null +++ b/calendar_utils/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CalendarUtilsConfig(AppConfig): + name = "calendar utils" diff --git a/calendar_utils/management/__init__.py b/calendar_utils/management/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/calendar_utils/management/commands/__init__.py b/calendar_utils/management/commands/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/calendar_utils/management/commands/update_callendars.py b/calendar_utils/management/commands/update_callendars.py new file mode 100644 index 0000000000000000000000000000000000000000..facfef933a50733951e15b4589ce3637e2d6ee14 --- /dev/null +++ b/calendar_utils/management/commands/update_callendars.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand + +from ...models import Calendar + + +class Command(BaseCommand): + def handle(self, *args, **options): + self.stdout.write("Updating calendars...") + for cal in Calendar.objects.all(): + self.stdout.write(f"+ {cal.url}") + cal.update_source() + self.stdout.write("Updating calendars finished!") diff --git a/calendar_utils/migrations/0001_initial.py b/calendar_utils/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..11e59387218badc4efddc33074c3410229886fee --- /dev/null +++ b/calendar_utils/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.0.6 on 2020-05-22 08:30 + +import django.core.serializers.json +from django.db import migrations, models + +import calendar_utils.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Calendar", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("url", models.URLField(unique=True)), + ("source", models.TextField(null=True)), + ("last_update", models.DateTimeField(null=True)), + ( + "past_events", + calendar_utils.models.EventsJSONField( + encoder=django.core.serializers.json.DjangoJSONEncoder, + null=True, + ), + ), + ( + "future_events", + calendar_utils.models.EventsJSONField( + encoder=django.core.serializers.json.DjangoJSONEncoder, + null=True, + ), + ), + ], + ), + ] diff --git a/calendar_utils/migrations/__init__.py b/calendar_utils/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/calendar_utils/models.py b/calendar_utils/models.py new file mode 100644 index 0000000000000000000000000000000000000000..1b3f4abf15dffcb62388505cc32393407cba7142 --- /dev/null +++ b/calendar_utils/models.py @@ -0,0 +1,45 @@ +import arrow +import requests +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 + + +def _convert_arrow_to_datetime(event): + event["begin"] = event["begin"].datetime + event["end"] = event["end"].datetime + return event + + +class EventsJSONField(JSONField): + """ + JSONField for lists of events which converts `begin` and `end` to datetime + on load from DB. + """ + + def from_db_value(self, value, expression, connection): + if value: + for event in value: + event["begin"] = arrow.get(event["begin"]).datetime + event["end"] = arrow.get(event["end"]).datetime + return value + + +class Calendar(models.Model): + url = models.URLField(unique=True) + source = models.TextField(null=True) + last_update = models.DateTimeField(null=True) + past_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True) + future_events = EventsJSONField(encoder=DjangoJSONEncoder, null=True) + + def update_source(self): + source = requests.get(self.url).text + if self.source != source: + self.source = source + past, future = split_events(parse_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() diff --git a/calendar_utils/parser.py b/calendar_utils/parser.py new file mode 100644 index 0000000000000000000000000000000000000000..fe2fff3e793af1e53ce45a6621e3455bf71af295 --- /dev/null +++ b/calendar_utils/parser.py @@ -0,0 +1,23 @@ +from operator import attrgetter + +import arrow +from ics import Calendar + +EVENT_KEYS = ("begin", "end", "all_day", "name", "description", "location") + + +def parse_ical(source): + """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): + events.append({key: getattr(event, key) for key in EVENT_KEYS}) + return events + + +def split_events(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 diff --git a/majak/settings/base.py b/majak/settings/base.py index 34391bdf3cb980f0511fcd1bdb7972b31255b912..4b8f1fec921b948ff825b657a76e93f48ffd26e4 100644 --- a/majak/settings/base.py +++ b/majak/settings/base.py @@ -33,6 +33,7 @@ DATABASES["default"]["ATOMIC_REQUESTS"] = True INSTALLED_APPS = [ "senat_campaign", "home", + "calendar_utils", "users", "pirates", "search", diff --git a/pytest.ini b/pytest.ini index c9a5798015bb1749a83088367188779e861ab9ab..d56649df501e8f881900af8bf5833bbbcc64ffa7 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] -addopts = --ds=majak.settings.dev +addopts = --ds=majak.settings.dev -p no:warnings --nomigrations python_files = test_*.py diff --git a/requirements/base.in b/requirements/base.in index 5bb5818a2dc0f294e38198ee065fa950d862e36c..902b800aeb86fa4941dca0fb705509aa2914b3b4 100644 --- a/requirements/base.in +++ b/requirements/base.in @@ -6,3 +6,6 @@ psycopg2-binary pirates<=0.4 whitenoise opencv-python +requests +ics +arrow diff --git a/requirements/base.txt b/requirements/base.txt index b0220791641e1e4a29c8df30656c0c39310a827c..377af2d3d0c9f4aec520ea8e602744e9a98f84b6 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -4,6 +4,7 @@ # # pip-compile base.in # +arrow==0.14.7 # via -r base.in, ics asgiref==3.2.7 # via django beautifulsoup4==4.8.2 # via wagtail certifi==2020.4.5.1 # via requests @@ -20,6 +21,7 @@ django==3.0.6 # via django-taggit, django-treebeard, djangorestframe djangorestframework==3.11.0 # via wagtail draftjs-exporter==2.1.7 # via wagtail html5lib==1.0.1 # via wagtail +ics==0.7 # via -r base.in idna==2.9 # via requests josepy==1.3.0 # via mozilla-django-oidc l18n==2018.5 # via wagtail @@ -33,12 +35,14 @@ pyasn1-modules==0.2.8 # via python-ldap pyasn1==0.4.8 # via pyasn1-modules, python-ldap pycparser==2.20 # via cffi pyopenssl==19.1.0 # via josepy +python-dateutil==2.8.1 # via arrow, ics python-ldap==3.2.0 # via pirates pytz==2020.1 # via django, django-modelcluster, l18n -requests==2.23.0 # via mozilla-django-oidc, wagtail -six==1.14.0 # via cryptography, django-extensions, html5lib, josepy, l18n, mozilla-django-oidc, pyopenssl +requests==2.23.0 # via -r base.in, mozilla-django-oidc, wagtail +six==1.14.0 # via cryptography, django-extensions, html5lib, ics, josepy, l18n, mozilla-django-oidc, pyopenssl, python-dateutil soupsieve==2.0 # via beautifulsoup4 sqlparse==0.3.1 # via django +tatsu==5.5.0 # via ics unidecode==1.1.1 # via wagtail urllib3==1.25.9 # via requests wagtail==2.9 # via -r base.in diff --git a/requirements/dev.in b/requirements/dev.in index bcccdaa4bd9ff4f4411120ff969a1c5ac9769a7d..0ffbf3d4fd22f9bc592e39fe912feaac6d2f19db 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -4,4 +4,5 @@ pytest-factoryboy pytest-cov pytest-django pytest-freezegun +pytest-mock snapshottest diff --git a/requirements/dev.txt b/requirements/dev.txt index 1bc61d2562e2ed08efea1ccd3683ab98f5983ae2..b20c32aaa83179e999228beafd5c120b1b2bab46 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -20,8 +20,9 @@ pytest-cov==2.8.1 # via -r dev.in pytest-django==3.9.0 # via -r dev.in pytest-factoryboy==2.0.3 # via -r dev.in pytest-freezegun==0.4.1 # via -r dev.in +pytest-mock==3.1.0 # via -r dev.in pytest-sugar==0.9.3 # via -r dev.in -pytest==5.4.2 # via -r dev.in, pytest-cov, pytest-django, pytest-factoryboy, pytest-freezegun, pytest-sugar +pytest==5.4.2 # via -r dev.in, pytest-cov, pytest-django, pytest-factoryboy, pytest-freezegun, pytest-mock, pytest-sugar python-dateutil==2.8.1 # via faker, freezegun six==1.14.0 # via freezegun, packaging, python-dateutil, snapshottest snapshottest==0.5.1 # via -r dev.in diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/calendar_utils/__init__.py b/tests/calendar_utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/calendar_utils/conftest.py b/tests/calendar_utils/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..736f38060ae6167881bc105562e4a609d52dc5e8 --- /dev/null +++ b/tests/calendar_utils/conftest.py @@ -0,0 +1,8 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def sample(): + return (Path(__file__).parent / "sample.ics").read_text() diff --git a/tests/calendar_utils/sample.ics b/tests/calendar_utils/sample.ics new file mode 100644 index 0000000000000000000000000000000000000000..895c4856917c5b6b69bc73c14da82de3aab37999 --- /dev/null +++ b/tests/calendar_utils/sample.ics @@ -0,0 +1,215 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Piráti XOLK +X-WR-TIMEZONE:Europe/Prague +X-WR-CALDESC:Kalendář Pirátů olomouckého krajského sdružení.\nVíce zde: htt + ps://olomoucky.pirati.cz\nnebo na našem facebooku: https://www.facebook.com + /piratiOlomoucko/ +BEGIN:VTIMEZONE +TZID:Europe/Prague +X-LIC-LOCATION:Europe/Prague +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Prague:20200107T180000 +DTEND;TZID=Europe/Prague:20200107T210000 +RRULE:FREQ=MONTHLY;BYDAY=1TU +DTSTAMP:20200519T130457Z +UID:nmrttnjrbd0bjsmj4oabog7g46@google.com +CREATED:20180926T191919Z +DESCRIPTION: +LAST-MODIFIED:20200505T161640Z +LOCATION:PiCOlo\, 8. května 522/5\, 779 00 Olomouc\, Česko +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:Veřejná schůze Pirátů Olomouckého kraje +TRANSP:OPAQUE +X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-APPLE-MAPKIT-HANDLE=CAESswEaEgkf60w + rNMxIQBEtRW0jpz8xQCJaChHEjGVza8OhIHJlcHVibGlrYRICQ1oaCk9sb21vdWNrw70qB09sb2 + 1vdWMyB09sb21vdWM6Bjc3OSAwMFIKOC4ga3bEm3RuYVoBOGIMOC4ga3bEm3RuYSA4Kgw4LiBrd + sSbdG5hIDgyDDguIGt2xJt0bmEgODIONzc5IDAwIE9sb21vdWMyEcSMZXNrw6EgcmVwdWJsaWth + ODlAAQ==;X-APPLE-RADIUS=70.58737122558885;X-APPLE-REFERENCEFRAME=1;X-TITLE= + "PiCOlo, 8. května 522/5, 779 00 Olomouc, Česko":geo:49.595342,17.248644 +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR;ACKNOWLEDGED=20200505T161640Z:AUTOMATIC +BEGIN:VALARM +ACTION:NONE +TRIGGER;VALUE=DATE-TIME:19760401T005545Z +X-WR-ALARMUID:298355D7-4042-4B14-9109-5936F652F4AA +UID:298355D7-4042-4B14-9109-5936F652F4AA +ACKNOWLEDGED:20200505T161640Z +X-APPLE-DEFAULT-ALARM:TRUE +END:VALARM +END:VEVENT +BEGIN:VEVENT +DTSTART:20200408T160000Z +DTEND:20200408T173000Z +DTSTAMP:20200519T130457Z +UID:6e2plsoro6fu52ufhd7iq5fpd1@google.com +CREATED:20200308T175153Z +DESCRIPTION: +LAST-MODIFIED:20200308T175206Z +LOCATION:Picolo - Pirátské centrum Olomouc\, 8. května 522/5\, 779 00 Olomo + uc\, Česko +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Setkání s místopředsedou Evropského parlamentu Marcelem Kolalokou +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20200301T140000Z +DTEND:20200301T170000Z +DTSTAMP:20200519T130457Z +UID:2e3hpgkhl60mmne8012hghfnfp@google.com +CREATED:20200226T180446Z +DESCRIPTION:Tato událost má videohovor.\nPřipojit se: https://meet.google.c + om/ahv-nrmw-kmp\n+1 484-696-1205 PIN: 546973991# +LAST-MODIFIED:20200226T192157Z +LOCATION:Koliba & Pivovar U Tří králů\, Finská 4592/8\, 796 01 Prostějov\, + Česko +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Veřejná schůze Prostějovských Pirátů 01.03.2020 +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20200221T173000Z +DTEND:20200221T210000Z +DTSTAMP:20200519T130457Z +UID:3o7vh5n89qlt79qorsfsbsmnjm@google.com +CREATED:20200120T165424Z +DESCRIPTION:Tradiční páteční deskohraní s Karlem Bezejmeným. K zapůjčení bud + ou 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ět + lena. Nemusíte se bát. +LAST-MODIFIED:20200219T172956Z +LOCATION:Picolo - Pirátské centrum Olomouc\, 8. května 522/5\, 779 00 Olomo + uc\, Česko +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Páteční deskovky v Picolu - Karel Bezejmenný +TRANSP:OPAQUE +X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-APPLE-MAPKIT-HANDLE=CAES6wEI2TIaEgk + czMsGPsxIQBF9BWnGoj8xQCJiChHEjGVza8OhIHJlcHVibGlrYRICQ1oaCk9sb21vdWNrw70qB0 + 9sb21vdWMyB09sb21vdWM6Bjc3OSAwMFIKOC4ga3bEm3RuYVoFNTIyLzViEDguIGt2xJt0bmEgN + TIyLzUqEDguIGt2xJt0bmEgNTIyLzUyEDguIGt2xJt0bmEgNTIyLzUyDjc3OSAwMCBPbG9tb3Vj + MhHEjGVza8OhIHJlcHVibGlrYTg5QABaIwohEhIJHMzLBj7MSEARfQVpxqI/MUAY2TIgh6v++tO + opP88;X-APPLE-RADIUS=70.58737122607558;X-APPLE-REFERENCEFRAME=1;X-TITLE="Pi + colo - Pirátské centrum Olomouc, 8. května 522/5, 779 00 Olomouc, Česko":ge + o:49.595643,17.248577 +END:VEVENT +BEGIN:VEVENT +DTSTART:20200324T170000Z +DTEND:20200324T183000Z +DTSTAMP:20200519T130457Z +UID:0lgp3r3r3s6a8fu0ubedd0lfov@google.com +CREATED:20200210T181941Z +DESCRIPTION:Přednáší MUDr. Marti Moron +LAST-MODIFIED:20200210T181941Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Je kouření stejně škodlivé jako vapování? +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20200217T173000Z +DTEND:20200217T203000Z +DTSTAMP:20200519T130457Z +UID:ccr66c9p6kp6ab9k71j32b9k69im6b9p69hj8b9mchim2dpn6cp3idhl6o@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;X-NUM-GUE + STS=0:mailto:0014epo7k8kbgpgq3gaudeodnc@group.calendar.google.com +CREATED:20200118T124251Z +DESCRIPTION: +LAST-MODIFIED:20200203T194315Z +LOCATION:Nebe počká\, 20\, Kratochvílova 122\, Město\, 750 02 Přerov\, Česk + o +SEQUENCE:2 +STATUS:CONFIRMED +SUMMARY:Schůzka Piráti Přerov +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART:20200123T170000Z +DTEND:20200123T200000Z +DTSTAMP:20200519T130457Z +UID:28at6d7pbb6u7un4c8dhk54n3m@google.com +CREATED:20200120T201202Z +DESCRIPTION: +LAST-MODIFIED:20200120T201202Z +LOCATION:The 27 Music Bar\, Školní 24\, 796 01 Prostějov\, Česko +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Veřejná schůze Prostějovských Pirátů +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20200111 +DTEND;VALUE=DATE:20200113 +DTSTAMP:20200519T130457Z +UID:c9i3gohj70rj8b9k65j68b9k70q3ibb16sqm4b9m6thm8dj4cpijcdhj6s@google.com +CREATED:20191117T100010Z +DESCRIPTION: +LAST-MODIFIED:20200110T192704Z +LOCATION: +SEQUENCE:1 +STATUS:TENTATIVE +SUMMARY:CF Ostrava +TRANSP:OPAQUE +X-APPLE-TRAVEL-ADVISORY-BEHAVIOR:AUTOMATIC +BEGIN:VALARM +ACTION:AUDIO +TRIGGER:-PT15H +X-WR-ALARMUID:F3784E51-6388-41CD-B559-202442F2AE8D +UID:F3784E51-6388-41CD-B559-202442F2AE8D +ATTACH;VALUE=URI:Basso +X-APPLE-DEFAULT-ALARM:TRUE +ACKNOWLEDGED:20200110T192657Z +END:VALARM +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Prague:20200115T180000 +DTEND;TZID=Europe/Prague:20200115T210000 +RRULE:FREQ=MONTHLY;BYDAY=3WE +DTSTAMP:20200519T130457Z +UID:mt8j5rq9jsh2p1ped9belnb234@google.com +CREATED:20190906T082036Z +DESCRIPTION: +LAST-MODIFIED:20200109T171048Z +LOCATION: +SEQUENCE:3 +STATUS:CONFIRMED +SUMMARY:Veřejná schůze Pirátů MS Olomouc +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Prague:20190410T180000 +DTEND;TZID=Europe/Prague:20190410T210000 +DTSTAMP:20200519T130457Z +UID:kfj8altfbk38ils1cs1247kv6c@google.com +RECURRENCE-ID;TZID=Europe/Prague:20190410T180000 +CREATED:20180926T191919Z +DESCRIPTION: +LAST-MODIFIED:20191213T161303Z +LOCATION:Jeseník +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Veřejná schůze Pirátů XOLK v Jeseníku +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR diff --git a/tests/calendar_utils/snapshots/__init__.py b/tests/calendar_utils/snapshots/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/calendar_utils/snapshots/snap_test_models.py b/tests/calendar_utils/snapshots/snap_test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..2114165718b84c1fd6b2e99447221e1af00e12d6 --- /dev/null +++ b/tests/calendar_utils/snapshots/snap_test_models.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import GenericRepr, Snapshot + + +snapshots = Snapshot() + +snapshots['test_calendar__update_source 1'] = [ + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 1, 23, 17, 0, tzinfo=tzutc())'), + 'description': '', + '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ů' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 1, 15, 18, 0, tzinfo=tzoffset(None, 3600))'), + 'description': '', + 'end': GenericRepr('FakeDatetime(2020, 1, 15, 21, 0, tzinfo=tzoffset(None, 3600))'), + 'location': '', + 'name': 'Veřejná schůze Pirátů MS Olomouc' + }, + { + 'all_day': True, + 'begin': GenericRepr('FakeDatetime(2020, 1, 11, 0, 0, tzinfo=tzutc())'), + 'description': '', + 'end': GenericRepr('FakeDatetime(2020, 1, 13, 0, 0, tzinfo=tzutc())'), + 'location': '', + 'name': 'CF Ostrava' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 1, 7, 18, 0, tzinfo=tzoffset(None, 3600))'), + 'description': '', + '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' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2019, 4, 10, 18, 0, tzinfo=tzoffset(None, 7200))'), + 'description': '', + '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' + } +] + +snapshots['test_calendar__update_source 2'] = [ + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 4, 8, 16, 0, tzinfo=tzutc())'), + 'description': '', + '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' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 3, 24, 17, 0, tzinfo=tzutc())'), + 'description': 'Přednáší MUDr. Marti Moron', + 'end': GenericRepr('FakeDatetime(2020, 3, 24, 18, 30, tzinfo=tzutc())'), + 'location': '', + 'name': 'Je kouření stejně škodlivé jako vapování?' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 3, 1, 14, 0, tzinfo=tzutc())'), + 'description': '''Tato událost má videohovor. +Připojit se: https://meet.google.com/ahv-nrmw-kmp ++1 484-696-1205 PIN: 546973991#''', + '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' + }, + { + '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.', + '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ý' + }, + { + 'all_day': False, + 'begin': GenericRepr('FakeDatetime(2020, 2, 17, 17, 30, tzinfo=tzutc())'), + 'description': '', + '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 new file mode 100644 index 0000000000000000000000000000000000000000..e884ab98c4752c52a5d83bf398813cb8bfda51ac --- /dev/null +++ b/tests/calendar_utils/snapshots/snap_test_parser.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# snapshottest: v1 - https://goo.gl/zC4yUc +from __future__ import unicode_literals + +from snapshottest import GenericRepr, Snapshot + + +snapshots = Snapshot() + +snapshots['test_parse_ical 1'] = [ + { + 'all_day': False, + 'begin': GenericRepr('<Arrow [2020-04-08T16:00:00+00:00]>'), + 'description': '', + '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', + '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#''', + '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.', + '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': '', + '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' + }, + { + 'all_day': False, + 'begin': GenericRepr('<Arrow [2020-01-23T17:00:00+00:00]>'), + 'description': '', + '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': '', + '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': '', + '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': '', + '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': '', + '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_split_events 1'] = [ + { + 'all_day': False, + 'begin': GenericRepr('<Arrow [2020-01-23T17:00:00+00:00]>'), + 'description': '', + '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': '', + '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': '', + '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': '', + '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': '', + '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_split_events 2'] = [ + { + 'all_day': False, + 'begin': GenericRepr('<Arrow [2020-04-08T16:00:00+00:00]>'), + 'description': '', + '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', + '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#''', + '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.', + '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': '', + '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_models.py b/tests/calendar_utils/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..b81e21484bcc58e40b00f2601fd09b5137adc378 --- /dev/null +++ b/tests/calendar_utils/test_models.py @@ -0,0 +1,81 @@ +import arrow +import pytest +import pytz +from faker import Faker + +from calendar_utils.models import Calendar + +pytestmark = pytest.mark.django_db + +fake = Faker() + + +@pytest.mark.freeze_time("2020-02-02") +def test_calendar__update_source(sample, mocker, snapshot): + m_get = mocker.patch( + "calendar_utils.models.requests.get", return_value=mocker.Mock(text=sample) + ) + url = "http://foo" + + cal = Calendar.objects.create(url=url) + cal.update_source() + + m_get.assert_called_once_with(url) + cal.refresh_from_db() + assert cal.source == sample + assert cal.last_update == arrow.get("2020-02-02").datetime + snapshot.assert_match(cal.past_events) + snapshot.assert_match(cal.future_events) + + +@pytest.mark.freeze_time("2020-03-30") +def test_calendar__update_source__unchanged_source(sample, mocker): + m_get = mocker.patch( + "calendar_utils.models.requests.get", return_value=mocker.Mock(text=sample) + ) + url = "http://foo" + + cal = Calendar.objects.create(url=url, source=sample) + cal.update_source() + + m_get.assert_called_once_with(url) + cal.refresh_from_db() + assert cal.source == sample + assert cal.last_update == arrow.get("2020-03-30").datetime + assert cal.past_events is None + assert cal.future_events is None + + +def test_calendar__save_and_load_events(): + past_events = [ + { + "begin": fake.date_time(tzinfo=pytz.UTC), + "end": fake.date_time(tzinfo=pytz.UTC), + }, + { + "begin": fake.date_time(tzinfo=pytz.UTC), + "end": fake.date_time(tzinfo=pytz.UTC), + }, + ] + future_events = [ + { + "begin": fake.date_time(tzinfo=pytz.UTC), + "end": fake.date_time(tzinfo=pytz.UTC), + }, + ] + + cal = Calendar.objects.create( + url=fake.url(), past_events=past_events, future_events=future_events + ) + + cal.refresh_from_db() + assert cal.past_events == past_events + assert cal.future_events == future_events + + +def test_calendar__save_and_load_events__no_values(): + cal = Calendar.objects.create(url=fake.url()) + + cal.refresh_from_db() + assert cal.past_events is None + assert cal.future_events is None diff --git a/tests/calendar_utils/test_parser.py b/tests/calendar_utils/test_parser.py new file mode 100644 index 0000000000000000000000000000000000000000..3ad885209f75d4cfad79bd43a7cdc13b94c4112c --- /dev/null +++ b/tests/calendar_utils/test_parser.py @@ -0,0 +1,15 @@ +import pytest + +from calendar_utils.parser import parse_ical, split_events + + +def test_parse_ical(sample, snapshot): + events = parse_ical(sample) + snapshot.assert_match(events) + + +@pytest.mark.freeze_time("2020-02-02") +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)