Files
agendula/scripts/make_import_fixture.py
T
Jean-Luc Makiolaandmakiolaj ac01993d41 Release 1.0.0 (#20)
First stable release. Merging this bumps versionName to 1.0.0 and triggers the release pipeline (F-Droid, Codeberg, Play).

**App**
- CalDAV sync built in, with Agendula's own task store; OpenTasks / tasks.org stay available and can be copied over in Settings → Storage
- repeating tasks, several reminders per task, lists managed in the app, iCalendar import/export, widget and Quick Settings tile
- a list can be kept out of the smart lists (#18) and gets its own notification channel (#17)
- duplicate a task with its subtasks (#16)
- HTML descriptions shown as plain text (#15)
- relative day words in reminder notifications (#14)
- asks for exact-alarm access instead of claiming USE_EXACT_ALARM, and re-arms reminders when that access changes

**Release plumbing**
- floret-kit bumped to v0.4.0; the old pin was never pushed, so a clean clone couldn't check out the submodule. 0.4.0 drops CrashConfig.issueTitle (crash issues are always filed in English)
- prebuilt .so files ship unstripped, so the build no longer depends on whether an NDK is installed; now checked by check_reproducible_release.sh
- official F-Droid recipe in docs/fdroid-official/, to submit to fdroiddata once v1.0.0 is tagged
- Google Play: fastlane uploads the AAB and every locale's What's New after the F-Droid release; a separate listing lane pushes text and graphics from the fastlane tree, which CI now checks against Play's limits
- store listing: title "Agendula: Tasks" in every locale, icon, feature graphic, screenshots and 1.0.0 changelogs in en-US, en-GB, de-DE and pt-BR

crash_report_issue_title is now unused but stays until Weblate removes the translated copies.

Closes #14, closes #15, closes #16, closes #17, closes #18

Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de>
Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/20
2026-09-24 16:36:49 +02:00

143 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Build the dmfs `tasks.db` fixture the one-shot import is tested against.
The provider is deleted, 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())