Improvements to StatisticsDialog
It now shows statistics about logged time, reminders, etc. Sections are grouped for better readability. Improvements to Manage Reminders dialog to show date of alarm
This commit is contained in:
parent
3106d408ab
commit
206670454f
9 changed files with 438 additions and 91 deletions
|
|
@ -5,6 +5,7 @@
|
|||
* Invoicing should not be enabled by default
|
||||
* Fix Reminders to fire right on the minute after adding them during runtime
|
||||
* It is now possible to set up Webhooks for Reminders! A URL and a secret value (sent as X-Bouquin-Header) can be set in the Settings.
|
||||
* Improvements to StatisticsDialog: it now shows statistics about logged time, reminders, etc. Sections are grouped for better readability
|
||||
|
||||
# 0.7.0
|
||||
|
||||
|
|
|
|||
130
bouquin/db.py
130
bouquin/db.py
|
|
@ -95,6 +95,8 @@ class DBConfig:
|
|||
tags: bool = True
|
||||
time_log: bool = True
|
||||
reminders: bool = True
|
||||
reminders_webhook_url: str = (None,)
|
||||
reminders_webhook_secret: str = (None,)
|
||||
documents: bool = True
|
||||
invoicing: bool = False
|
||||
locale: str = "en"
|
||||
|
|
@ -971,7 +973,7 @@ class DBManager:
|
|||
|
||||
# 2 & 3) total revisions + page with most revisions + per-date counts
|
||||
total_revisions = 0
|
||||
page_most_revisions = None
|
||||
page_most_revisions: str | None = None
|
||||
page_most_revisions_count = 0
|
||||
revisions_by_date: Dict[_dt.date, int] = {}
|
||||
|
||||
|
|
@ -1008,7 +1010,6 @@ class DBManager:
|
|||
words_by_date[d] = wc
|
||||
|
||||
# tags + page with most tags
|
||||
|
||||
rows = cur.execute("SELECT COUNT(*) AS total_unique FROM tags;").fetchall()
|
||||
unique_tags = int(rows[0]["total_unique"]) if rows else 0
|
||||
|
||||
|
|
@ -1029,6 +1030,119 @@ class DBManager:
|
|||
page_most_tags = None
|
||||
page_most_tags_count = 0
|
||||
|
||||
# 5) Time logging stats (minutes / hours)
|
||||
time_minutes_by_date: Dict[_dt.date, int] = {}
|
||||
total_time_minutes = 0
|
||||
day_most_time: str | None = None
|
||||
day_most_time_minutes = 0
|
||||
|
||||
try:
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT page_date, SUM(minutes) AS total_minutes
|
||||
FROM time_log
|
||||
GROUP BY page_date
|
||||
ORDER BY page_date;
|
||||
"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
for r in rows:
|
||||
date_iso = r["page_date"]
|
||||
if not date_iso:
|
||||
continue
|
||||
m = int(r["total_minutes"] or 0)
|
||||
total_time_minutes += m
|
||||
if m > day_most_time_minutes:
|
||||
day_most_time_minutes = m
|
||||
day_most_time = date_iso
|
||||
try:
|
||||
d = _dt.date.fromisoformat(date_iso)
|
||||
except Exception: # nosec B112
|
||||
continue
|
||||
time_minutes_by_date[d] = m
|
||||
|
||||
# Project with most logged time
|
||||
project_most_minutes_name: str | None = None
|
||||
project_most_minutes = 0
|
||||
|
||||
try:
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT p.name AS project_name,
|
||||
SUM(t.minutes) AS total_minutes
|
||||
FROM time_log t
|
||||
JOIN projects p ON p.id = t.project_id
|
||||
GROUP BY t.project_id, p.name
|
||||
ORDER BY total_minutes DESC, LOWER(project_name) ASC
|
||||
LIMIT 1;
|
||||
"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
if rows:
|
||||
project_most_minutes_name = rows[0]["project_name"]
|
||||
project_most_minutes = int(rows[0]["total_minutes"] or 0)
|
||||
|
||||
# Activity with most logged time
|
||||
activity_most_minutes_name: str | None = None
|
||||
activity_most_minutes = 0
|
||||
|
||||
try:
|
||||
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
|
||||
GROUP BY t.activity_id, a.name
|
||||
ORDER BY total_minutes DESC, LOWER(activity_name) ASC
|
||||
LIMIT 1;
|
||||
"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
if rows:
|
||||
activity_most_minutes_name = rows[0]["activity_name"]
|
||||
activity_most_minutes = int(rows[0]["total_minutes"] or 0)
|
||||
|
||||
# 6) Reminder stats
|
||||
reminders_by_date: Dict[_dt.date, int] = {}
|
||||
total_reminders = 0
|
||||
day_most_reminders: str | None = None
|
||||
day_most_reminders_count = 0
|
||||
|
||||
try:
|
||||
rows = cur.execute(
|
||||
"""
|
||||
SELECT substr(created_at, 1, 10) AS date_iso,
|
||||
COUNT(*) AS c
|
||||
FROM reminders
|
||||
GROUP BY date_iso
|
||||
ORDER BY date_iso;
|
||||
"""
|
||||
).fetchall()
|
||||
except Exception:
|
||||
rows = []
|
||||
|
||||
for r in rows:
|
||||
date_iso = r["date_iso"]
|
||||
if not date_iso:
|
||||
continue
|
||||
c = int(r["c"] or 0)
|
||||
total_reminders += c
|
||||
if c > day_most_reminders_count:
|
||||
day_most_reminders_count = c
|
||||
day_most_reminders = date_iso
|
||||
try:
|
||||
d = _dt.date.fromisoformat(date_iso)
|
||||
except Exception: # nosec B112
|
||||
continue
|
||||
reminders_by_date[d] = c
|
||||
|
||||
return (
|
||||
pages_with_content,
|
||||
total_revisions,
|
||||
|
|
@ -1040,6 +1154,18 @@ class DBManager:
|
|||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
time_minutes_by_date,
|
||||
total_time_minutes,
|
||||
day_most_time,
|
||||
day_most_time_minutes,
|
||||
project_most_minutes_name,
|
||||
project_most_minutes,
|
||||
activity_most_minutes_name,
|
||||
activity_most_minutes,
|
||||
reminders_by_date,
|
||||
total_reminders,
|
||||
day_most_reminders,
|
||||
day_most_reminders_count,
|
||||
)
|
||||
|
||||
# -------- Time logging: projects & activities ---------------------
|
||||
|
|
|
|||
|
|
@ -154,6 +154,11 @@
|
|||
"tag_already_exists_with_that_name": "A tag already exists with that name",
|
||||
"statistics": "Statistics",
|
||||
"main_window_statistics_accessible_flag": "Stat&istics",
|
||||
"stats_group_pages": "Pages",
|
||||
"stats_group_tags": "Tags",
|
||||
"stats_group_documents": "Documents",
|
||||
"stats_group_time_logging": "Time logging",
|
||||
"stats_group_reminders": "Reminders",
|
||||
"stats_pages_with_content": "Pages with content (current version)",
|
||||
"stats_total_revisions": "Total revisions",
|
||||
"stats_page_most_revisions": "Page with most revisions",
|
||||
|
|
@ -168,6 +173,14 @@
|
|||
"stats_total_documents": "Total 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",
|
||||
"stats_time_project_most_hours": "Project with most hours logged",
|
||||
"stats_time_activity_most_hours": "Activity with most hours logged",
|
||||
"stats_total_reminders": "Total reminders",
|
||||
"stats_date_most_reminders": "Day with most reminders",
|
||||
"stats_metric_hours": "Hours",
|
||||
"stats_metric_reminders": "Reminders",
|
||||
"select_notebook": "Select notebook",
|
||||
"bug_report_explanation": "Describe what went wrong, what you expected to happen, and any steps to reproduce.\n\nWe do not collect anything else except the Bouquin version number.\n\nIf you wish to be contacted, please leave contact information.\n\nYour request will be sent over HTTPS.",
|
||||
"bug_report_placeholder": "Type your bug report here",
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtCore import QDate, QDateTime, Qt, QTime, QTimer, Signal, Slot, QObject
|
||||
from PySide6.QtCore import QDate, QDateTime, Qt, QTime, QTimer, Signal, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QComboBox,
|
||||
|
|
@ -710,6 +710,7 @@ class ManageRemindersDialog(QDialog):
|
|||
self.table.setHorizontalHeaderLabels(
|
||||
[
|
||||
strings._("text"),
|
||||
strings._("date"),
|
||||
strings._("time"),
|
||||
strings._("type"),
|
||||
strings._("active"),
|
||||
|
|
@ -755,12 +756,24 @@ class ManageRemindersDialog(QDialog):
|
|||
text_item.setData(Qt.UserRole, reminder)
|
||||
self.table.setItem(row, 0, text_item)
|
||||
|
||||
# Date
|
||||
date_display = ""
|
||||
if reminder.reminder_type == ReminderType.ONCE and reminder.date_iso:
|
||||
d = QDate.fromString(reminder.date_iso, "yyyy-MM-dd")
|
||||
if d.isValid():
|
||||
date_display = d.toString("yyyy-MM-dd")
|
||||
else:
|
||||
date_display = reminder.date_iso
|
||||
|
||||
date_item = QTableWidgetItem(date_display)
|
||||
self.table.setItem(row, 1, date_item)
|
||||
|
||||
# Time
|
||||
time_item = QTableWidgetItem(reminder.time_str)
|
||||
self.table.setItem(row, 1, time_item)
|
||||
self.table.setItem(row, 2, time_item)
|
||||
|
||||
# Type
|
||||
type_str = {
|
||||
base_type_strs = {
|
||||
ReminderType.ONCE: "Once",
|
||||
ReminderType.DAILY: "Daily",
|
||||
ReminderType.WEEKDAYS: "Weekdays",
|
||||
|
|
@ -768,19 +781,11 @@ class ManageRemindersDialog(QDialog):
|
|||
ReminderType.FORTNIGHTLY: "Fortnightly",
|
||||
ReminderType.MONTHLY_DATE: "Monthly (date)",
|
||||
ReminderType.MONTHLY_NTH_WEEKDAY: "Monthly (nth weekday)",
|
||||
}.get(reminder.reminder_type, "Unknown")
|
||||
}
|
||||
type_str = base_type_strs.get(reminder.reminder_type, "Unknown")
|
||||
|
||||
# Add day-of-week annotation where it makes sense
|
||||
if (
|
||||
reminder.reminder_type
|
||||
in (
|
||||
ReminderType.WEEKLY,
|
||||
ReminderType.FORTNIGHTLY,
|
||||
ReminderType.MONTHLY_NTH_WEEKDAY,
|
||||
)
|
||||
and reminder.weekday is not None
|
||||
):
|
||||
days = [
|
||||
# Short day names we can reuse
|
||||
days_short = [
|
||||
strings._("monday_short"),
|
||||
strings._("tuesday_short"),
|
||||
strings._("wednesday_short"),
|
||||
|
|
@ -789,14 +794,50 @@ class ManageRemindersDialog(QDialog):
|
|||
strings._("saturday_short"),
|
||||
strings._("sunday_short"),
|
||||
]
|
||||
type_str += f" ({days[reminder.weekday]})"
|
||||
|
||||
if reminder.reminder_type == ReminderType.MONTHLY_NTH_WEEKDAY:
|
||||
# Show something like: Monthly (3rd Mon)
|
||||
day_name = ""
|
||||
if reminder.weekday is not None and 0 <= reminder.weekday < len(
|
||||
days_short
|
||||
):
|
||||
day_name = days_short[reminder.weekday]
|
||||
|
||||
nth_label = ""
|
||||
if reminder.date_iso:
|
||||
anchor = QDate.fromString(reminder.date_iso, "yyyy-MM-dd")
|
||||
if anchor.isValid():
|
||||
nth_index = (anchor.day() - 1) // 7 # 0-based (0..4)
|
||||
ordinals = ["1st", "2nd", "3rd", "4th", "5th"]
|
||||
if 0 <= nth_index < len(ordinals):
|
||||
nth_label = ordinals[nth_index]
|
||||
|
||||
parts = []
|
||||
if nth_label:
|
||||
parts.append(nth_label)
|
||||
if day_name:
|
||||
parts.append(day_name)
|
||||
|
||||
if parts:
|
||||
type_str = f"Monthly ({' '.join(parts)})"
|
||||
# else: fall back to the generic "Monthly (nth weekday)"
|
||||
|
||||
else:
|
||||
# For weekly / fortnightly types, still append the day name
|
||||
if (
|
||||
reminder.reminder_type
|
||||
in (ReminderType.WEEKLY, ReminderType.FORTNIGHTLY)
|
||||
and reminder.weekday is not None
|
||||
and 0 <= reminder.weekday < len(days_short)
|
||||
):
|
||||
type_str += f" ({days_short[reminder.weekday]})"
|
||||
|
||||
type_item = QTableWidgetItem(type_str)
|
||||
self.table.setItem(row, 2, type_item)
|
||||
self.table.setItem(row, 3, type_item)
|
||||
|
||||
# Active
|
||||
active_item = QTableWidgetItem("✓" if reminder.active else "✗")
|
||||
self.table.setItem(row, 3, active_item)
|
||||
self.table.setItem(row, 4, active_item)
|
||||
|
||||
# Actions
|
||||
actions_widget = QWidget()
|
||||
|
|
@ -813,7 +854,7 @@ class ManageRemindersDialog(QDialog):
|
|||
)
|
||||
actions_layout.addWidget(delete_btn)
|
||||
|
||||
self.table.setCellWidget(row, 4, actions_widget)
|
||||
self.table.setCellWidget(row, 5, actions_widget)
|
||||
|
||||
def _add_reminder(self):
|
||||
"""Add a new reminder."""
|
||||
|
|
@ -865,7 +906,7 @@ class ReminderWebHook:
|
|||
|
||||
if url:
|
||||
try:
|
||||
resp = requests.post(
|
||||
requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
timeout=10,
|
||||
|
|
|
|||
|
|
@ -248,8 +248,9 @@ class StatisticsDialog(QDialog):
|
|||
self._db = db
|
||||
|
||||
self.setWindowTitle(strings._("statistics"))
|
||||
self.setMinimumWidth(600)
|
||||
self.setMinimumHeight(400)
|
||||
self.setMinimumWidth(650)
|
||||
self.setMinimumHeight(650)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
|
||||
(
|
||||
|
|
@ -263,12 +264,23 @@ class StatisticsDialog(QDialog):
|
|||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
time_minutes_by_date,
|
||||
total_time_minutes,
|
||||
day_most_time,
|
||||
day_most_time_minutes,
|
||||
project_most_minutes_name,
|
||||
project_most_minutes,
|
||||
activity_most_minutes_name,
|
||||
activity_most_minutes,
|
||||
reminders_by_date,
|
||||
total_reminders,
|
||||
day_most_reminders,
|
||||
day_most_reminders_count,
|
||||
) = self._gather_stats()
|
||||
|
||||
# Optional: per-date document counts for the heatmap.
|
||||
# This uses project_documents.uploaded_at aggregated by day, if the
|
||||
# Documents feature is enabled.
|
||||
self.cfg = load_db_config()
|
||||
|
||||
# Optional: per-date document counts for the heatmap.
|
||||
documents_by_date: Dict[_dt.date, int] = {}
|
||||
total_documents = 0
|
||||
date_most_documents: _dt.date | None = None
|
||||
|
|
@ -288,68 +300,176 @@ class StatisticsDialog(QDialog):
|
|||
key=lambda item: (-item[1], item[0]),
|
||||
)[0]
|
||||
|
||||
# for the heatmap
|
||||
# For the heatmap
|
||||
self._documents_by_date = documents_by_date
|
||||
self._time_by_date = time_minutes_by_date
|
||||
self._reminders_by_date = reminders_by_date
|
||||
self._words_by_date = words_by_date
|
||||
self._revisions_by_date = revisions_by_date
|
||||
|
||||
# --- Numeric summary at the top ----------------------------------
|
||||
form = QFormLayout()
|
||||
root.addLayout(form)
|
||||
# ------------------------------------------------------------------
|
||||
# Feature groups
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
form.addRow(
|
||||
# --- Pages / words / revisions -----------------------------------
|
||||
pages_group = QGroupBox(strings._("stats_group_pages"))
|
||||
pages_form = QFormLayout(pages_group)
|
||||
|
||||
pages_form.addRow(
|
||||
strings._("stats_pages_with_content"),
|
||||
QLabel(str(pages_with_content)),
|
||||
)
|
||||
form.addRow(
|
||||
pages_form.addRow(
|
||||
strings._("stats_total_revisions"),
|
||||
QLabel(str(total_revisions)),
|
||||
)
|
||||
|
||||
if page_most_revisions:
|
||||
form.addRow(
|
||||
pages_form.addRow(
|
||||
strings._("stats_page_most_revisions"),
|
||||
QLabel(f"{page_most_revisions} ({page_most_revisions_count})"),
|
||||
)
|
||||
else:
|
||||
form.addRow(strings._("stats_page_most_revisions"), QLabel("—"))
|
||||
pages_form.addRow(
|
||||
strings._("stats_page_most_revisions"),
|
||||
QLabel("—"),
|
||||
)
|
||||
|
||||
form.addRow(
|
||||
pages_form.addRow(
|
||||
strings._("stats_total_words"),
|
||||
QLabel(str(total_words)),
|
||||
)
|
||||
|
||||
# Tags
|
||||
root.addWidget(pages_group)
|
||||
|
||||
# --- Tags ---------------------------------------------------------
|
||||
if self.cfg.tags:
|
||||
form.addRow(
|
||||
tags_group = QGroupBox(strings._("stats_group_tags"))
|
||||
tags_form = QFormLayout(tags_group)
|
||||
|
||||
tags_form.addRow(
|
||||
strings._("stats_unique_tags"),
|
||||
QLabel(str(unique_tags)),
|
||||
)
|
||||
|
||||
if page_most_tags:
|
||||
form.addRow(
|
||||
tags_form.addRow(
|
||||
strings._("stats_page_most_tags"),
|
||||
QLabel(f"{page_most_tags} ({page_most_tags_count})"),
|
||||
)
|
||||
else:
|
||||
form.addRow(strings._("stats_page_most_tags"), QLabel("—"))
|
||||
tags_form.addRow(
|
||||
strings._("stats_page_most_tags"),
|
||||
QLabel("—"),
|
||||
)
|
||||
|
||||
# Documents
|
||||
if date_most_documents:
|
||||
form.addRow(
|
||||
root.addWidget(tags_group)
|
||||
|
||||
# --- Documents ----------------------------------------------------
|
||||
if self.cfg.documents:
|
||||
docs_group = QGroupBox(strings._("stats_group_documents"))
|
||||
docs_form = QFormLayout(docs_group)
|
||||
|
||||
docs_form.addRow(
|
||||
strings._("stats_total_documents"),
|
||||
QLabel(str(total_documents)),
|
||||
)
|
||||
|
||||
if date_most_documents:
|
||||
doc_most_label = (
|
||||
f"{date_most_documents.isoformat()} ({date_most_documents_count})"
|
||||
)
|
||||
else:
|
||||
doc_most_label = "—"
|
||||
|
||||
form.addRow(
|
||||
docs_form.addRow(
|
||||
strings._("stats_date_most_documents"),
|
||||
QLabel(doc_most_label),
|
||||
)
|
||||
|
||||
# --- Heatmap with switcher ---------------------------------------
|
||||
if words_by_date or revisions_by_date or documents_by_date:
|
||||
root.addWidget(docs_group)
|
||||
|
||||
# --- Time logging -------------------------------------------------
|
||||
if self.cfg.time_log:
|
||||
time_group = QGroupBox(strings._("stats_group_time_logging"))
|
||||
time_form = QFormLayout(time_group)
|
||||
|
||||
total_hours = total_time_minutes / 60.0 if total_time_minutes else 0.0
|
||||
time_form.addRow(
|
||||
strings._("stats_time_total_hours"),
|
||||
QLabel(f"{total_hours:.2f}h"),
|
||||
)
|
||||
|
||||
if day_most_time:
|
||||
day_hours = (
|
||||
day_most_time_minutes / 60.0 if day_most_time_minutes else 0.0
|
||||
)
|
||||
day_label = f"{day_most_time} ({day_hours:.2f}h)"
|
||||
else:
|
||||
day_label = "—"
|
||||
time_form.addRow(
|
||||
strings._("stats_time_day_most_hours"),
|
||||
QLabel(day_label),
|
||||
)
|
||||
|
||||
if project_most_minutes_name:
|
||||
proj_hours = (
|
||||
project_most_minutes / 60.0 if project_most_minutes else 0.0
|
||||
)
|
||||
proj_label = f"{project_most_minutes_name} ({proj_hours:.2f}h)"
|
||||
else:
|
||||
proj_label = "—"
|
||||
time_form.addRow(
|
||||
strings._("stats_time_project_most_hours"),
|
||||
QLabel(proj_label),
|
||||
)
|
||||
|
||||
if activity_most_minutes_name:
|
||||
act_hours = (
|
||||
activity_most_minutes / 60.0 if activity_most_minutes else 0.0
|
||||
)
|
||||
act_label = f"{activity_most_minutes_name} ({act_hours:.2f}h)"
|
||||
else:
|
||||
act_label = "—"
|
||||
time_form.addRow(
|
||||
strings._("stats_time_activity_most_hours"),
|
||||
QLabel(act_label),
|
||||
)
|
||||
|
||||
root.addWidget(time_group)
|
||||
|
||||
# --- Reminders ----------------------------------------------------
|
||||
if self.cfg.reminders:
|
||||
rem_group = QGroupBox(strings._("stats_group_reminders"))
|
||||
rem_form = QFormLayout(rem_group)
|
||||
|
||||
rem_form.addRow(
|
||||
strings._("stats_total_reminders"),
|
||||
QLabel(str(total_reminders)),
|
||||
)
|
||||
|
||||
if day_most_reminders:
|
||||
rem_label = f"{day_most_reminders} ({day_most_reminders_count})"
|
||||
else:
|
||||
rem_label = "—"
|
||||
|
||||
rem_form.addRow(
|
||||
strings._("stats_date_most_reminders"),
|
||||
QLabel(rem_label),
|
||||
)
|
||||
|
||||
root.addWidget(rem_group)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Heatmap with metric switcher
|
||||
# ------------------------------------------------------------------
|
||||
if (
|
||||
words_by_date
|
||||
or revisions_by_date
|
||||
or documents_by_date
|
||||
or time_minutes_by_date
|
||||
or reminders_by_date
|
||||
):
|
||||
group = QGroupBox(strings._("stats_activity_heatmap"))
|
||||
group_layout = QVBoxLayout(group)
|
||||
|
||||
|
|
@ -358,18 +478,30 @@ class StatisticsDialog(QDialog):
|
|||
combo_row.addWidget(QLabel(strings._("stats_heatmap_metric")))
|
||||
self.metric_combo = QComboBox()
|
||||
self.metric_combo.addItem(strings._("stats_metric_words"), "words")
|
||||
self.metric_combo.addItem(strings._("stats_metric_revisions"), "revisions")
|
||||
self.metric_combo.addItem(
|
||||
strings._("stats_metric_revisions"),
|
||||
"revisions",
|
||||
)
|
||||
if documents_by_date:
|
||||
self.metric_combo.addItem(
|
||||
strings._("stats_metric_documents"), "documents"
|
||||
strings._("stats_metric_documents"),
|
||||
"documents",
|
||||
)
|
||||
if self.cfg.time_log and time_minutes_by_date:
|
||||
self.metric_combo.addItem(
|
||||
strings._("stats_metric_hours"),
|
||||
"hours",
|
||||
)
|
||||
if self.cfg.reminders and reminders_by_date:
|
||||
self.metric_combo.addItem(
|
||||
strings._("stats_metric_reminders"),
|
||||
"reminders",
|
||||
)
|
||||
combo_row.addWidget(self.metric_combo)
|
||||
combo_row.addStretch(1)
|
||||
group_layout.addLayout(combo_row)
|
||||
|
||||
self._heatmap = DateHeatmap()
|
||||
self._words_by_date = words_by_date
|
||||
self._revisions_by_date = revisions_by_date
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
|
|
@ -386,6 +518,8 @@ class StatisticsDialog(QDialog):
|
|||
else:
|
||||
root.addWidget(QLabel(strings._("stats_no_data")))
|
||||
|
||||
self.resize(self.sizeHint().width(), self.sizeHint().height())
|
||||
|
||||
# ---------- internal helpers ----------
|
||||
|
||||
def _apply_metric(self, metric: str) -> None:
|
||||
|
|
@ -393,6 +527,10 @@ class StatisticsDialog(QDialog):
|
|||
self._heatmap.set_data(self._revisions_by_date)
|
||||
elif metric == "documents":
|
||||
self._heatmap.set_data(self._documents_by_date)
|
||||
elif metric == "hours":
|
||||
self._heatmap.set_data(self._time_by_date)
|
||||
elif metric == "reminders":
|
||||
self._heatmap.set_data(self._reminders_by_date)
|
||||
else:
|
||||
self._heatmap.set_data(self._words_by_date)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "bouquin"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -373,7 +373,7 @@ def test_db_gather_stats_empty_database(fresh_db):
|
|||
"""Test gather_stats on empty database."""
|
||||
stats = fresh_db.gather_stats()
|
||||
|
||||
assert len(stats) == 10
|
||||
assert len(stats) == 22
|
||||
(
|
||||
pages_with_content,
|
||||
total_revisions,
|
||||
|
|
@ -385,6 +385,18 @@ def test_db_gather_stats_empty_database(fresh_db):
|
|||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
time_minutes_by_date,
|
||||
total_time_minutes,
|
||||
day_most_time,
|
||||
day_most_time_minutes,
|
||||
project_most_minutes_name,
|
||||
project_most_minutes,
|
||||
activity_most_minutes_name,
|
||||
activity_most_minutes,
|
||||
reminders_by_date,
|
||||
total_reminders,
|
||||
day_most_reminders,
|
||||
day_most_reminders_count,
|
||||
) = stats
|
||||
|
||||
assert pages_with_content == 0
|
||||
|
|
@ -421,6 +433,7 @@ def test_db_gather_stats_with_content(fresh_db):
|
|||
page_most_tags,
|
||||
page_most_tags_count,
|
||||
revisions_by_date,
|
||||
*_rest,
|
||||
) = stats
|
||||
|
||||
assert pages_with_content == 2
|
||||
|
|
@ -437,7 +450,7 @@ def test_db_gather_stats_word_counting(fresh_db):
|
|||
fresh_db.save_new_version("2024-01-01", "one two three four five", "test")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, _ = stats
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, *_rest = stats
|
||||
|
||||
assert total_words == 5
|
||||
|
||||
|
|
@ -463,7 +476,7 @@ def test_db_gather_stats_with_tags(fresh_db):
|
|||
fresh_db.set_tags_for_page("2024-01-02", ["tag1"]) # Page 2 has 1 tag
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, _ = stats
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, *_rest = stats
|
||||
|
||||
assert unique_tags == 3
|
||||
assert page_most_tags == "2024-01-01"
|
||||
|
|
@ -479,7 +492,7 @@ def test_db_gather_stats_revisions_by_date(fresh_db):
|
|||
fresh_db.save_new_version("2024-01-02", "Fourth", "v1")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date = stats
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date, *_rest = stats
|
||||
|
||||
assert date(2024, 1, 1) in revisions_by_date
|
||||
assert revisions_by_date[date(2024, 1, 1)] == 3
|
||||
|
|
@ -494,7 +507,7 @@ def test_db_gather_stats_handles_malformed_dates(fresh_db):
|
|||
fresh_db.save_new_version("2024-01-15", "Test", "v1")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date = stats
|
||||
_, _, _, _, _, _, _, _, _, revisions_by_date, *_rest = stats
|
||||
|
||||
# Should have parsed the date correctly
|
||||
assert date(2024, 1, 15) in revisions_by_date
|
||||
|
|
@ -507,7 +520,7 @@ def test_db_gather_stats_current_version_only(fresh_db):
|
|||
fresh_db.save_new_version("2024-01-01", "one two three four five", "v2")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, _ = stats
|
||||
_, _, _, _, words_by_date, total_words, _, _, _, *_rest = stats
|
||||
|
||||
# Should count words from current version (5 words), not old version
|
||||
assert total_words == 5
|
||||
|
|
@ -519,7 +532,7 @@ def test_db_gather_stats_no_tags(fresh_db):
|
|||
fresh_db.save_new_version("2024-01-01", "No tags here", "test")
|
||||
|
||||
stats = fresh_db.gather_stats()
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, _ = stats
|
||||
_, _, _, _, _, _, unique_tags, page_most_tags, page_most_tags_count, *_rest = stats
|
||||
|
||||
assert unique_tags == 0
|
||||
assert page_most_tags is None
|
||||
|
|
|
|||
|
|
@ -414,17 +414,6 @@ def test_upcoming_reminders_widget_check_reminders_no_db(qtbot, app):
|
|||
widget._check_reminders()
|
||||
|
||||
|
||||
def test_upcoming_reminders_widget_start_regular_timer(qtbot, app, fresh_db):
|
||||
"""Test starting the regular check timer."""
|
||||
widget = UpcomingRemindersWidget(fresh_db)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
widget._start_regular_timer()
|
||||
|
||||
# Timer should be running
|
||||
assert widget._check_timer.isActive()
|
||||
|
||||
|
||||
def test_manage_reminders_dialog_init(qtbot, app, fresh_db):
|
||||
"""Test ManageRemindersDialog initialization."""
|
||||
dialog = ManageRemindersDialog(fresh_db)
|
||||
|
|
@ -586,7 +575,7 @@ def test_manage_reminders_dialog_weekly_reminder_display(qtbot, app, fresh_db):
|
|||
qtbot.addWidget(dialog)
|
||||
|
||||
# Check that the type column shows the day
|
||||
type_item = dialog.table.item(0, 2)
|
||||
type_item = dialog.table.item(0, 3)
|
||||
assert "Wed" in type_item.text()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class FakeStatsDB:
|
|||
def __init__(self):
|
||||
d1 = _dt.date(2024, 1, 1)
|
||||
d2 = _dt.date(2024, 1, 2)
|
||||
|
||||
self.stats = (
|
||||
2, # pages_with_content
|
||||
5, # total_revisions
|
||||
|
|
@ -25,7 +26,20 @@ class FakeStatsDB:
|
|||
"2024-01-02", # page_most_tags
|
||||
2, # page_most_tags_count
|
||||
{d1: 1, d2: 2}, # revisions_by_date
|
||||
{d1: 60, d2: 120}, # time_minutes_by_date
|
||||
180, # total_time_minutes
|
||||
"2024-01-02", # day_most_time
|
||||
120, # day_most_time_minutes
|
||||
"Project A", # project_most_minutes_name
|
||||
120, # project_most_minutes
|
||||
"Activity A", # activity_most_minutes_name
|
||||
120, # activity_most_minutes
|
||||
{d1: 1, d2: 3}, # reminders_by_date
|
||||
4, # total_reminders
|
||||
"2024-01-02", # day_most_reminders
|
||||
3, # day_most_reminders_count
|
||||
)
|
||||
|
||||
self.called = False
|
||||
|
||||
def gather_stats(self):
|
||||
|
|
@ -57,7 +71,7 @@ def test_statistics_dialog_populates_fields_and_heatmap(qtbot):
|
|||
|
||||
# Heatmap is created and uses "words" by default
|
||||
words_by_date = db.stats[4]
|
||||
revisions_by_date = db.stats[-1]
|
||||
revisions_by_date = db.stats[9]
|
||||
|
||||
assert hasattr(dlg, "_heatmap")
|
||||
assert dlg._heatmap._data == words_by_date
|
||||
|
|
@ -80,13 +94,25 @@ class EmptyStatsDB:
|
|||
0, # pages_with_content
|
||||
0, # total_revisions
|
||||
None, # page_most_revisions
|
||||
0,
|
||||
0, # page_most_revisions_count
|
||||
{}, # words_by_date
|
||||
0, # total_words
|
||||
0, # unique_tags
|
||||
None, # page_most_tags
|
||||
0,
|
||||
0, # page_most_tags_count
|
||||
{}, # revisions_by_date
|
||||
{}, # time_minutes_by_date
|
||||
0, # total_time_minutes
|
||||
None, # day_most_time
|
||||
0, # day_most_time_minutes
|
||||
None, # project_most_minutes_name
|
||||
0, # project_most_minutes
|
||||
None, # activity_most_minutes_name
|
||||
0, # activity_most_minutes
|
||||
{}, # reminders_by_date
|
||||
0, # total_reminders
|
||||
None, # day_most_reminders
|
||||
0, # day_most_reminders_count
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue