bouquin 0.7.2__tar.gz → 0.7.4__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.7.2 → bouquin-0.7.4}/PKG-INFO +5 -4
- {bouquin-0.7.2 → bouquin-0.7.4}/README.md +3 -2
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/db.py +57 -4
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/fonts/DejaVu.license +1 -1
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/fonts/Noto.license +1 -1
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/locales/en.json +4 -2
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/main_window.py +11 -2
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/settings.py +5 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/settings_dialog.py +20 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/time_log.py +102 -47
- {bouquin-0.7.2 → bouquin-0.7.4}/pyproject.toml +2 -2
- {bouquin-0.7.2 → bouquin-0.7.4}/LICENSE +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/__init__.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/__main__.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/bug_report_dialog.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/code_block_editor_dialog.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/code_highlighter.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/document_utils.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/documents.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/find_bar.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/flow_layout.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/fonts/DejaVuSans.ttf +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/fonts/NotoSansSymbols2-Regular.ttf +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/history_dialog.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/icons/bouquin.svg +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/invoices.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/key_prompt.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/keys/mig5.asc +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/locales/fr.json +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/locales/it.json +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/lock_overlay.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/main.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/markdown_editor.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/markdown_highlighter.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/pomodoro_timer.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/reminders.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/save_dialog.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/search.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/statistics_dialog.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/strings.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/tag_browser.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/tags_widget.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/theme.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/toolbar.py +0 -0
- {bouquin-0.7.2 → bouquin-0.7.4}/bouquin/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bouquin
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.4
|
|
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
|
|
@@ -12,10 +12,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: bouquin-sqlcipher4 (>=4.12.0,<5.0.0)
|
|
15
16
|
Requires-Dist: markdown (>=3.10,<4.0)
|
|
16
17
|
Requires-Dist: pyside6 (>=6.8.1,<7.0.0)
|
|
17
18
|
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
18
|
-
Requires-Dist: sqlcipher3-wheels (>=0.5.5.post0,<0.6.0)
|
|
19
19
|
Project-URL: Repository, https://git.mig5.net/mig5/bouquin
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
@@ -33,8 +33,9 @@ It is designed to treat each day as its own 'page', complete with Markdown rende
|
|
|
33
33
|
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
|
34
34
|
also how long we spent on them.
|
|
35
35
|
|
|
36
|
-
It uses
|
|
37
|
-
|
|
36
|
+
It uses SQLCipher as a drop-in replacement for SQLite3.
|
|
37
|
+
|
|
38
|
+
This means that the underlying database for the notebook is encrypted at rest.
|
|
38
39
|
|
|
39
40
|
To increase security, the SQLCipher key is requested when the app is opened, and is not written
|
|
40
41
|
to disk unless the user configures it to be in the settings.
|
|
@@ -12,8 +12,9 @@ It is designed to treat each day as its own 'page', complete with Markdown rende
|
|
|
12
12
|
search, reminders and time logging for those of us who need to keep track of not just TODOs, but
|
|
13
13
|
also how long we spent on them.
|
|
14
14
|
|
|
15
|
-
It uses
|
|
16
|
-
|
|
15
|
+
It uses SQLCipher as a drop-in replacement for SQLite3.
|
|
16
|
+
|
|
17
|
+
This means that the underlying database for the notebook is encrypted at rest.
|
|
17
18
|
|
|
18
19
|
To increase security, the SQLCipher key is requested when the app is opened, and is not written
|
|
19
20
|
to disk unless the user configures it to be in the settings.
|
|
@@ -92,6 +92,7 @@ class DBConfig:
|
|
|
92
92
|
idle_minutes: int = 15 # 0 = never lock
|
|
93
93
|
theme: str = "system"
|
|
94
94
|
move_todos: bool = False
|
|
95
|
+
move_todos_include_weekends: bool = False
|
|
95
96
|
tags: bool = True
|
|
96
97
|
time_log: bool = True
|
|
97
98
|
reminders: bool = True
|
|
@@ -1351,7 +1352,7 @@ class DBManager:
|
|
|
1351
1352
|
project_id: int,
|
|
1352
1353
|
start_date_iso: str,
|
|
1353
1354
|
end_date_iso: str,
|
|
1354
|
-
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
|
1355
|
+
granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none'
|
|
1355
1356
|
) -> list[tuple[str, str, str, int]]:
|
|
1356
1357
|
"""
|
|
1357
1358
|
Return (time_period, activity_name, total_minutes) tuples between start and end
|
|
@@ -1360,7 +1361,8 @@ class DBManager:
|
|
|
1360
1361
|
- 'YYYY-MM-DD' for day
|
|
1361
1362
|
- 'YYYY-WW' for week
|
|
1362
1363
|
- 'YYYY-MM' for month
|
|
1363
|
-
For '
|
|
1364
|
+
For 'activity' granularity, results are grouped by activity only (no time bucket).
|
|
1365
|
+
For 'none' granularity, each individual time log entry becomes a row.
|
|
1364
1366
|
"""
|
|
1365
1367
|
cur = self.conn.cursor()
|
|
1366
1368
|
|
|
@@ -1387,6 +1389,26 @@ class DBManager:
|
|
|
1387
1389
|
for r in rows
|
|
1388
1390
|
]
|
|
1389
1391
|
|
|
1392
|
+
if granularity == "activity":
|
|
1393
|
+
rows = cur.execute(
|
|
1394
|
+
"""
|
|
1395
|
+
SELECT
|
|
1396
|
+
a.name AS activity_name,
|
|
1397
|
+
SUM(t.minutes) AS total_minutes
|
|
1398
|
+
FROM time_log t
|
|
1399
|
+
JOIN activities a ON a.id = t.activity_id
|
|
1400
|
+
WHERE t.project_id = ?
|
|
1401
|
+
AND t.page_date BETWEEN ? AND ?
|
|
1402
|
+
GROUP BY activity_name
|
|
1403
|
+
ORDER BY LOWER(activity_name);
|
|
1404
|
+
""",
|
|
1405
|
+
(project_id, start_date_iso, end_date_iso),
|
|
1406
|
+
).fetchall()
|
|
1407
|
+
|
|
1408
|
+
# period column is unused for activity grouping in the UI, but we keep
|
|
1409
|
+
# the tuple shape consistent.
|
|
1410
|
+
return [("", r["activity_name"], "", r["total_minutes"]) for r in rows]
|
|
1411
|
+
|
|
1390
1412
|
if granularity == "day":
|
|
1391
1413
|
bucket_expr = "page_date"
|
|
1392
1414
|
elif granularity == "week":
|
|
@@ -1417,11 +1439,14 @@ class DBManager:
|
|
|
1417
1439
|
self,
|
|
1418
1440
|
start_date_iso: str,
|
|
1419
1441
|
end_date_iso: str,
|
|
1420
|
-
granularity: str = "day", # 'day' | 'week' | 'month' | 'none'
|
|
1442
|
+
granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none'
|
|
1421
1443
|
) -> list[tuple[str, str, str, str, int]]:
|
|
1422
1444
|
"""
|
|
1423
1445
|
Return (project_name, time_period, activity_name, note, total_minutes)
|
|
1424
|
-
across *all* projects between start and end
|
|
1446
|
+
across *all* projects between start and end.
|
|
1447
|
+
- For 'day'/'week'/'month', grouped by project + period + activity.
|
|
1448
|
+
- For 'activity', grouped by project + activity.
|
|
1449
|
+
- For 'none', one row per time_log entry.
|
|
1425
1450
|
"""
|
|
1426
1451
|
cur = self.conn.cursor()
|
|
1427
1452
|
|
|
@@ -1455,6 +1480,34 @@ class DBManager:
|
|
|
1455
1480
|
for r in rows
|
|
1456
1481
|
]
|
|
1457
1482
|
|
|
1483
|
+
if granularity == "activity":
|
|
1484
|
+
rows = cur.execute(
|
|
1485
|
+
"""
|
|
1486
|
+
SELECT
|
|
1487
|
+
p.name AS project_name,
|
|
1488
|
+
a.name AS activity_name,
|
|
1489
|
+
SUM(t.minutes) AS total_minutes
|
|
1490
|
+
FROM time_log t
|
|
1491
|
+
JOIN projects p ON p.id = t.project_id
|
|
1492
|
+
JOIN activities a ON a.id = t.activity_id
|
|
1493
|
+
WHERE t.page_date BETWEEN ? AND ?
|
|
1494
|
+
GROUP BY p.id, activity_name
|
|
1495
|
+
ORDER BY LOWER(p.name), LOWER(activity_name);
|
|
1496
|
+
""",
|
|
1497
|
+
(start_date_iso, end_date_iso),
|
|
1498
|
+
).fetchall()
|
|
1499
|
+
|
|
1500
|
+
return [
|
|
1501
|
+
(
|
|
1502
|
+
r["project_name"],
|
|
1503
|
+
"",
|
|
1504
|
+
r["activity_name"],
|
|
1505
|
+
"",
|
|
1506
|
+
r["total_minutes"],
|
|
1507
|
+
)
|
|
1508
|
+
for r in rows
|
|
1509
|
+
]
|
|
1510
|
+
|
|
1458
1511
|
if granularity == "day":
|
|
1459
1512
|
bucket_expr = "page_date"
|
|
1460
1513
|
elif granularity == "week":
|
|
@@ -74,7 +74,7 @@ Fonts, only if the fonts are renamed to names not containing either
|
|
|
74
74
|
the words "Tavmjong Bah" or the word "Arev".
|
|
75
75
|
|
|
76
76
|
This License becomes null and void to the extent applicable to Fonts
|
|
77
|
-
or Font Software that has been modified and is distributed under the
|
|
77
|
+
or Font Software that has been modified and is distributed under the
|
|
78
78
|
"Tavmjong Bah Arev" names.
|
|
79
79
|
|
|
80
80
|
The Font Software may be sold as part of a larger software package but
|
|
@@ -18,7 +18,7 @@ with others.
|
|
|
18
18
|
|
|
19
19
|
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
20
|
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
-
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
22
|
redistributed and/or sold with any software provided that any reserved
|
|
23
23
|
names are not used by derivative works. The fonts and derivatives,
|
|
24
24
|
however, cannot be released under any other type of license. The
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"autosave": "autosave",
|
|
104
104
|
"unchecked_checkbox_items_moved_to_next_day": "Unchecked checkbox items moved to next day",
|
|
105
105
|
"move_unchecked_todos_to_today_on_startup": "Automatically move unchecked TODOs\nfrom the last 7 days to next weekday",
|
|
106
|
+
"move_todos_include_weekends": "Allow moving unchecked TODOs to a weekend\nrather than next weekday",
|
|
106
107
|
"insert_images": "Insert images",
|
|
107
108
|
"images": "Images",
|
|
108
109
|
"reopen_failed": "Re-open failed",
|
|
@@ -171,7 +172,7 @@
|
|
|
171
172
|
"stats_metric_revisions": "Revisions",
|
|
172
173
|
"stats_metric_documents": "Documents",
|
|
173
174
|
"stats_total_documents": "Total documents",
|
|
174
|
-
"stats_date_most_documents": "Date with most documents",
|
|
175
|
+
"stats_date_most_documents": "Date with most documents",
|
|
175
176
|
"stats_no_data": "No statistics available yet.",
|
|
176
177
|
"stats_time_total_hours": "Total hours logged",
|
|
177
178
|
"stats_time_day_most_hours": "Day with most hours logged",
|
|
@@ -209,6 +210,7 @@
|
|
|
209
210
|
"add_time_entry": "Add time entry",
|
|
210
211
|
"time_period": "Time period",
|
|
211
212
|
"dont_group": "Don't group",
|
|
213
|
+
"by_activity": "by activity",
|
|
212
214
|
"by_day": "by day",
|
|
213
215
|
"by_month": "by month",
|
|
214
216
|
"by_week": "by week",
|
|
@@ -375,7 +377,7 @@
|
|
|
375
377
|
"documents_missing_file": "The file does not exist:\n{path}",
|
|
376
378
|
"documents_confirm_delete": "Remove this document from the project?\n(The file on disk will not be deleted.)",
|
|
377
379
|
"documents_search_label": "Search",
|
|
378
|
-
"documents_search_placeholder": "Type to search documents (all projects)",
|
|
380
|
+
"documents_search_placeholder": "Type to search documents (all projects)",
|
|
379
381
|
"todays_documents": "Documents from this day",
|
|
380
382
|
"todays_documents_none": "No documents yet.",
|
|
381
383
|
"manage_invoices": "Manage Invoices",
|
|
@@ -822,9 +822,13 @@ class MainWindow(QMainWindow):
|
|
|
822
822
|
Given a 'new day' (system date), return the date we should move
|
|
823
823
|
unfinished todos *to*.
|
|
824
824
|
|
|
825
|
-
|
|
826
|
-
|
|
825
|
+
By default, if the new day is Saturday or Sunday we skip ahead to the
|
|
826
|
+
next Monday (i.e., "next available weekday"). If the optional setting
|
|
827
|
+
`move_todos_include_weekends` is enabled, we move to the very next day
|
|
828
|
+
even if it's a weekend.
|
|
827
829
|
"""
|
|
830
|
+
if getattr(self.cfg, "move_todos_include_weekends", False):
|
|
831
|
+
return day
|
|
828
832
|
# Qt: Monday=1 ... Sunday=7
|
|
829
833
|
dow = day.dayOfWeek()
|
|
830
834
|
if dow >= 6: # Saturday (6) or Sunday (7)
|
|
@@ -1566,6 +1570,11 @@ class MainWindow(QMainWindow):
|
|
|
1566
1570
|
self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
|
|
1567
1571
|
self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme)
|
|
1568
1572
|
self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos)
|
|
1573
|
+
self.cfg.move_todos_include_weekends = getattr(
|
|
1574
|
+
new_cfg,
|
|
1575
|
+
"move_todos_include_weekends",
|
|
1576
|
+
getattr(self.cfg, "move_todos_include_weekends", False),
|
|
1577
|
+
)
|
|
1569
1578
|
self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags)
|
|
1570
1579
|
self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log)
|
|
1571
1580
|
self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders)
|
|
@@ -42,6 +42,9 @@ def load_db_config() -> DBConfig:
|
|
|
42
42
|
idle = s.value("ui/idle_minutes", 15, type=int)
|
|
43
43
|
theme = s.value("ui/theme", "system", type=str)
|
|
44
44
|
move_todos = s.value("ui/move_todos", False, type=bool)
|
|
45
|
+
move_todos_include_weekends = s.value(
|
|
46
|
+
"ui/move_todos_include_weekends", False, type=bool
|
|
47
|
+
)
|
|
45
48
|
tags = s.value("ui/tags", True, type=bool)
|
|
46
49
|
time_log = s.value("ui/time_log", True, type=bool)
|
|
47
50
|
reminders = s.value("ui/reminders", True, type=bool)
|
|
@@ -57,6 +60,7 @@ def load_db_config() -> DBConfig:
|
|
|
57
60
|
idle_minutes=idle,
|
|
58
61
|
theme=theme,
|
|
59
62
|
move_todos=move_todos,
|
|
63
|
+
move_todos_include_weekends=move_todos_include_weekends,
|
|
60
64
|
tags=tags,
|
|
61
65
|
time_log=time_log,
|
|
62
66
|
reminders=reminders,
|
|
@@ -76,6 +80,7 @@ def save_db_config(cfg: DBConfig) -> None:
|
|
|
76
80
|
s.setValue("ui/idle_minutes", str(cfg.idle_minutes))
|
|
77
81
|
s.setValue("ui/theme", str(cfg.theme))
|
|
78
82
|
s.setValue("ui/move_todos", str(cfg.move_todos))
|
|
83
|
+
s.setValue("ui/move_todos_include_weekends", str(cfg.move_todos_include_weekends))
|
|
79
84
|
s.setValue("ui/tags", str(cfg.tags))
|
|
80
85
|
s.setValue("ui/time_log", str(cfg.time_log))
|
|
81
86
|
s.setValue("ui/reminders", str(cfg.reminders))
|
|
@@ -169,6 +169,25 @@ class SettingsDialog(QDialog):
|
|
|
169
169
|
self.move_todos.setCursor(Qt.PointingHandCursor)
|
|
170
170
|
features_layout.addWidget(self.move_todos)
|
|
171
171
|
|
|
172
|
+
# Optional: allow moving to the very next day even if it is a weekend.
|
|
173
|
+
self.move_todos_include_weekends = QCheckBox(
|
|
174
|
+
strings._("move_todos_include_weekends")
|
|
175
|
+
)
|
|
176
|
+
self.move_todos_include_weekends.setChecked(
|
|
177
|
+
getattr(self.current_settings, "move_todos_include_weekends", False)
|
|
178
|
+
)
|
|
179
|
+
self.move_todos_include_weekends.setCursor(Qt.PointingHandCursor)
|
|
180
|
+
self.move_todos_include_weekends.setEnabled(self.move_todos.isChecked())
|
|
181
|
+
|
|
182
|
+
move_todos_opts = QWidget()
|
|
183
|
+
move_todos_opts_layout = QVBoxLayout(move_todos_opts)
|
|
184
|
+
move_todos_opts_layout.setContentsMargins(24, 0, 0, 0)
|
|
185
|
+
move_todos_opts_layout.setSpacing(4)
|
|
186
|
+
move_todos_opts_layout.addWidget(self.move_todos_include_weekends)
|
|
187
|
+
features_layout.addWidget(move_todos_opts)
|
|
188
|
+
|
|
189
|
+
self.move_todos.toggled.connect(self.move_todos_include_weekends.setEnabled)
|
|
190
|
+
|
|
172
191
|
self.tags = QCheckBox(strings._("enable_tags_feature"))
|
|
173
192
|
self.tags.setChecked(self.current_settings.tags)
|
|
174
193
|
self.tags.setCursor(Qt.PointingHandCursor)
|
|
@@ -441,6 +460,7 @@ class SettingsDialog(QDialog):
|
|
|
441
460
|
idle_minutes=self.idle_spin.value(),
|
|
442
461
|
theme=selected_theme.value,
|
|
443
462
|
move_todos=self.move_todos.isChecked(),
|
|
463
|
+
move_todos_include_weekends=self.move_todos_include_weekends.isChecked(),
|
|
444
464
|
tags=self.tags.isChecked(),
|
|
445
465
|
time_log=self.time_log.isChecked(),
|
|
446
466
|
reminders=self.reminders.isChecked(),
|
|
@@ -1083,6 +1083,7 @@ class TimeReportDialog(QDialog):
|
|
|
1083
1083
|
self.granularity.addItem(strings._("by_day"), "day")
|
|
1084
1084
|
self.granularity.addItem(strings._("by_week"), "week")
|
|
1085
1085
|
self.granularity.addItem(strings._("by_month"), "month")
|
|
1086
|
+
self.granularity.addItem(strings._("by_activity"), "activity")
|
|
1086
1087
|
form.addRow(strings._("group_by"), self.granularity)
|
|
1087
1088
|
|
|
1088
1089
|
root.addLayout(form)
|
|
@@ -1161,6 +1162,20 @@ class TimeReportDialog(QDialog):
|
|
|
1161
1162
|
header.setSectionResizeMode(2, QHeaderView.Stretch)
|
|
1162
1163
|
header.setSectionResizeMode(3, QHeaderView.Stretch)
|
|
1163
1164
|
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
|
1165
|
+
elif granularity == "activity":
|
|
1166
|
+
# Grouped by activity only: no time period, no note column
|
|
1167
|
+
self.table.setColumnCount(3)
|
|
1168
|
+
self.table.setHorizontalHeaderLabels(
|
|
1169
|
+
[
|
|
1170
|
+
strings._("project"),
|
|
1171
|
+
strings._("activity"),
|
|
1172
|
+
strings._("hours"),
|
|
1173
|
+
]
|
|
1174
|
+
)
|
|
1175
|
+
header = self.table.horizontalHeader()
|
|
1176
|
+
header.setSectionResizeMode(0, QHeaderView.Stretch)
|
|
1177
|
+
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
|
1178
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
|
1164
1179
|
else:
|
|
1165
1180
|
# Grouped: no note column
|
|
1166
1181
|
self.table.setColumnCount(4)
|
|
@@ -1272,16 +1287,21 @@ class TimeReportDialog(QDialog):
|
|
|
1272
1287
|
rows_for_table
|
|
1273
1288
|
):
|
|
1274
1289
|
hrs = minutes / 60.0
|
|
1275
|
-
self.
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if self._last_gran == "none":
|
|
1280
|
-
self.table.setItem(i, 3, QTableWidgetItem(note or ""))
|
|
1281
|
-
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
|
1290
|
+
if self._last_gran == "activity":
|
|
1291
|
+
self.table.setItem(i, 0, QTableWidgetItem(project))
|
|
1292
|
+
self.table.setItem(i, 1, QTableWidgetItem(activity_name))
|
|
1293
|
+
self.table.setItem(i, 2, QTableWidgetItem(f"{hrs:.2f}"))
|
|
1282
1294
|
else:
|
|
1283
|
-
|
|
1284
|
-
self.table.setItem(i,
|
|
1295
|
+
self.table.setItem(i, 0, QTableWidgetItem(project))
|
|
1296
|
+
self.table.setItem(i, 1, QTableWidgetItem(time_period))
|
|
1297
|
+
self.table.setItem(i, 2, QTableWidgetItem(activity_name))
|
|
1298
|
+
|
|
1299
|
+
if self._last_gran == "none":
|
|
1300
|
+
self.table.setItem(i, 3, QTableWidgetItem(note or ""))
|
|
1301
|
+
self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}"))
|
|
1302
|
+
else:
|
|
1303
|
+
# no note column
|
|
1304
|
+
self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}"))
|
|
1285
1305
|
|
|
1286
1306
|
# Summary label - include per-project totals when in "all projects" mode
|
|
1287
1307
|
total_hours = self._last_total_minutes / 60.0
|
|
@@ -1325,14 +1345,15 @@ class TimeReportDialog(QDialog):
|
|
|
1325
1345
|
with open(filename, "w", newline="", encoding="utf-8") as f:
|
|
1326
1346
|
writer = csv.writer(f)
|
|
1327
1347
|
|
|
1328
|
-
|
|
1348
|
+
gran = getattr(self, "_last_gran", "day")
|
|
1349
|
+
show_note = gran == "none"
|
|
1350
|
+
show_period = gran != "activity"
|
|
1329
1351
|
|
|
1330
1352
|
# Header
|
|
1331
|
-
header = [
|
|
1332
|
-
|
|
1333
|
-
strings._("time_period")
|
|
1334
|
-
|
|
1335
|
-
]
|
|
1353
|
+
header: list[str] = [strings._("project")]
|
|
1354
|
+
if show_period:
|
|
1355
|
+
header.append(strings._("time_period"))
|
|
1356
|
+
header.append(strings._("activity"))
|
|
1336
1357
|
if show_note:
|
|
1337
1358
|
header.append(strings._("note"))
|
|
1338
1359
|
header.append(strings._("hours"))
|
|
@@ -1347,16 +1368,22 @@ class TimeReportDialog(QDialog):
|
|
|
1347
1368
|
minutes,
|
|
1348
1369
|
) in self._last_rows:
|
|
1349
1370
|
hours = minutes / 60.0
|
|
1350
|
-
row = [project
|
|
1371
|
+
row: list[str] = [project]
|
|
1372
|
+
if show_period:
|
|
1373
|
+
row.append(time_period)
|
|
1374
|
+
row.append(activity_name)
|
|
1351
1375
|
if show_note:
|
|
1352
|
-
row.append(note)
|
|
1376
|
+
row.append(note or "")
|
|
1353
1377
|
row.append(f"{hours:.2f}")
|
|
1354
1378
|
writer.writerow(row)
|
|
1355
1379
|
|
|
1356
1380
|
# Blank line + total
|
|
1357
1381
|
total_hours = self._last_total_minutes / 60.0
|
|
1358
1382
|
writer.writerow([])
|
|
1359
|
-
|
|
1383
|
+
total_row = [""] * len(header)
|
|
1384
|
+
total_row[0] = strings._("total")
|
|
1385
|
+
total_row[-1] = f"{total_hours:.2f}"
|
|
1386
|
+
writer.writerow(total_row)
|
|
1360
1387
|
except OSError as exc:
|
|
1361
1388
|
QMessageBox.warning(
|
|
1362
1389
|
self,
|
|
@@ -1384,17 +1411,20 @@ class TimeReportDialog(QDialog):
|
|
|
1384
1411
|
if not filename.endswith(".pdf"):
|
|
1385
1412
|
filename = f"{filename}.pdf"
|
|
1386
1413
|
|
|
1387
|
-
# ---------- Build chart image
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1414
|
+
# ---------- Build chart image ----------
|
|
1415
|
+
# Default: hours per time period. If grouped by activity: hours per activity.
|
|
1416
|
+
gran = getattr(self, "_last_gran", "day")
|
|
1417
|
+
per_bucket_minutes: dict[str, int] = defaultdict(int)
|
|
1418
|
+
for _project, period, activity, _note, minutes in self._last_rows:
|
|
1419
|
+
bucket = activity if gran == "activity" else period
|
|
1420
|
+
per_bucket_minutes[bucket] += minutes
|
|
1391
1421
|
|
|
1392
|
-
|
|
1422
|
+
buckets = sorted(per_bucket_minutes.keys())
|
|
1393
1423
|
chart_w, chart_h = 800, 220
|
|
1394
1424
|
chart = QImage(chart_w, chart_h, QImage.Format_ARGB32)
|
|
1395
1425
|
chart.fill(Qt.white)
|
|
1396
1426
|
|
|
1397
|
-
if
|
|
1427
|
+
if buckets:
|
|
1398
1428
|
painter = QPainter(chart)
|
|
1399
1429
|
try:
|
|
1400
1430
|
painter.setRenderHint(QPainter.Antialiasing, True)
|
|
@@ -1422,9 +1452,9 @@ class TimeReportDialog(QDialog):
|
|
|
1422
1452
|
# Border
|
|
1423
1453
|
painter.drawRect(left, top, width, height)
|
|
1424
1454
|
|
|
1425
|
-
max_hours = max(
|
|
1455
|
+
max_hours = max(per_bucket_minutes[p] for p in buckets) / 60.0
|
|
1426
1456
|
if max_hours > 0:
|
|
1427
|
-
n = len(
|
|
1457
|
+
n = len(buckets)
|
|
1428
1458
|
bar_spacing = width / max(1, n)
|
|
1429
1459
|
bar_width = bar_spacing * 0.6
|
|
1430
1460
|
|
|
@@ -1449,8 +1479,8 @@ class TimeReportDialog(QDialog):
|
|
|
1449
1479
|
painter.setBrush(QColor(80, 140, 200))
|
|
1450
1480
|
painter.setPen(Qt.NoPen)
|
|
1451
1481
|
|
|
1452
|
-
for i,
|
|
1453
|
-
hours =
|
|
1482
|
+
for i, label in enumerate(buckets):
|
|
1483
|
+
hours = per_bucket_minutes[label] / 60.0
|
|
1454
1484
|
bar_h = int((hours / max_hours) * (height - 10))
|
|
1455
1485
|
if bar_h <= 0:
|
|
1456
1486
|
continue # pragma: no cover
|
|
@@ -1463,7 +1493,7 @@ class TimeReportDialog(QDialog):
|
|
|
1463
1493
|
|
|
1464
1494
|
# X labels after bars, in black
|
|
1465
1495
|
painter.setPen(Qt.black)
|
|
1466
|
-
for i,
|
|
1496
|
+
for i, label in enumerate(buckets):
|
|
1467
1497
|
x_center = left + bar_spacing * (i + 0.5)
|
|
1468
1498
|
x = int(x_center - bar_width / 2)
|
|
1469
1499
|
painter.drawText(
|
|
@@ -1472,7 +1502,7 @@ class TimeReportDialog(QDialog):
|
|
|
1472
1502
|
int(bar_width),
|
|
1473
1503
|
20,
|
|
1474
1504
|
Qt.AlignHCenter | Qt.AlignTop,
|
|
1475
|
-
|
|
1505
|
+
label,
|
|
1476
1506
|
)
|
|
1477
1507
|
finally:
|
|
1478
1508
|
painter.end()
|
|
@@ -1481,23 +1511,53 @@ class TimeReportDialog(QDialog):
|
|
|
1481
1511
|
project = html.escape(self._last_project_name or "")
|
|
1482
1512
|
start = html.escape(self._last_start or "")
|
|
1483
1513
|
end = html.escape(self._last_end or "")
|
|
1484
|
-
|
|
1514
|
+
gran_key = getattr(self, "_last_gran", "day")
|
|
1515
|
+
gran_label = html.escape(self._last_gran_label or "")
|
|
1485
1516
|
|
|
1486
1517
|
total_hours = self._last_total_minutes / 60.0
|
|
1487
1518
|
|
|
1488
|
-
# Table rows
|
|
1519
|
+
# Table rows
|
|
1489
1520
|
row_html_parts: list[str] = []
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1521
|
+
if gran_key == "activity":
|
|
1522
|
+
for project, _period, activity, _note, minutes in self._last_rows:
|
|
1523
|
+
hours = minutes / 60.0
|
|
1524
|
+
row_html_parts.append(
|
|
1525
|
+
"<tr>"
|
|
1526
|
+
f"<td>{html.escape(project)}</td>"
|
|
1527
|
+
f"<td>{html.escape(activity)}</td>"
|
|
1528
|
+
f"<td style='text-align:right'>{hours:.2f}</td>"
|
|
1529
|
+
"</tr>"
|
|
1530
|
+
)
|
|
1531
|
+
else:
|
|
1532
|
+
for project, period, activity, _note, minutes in self._last_rows:
|
|
1533
|
+
hours = minutes / 60.0
|
|
1534
|
+
row_html_parts.append(
|
|
1535
|
+
"<tr>"
|
|
1536
|
+
f"<td>{html.escape(project)}</td>"
|
|
1537
|
+
f"<td>{html.escape(period)}</td>"
|
|
1538
|
+
f"<td>{html.escape(activity)}</td>"
|
|
1539
|
+
f"<td style='text-align:right'>{hours:.2f}</td>"
|
|
1540
|
+
"</tr>"
|
|
1541
|
+
)
|
|
1542
|
+
rows_html = "\n".join(row_html_parts)
|
|
1543
|
+
|
|
1544
|
+
if gran_key == "activity":
|
|
1545
|
+
table_header_html = (
|
|
1493
1546
|
"<tr>"
|
|
1494
|
-
f"<
|
|
1495
|
-
f"<
|
|
1496
|
-
f"<
|
|
1497
|
-
|
|
1547
|
+
f"<th>{html.escape(strings._('project'))}</th>"
|
|
1548
|
+
f"<th>{html.escape(strings._('activity'))}</th>"
|
|
1549
|
+
f"<th>{html.escape(strings._('hours'))}</th>"
|
|
1550
|
+
"</tr>"
|
|
1551
|
+
)
|
|
1552
|
+
else:
|
|
1553
|
+
table_header_html = (
|
|
1554
|
+
"<tr>"
|
|
1555
|
+
f"<th>{html.escape(strings._('project'))}</th>"
|
|
1556
|
+
f"<th>{html.escape(strings._('time_period'))}</th>"
|
|
1557
|
+
f"<th>{html.escape(strings._('activity'))}</th>"
|
|
1558
|
+
f"<th>{html.escape(strings._('hours'))}</th>"
|
|
1498
1559
|
"</tr>"
|
|
1499
1560
|
)
|
|
1500
|
-
rows_html = "\n".join(row_html_parts)
|
|
1501
1561
|
|
|
1502
1562
|
html_doc = f"""
|
|
1503
1563
|
<!DOCTYPE html>
|
|
@@ -1544,16 +1604,11 @@ class TimeReportDialog(QDialog):
|
|
|
1544
1604
|
<h1>{html.escape(strings._("time_log_report_title").format(project=project))}</h1>
|
|
1545
1605
|
<p class="meta">
|
|
1546
1606
|
{html.escape(strings._("time_log_report_meta").format(
|
|
1547
|
-
start=start, end=end, granularity=
|
|
1607
|
+
start=start, end=end, granularity=gran_label))}
|
|
1548
1608
|
</p>
|
|
1549
1609
|
<p><img src="chart" class="chart" /></p>
|
|
1550
1610
|
<table>
|
|
1551
|
-
|
|
1552
|
-
<th>{html.escape(strings._("project"))}</th>
|
|
1553
|
-
<th>{html.escape(strings._("time_period"))}</th>
|
|
1554
|
-
<th>{html.escape(strings._("activity"))}</th>
|
|
1555
|
-
<th>{html.escape(strings._("hours"))}</th>
|
|
1556
|
-
</tr>
|
|
1611
|
+
{table_header_html}
|
|
1557
1612
|
{rows_html}
|
|
1558
1613
|
</table>
|
|
1559
1614
|
<p><b>{html.escape(strings._("time_report_total").format(hours=total_hours))}</b></p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "bouquin"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.4"
|
|
4
4
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
|
5
5
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -12,7 +12,7 @@ include = ["bouquin/locales/*.json", "bouquin/keys/mig5.asc", "bouquin/fonts/Not
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
13
|
python = ">=3.10,<3.14"
|
|
14
14
|
pyside6 = ">=6.8.1,<7.0.0"
|
|
15
|
-
|
|
15
|
+
bouquin-sqlcipher4 = "^4.12.0"
|
|
16
16
|
requests = "^2.32.5"
|
|
17
17
|
markdown = "^3.10"
|
|
18
18
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|