~fabrixxm/confy

00b88b02ca719529be4807b5b272cf4a817503d2 — fabrixxm 3 months ago 3711232
[WIP] Timezone support

- store datetime as UTC in db
- handle all datetime in UTC tz
- add a "timezone" setting (always `tzlocal()` at this point, without UI
  to change it)
- show datetimes in setting timezone
7 files changed, 64 insertions(+), 29 deletions(-)

M src/models.py
M src/pages.py
M src/remotes/ics.py
M src/remotes/pentabarf.py
M src/settings.py
M src/widgets.py
M src/window.py
M src/models.py => src/models.py +15 -1
@@ 19,8 19,10 @@ import datetime
import time

from gi.repository import GObject
from dateutil.tz import UTC

from . import local
from .settings import Settings
from .fetcher import Fetcher

## models


@@ 337,6 339,18 @@ class Event(GObject.GObject):
        self.notified = notified
        self.emit('update')


    def start_in_tz(self):
        return self._dt_in_tz(self.start)

    def end_in_tz(self):
        return self._dt_in_tz(self.end)

    def _dt_in_tz(self, dt):
        dt = dt.replace(tzinfo=UTC)
        dt = dt.astimezone(Settings.instance().get_timezone())
        return dt

    def persons(self):
        #print("event", self.id, "persons()")
        for row in local.getDb().execute(


@@ 384,7 398,7 @@ class Event(GObject.GObject):
    def nextup(cls, minutes):
        """get events starting in next `minutes`"""
        minutes = min(int(minutes), 30)  # some sanity check
        now = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S.000")
        now = datetime.datetime.now(UTC).replace(tzinfo=None).strftime("%Y-%m-%dT%H:%M:%S.000")
        # now = "now" # is "now" somehow broken?
        query = """SELECT * FROM events
                    WHERE starred=1 AND notified=0

M src/pages.py => src/pages.py +10 -6
@@ 18,6 18,8 @@ import datetime
import re
import html

from dateutil.tz import UTC

from gi.repository import GLib
from gi.repository import Gtk
from gi.repository import GObject


@@ 34,6 36,7 @@ from .config import APP_NAME, APP_ID, APP_VERSION, FMT_DAY_LIST

from . import models


def _clean_markup(text):
    """remove unsupported markup from text to use in label"""
    text = text.replace("<p>", "").replace("</p>", "\n\n")


@@ 197,9 200,10 @@ class ConferencePage(BasePage):
        f = conf.get_logo_file(cbk=self._set_logo)
        box.pack_start(self.logoimage, False, False, 16)

        label = Gtk.Label(**LBL_PROPS)
        label.set_markup("<big><b>{}</b></big>".format(_clean_markup(meta.title)))
        box.pack_start(label, False, False, 16)
        if meta.title:
            label = Gtk.Label(**LBL_PROPS)
            label.set_markup("<big><b>{}</b></big>".format(_clean_markup(meta.title)))
            box.pack_start(label, False, False, 16)

        if meta.venue is not None:
            label = Gtk.Label(html.unescape(meta.venue), **LBL_PROPS)


@@ 418,8 422,8 @@ class EventDetailPage(BasePage):
        titlebox = Gtk.VBox()
        label = Gtk.Label(**LBL_PROPS)
        label.set_markup("<b>{}-{}</b>".format(
            obj.start.strftime("%a %H:%M"),
            obj.end.strftime("%H:%M")))
            obj.start_in_tz().strftime("%a %H:%M"),
            obj.end_in_tz().strftime("%H:%M")))
        titlebox.pack_start(label, False, False, 0)

        if obj.room:


@@ 500,7 504,7 @@ class EventDetailPage(BasePage):
        box.connect("draw", self._on_draw)

    def _on_draw(self, widget, cr):
        now = datetime.datetime.now()
        now = datetime.datetime.now(UTC).replace(tzinfo=None)
        obj = self.obj

        w = self.get_allocated_width()

M src/remotes/ics.py => src/remotes/ics.py +9 -15
@@ 18,27 18,21 @@
import time
import html
from datetime import datetime, timedelta, timezone
from dateutil.tz import UTC
from icalendar import Calendar, Event

from .exceptions import InvalidFormatException
from .. import local
from ..models import Meta

def utc_to_local(utc_dt):
    """remove timezone info from date object

    Calendar items comes with timezone info. We could keep this info and allow
    users to select wich timezone they want to see. Atm, all code works without
    timezones (and sqlite3 driver spit errors if we try to save a timezone-aware datetime).
    We assume that this thing will be used "on premise", so every date/time will
    be in conference location tz.
    You have to live with this until a better option come up.
    I hate timezones and datetime handling.
    """
    return utc_dt.replace(tzinfo=None)
def import_ics(content:str, url:str):
    """Import data from ICS

    all dates will be converted to UTC and stored in db without tzinfo,
    because sqlite3 driver spits errors otherwise.
    """

def import_ics(content:str, url:str):
    cal = Calendar.from_ical(content)

    _db = local.getDb()


@@ 55,13 49,13 @@ def import_ics(content:str, url:str):
            fulltextsearch = []
            eventid = c['UID']
            if c.get('DTSTART'):
                start = utc_to_local(c['DTSTART'].dt)
                start = c['DTSTART'].dt.astimezone(UTC).replace(tzinfo=None)
            else:
                start = utc_to_local(c['DTSTAMP'].dt)
                start = c['DTSTAMP'].dt.astimezone(UTC).replace(tzinfo=None)
            if c.get('DURATION'):
                end = start + c['DURATION'].dt
            elif c.get('DTEND'):
                end = utc_to_local(c['DTEND'].dt)
                end = c['DTEND'].dt.astimezone(UTC).replace(tzinfo=None)
            else:
                end = start + timedelta(hours=1)
            evtdate = start.date()

M src/remotes/pentabarf.py => src/remotes/pentabarf.py +19 -1
@@ 19,11 19,18 @@ import datetime
import time
import xml.etree.ElementTree as ET

from dateutil.tz import UTC, tzlocal

from .exceptions import InvalidFormatException
from .. import local
from ..models import Meta


def _local_to_utc_to_unset(dt):
    dt = dt.replace(tzinfo=tzlocal())
    dt = dt.astimezone(UTC)
    return dt.replace(tzinfo=None)

def _get_text(root, nodename):
    text = ""
    if root is not None:


@@ 32,7 39,16 @@ def _get_text(root, nodename):
            text = node.text
    return text




def import_pentabarf(xmlstr:str, url:str):
    """Import data from Pentabarf XML

    As far I can tell, Pentabarf XML doesn't declare timezone.
    We will import dates in local timezone, then convert to UTC, then
    remove timezone info to store dates in db (because sqlite3 cries otherwise)
    """
    root = ET.fromstring(xmlstr)
    if root.tag != "schedule":
        raise InvalidFormatException(_("Invalid pentabarf format"))


@@ 56,7 72,9 @@ def import_pentabarf(xmlstr:str, url:str):
        for eevent in eday.iter('event'):
            fulltextsearch = []
            eventid = eevent.attrib['id']
            start = datetime.datetime.strptime(date + " " + eevent.find('start').text, "%Y-%m-%d %H:%M")
            start = _local_to_utc_to_unset(
                datetime.datetime.strptime(date + " " + eevent.find('start').text, "%Y-%m-%d %H:%M")
            )
            end = eevent.find('duration').text.split(":")
            end = start + datetime.timedelta(hours=int(end[0]), minutes=int(end[1]))
            evtdate = start.date()

M src/settings.py => src/settings.py +4 -1
@@ 17,6 17,7 @@

from gi.repository import Gio
from gi.repository import Handy
from dateutil import tz

# in secs
CACHE_NOCACHE = 0


@@ 78,4 79,6 @@ class Settings(Gio.Settings):
    def get_event_cache(self):
        return CacheDuration.get_duration(self.get_int('event-cache'))


    def get_timezone(self):
        return tz.tzlocal()
        

M src/widgets.py => src/widgets.py +5 -3
@@ 18,6 18,8 @@ from os import path
import datetime
import html

from dateutil.tz import UTC

from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gio


@@ 83,8 85,8 @@ class EventActionRow(Handy.ActionRow):
        super().__init__(activatable=True, selectable=False)

        subtitle = "{}-{}".format(
            obj.start.strftime("%a %H:%M"),
            obj.end.strftime("%H:%M"))
            obj.start_in_tz().strftime("%a %H:%M"),
            obj.end_in_tz().strftime("%H:%M"))

        if obj.room:
            subtitle = "{} ({})".format(


@@ 120,7 122,7 @@ class EventActionRow(Handy.ActionRow):
        self.connect("draw", self._on_draw)

    def _on_draw(self, widget, cr):
        now = datetime.datetime.now()
        now = datetime.datetime.now(UTC).replace(tzinfo=None)
        obj = self.obj

        h = self.get_allocated_height()

M src/window.py => src/window.py +2 -2
@@ 616,11 616,11 @@ class MainView(Gtk.VBox):
            # fast and stupid clean html
            cleanr = re.compile('<.*?>|&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});')
            title = re.sub(cleanr, '', e.title)
            print("nextup:", e.start.strftime("%H:%M"), title, "@", e.room)
            print("nextup:", e.start_in_tz().strftime("%H:%M"), title, "@", e.room)
            self._send_desktop_notification(
                _("Next up: {}").format(title),
                _("at {} in {}").format(
                    e.start.strftime("%H:%M"),
                    e.start_in_tz().strftime("%H:%M"),
                    e.room
                ),
                f"nextup-{e.id}")