bouquin 0.3.2__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bouquin-0.3.2 → bouquin-0.4.1}/PKG-INFO +18 -12
- {bouquin-0.3.2 → bouquin-0.4.1}/README.md +16 -9
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/bug_report_dialog.py +3 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/db.py +258 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/locales/en.json +84 -4
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/main_window.py +214 -24
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/markdown_editor.py +202 -47
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/markdown_highlighter.py +23 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/settings.py +1 -1
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/statistics_dialog.py +1 -1
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/tag_browser.py +5 -5
- bouquin-0.4.1/bouquin/time_log.py +1217 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/toolbar.py +11 -9
- {bouquin-0.3.2 → bouquin-0.4.1}/pyproject.toml +2 -2
- bouquin-0.3.2/bouquin/screenshot.py +0 -164
- {bouquin-0.3.2 → bouquin-0.4.1}/LICENSE +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/__init__.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/__main__.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/find_bar.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/flow_layout.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/history_dialog.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/key_prompt.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/locales/fr.json +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/locales/it.json +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/lock_overlay.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/main.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/save_dialog.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/search.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/settings_dialog.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/strings.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/tags_widget.py +0 -0
- {bouquin-0.3.2 → bouquin-0.4.1}/bouquin/theme.py +0 -0
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bouquin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher.
|
|
5
5
|
Home-page: https://git.mig5.net/mig5/bouquin
|
|
6
6
|
License: GPL-3.0-or-later
|
|
7
7
|
Author: Miguel Jacq
|
|
8
8
|
Author-email: mig@mig5.net
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.10,<3.14
|
|
10
10
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -24,7 +23,11 @@ Description-Content-Type: text/markdown
|
|
|
24
23
|
|
|
25
24
|
## Introduction
|
|
26
25
|
|
|
27
|
-
Bouquin ("Book-ahn") is a
|
|
26
|
+
Bouquin ("Book-ahn") is a notebook and planner application written in Python, PyQt and SQLCipher.
|
|
27
|
+
|
|
28
|
+
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
|
|
29
|
+
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
|
30
|
+
also how long we spent on them.
|
|
28
31
|
|
|
29
32
|
It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a drop-in replacement
|
|
30
33
|
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
|
|
@@ -37,31 +40,34 @@ report from within the app.
|
|
|
37
40
|
|
|
38
41
|
## Screenshots
|
|
39
42
|
|
|
43
|
+
### General view
|
|
40
44
|

|
|
45
|
+
|
|
46
|
+
### History panes
|
|
41
47
|

|
|
42
48
|

