Compare commits
7 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 886b809bd3 | |||
| e6010969cb | |||
| 492633df9f | |||
| dcb62d34af | |||
| 13b1ad7373 | |||
| 7abd99fe24 | |||
| 2112de39b8 |
16 changed files with 273 additions and 66 deletions
26
.pre-commit-config.yaml
Normal file
26
.pre-commit-config.yaml
Normal file
|
|
@ -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"]
|
||||
|
|
@ -1,3 +1,12 @@
|
|||
# 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)
|
||||
|
||||
# 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)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
"<tr>"
|
||||
f"<td>{html.escape(project)}</td>"
|
||||
f"<td>{html.escape(activity)}</td>"
|
||||
f"<td style='text-align:right'>{hours:.2f}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
else:
|
||||
for project, period, activity, _note, minutes in self._last_rows:
|
||||
hours = minutes / 60.0
|
||||
row_html_parts.append(
|
||||
"<tr>"
|
||||
f"<td>{html.escape(project)}</td>"
|
||||
f"<td>{html.escape(period)}</td>"
|
||||
f"<td>{html.escape(activity)}</td>"
|
||||
f"<td style='text-align:right'>{hours:.2f}</td>"
|
||||
"</tr>"
|
||||
)
|
||||
rows_html = "\n".join(row_html_parts)
|
||||
|
||||
if gran_key == "activity":
|
||||
table_header_html = (
|
||||
"<tr>"
|
||||
f"<td>{html.escape(project)}</td>"
|
||||
f"<td>{html.escape(period)}</td>"
|
||||
f"<td>{html.escape(activity)}</td>"
|
||||
f"<td style='text-align:right'>{hours:.2f}</td>"
|
||||
f"<th>{html.escape(strings._('project'))}</th>"
|
||||
f"<th>{html.escape(strings._('activity'))}</th>"
|
||||
f"<th>{html.escape(strings._('hours'))}</th>"
|
||||
"</tr>"
|
||||
)
|
||||
else:
|
||||
table_header_html = (
|
||||
"<tr>"
|
||||
f"<th>{html.escape(strings._('project'))}</th>"
|
||||
f"<th>{html.escape(strings._('time_period'))}</th>"
|
||||
f"<th>{html.escape(strings._('activity'))}</th>"
|
||||
f"<th>{html.escape(strings._('hours'))}</th>"
|
||||
"</tr>"
|
||||
)
|
||||
rows_html = "\n".join(row_html_parts)
|
||||
|
||||
html_doc = f"""
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -1544,16 +1604,11 @@ class TimeReportDialog(QDialog):
|
|||
<h1>{html.escape(strings._("time_log_report_title").format(project=project))}</h1>
|
||||
<p class="meta">
|
||||
{html.escape(strings._("time_log_report_meta").format(
|
||||
start=start, end=end, granularity=gran))}
|
||||
start=start, end=end, granularity=gran_label))}
|
||||
</p>
|
||||
<p><img src="chart" class="chart" /></p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>{html.escape(strings._("project"))}</th>
|
||||
<th>{html.escape(strings._("time_period"))}</th>
|
||||
<th>{html.escape(strings._("activity"))}</th>
|
||||
<th>{html.escape(strings._("hours"))}</th>
|
||||
</tr>
|
||||
{table_header_html}
|
||||
{rows_html}
|
||||
</table>
|
||||
<p><b>{html.escape(strings._("time_report_total").format(hours=total_hours))}</b></p>
|
||||
|
|
|
|||
6
poetry.lock
generated
6
poetry.lock
generated
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "bouquin"
|
||||
version = "0.7.1"
|
||||
version = "0.7.3"
|
||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
32
release.sh
32
release.sh
|
|
@ -2,6 +2,34 @@
|
|||
|
||||
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
|
||||
|
||||
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 .
|
||||
|
||||
|
|
@ -10,11 +38,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"
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue