From 2112de39b849496e2fddcf453cc679effd84e634 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 13 Dec 2025 10:39:49 +1100 Subject: [PATCH 1/7] Fix Manage Reminders dialog (the actions column was missing, to edit/delete reminders) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a74cc..9fe960b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.7.2 + + * Fix Manage Reminders dialog (the actions column was missing, to edit/delete reminders) + # 0.7.1 * Reduce the scope for toggling a checkbox on/off when not clicking precisely on it (must be to the left of the first letter) From 7abd99fe24585348df9180592a7aa5ae85152c81 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 13 Dec 2025 10:45:10 +1100 Subject: [PATCH 2/7] Bump to 0.7.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 61f8f05..d75bd29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.7.1" +version = "0.7.2" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" From 13b1ad73736ec32abfc62e9a3e1f20e2b258de85 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sat, 13 Dec 2025 10:48:10 +1100 Subject: [PATCH 3/7] Fix Manage Reminders dialog (the actions column was missing, to edit/delete reminders) --- bouquin/reminders.py | 2 +- release.sh | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/bouquin/reminders.py b/bouquin/reminders.py index fe5e031..6d8b0a1 100644 --- a/bouquin/reminders.py +++ b/bouquin/reminders.py @@ -706,7 +706,7 @@ class ManageRemindersDialog(QDialog): # Reminder list table self.table = QTableWidget() - self.table.setColumnCount(5) + self.table.setColumnCount(6) self.table.setHorizontalHeaderLabels( [ strings._("text"), diff --git a/release.sh b/release.sh index 9f8b3c8..f086e0c 100755 --- a/release.sh +++ b/release.sh @@ -2,6 +2,31 @@ set -eo pipefail +# Parse the args +while getopts "v:" OPTION +do + case $OPTION in + v) + VERSION=$OPTARG + ;; + ?) + usage + exit + ;; + esac +done + +if [[ -z "${VERSION}" ]]; then + echo "You forgot to pass -v [version]!" + exit 1 +fi + +sed -i s/version.*$/version\ =\ \"${VERSION}\"/g pyproject.toml + +git add pyproject.toml +git commit -m "Bump to ${VERSION}" +git push origin main + # Clean caches etc filedust -y . @@ -10,11 +35,11 @@ poetry build poetry publish # Make AppImage -sudo apt-get install libfuse-dev +sudo apt-get -y install libfuse-dev poetry run pyproject-appimage mv Bouquin.AppImage dist/ # Sign packages for file in `ls -1 dist/`; do qubes-gpg-client --batch --armor --detach-sign dist/$file > dist/$file.asc; done -echo "Don't forget to update version string on remote server." +ssh wolverine.mig5.net "echo ${VERSION} | tee /opt/www/mig5.net/bouquin/version.txt" From dcb62d34afed2c7c4d465366b648d9bcb339d9e6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 16 Dec 2025 15:15:38 +1100 Subject: [PATCH 4/7] Allow carrying unchecked TODOs to weekends. Add 'group by activity' in time log reports --- CHANGELOG.md | 5 ++ bouquin/db.py | 61 ++++++++++++++- bouquin/locales/en.json | 2 + bouquin/main_window.py | 13 +++- bouquin/settings.py | 5 ++ bouquin/settings_dialog.py | 20 +++++ bouquin/time_log.py | 149 +++++++++++++++++++++++++------------ pyproject.toml | 2 +- tests/test_time_log.py | 2 +- 9 files changed, 204 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fe960b..45edf09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# 0.7.3 + + * Allow optionally moving unchecked TODOs to the next day (even if it's the weekend) rather than next weekday. + * Add 'group by activity' in timesheet/invoice reports, rather than just by time period. + # 0.7.2 * Fix Manage Reminders dialog (the actions column was missing, to edit/delete reminders) diff --git a/bouquin/db.py b/bouquin/db.py index 3e4c388..f0d5b5f 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -92,6 +92,7 @@ class DBConfig: idle_minutes: int = 15 # 0 = never lock theme: str = "system" move_todos: bool = False + move_todos_include_weekends: bool = False tags: bool = True time_log: bool = True reminders: bool = True @@ -1351,7 +1352,7 @@ class DBManager: project_id: int, start_date_iso: str, end_date_iso: str, - granularity: str = "day", # 'day' | 'week' | 'month' | 'none' + granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none' ) -> list[tuple[str, str, str, int]]: """ Return (time_period, activity_name, total_minutes) tuples between start and end @@ -1360,7 +1361,8 @@ class DBManager: - 'YYYY-MM-DD' for day - 'YYYY-WW' for week - 'YYYY-MM' for month - For 'none' granularity, each individual time log entry becomes a row. + For 'activity' granularity, results are grouped by activity only (no time bucket). + For 'none' granularity, each individual time log entry becomes a row. """ cur = self.conn.cursor() @@ -1387,6 +1389,26 @@ class DBManager: for r in rows ] + if granularity == "activity": + rows = cur.execute( + """ + SELECT + a.name AS activity_name, + SUM(t.minutes) AS total_minutes + FROM time_log t + JOIN activities a ON a.id = t.activity_id + WHERE t.project_id = ? + AND t.page_date BETWEEN ? AND ? + GROUP BY activity_name + ORDER BY LOWER(activity_name); + """, + (project_id, start_date_iso, end_date_iso), + ).fetchall() + + # period column is unused for activity grouping in the UI, but we keep + # the tuple shape consistent. + return [("", r["activity_name"], "", r["total_minutes"]) for r in rows] + if granularity == "day": bucket_expr = "page_date" elif granularity == "week": @@ -1417,11 +1439,14 @@ class DBManager: self, start_date_iso: str, end_date_iso: str, - granularity: str = "day", # 'day' | 'week' | 'month' | 'none' + granularity: str = "day", # 'day' | 'week' | 'month' | 'activity' | 'none' ) -> list[tuple[str, str, str, str, int]]: """ Return (project_name, time_period, activity_name, note, total_minutes) - across *all* projects between start and end, grouped by project + period + activity. + across *all* projects between start and end. + - For 'day'/'week'/'month', grouped by project + period + activity. + - For 'activity', grouped by project + activity. + - For 'none', one row per time_log entry. """ cur = self.conn.cursor() @@ -1455,6 +1480,34 @@ class DBManager: for r in rows ] + if granularity == "activity": + rows = cur.execute( + """ + SELECT + p.name AS project_name, + a.name AS activity_name, + SUM(t.minutes) AS total_minutes + FROM time_log t + JOIN projects p ON p.id = t.project_id + JOIN activities a ON a.id = t.activity_id + WHERE t.page_date BETWEEN ? AND ? + GROUP BY p.id, activity_name + ORDER BY LOWER(p.name), LOWER(activity_name); + """, + (start_date_iso, end_date_iso), + ).fetchall() + + return [ + ( + r["project_name"], + "", + r["activity_name"], + "", + r["total_minutes"], + ) + for r in rows + ] + if granularity == "day": bucket_expr = "page_date" elif granularity == "week": diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index f0aed54..e8e0864 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -103,6 +103,7 @@ "autosave": "autosave", "unchecked_checkbox_items_moved_to_next_day": "Unchecked checkbox items moved to next day", "move_unchecked_todos_to_today_on_startup": "Automatically move unchecked TODOs\nfrom the last 7 days to next weekday", + "move_todos_include_weekends": "Allow moving unchecked TODOs to a weekend\nrather than next weekday", "insert_images": "Insert images", "images": "Images", "reopen_failed": "Re-open failed", @@ -209,6 +210,7 @@ "add_time_entry": "Add time entry", "time_period": "Time period", "dont_group": "Don't group", + "by_activity": "by activity", "by_day": "by day", "by_month": "by month", "by_week": "by week", diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 89bc9a9..9b812b4 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -822,9 +822,13 @@ class MainWindow(QMainWindow): Given a 'new day' (system date), return the date we should move unfinished todos *to*. - If the new day is Saturday or Sunday, we skip ahead to the next Monday. - Otherwise we just return the same day. + By default, if the new day is Saturday or Sunday we skip ahead to the + next Monday (i.e., "next available weekday"). If the optional setting + `move_todos_include_weekends` is enabled, we move to the very next day + even if it's a weekend. """ + if getattr(self.cfg, "move_todos_include_weekends", False): + return day # Qt: Monday=1 ... Sunday=7 dow = day.dayOfWeek() if dow >= 6: # Saturday (6) or Sunday (7) @@ -1566,6 +1570,11 @@ class MainWindow(QMainWindow): self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes) self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme) self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos) + self.cfg.move_todos_include_weekends = getattr( + new_cfg, + "move_todos_include_weekends", + getattr(self.cfg, "move_todos_include_weekends", False), + ) self.cfg.tags = getattr(new_cfg, "tags", self.cfg.tags) self.cfg.time_log = getattr(new_cfg, "time_log", self.cfg.time_log) self.cfg.reminders = getattr(new_cfg, "reminders", self.cfg.reminders) diff --git a/bouquin/settings.py b/bouquin/settings.py index 0c5b614..fde863d 100644 --- a/bouquin/settings.py +++ b/bouquin/settings.py @@ -42,6 +42,9 @@ def load_db_config() -> DBConfig: idle = s.value("ui/idle_minutes", 15, type=int) theme = s.value("ui/theme", "system", type=str) move_todos = s.value("ui/move_todos", False, type=bool) + move_todos_include_weekends = s.value( + "ui/move_todos_include_weekends", False, type=bool + ) tags = s.value("ui/tags", True, type=bool) time_log = s.value("ui/time_log", True, type=bool) reminders = s.value("ui/reminders", True, type=bool) @@ -57,6 +60,7 @@ def load_db_config() -> DBConfig: idle_minutes=idle, theme=theme, move_todos=move_todos, + move_todos_include_weekends=move_todos_include_weekends, tags=tags, time_log=time_log, reminders=reminders, @@ -76,6 +80,7 @@ def save_db_config(cfg: DBConfig) -> None: s.setValue("ui/idle_minutes", str(cfg.idle_minutes)) s.setValue("ui/theme", str(cfg.theme)) s.setValue("ui/move_todos", str(cfg.move_todos)) + s.setValue("ui/move_todos_include_weekends", str(cfg.move_todos_include_weekends)) s.setValue("ui/tags", str(cfg.tags)) s.setValue("ui/time_log", str(cfg.time_log)) s.setValue("ui/reminders", str(cfg.reminders)) diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index 8835493..bec0627 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -169,6 +169,25 @@ class SettingsDialog(QDialog): self.move_todos.setCursor(Qt.PointingHandCursor) features_layout.addWidget(self.move_todos) + # Optional: allow moving to the very next day even if it is a weekend. + self.move_todos_include_weekends = QCheckBox( + strings._("move_todos_include_weekends") + ) + self.move_todos_include_weekends.setChecked( + getattr(self.current_settings, "move_todos_include_weekends", False) + ) + self.move_todos_include_weekends.setCursor(Qt.PointingHandCursor) + self.move_todos_include_weekends.setEnabled(self.move_todos.isChecked()) + + move_todos_opts = QWidget() + move_todos_opts_layout = QVBoxLayout(move_todos_opts) + move_todos_opts_layout.setContentsMargins(24, 0, 0, 0) + move_todos_opts_layout.setSpacing(4) + move_todos_opts_layout.addWidget(self.move_todos_include_weekends) + features_layout.addWidget(move_todos_opts) + + self.move_todos.toggled.connect(self.move_todos_include_weekends.setEnabled) + self.tags = QCheckBox(strings._("enable_tags_feature")) self.tags.setChecked(self.current_settings.tags) self.tags.setCursor(Qt.PointingHandCursor) @@ -441,6 +460,7 @@ class SettingsDialog(QDialog): idle_minutes=self.idle_spin.value(), theme=selected_theme.value, move_todos=self.move_todos.isChecked(), + move_todos_include_weekends=self.move_todos_include_weekends.isChecked(), tags=self.tags.isChecked(), time_log=self.time_log.isChecked(), reminders=self.reminders.isChecked(), diff --git a/bouquin/time_log.py b/bouquin/time_log.py index 1adf3c3..05d7e98 100644 --- a/bouquin/time_log.py +++ b/bouquin/time_log.py @@ -1083,6 +1083,7 @@ class TimeReportDialog(QDialog): self.granularity.addItem(strings._("by_day"), "day") self.granularity.addItem(strings._("by_week"), "week") self.granularity.addItem(strings._("by_month"), "month") + self.granularity.addItem(strings._("by_activity"), "activity") form.addRow(strings._("group_by"), self.granularity) root.addLayout(form) @@ -1161,6 +1162,20 @@ class TimeReportDialog(QDialog): header.setSectionResizeMode(2, QHeaderView.Stretch) header.setSectionResizeMode(3, QHeaderView.Stretch) header.setSectionResizeMode(4, QHeaderView.ResizeToContents) + elif granularity == "activity": + # Grouped by activity only: no time period, no note column + self.table.setColumnCount(3) + self.table.setHorizontalHeaderLabels( + [ + strings._("project"), + strings._("activity"), + strings._("hours"), + ] + ) + header = self.table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.Stretch) + header.setSectionResizeMode(1, QHeaderView.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeToContents) else: # Grouped: no note column self.table.setColumnCount(4) @@ -1272,16 +1287,21 @@ class TimeReportDialog(QDialog): rows_for_table ): hrs = minutes / 60.0 - self.table.setItem(i, 0, QTableWidgetItem(project)) - self.table.setItem(i, 1, QTableWidgetItem(time_period)) - self.table.setItem(i, 2, QTableWidgetItem(activity_name)) - - if self._last_gran == "none": - self.table.setItem(i, 3, QTableWidgetItem(note or "")) - self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}")) + if self._last_gran == "activity": + self.table.setItem(i, 0, QTableWidgetItem(project)) + self.table.setItem(i, 1, QTableWidgetItem(activity_name)) + self.table.setItem(i, 2, QTableWidgetItem(f"{hrs:.2f}")) else: - # no note column - self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}")) + self.table.setItem(i, 0, QTableWidgetItem(project)) + self.table.setItem(i, 1, QTableWidgetItem(time_period)) + self.table.setItem(i, 2, QTableWidgetItem(activity_name)) + + if self._last_gran == "none": + self.table.setItem(i, 3, QTableWidgetItem(note or "")) + self.table.setItem(i, 4, QTableWidgetItem(f"{hrs:.2f}")) + else: + # no note column + self.table.setItem(i, 3, QTableWidgetItem(f"{hrs:.2f}")) # Summary label - include per-project totals when in "all projects" mode total_hours = self._last_total_minutes / 60.0 @@ -1325,14 +1345,15 @@ class TimeReportDialog(QDialog): with open(filename, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) - show_note = getattr(self, "_last_gran", "day") == "none" + gran = getattr(self, "_last_gran", "day") + show_note = gran == "none" + show_period = gran != "activity" # Header - header = [ - strings._("project"), - strings._("time_period"), - strings._("activity"), - ] + header: list[str] = [strings._("project")] + if show_period: + header.append(strings._("time_period")) + header.append(strings._("activity")) if show_note: header.append(strings._("note")) header.append(strings._("hours")) @@ -1347,16 +1368,22 @@ class TimeReportDialog(QDialog): minutes, ) in self._last_rows: hours = minutes / 60.0 - row = [project, time_period, activity_name] + row: list[str] = [project] + if show_period: + row.append(time_period) + row.append(activity_name) if show_note: - row.append(note) + row.append(note or "") row.append(f"{hours:.2f}") writer.writerow(row) # Blank line + total total_hours = self._last_total_minutes / 60.0 writer.writerow([]) - writer.writerow([strings._("total"), "", f"{total_hours:.2f}"]) + total_row = [""] * len(header) + total_row[0] = strings._("total") + total_row[-1] = f"{total_hours:.2f}" + writer.writerow(total_row) except OSError as exc: QMessageBox.warning( self, @@ -1384,17 +1411,20 @@ class TimeReportDialog(QDialog): if not filename.endswith(".pdf"): filename = f"{filename}.pdf" - # ---------- Build chart image (hours per period) ---------- - per_period_minutes: dict[str, int] = defaultdict(int) - for _project, period, _activity, note, minutes in self._last_rows: - per_period_minutes[period] += minutes + # ---------- Build chart image ---------- + # Default: hours per time period. If grouped by activity: hours per activity. + gran = getattr(self, "_last_gran", "day") + per_bucket_minutes: dict[str, int] = defaultdict(int) + for _project, period, activity, _note, minutes in self._last_rows: + bucket = activity if gran == "activity" else period + per_bucket_minutes[bucket] += minutes - periods = sorted(per_period_minutes.keys()) + buckets = sorted(per_bucket_minutes.keys()) chart_w, chart_h = 800, 220 chart = QImage(chart_w, chart_h, QImage.Format_ARGB32) chart.fill(Qt.white) - if periods: + if buckets: painter = QPainter(chart) try: painter.setRenderHint(QPainter.Antialiasing, True) @@ -1422,9 +1452,9 @@ class TimeReportDialog(QDialog): # Border painter.drawRect(left, top, width, height) - max_hours = max(per_period_minutes[p] for p in periods) / 60.0 + max_hours = max(per_bucket_minutes[p] for p in buckets) / 60.0 if max_hours > 0: - n = len(periods) + n = len(buckets) bar_spacing = width / max(1, n) bar_width = bar_spacing * 0.6 @@ -1449,8 +1479,8 @@ class TimeReportDialog(QDialog): painter.setBrush(QColor(80, 140, 200)) painter.setPen(Qt.NoPen) - for i, period in enumerate(periods): - hours = per_period_minutes[period] / 60.0 + for i, label in enumerate(buckets): + hours = per_bucket_minutes[label] / 60.0 bar_h = int((hours / max_hours) * (height - 10)) if bar_h <= 0: continue # pragma: no cover @@ -1463,7 +1493,7 @@ class TimeReportDialog(QDialog): # X labels after bars, in black painter.setPen(Qt.black) - for i, period in enumerate(periods): + for i, label in enumerate(buckets): x_center = left + bar_spacing * (i + 0.5) x = int(x_center - bar_width / 2) painter.drawText( @@ -1472,7 +1502,7 @@ class TimeReportDialog(QDialog): int(bar_width), 20, Qt.AlignHCenter | Qt.AlignTop, - period, + label, ) finally: painter.end() @@ -1481,23 +1511,53 @@ class TimeReportDialog(QDialog): project = html.escape(self._last_project_name or "") start = html.escape(self._last_start or "") end = html.escape(self._last_end or "") - gran = html.escape(self._last_gran_label or "") + gran_key = getattr(self, "_last_gran", "day") + gran_label = html.escape(self._last_gran_label or "") total_hours = self._last_total_minutes / 60.0 - # Table rows (period, activity, hours) + # Table rows row_html_parts: list[str] = [] - for project, period, activity, note, minutes in self._last_rows: - hours = minutes / 60.0 - row_html_parts.append( + if gran_key == "activity": + for project, _period, activity, _note, minutes in self._last_rows: + hours = minutes / 60.0 + row_html_parts.append( + "" + f"{html.escape(project)}" + f"{html.escape(activity)}" + f"{hours:.2f}" + "" + ) + else: + for project, period, activity, _note, minutes in self._last_rows: + hours = minutes / 60.0 + row_html_parts.append( + "" + f"{html.escape(project)}" + f"{html.escape(period)}" + f"{html.escape(activity)}" + f"{hours:.2f}" + "" + ) + rows_html = "\n".join(row_html_parts) + + if gran_key == "activity": + table_header_html = ( "" - f"{html.escape(project)}" - f"{html.escape(period)}" - f"{html.escape(activity)}" - f"{hours:.2f}" + f"{html.escape(strings._('project'))}" + f"{html.escape(strings._('activity'))}" + f"{html.escape(strings._('hours'))}" + "" + ) + else: + table_header_html = ( + "" + f"{html.escape(strings._('project'))}" + f"{html.escape(strings._('time_period'))}" + f"{html.escape(strings._('activity'))}" + f"{html.escape(strings._('hours'))}" "" ) - rows_html = "\n".join(row_html_parts) html_doc = f""" @@ -1544,16 +1604,11 @@ class TimeReportDialog(QDialog):