|
|
43
49
|
|
|
44
|
-
##
|
|
50
|
+
## Some of the features
|
|
45
51
|
|
|
46
52
|
* Data is encrypted at rest
|
|
47
53
|
* Encryption key is prompted for and never stored, unless user chooses to via Settings
|
|
48
|
-
* Every 'page' is linked to the calendar day
|
|
49
54
|
* All changes are version controlled, with ability to view/diff versions and revert
|
|
50
|
-
*
|
|
55
|
+
* Automatic rendering of basic Markdown syntax
|
|
51
56
|
* Tabs are supported - right-click on a date from the calendar to open it in a new tab.
|
|
52
|
-
* Images are supported
|
|
53
|
-
* Search all pages, or find text on page
|
|
54
|
-
* Add
|
|
57
|
+
* Images are supported
|
|
58
|
+
* Search all pages, or find text on current page
|
|
59
|
+
* Add and manage tags
|
|
55
60
|
* Automatic periodic saving (or explicitly save)
|
|
56
|
-
* Transparent integrity checking of the database when it opens
|
|
57
61
|
* Automatic locking of the app after a period of inactivity (default 15 min)
|
|
58
62
|
* Rekey the database (change the password)
|
|
59
63
|
* Export the database to json, html, csv, markdown or .sql (for sqlite3)
|
|
60
64
|
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
|
61
|
-
* Dark and light
|
|
65
|
+
* Dark and light theme support
|
|
62
66
|
* Automatically generate checkboxes when typing 'TODO'
|
|
63
67
|
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
|
|
64
68
|
* English, French and Italian locales provided
|
|
69
|
+
* Ability to set reminder alarms in the app against the current line of text on today's date
|
|
70
|
+
* Ability to log time per day and run timesheet reports
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
## How to install
|
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
## Introduction
|
|
5
5
|
|
|
6
|
-
Bouquin ("Book-ahn") is a
|
|
6
|
+
Bouquin ("Book-ahn") is a notebook and planner application written in Python, PyQt and SQLCipher.
|
|
7
|
+
|
|
8
|
+
It is designed to treat each day as its own 'page', complete with Markdown rendering, tagging,
|
|
9
|
+
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
|
10
|
+
also how long we spent on them.
|
|
7
11
|
|
|
8
12
|
It uses [SQLCipher bindings](https://pypi.org/project/sqlcipher3-wheels) as a drop-in replacement
|
|
9
13
|
for SQLite3. This means that the underlying database for the notebook is encrypted at rest.
|
|
@@ -16,31 +20,34 @@ report from within the app.
|
|
|
16
20
|
|
|
17
21
|
## Screenshots
|
|
18
22
|
|
|
23
|
+
### General view
|
|
19
24
|

|
|
25
|
+
|
|
26
|
+
### History panes
|
|
20
27
|

|
|
21
28
|

|
|
22
29
|
|
|
23
|
-
##
|
|
30
|
+
## Some of the features
|
|
24
31
|
|
|
25
32
|
* Data is encrypted at rest
|
|
26
33
|
* Encryption key is prompted for and never stored, unless user chooses to via Settings
|
|
27
|
-
* Every 'page' is linked to the calendar day
|
|
28
34
|
* All changes are version controlled, with ability to view/diff versions and revert
|
|
29
|
-
*
|
|
35
|
+
* Automatic rendering of basic Markdown syntax
|
|
30
36
|
* Tabs are supported - right-click on a date from the calendar to open it in a new tab.
|
|
31
|
-
* Images are supported
|
|
32
|
-
* Search all pages, or find text on page
|
|
33
|
-
* Add
|
|
37
|
+
* Images are supported
|
|
38
|
+
* Search all pages, or find text on current page
|
|
39
|
+
* Add and manage tags
|
|
34
40
|
* Automatic periodic saving (or explicitly save)
|
|
35
|
-
* Transparent integrity checking of the database when it opens
|
|
36
41
|
* Automatic locking of the app after a period of inactivity (default 15 min)
|
|
37
42
|
* Rekey the database (change the password)
|
|
38
43
|
* Export the database to json, html, csv, markdown or .sql (for sqlite3)
|
|
39
44
|
* Backup the database to encrypted SQLCipher format (which can then be loaded back in to a Bouquin)
|
|
40
|
-
* Dark and light
|
|
45
|
+
* Dark and light theme support
|
|
41
46
|
* Automatically generate checkboxes when typing 'TODO'
|
|
42
47
|
* It is possible to automatically move unchecked checkboxes from yesterday to today, on startup
|
|
43
48
|
* English, French and Italian locales provided
|
|
49
|
+
* Ability to set reminder alarms in the app against the current line of text on today's date
|
|
50
|
+
* Ability to log time per day and run timesheet reports
|
|
44
51
|
|
|
45
52
|
|
|
46
53
|
## How to install
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
|
+
|
|
4
5
|
import requests
|
|
5
6
|
|
|
6
7
|
from PySide6.QtWidgets import (
|
|
@@ -49,6 +50,8 @@ class BugReportDialog(QDialog):
|
|
|
49
50
|
button_box.rejected.connect(self.reject)
|
|
50
51
|
layout.addWidget(button_box)
|
|
51
52
|
|
|
53
|
+
self.setMinimumWidth(560)
|
|
54
|
+
|
|
52
55
|
self.text_edit.setFocus()
|
|
53
56
|
|
|
54
57
|
# ------------Helpers ------------ #
|
|
@@ -17,6 +17,18 @@ from . import strings
|
|
|
17
17
|
|
|
18
18
|
Entry = Tuple[str, str]
|
|
19
19
|
TagRow = Tuple[int, str, str]
|
|
20
|
+
ProjectRow = Tuple[int, str] # (id, name)
|
|
21
|
+
ActivityRow = Tuple[int, str] # (id, name)
|
|
22
|
+
TimeLogRow = Tuple[
|
|
23
|
+
int, # id
|
|
24
|
+
str, # page_date (yyyy-MM-dd)
|
|
25
|
+
int,
|
|
26
|
+
str, # project_id, project_name
|
|
27
|
+
int,
|
|
28
|
+
str, # activity_id, activity_name
|
|
29
|
+
int, # minutes
|
|
30
|
+
str | None, # note
|
|
31
|
+
]
|
|
20
32
|
|
|
21
33
|
_TAG_COLORS = [
|
|
22
34
|
"#FFB3BA", # soft red
|
|
@@ -148,6 +160,38 @@ class DBManager:
|
|
|
148
160
|
);
|
|
149
161
|
|
|
150
162
|
CREATE INDEX IF NOT EXISTS ix_page_tags_tag_id ON page_tags(tag_id);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
165
|
+
id INTEGER PRIMARY KEY,
|
|
166
|
+
name TEXT NOT NULL UNIQUE
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE TABLE IF NOT EXISTS activities (
|
|
170
|
+
id INTEGER PRIMARY KEY,
|
|
171
|
+
name TEXT NOT NULL UNIQUE
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
CREATE TABLE IF NOT EXISTS time_log (
|
|
175
|
+
id INTEGER PRIMARY KEY,
|
|
176
|
+
page_date TEXT NOT NULL, -- FK to pages.date (yyyy-MM-dd)
|
|
177
|
+
project_id INTEGER NOT NULL, -- FK to projects.id
|
|
178
|
+
activity_id INTEGER NOT NULL, -- FK to activities.id
|
|
179
|
+
minutes INTEGER NOT NULL, -- duration in minutes
|
|
180
|
+
note TEXT,
|
|
181
|
+
created_at TEXT NOT NULL DEFAULT (
|
|
182
|
+
strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
183
|
+
),
|
|
184
|
+
FOREIGN KEY(page_date) REFERENCES pages(date) ON DELETE CASCADE,
|
|
185
|
+
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE RESTRICT,
|
|
186
|
+
FOREIGN KEY(activity_id) REFERENCES activities(id) ON DELETE RESTRICT
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
CREATE INDEX IF NOT EXISTS ix_time_log_date
|
|
190
|
+
ON time_log(page_date);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS ix_time_log_project
|
|
192
|
+
ON time_log(project_id);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS ix_time_log_activity
|
|
194
|
+
ON time_log(activity_id);
|
|
151
195
|
"""
|
|
152
196
|
)
|
|
153
197
|
self.conn.commit()
|
|
@@ -746,6 +790,220 @@ class DBManager:
|
|
|
746
790
|
revisions_by_date,
|
|
747
791
|
)
|
|
748
792
|
|
|
793
|
+
# -------- Time logging: projects & activities ---------------------
|
|
794
|
+
|
|
795
|
+
def list_projects(self) -> list[ProjectRow]:
|
|
796
|
+
cur = self.conn.cursor()
|
|
797
|
+
rows = cur.execute(
|
|
798
|
+
"SELECT id, name FROM projects ORDER BY LOWER(name);"
|
|
799
|
+
).fetchall()
|
|
800
|
+
return [(r["id"], r["name"]) for r in rows]
|
|
801
|
+
|
|
802
|
+
def add_project(self, name: str) -> int:
|
|
803
|
+
name = name.strip()
|
|
804
|
+
if not name:
|
|
805
|
+
raise ValueError("empty project name")
|
|
806
|
+
with self.conn:
|
|
807
|
+
cur = self.conn.cursor()
|
|
808
|
+
cur.execute(
|
|
809
|
+
"INSERT OR IGNORE INTO projects(name) VALUES (?);",
|
|
810
|
+
(name,),
|
|
811
|
+
)
|
|
812
|
+
row = cur.execute(
|
|
813
|
+
"SELECT id, name FROM projects WHERE name = ?;",
|
|
814
|
+
(name,),
|
|
815
|
+
).fetchone()
|
|
816
|
+
return row["id"]
|
|
817
|
+
|
|
818
|
+
def rename_project(self, project_id: int, new_name: str) -> None:
|
|
819
|
+
new_name = new_name.strip()
|
|
820
|
+
if not new_name:
|
|
821
|
+
return
|
|
822
|
+
with self.conn:
|
|
823
|
+
self.conn.execute(
|
|
824
|
+
"UPDATE projects SET name = ? WHERE id = ?;",
|
|
825
|
+
(new_name, project_id),
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
def delete_project(self, project_id: int) -> None:
|
|
829
|
+
with self.conn:
|
|
830
|
+
self.conn.execute(
|
|
831
|
+
"DELETE FROM projects WHERE id = ?;",
|
|
832
|
+
(project_id,),
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
def list_activities(self) -> list[ActivityRow]:
|
|
836
|
+
cur = self.conn.cursor()
|
|
837
|
+
rows = cur.execute(
|
|
838
|
+
"SELECT id, name FROM activities ORDER BY LOWER(name);"
|
|
839
|
+
).fetchall()
|
|
840
|
+
return [(r["id"], r["name"]) for r in rows]
|
|
841
|
+
|
|
842
|
+
def add_activity(self, name: str) -> int:
|
|
843
|
+
name = name.strip()
|
|
844
|
+
if not name:
|
|
845
|
+
raise ValueError("empty activity name")
|
|
846
|
+
with self.conn:
|
|
847
|
+
cur = self.conn.cursor()
|
|
848
|
+
cur.execute(
|
|
849
|
+
"INSERT OR IGNORE INTO activities(name) VALUES (?);",
|
|
850
|
+
(name,),
|
|
851
|
+
)
|
|
852
|
+
row = cur.execute(
|
|
853
|
+
"SELECT id, name FROM activities WHERE name = ?;",
|
|
854
|
+
(name,),
|
|
855
|
+
).fetchone()
|
|
856
|
+
return row["id"]
|
|
857
|
+
|
|
858
|
+
def rename_activity(self, activity_id: int, new_name: str) -> None:
|
|
859
|
+
new_name = new_name.strip()
|
|
860
|
+
if not new_name:
|
|
861
|
+
return
|
|
862
|
+
with self.conn:
|
|
863
|
+
self.conn.execute(
|
|
864
|
+
"UPDATE activities SET name = ? WHERE id = ?;",
|
|
865
|
+
(new_name, activity_id),
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
def delete_activity(self, activity_id: int) -> None:
|
|
869
|
+
with self.conn:
|
|
870
|
+
self.conn.execute(
|
|
871
|
+
"DELETE FROM activities WHERE id = ?;",
|
|
872
|
+
(activity_id,),
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
# -------- Time logging: entries -----------------------------------
|
|
876
|
+
|
|
877
|
+
def add_time_log(
|
|
878
|
+
self,
|
|
879
|
+
date_iso: str,
|
|
880
|
+
project_id: int,
|
|
881
|
+
activity_id: int,
|
|
882
|
+
minutes: int,
|
|
883
|
+
note: str | None = None,
|
|
884
|
+
) -> int:
|
|
885
|
+
with self.conn:
|
|
886
|
+
cur = self.conn.cursor()
|
|
887
|
+
# Ensure a page row exists even if there is no text content yet
|
|
888
|
+
cur.execute("INSERT OR IGNORE INTO pages(date) VALUES (?);", (date_iso,))
|
|
889
|
+
cur.execute(
|
|
890
|
+
"""
|
|
891
|
+
INSERT INTO time_log(page_date, project_id, activity_id, minutes, note)
|
|
892
|
+
VALUES (?, ?, ?, ?, ?);
|
|
893
|
+
""",
|
|
894
|
+
(date_iso, project_id, activity_id, minutes, note),
|
|
895
|
+
)
|
|
896
|
+
return cur.lastrowid
|
|
897
|
+
|
|
898
|
+
def update_time_log(
|
|
899
|
+
self,
|
|
900
|
+
entry_id: int,
|
|
901
|
+
project_id: int,
|
|
902
|
+
activity_id: int,
|
|
903
|
+
minutes: int,
|
|
904
|
+
note: str | None = None,
|
|
905
|
+
) -> None:
|
|
906
|
+
with self.conn:
|
|
907
|
+
self.conn.execute(
|
|
908
|
+
"""
|
|
909
|
+
UPDATE time_log
|
|
910
|
+
SET project_id = ?, activity_id = ?, minutes = ?, note = ?
|
|
911
|
+
WHERE id = ?;
|
|
912
|
+
""",
|
|
913
|
+
(project_id, activity_id, minutes, note, entry_id),
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
def delete_time_log(self, entry_id: int) -> None:
|
|
917
|
+
with self.conn:
|
|
918
|
+
self.conn.execute(
|
|
919
|
+
"DELETE FROM time_log WHERE id = ?;",
|
|
920
|
+
(entry_id,),
|
|
921
|
+
)
|
|
922
|
+
|
|
923
|
+
def time_log_for_date(self, date_iso: str) -> list[TimeLogRow]:
|
|
924
|
+
cur = self.conn.cursor()
|
|
925
|
+
rows = cur.execute(
|
|
926
|
+
"""
|
|
927
|
+
SELECT
|
|
928
|
+
t.id,
|
|
929
|
+
t.page_date,
|
|
930
|
+
t.project_id,
|
|
931
|
+
p.name AS project_name,
|
|
932
|
+
t.activity_id,
|
|
933
|
+
a.name AS activity_name,
|
|
934
|
+
t.minutes,
|
|
935
|
+
t.note
|
|
936
|
+
FROM time_log t
|
|
937
|
+
JOIN projects p ON p.id = t.project_id
|
|
938
|
+
JOIN activities a ON a.id = t.activity_id
|
|
939
|
+
WHERE t.page_date = ?
|
|
940
|
+
ORDER BY LOWER(p.name), LOWER(a.name), t.id;
|
|
941
|
+
""",
|
|
942
|
+
(date_iso,),
|
|
943
|
+
).fetchall()
|
|
944
|
+
|
|
945
|
+
result: list[TimeLogRow] = []
|
|
946
|
+
for r in rows:
|
|
947
|
+
result.append(
|
|
948
|
+
(
|
|
949
|
+
r["id"],
|
|
950
|
+
r["page_date"],
|
|
951
|
+
r["project_id"],
|
|
952
|
+
r["project_name"],
|
|
953
|
+
r["activity_id"],
|
|
954
|
+
r["activity_name"],
|
|
955
|
+
r["minutes"],
|
|
956
|
+
r["note"],
|
|
957
|
+
)
|
|
958
|
+
)
|
|
959
|
+
return result
|
|
960
|
+
|
|
961
|
+
def time_report(
|
|
962
|
+
self,
|
|
963
|
+
project_id: int,
|
|
964
|
+
start_date_iso: str,
|
|
965
|
+
end_date_iso: str,
|
|
966
|
+
granularity: str = "day", # 'day' | 'week' | 'month'
|
|
967
|
+
) -> list[tuple[str, str, int]]:
|
|
968
|
+
"""
|
|
969
|
+
Return (time_period, activity_name, total_minutes) tuples between start and end
|
|
970
|
+
for a project, grouped by period and activity.
|
|
971
|
+
time_period is:
|
|
972
|
+
- 'YYYY-MM-DD' for day
|
|
973
|
+
- 'YYYY-WW' for week
|
|
974
|
+
- 'YYYY-MM' for month
|
|
975
|
+
"""
|
|
976
|
+
if granularity == "day":
|
|
977
|
+
bucket_expr = "page_date"
|
|
978
|
+
elif granularity == "week":
|
|
979
|
+
# ISO-like year-week; SQLite weeks start at 00
|
|
980
|
+
bucket_expr = "strftime('%Y-%W', page_date)"
|
|
981
|
+
else: # month
|
|
982
|
+
bucket_expr = "substr(page_date, 1, 7)" # YYYY-MM
|
|
983
|
+
|
|
984
|
+
cur = self.conn.cursor()
|
|
985
|
+
rows = cur.execute(
|
|
986
|
+
f"""
|
|
987
|
+
SELECT
|
|
988
|
+
{bucket_expr} AS bucket,
|
|
989
|
+
a.name AS activity_name,
|
|
990
|
+
t.note AS note,
|
|
991
|
+
SUM(t.minutes) AS total_minutes
|
|
992
|
+
FROM time_log t
|
|
993
|
+
JOIN activities a ON a.id = t.activity_id
|
|
994
|
+
WHERE t.project_id = ?
|
|
995
|
+
AND t.page_date BETWEEN ? AND ?
|
|
996
|
+
GROUP BY bucket, activity_name
|
|
997
|
+
ORDER BY bucket, LOWER(activity_name);
|
|
998
|
+
""", # nosec
|
|
999
|
+
(project_id, start_date_iso, end_date_iso),
|
|
1000
|
+
).fetchall()
|
|
1001
|
+
|
|
1002
|
+
return [
|
|
1003
|
+
(r["bucket"], r["activity_name"], r["note"], r["total_minutes"])
|
|
1004
|
+
for r in rows
|
|
1005
|
+
]
|
|
1006
|
+
|
|
749
1007
|
def close(self) -> None:
|
|
750
1008
|
if self.conn is not None:
|
|
751
1009
|
self.conn.close()
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"behaviour": "Behaviour",
|
|
37
37
|
"never": "Never",
|
|
38
38
|
"browse": "Browse",
|
|
39
|
+
"close_tab": "Close tab",
|
|
39
40
|
"previous": "Previous",
|
|
40
41
|
"previous_day": "Previous day",
|
|
41
42
|
"next": "Next",
|
|
@@ -155,8 +156,87 @@
|
|
|
155
156
|
"bug_report_send_failed": "Could not send bug report.",
|
|
156
157
|
"bug_report_sent_ok": "Bug report sent. Thank you!",
|
|
157
158
|
"send": "Send",
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"
|
|
159
|
+
"set_reminder": "Set reminder prompt",
|
|
160
|
+
"set_reminder_prompt": "Enter a time",
|
|
161
|
+
"reminder_no_text_fallback": "You scheduled a reminder to alert you now!",
|
|
162
|
+
"invalid_time_title": "Invalid time",
|
|
163
|
+
"invalid_time_message": "Please enter a time in the format HH:MM",
|
|
164
|
+
"dismiss": "Dismiss",
|
|
165
|
+
"toolbar_alarm": "Set reminder alarm",
|
|
166
|
+
"activities": "Activities",
|
|
167
|
+
"activity": "Activity",
|
|
168
|
+
"note": "Note",
|
|
169
|
+
"activity_delete_error_message": "A problem occurred deleting the activity",
|
|
170
|
+
"activity_delete_error_title": "Problem deleting activity",
|
|
171
|
+
"activity_rename_error_message": "A problem occurred renaming the activity",
|
|
172
|
+
"activity_rename_error_title": "Problem renaming activity",
|
|
173
|
+
"activity_required_message": "An activity name is required",
|
|
174
|
+
"activity_required_title": "Activity name required",
|
|
175
|
+
"add_activity": "Add activity",
|
|
176
|
+
"add_project": "Add project",
|
|
177
|
+
"add_time_entry": "Add time entry",
|
|
178
|
+
"time_period": "Time period",
|
|
179
|
+
"by_day": "by day",
|
|
180
|
+
"by_month": "by month",
|
|
181
|
+
"by_week": "by week",
|
|
182
|
+
"date_range": "Date range",
|
|
183
|
+
"delete_activity": "Delete activity",
|
|
184
|
+
"delete_activity_confirm": "Are you sure you want to delete this activity?",
|
|
185
|
+
"delete_activity_title": "Delete activity - are you sure?",
|
|
186
|
+
"delete_project": "Delete project",
|
|
187
|
+
"delete_project_confirm": "Are you sure you want to delete this project?",
|
|
188
|
+
"delete_project_title": "Delete project - are you sure?",
|
|
189
|
+
"delete_time_entry": "Delete time entry",
|
|
190
|
+
"group_by": "Group by",
|
|
191
|
+
"hours": "Hours",
|
|
192
|
+
"invalid_activity_message": "The activity is invalid",
|
|
193
|
+
"invalid_activity_title": "Invalid activity",
|
|
194
|
+
"invalid_project_message": "The project is invalid",
|
|
195
|
+
"invalid_project_title": "Invalid project",
|
|
196
|
+
"label_key": "Label",
|
|
197
|
+
"manage_activities": "Manage activities",
|
|
198
|
+
"manage_projects": "Manage projects",
|
|
199
|
+
"manage_projects_activities": "Manage project activities",
|
|
200
|
+
"open_time_log": "Open time log",
|
|
201
|
+
"project": "Project",
|
|
202
|
+
"project_delete_error_message": "A problem occurred deleting the project",
|
|
203
|
+
"project_delete_error_title": "Problem deleting project",
|
|
204
|
+
"project_rename_error_message": "A problem occurred renaming the project",
|
|
205
|
+
"project_rename_error_title": "Problem renaming project",
|
|
206
|
+
"project_required_message": "A project is required",
|
|
207
|
+
"project_required_title": "Project required",
|
|
208
|
+
"projects": "Projects",
|
|
209
|
+
"rename_activity": "Rename activity",
|
|
210
|
+
"rename_project": "Rename project",
|
|
211
|
+
"run_report": "Run report",
|
|
212
|
+
"add_project_label": "Add a project",
|
|
213
|
+
"add_activity_label": "Add an activity",
|
|
214
|
+
"select_activity_message": "Select an activity",
|
|
215
|
+
"select_activity_title": "Select activity",
|
|
216
|
+
"select_project_message": "Select a project",
|
|
217
|
+
"select_project_title": "Select project",
|
|
218
|
+
"time_log": "Time log",
|
|
219
|
+
"time_log_collapsed_hint": "Time log",
|
|
220
|
+
"time_log_date_label": "Time log date: {date}",
|
|
221
|
+
"time_log_for": "Time log for {date}",
|
|
222
|
+
"time_log_no_date": "Time log",
|
|
223
|
+
"time_log_no_entries": "No time entries yet",
|
|
224
|
+
"time_log_report": "Time log report",
|
|
225
|
+
"time_log_report_title": "Time log for {project}",
|
|
226
|
+
"time_log_report_meta": "From {start} to {end}, grouped {granularity}",
|
|
227
|
+
"time_log_total_hours": "Total time spent",
|
|
228
|
+
"time_log_with_total": "Time log ({hours:.2f}h)",
|
|
229
|
+
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
|
230
|
+
"title_key": "title",
|
|
231
|
+
"update_time_entry": "Update time entry",
|
|
232
|
+
"time_report_total": "Total: {hours:.2f} hours",
|
|
233
|
+
"no_report_title": "No report",
|
|
234
|
+
"no_report_message": "Please run a report before exporting.",
|
|
235
|
+
"total": "Total",
|
|
236
|
+
"export_csv": "Export CSV",
|
|
237
|
+
"export_csv_error_title": "Export failed",
|
|
238
|
+
"export_csv_error_message": "Could not write CSV file:\n{error}",
|
|
239
|
+
"export_pdf": "Export PDF",
|
|
240
|
+
"export_pdf_error_title": "PDF export failed",
|
|
241
|
+
"export_pdf_error_message": "Could not write PDF file:\n{error}"
|
|
162
242
|
}
|