The own store expands recurrences itself, so lib-recur is a direct dependency now rather than something :provider drags in. Still pinned at 0.12.2 — 0.16.0 removed RecurrenceSet. scripts/make_import_fixture.py writes the tasks.db the one-shot import will be tested against: the provider's DATABASE_VERSION 23 schema, seeded with the cases the import has to get right (a task with no UID, a deleted row, a recurring series, an all-day task, a subtask, an alarm property, and a list under a real CalDAV account). The provider is being deleted, so a fixture is the only way to keep testing against the schema it wrote.
143 lines
6.4 KiB
Python
143 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Build the dmfs `tasks.db` fixture the one-shot import is tested against.
|
|
|
|
The provider is being deleted (docs/OWN-STORE.md phase 5), so the import cannot
|
|
keep a real v0.3.x database around by asking the provider to create one. This
|
|
writes the schema the provider's TaskDatabaseHelper produces at DATABASE_VERSION
|
|
23 — tables `Lists`, `Tasks`, `Properties` only, which are the three the import
|
|
reads — and seeds a spread that covers what the import has to get right.
|
|
|
|
Regenerate with: python3 scripts/make_import_fixture.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
|
|
OUT = os.path.join(
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
"app/src/androidTest/assets/tasks-v23.db",
|
|
)
|
|
|
|
DDL = [
|
|
"""CREATE TABLE Lists (
|
|
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
account_name TEXT, account_type TEXT, list_name TEXT, list_color INTEGER,
|
|
list_access_level INTEGER, visible INTEGER, sync_enabled INTEGER,
|
|
list_owner TEXT, _dirty INTEGER DEFAULT 0, _sync_id TEXT,
|
|
sync_version TEXT, sync1 TEXT, sync2 TEXT, sync3 TEXT, sync4 TEXT,
|
|
sync5 TEXT, sync6 TEXT, sync7 TEXT, sync8 TEXT)""",
|
|
"""CREATE TABLE Tasks (
|
|
_id INTEGER PRIMARY KEY AUTOINCREMENT, version INTEGER DEFAULT 0,
|
|
list_id INTEGER NOT NULL, title TEXT, location TEXT, geo TEXT,
|
|
description TEXT, url TEXT, organizer TEXT, priority INTEGER,
|
|
task_color INTEGER, class INTEGER, completed INTEGER,
|
|
completed_is_allday INTEGER, percent_complete INTEGER,
|
|
status INTEGER DEFAULT 0, is_new INTEGER, is_closed INTEGER,
|
|
dtstart INTEGER, created INTEGER, last_modified INTEGER,
|
|
is_allday INTEGER, tz TEXT, due INTEGER, duration TEXT, rdate TEXT,
|
|
exdate TEXT, rrule TEXT, parent_id INTEGER, sorting TEXT,
|
|
has_alarms INTEGER, has_properties INTEGER, pinned INTEGER,
|
|
original_instance_sync_id TEXT, original_instance_id INTEGER,
|
|
original_instance_time INTEGER, original_instance_allday INTEGER,
|
|
_dirty INTEGER DEFAULT 1, _deleted INTEGER DEFAULT 0, _sync_id TEXT,
|
|
_uid TEXT, sync_version TEXT, sync1 TEXT, sync2 TEXT, sync3 TEXT,
|
|
sync4 TEXT, sync5 TEXT, sync6 TEXT, sync7 TEXT, sync8 TEXT)""",
|
|
"""CREATE TABLE Properties (
|
|
property_id INTEGER PRIMARY KEY AUTOINCREMENT, task_id INTEGER,
|
|
mimetype INTEGER, prop_version INTEGER,
|
|
data0 TEXT, data1 TEXT, data2 TEXT, data3 TEXT, data4 TEXT, data5 TEXT,
|
|
data6 TEXT, data7 TEXT, data8 TEXT, data9 TEXT, data10 TEXT,
|
|
data11 TEXT, data12 TEXT, data13 TEXT, data14 TEXT, data15 TEXT,
|
|
prop_sync1 TEXT, prop_sync2 TEXT, prop_sync3 TEXT, prop_sync4 TEXT,
|
|
prop_sync5 TEXT, prop_sync6 TEXT, prop_sync7 TEXT, prop_sync8 TEXT)""",
|
|
]
|
|
|
|
LOCAL = ("Local", "org.dmfs.account.LOCAL")
|
|
CALDAV = ("me@example.org", "bitfire.at.davdroid")
|
|
ALARM_MIMETYPE = "vnd.android.cursor.item/alarm"
|
|
|
|
# 2026-01-15T09:00:00Z, and a day in millis.
|
|
T0 = 1_768_467_600_000
|
|
DAY = 86_400_000
|
|
|
|
|
|
def seed(db: sqlite3.Connection) -> None:
|
|
lists = db.executemany(
|
|
"INSERT INTO Lists (_id, account_name, account_type, list_name, list_color,"
|
|
" visible, sync_enabled, list_owner) VALUES (?,?,?,?,?,?,?,?)",
|
|
[
|
|
(1, *LOCAL, "Personal", 0xFF7A5C6B, 1, 1, None),
|
|
(2, *LOCAL, "Hidden list", 0xFF445566, 0, 1, None),
|
|
# An external account inside our own authority: only reachable if the
|
|
# user pointed DAVx5 at us. Imported as a local list, UID preserved.
|
|
(3, *CALDAV, "Work", 0xFF2244AA, 1, 1, "Me"),
|
|
],
|
|
)
|
|
del lists
|
|
|
|
def task(**kw):
|
|
cols = ", ".join(kw)
|
|
marks = ", ".join("?" * len(kw))
|
|
db.execute(f"INSERT INTO Tasks ({cols}) VALUES ({marks})", tuple(kw.values()))
|
|
|
|
task(_id=1, list_id=1, title="Buy milk", due=T0 + DAY, status=0,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000001", created=T0, last_modified=T0)
|
|
# No UID: the import mints one.
|
|
task(_id=2, list_id=1, title="Call the dentist", due=T0 + 2 * DAY, status=1,
|
|
percent_complete=40, created=T0, last_modified=T0)
|
|
task(_id=3, list_id=1, title="Gather receipts", parent_id=1, status=0,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000003", created=T0, last_modified=T0)
|
|
task(_id=4, list_id=1, title="Renew domain", status=2, percent_complete=100,
|
|
completed=T0 - DAY, is_closed=1,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000004", created=T0, last_modified=T0)
|
|
task(_id=5, list_id=1, title="Water the plants", dtstart=T0, due=T0 + 3600_000,
|
|
rrule="FREQ=WEEKLY;BYDAY=MO,TH", tz="Europe/Berlin",
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000005", created=T0, last_modified=T0)
|
|
task(_id=6, list_id=1, title="Team offsite", dtstart=T0 - T0 % DAY,
|
|
due=T0 - T0 % DAY + DAY, is_allday=1,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000006", created=T0, last_modified=T0)
|
|
# Deleted-but-unsynced: gone as far as the user is concerned, so not imported.
|
|
task(_id=7, list_id=1, title="Cancelled thing", _deleted=1,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000007", created=T0, last_modified=T0)
|
|
task(_id=8, list_id=2, title="Task in a hidden list", status=0,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000008", created=T0, last_modified=T0)
|
|
task(_id=9, list_id=3, title="Ship the release", due=T0 + 5 * DAY, status=0,
|
|
_uid="a1b2c3d4-0000-4000-8000-000000000009", created=T0, last_modified=T0,
|
|
_sync_id="https://dav.example.org/tasks/9.ics")
|
|
|
|
db.executemany(
|
|
"INSERT INTO Properties (property_id, task_id, mimetype, data0, data1, data2, data3)"
|
|
" VALUES (?,?,?,?,?,?,?)",
|
|
[
|
|
# data0 minutes before, data1 reference (1 = DUE), data3 alarm type.
|
|
(1, 1, ALARM_MIMETYPE, "30", "1", None, "1"),
|
|
(2, 9, ALARM_MIMETYPE, "1440", "1", "Ship it", "1"),
|
|
# A non-alarm property the import must skip.
|
|
(3, 1, "vnd.android.cursor.item/category", "Errands", None, None, None),
|
|
],
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
|
if os.path.exists(OUT):
|
|
os.remove(OUT)
|
|
db = sqlite3.connect(OUT)
|
|
try:
|
|
for statement in DDL:
|
|
db.execute(statement)
|
|
db.execute("PRAGMA user_version = 23")
|
|
seed(db)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
print(f"wrote {OUT}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|