From 1acb555f146b972041db8ae1ea2878bf325a51a6 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 1 Nov 2023 15:04:13 +0000 Subject: [PATCH] Add calendar view via FullCalendar library Closes: #40 --- agenda/__init__.py | 77 +++++++++++++++++++++++++++++++++----------- agenda/types.py | 26 +++++++++++++-- templates/index.html | 41 ++++++++++++++++++++++- 3 files changed, 122 insertions(+), 22 deletions(-) diff --git a/agenda/__init__.py b/agenda/__init__.py index 24fa7de..62bff02 100644 --- a/agenda/__init__.py +++ b/agenda/__init__.py @@ -340,12 +340,17 @@ def windmill_hill_market_days(start_date: date) -> list[Event]: """Windmill Hill Market days for the next 12 months from a given date.""" events: list[Event] = [] current_date = start_date + url = ( + "https://www.windmillhillcityfarm.org.uk" + + "/visit-us/shops-more/windmill-hill-market-bristol-market/" + ) # To keep count of how many market days have been calculated count = 0 tz = pytz.timezone("Europe/London") - t = time(10, 0) + start = time(10, 0) + end = time(15, 0) while count < 12: # Skip months outside of April to December @@ -363,7 +368,9 @@ def windmill_hill_market_days(start_date: date) -> list[Event]: Event( name="market", title="Windmill Hill Market", - date=tz.localize(datetime.combine(first_saturday, t)), + date=tz.localize(datetime.combine(first_saturday, start)), + end_date=tz.localize(datetime.combine(first_saturday, end)), + url=url, ) ) count += 1 @@ -381,8 +388,11 @@ def tobacco_factory_market_days(start_date: date) -> list[Event]: current_date = start_date count = 0 + url = "https://tobaccofactory.com/whats-on/sunday-market/" + tz = pytz.timezone("Europe/London") - t = time(10, 0) + start = time(10, 0) + end = time(14, 30) while count < 52: # 52 weeks in a year # Calculate the next Sunday from the current date @@ -394,7 +404,9 @@ def tobacco_factory_market_days(start_date: date) -> list[Event]: Event( name="market", title="Tobacco Factory Sunday Market", - date=tz.localize(datetime.combine(next_sunday, t)), + date=tz.localize(datetime.combine(next_sunday, start)), + end_date=tz.localize(datetime.combine(next_sunday, end)), + url=url, ) ) count += 1 @@ -454,14 +466,16 @@ def get_conferences(input_date: date, filepath: str) -> List[Event]: events = [] for conf in data.get("conferences", []): - event_date = conf["start"] + start_date = conf["start"] + end_date = conf["end"] # Skip the conference if it is before the input date. - if as_date(event_date) < input_date: + if as_date(end_date) < input_date: continue event = Event( name="conference", - date=event_date, + date=start_date, + end_date=end_date, title=f'{conf["name"]} ({conf["location"]})', url=conf.get("url"), ) @@ -516,21 +530,24 @@ def get_accommodation(from_date: date, filepath: str) -> list[Event]: ) from_date = item["from"] to_date = item["to"] - nights = (to_date.date() - from_date.date()).days - night_str = f"{nights} nights" if nights != 1 else "1 night" - checkin = Event( + # nights = (to_date.date() - from_date.date()).days + # night_str = f"{nights} nights" if nights != 1 else "1 night" + e = Event( date=from_date, + end_date=to_date, name="accommodation", - title=f"check-in {title} ({night_str})", + # title=f"check-in {title} ({night_str})", + title=title, url=item.get("url"), ) - checkout = Event( - date=to_date, - name="accommodation", - title="check-out " + title, - url=item.get("url"), - ) - events += [checkin, checkout] + events.append(e) + # checkout = Event( + # date=to_date, + # name="accommodation", + # title="check-out " + title, + # url=item.get("url"), + # ) + # events += [checkin, checkout] return events @@ -554,6 +571,7 @@ def get_travel( return [ Event( date=item["depart"], + end_date=item["arrive"], name="transport", title=title(item), url=item.get("url"), @@ -618,6 +636,27 @@ def sunset(observer: ephem.Observer) -> datetime: return typing.cast(datetime, observer.next_setting(ephem.Sun(observer)).datetime()) +def build_events_for_calendar(events: list[Event]) -> list[dict[str, typing.Any]]: + """Build list of events for FullCalendar.""" + items: list[dict[str, typing.Any]] = [] + + for e in events: + if e.has_time: + end = e.end_date or e.date + timedelta(hours=1) + else: + end = (e.end_as_date if e.end_date else e.as_date) + timedelta(days=1) + item = { + "allDay": not e.has_time, + "title": e.display_title, + "start": e.date.isoformat(), + "end": end.isoformat(), + } + if e.url: + item["url"] = e.url + items.append(item) + return items + + def get_data(now: datetime) -> typing.Mapping[str, str | object]: """Get data to display on agenda dashboard.""" rocket_dir = os.path.join(data_dir, "thespacedevs") @@ -684,4 +723,6 @@ def get_data(now: datetime) -> typing.Mapping[str, str | object]: reply["events"] = events + reply["fullcalendar_events"] = build_events_for_calendar(events) + return reply diff --git a/agenda/types.py b/agenda/types.py index a2890bb..e209fea 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -2,7 +2,6 @@ import dataclasses import datetime -from datetime import date, timezone @dataclasses.dataclass @@ -10,7 +9,8 @@ class Event: """Event.""" name: str - date: date | datetime.datetime + date: datetime.date | datetime.datetime + end_date: datetime.date | datetime.datetime | None = None title: str | None = None url: str | None = None @@ -22,9 +22,14 @@ class Event: return ( d if isinstance(d, datetime.datetime) - else datetime.datetime.combine(d, t0).replace(tzinfo=timezone.utc) + else datetime.datetime.combine(d, t0).replace(tzinfo=datetime.timezone.utc) ) + @property + def has_time(self) -> bool: + """Event has a time associated with it.""" + return isinstance(self.date, datetime.datetime) + @property def as_date(self) -> datetime.date: """Date of event.""" @@ -32,6 +37,16 @@ class Event: self.date.date() if isinstance(self.date, datetime.datetime) else self.date ) + @property + def end_as_date(self) -> datetime.date: + """Date of event.""" + assert self.end_date + return ( + self.end_date.date() + if isinstance(self.end_date, datetime.datetime) + else self.end_date + ) + @property def display_time(self) -> str | None: """Time for display on web page.""" @@ -69,3 +84,8 @@ class Event: return self.date.strftime("%a, %d, %b %Y %H:%M %z") else: return self.date.strftime("%a, %d, %b %Y") + + @property + def display_title(self) -> str: + """Name for display.""" + return self.name + ": " + self.title if self.title else self.name diff --git a/templates/index.html b/templates/index.html index c17bdfc..0fdaa17 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,6 +7,41 @@ Agenda + + + + {% set event_labels = { @@ -64,7 +99,11 @@

{{ market }}

{% endfor %} - +
+ +

Agenda

+ +
{% for event in events %}