{html.escape(strings._("time_log_report_title").format(project=project))}

{html.escape(strings._("time_log_report_meta").format( - start=start, end=end, granularity=gran))} + start=start, end=end, granularity=gran_label))}

- - - - - - + {table_header_html} {rows_html}
{html.escape(strings._("project"))}{html.escape(strings._("time_period"))}{html.escape(strings._("activity"))}{html.escape(strings._("hours"))}

{html.escape(strings._("time_report_total").format(hours=total_hours))}

diff --git a/pyproject.toml b/pyproject.toml index d75bd29..521e3d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "bouquin" -version = "0.7.2" +version = "0.7.3" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" diff --git a/tests/test_time_log.py b/tests/test_time_log.py index 0a6797c..ff1d159 100644 --- a/tests/test_time_log.py +++ b/tests/test_time_log.py @@ -1185,7 +1185,7 @@ def test_time_report_dialog_creation(qtbot, fresh_db): qtbot.addWidget(dialog) assert dialog.project_combo.count() == 1 - assert dialog.granularity.count() == 4 + assert dialog.granularity.count() == 5 def test_time_report_dialog_loads_projects(qtbot, fresh_db): From 492633df9fa399f06d5fe7ff258df0f48898d3f6 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 16 Dec 2025 15:17:22 +1100 Subject: [PATCH 5/7] Update urllib3 --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index addf793..115621c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -747,13 +747,13 @@ files = [ [[package]] name = "urllib3" -version = "2.6.1" +version = "2.6.2" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" files = [ - {file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"}, - {file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"}, + {file = "urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd"}, + {file = "urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797"}, ] [package.extras] From e6010969cb698f91c69399ca4c7059947e38a50d Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 16 Dec 2025 15:28:24 +1100 Subject: [PATCH 6/7] Don't block on pyproject modification if the version has already been bumped --- release.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/release.sh b/release.sh index f086e0c..f416455 100755 --- a/release.sh +++ b/release.sh @@ -21,12 +21,15 @@ if [[ -z "${VERSION}" ]]; then exit 1 fi +set +e sed -i s/version.*$/version\ =\ \"${VERSION}\"/g pyproject.toml git add pyproject.toml git commit -m "Bump to ${VERSION}" git push origin main +set -e + # Clean caches etc filedust -y . From 886b809bd3ae640042b59aa6cb8e101a6daf2a5a Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Thu, 18 Dec 2025 13:48:42 +1100 Subject: [PATCH 7/7] Add pre-commit, fix trailing whitespace --- .pre-commit-config.yaml | 26 ++++++++++++++++++++++++++ bouquin/fonts/DejaVu.license | 2 +- bouquin/fonts/Noto.license | 2 +- bouquin/locales/en.json | 4 ++-- tests/test_markdown_editor.py | 2 +- 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6281daa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +repos: + - repo: https://github.com/pycqa/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + args: ["--select=F"] + types: [python] + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.11.0 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + + - repo: https://github.com/PyCQA/bandit + rev: 1.9.2 + hooks: + - id: bandit + files: ^bouquin/ + args: ["-s", "B110"] diff --git a/bouquin/fonts/DejaVu.license b/bouquin/fonts/DejaVu.license index df52c17..8d71958 100644 --- a/bouquin/fonts/DejaVu.license +++ b/bouquin/fonts/DejaVu.license @@ -74,7 +74,7 @@ Fonts, only if the fonts are renamed to names not containing either the words "Tavmjong Bah" or the word "Arev". This License becomes null and void to the extent applicable to Fonts -or Font Software that has been modified and is distributed under the +or Font Software that has been modified and is distributed under the "Tavmjong Bah Arev" names. The Font Software may be sold as part of a larger software package but diff --git a/bouquin/fonts/Noto.license b/bouquin/fonts/Noto.license index 106e5d8..c37cc47 100644 --- a/bouquin/fonts/Noto.license +++ b/bouquin/fonts/Noto.license @@ -18,7 +18,7 @@ with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, +fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json index e8e0864..6c13e42 100644 --- a/bouquin/locales/en.json +++ b/bouquin/locales/en.json @@ -172,7 +172,7 @@ "stats_metric_revisions": "Revisions", "stats_metric_documents": "Documents", "stats_total_documents": "Total documents", - "stats_date_most_documents": "Date with most documents", + "stats_date_most_documents": "Date with most documents", "stats_no_data": "No statistics available yet.", "stats_time_total_hours": "Total hours logged", "stats_time_day_most_hours": "Day with most hours logged", @@ -377,7 +377,7 @@ "documents_missing_file": "The file does not exist:\n{path}", "documents_confirm_delete": "Remove this document from the project?\n(The file on disk will not be deleted.)", "documents_search_label": "Search", - "documents_search_placeholder": "Type to search documents (all projects)", + "documents_search_placeholder": "Type to search documents (all projects)", "todays_documents": "Documents from this day", "todays_documents_none": "No documents yet.", "manage_invoices": "Manage Invoices", diff --git a/tests/test_markdown_editor.py b/tests/test_markdown_editor.py index 73f58f4..dcacbc5 100644 --- a/tests/test_markdown_editor.py +++ b/tests/test_markdown_editor.py @@ -1574,7 +1574,7 @@ def test_markdown_highlighter_special_characters(qtbot, app): highlighter = MarkdownHighlighter(doc, theme_manager) text = """ -Special chars: < > & " ' +Special chars: < > & " ' Escaped: \\* \\_ \\` Unicode: 你好 café résumé """