Compare commits
No commits in common. "01963ed6a72ad26d209deee93cc77a2ec00a96f3" and "6bc6fe4b8360d6dd29141f302095688b0b6ba7a0" have entirely different histories.
01963ed6a7
...
6bc6fe4b83
4 changed files with 96 additions and 190 deletions
|
|
@ -1,7 +1,3 @@
|
||||||
# 0.4.1
|
|
||||||
|
|
||||||
* Allow time log entries to be edited directly in their table cells
|
|
||||||
|
|
||||||
# 0.4
|
# 0.4
|
||||||
|
|
||||||
* Remove screenshot tool
|
* Remove screenshot tool
|
||||||
|
|
|
||||||
|
|
@ -162,80 +162,80 @@
|
||||||
"invalid_time_message": "Please enter a time in the format HH:MM",
|
"invalid_time_message": "Please enter a time in the format HH:MM",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"toolbar_alarm": "Set reminder alarm",
|
"toolbar_alarm": "Set reminder alarm",
|
||||||
"activities": "Activities",
|
"activities": "Activities",
|
||||||
"activity": "Activity",
|
"activity": "Activity",
|
||||||
"note": "Note",
|
"note": "Note",
|
||||||
"activity_delete_error_message": "A problem occurred deleting the activity",
|
"activity_delete_error_message": "A problem occurred deleting the activity",
|
||||||
"activity_delete_error_title": "Problem deleting activity",
|
"activity_delete_error_title": "Problem deleting activity",
|
||||||
"activity_rename_error_message": "A problem occurred renaming the activity",
|
"activity_rename_error_message": "A problem occurred renaming the activity",
|
||||||
"activity_rename_error_title": "Problem renaming activity",
|
"activity_rename_error_title": "Problem renaming activity",
|
||||||
"activity_required_message": "An activity name is required",
|
"activity_required_message": "An activity name is required",
|
||||||
"activity_required_title": "Activity name required",
|
"activity_required_title": "Activity name required",
|
||||||
"add_activity": "Add activity",
|
"add_activity": "Add activity",
|
||||||
"add_project": "Add project",
|
"add_project": "Add project",
|
||||||
"add_time_entry": "Add time entry",
|
"add_time_entry": "Add time entry",
|
||||||
"time_period": "Time period",
|
"time_period": "Time period",
|
||||||
"by_day": "by day",
|
"by_day": "by day",
|
||||||
"by_month": "by month",
|
"by_month": "by month",
|
||||||
"by_week": "by week",
|
"by_week": "by week",
|
||||||
"date_range": "Date range",
|
"date_range": "Date range",
|
||||||
"delete_activity": "Delete activity",
|
"delete_activity": "Delete activity",
|
||||||
"delete_activity_confirm": "Are you sure you want to delete this activity?",
|
"delete_activity_confirm": "Are you sure you want to delete this activity?",
|
||||||
"delete_activity_title": "Delete activity - are you sure?",
|
"delete_activity_title": "Delete activity - are you sure?",
|
||||||
"delete_project": "Delete project",
|
"delete_project": "Delete project",
|
||||||
"delete_project_confirm": "Are you sure you want to delete this project?",
|
"delete_project_confirm": "Are you sure you want to delete this project?",
|
||||||
"delete_project_title": "Delete project - are you sure?",
|
"delete_project_title": "Delete project - are you sure?",
|
||||||
"delete_time_entry": "Delete time entry",
|
"delete_time_entry": "Delete time entry",
|
||||||
"group_by": "Group by",
|
"group_by": "Group by",
|
||||||
"hours": "Hours",
|
"hours": "Hours",
|
||||||
"invalid_activity_message": "The activity is invalid",
|
"invalid_activity_message": "The activity is invalid",
|
||||||
"invalid_activity_title": "Invalid activity",
|
"invalid_activity_title": "Invalid activity",
|
||||||
"invalid_project_message": "The project is invalid",
|
"invalid_project_message": "The project is invalid",
|
||||||
"invalid_project_title": "Invalid project",
|
"invalid_project_title": "Invalid project",
|
||||||
"label_key": "Label",
|
"label_key": "Label",
|
||||||
"manage_activities": "Manage activities",
|
"manage_activities": "Manage activities",
|
||||||
"manage_projects": "Manage projects",
|
"manage_projects": "Manage projects",
|
||||||
"manage_projects_activities": "Manage project activities",
|
"manage_projects_activities": "Manage project activities",
|
||||||
"open_time_log": "Open time log",
|
"open_time_log": "Open time log",
|
||||||
"project": "Project",
|
"project": "Project",
|
||||||
"project_delete_error_message": "A problem occurred deleting the project",
|
"project_delete_error_message": "A problem occurred deleting the project",
|
||||||
"project_delete_error_title": "Problem deleting project",
|
"project_delete_error_title": "Problem deleting project",
|
||||||
"project_rename_error_message": "A problem occurred renaming the project",
|
"project_rename_error_message": "A problem occurred renaming the project",
|
||||||
"project_rename_error_title": "Problem renaming project",
|
"project_rename_error_title": "Problem renaming project",
|
||||||
"project_required_message": "A project is required",
|
"project_required_message": "A project is required",
|
||||||
"project_required_title": "Project required",
|
"project_required_title": "Project required",
|
||||||
"projects": "Projects",
|
"projects": "Projects",
|
||||||
"rename_activity": "Rename activity",
|
"rename_activity": "Rename activity",
|
||||||
"rename_project": "Rename project",
|
"rename_project": "Rename project",
|
||||||
"run_report": "Run report",
|
"run_report": "Run report",
|
||||||
"add_project_label": "Add a project",
|
"add_project_label": "Add a project",
|
||||||
"add_activity_label": "Add an activity",
|
"add_activity_label": "Add an activity",
|
||||||
"select_activity_message": "Select an activity",
|
"select_activity_message": "Select an activity",
|
||||||
"select_activity_title": "Select activity",
|
"select_activity_title": "Select activity",
|
||||||
"select_project_message": "Select a project",
|
"select_project_message": "Select a project",
|
||||||
"select_project_title": "Select project",
|
"select_project_title": "Select project",
|
||||||
"time_log": "Time log",
|
"time_log": "Time log",
|
||||||
"time_log_collapsed_hint": "Time log",
|
"time_log_collapsed_hint": "Time log",
|
||||||
"time_log_date_label": "Time log date: {date}",
|
"time_log_date_label": "Time log date: {date}",
|
||||||
"time_log_for": "Time log for {date}",
|
"time_log_for": "Time log for {date}",
|
||||||
"time_log_no_date": "Time log",
|
"time_log_no_date": "Time log",
|
||||||
"time_log_no_entries": "No time entries yet",
|
"time_log_no_entries": "No time entries yet",
|
||||||
"time_log_report": "Time log report",
|
"time_log_report": "Time log report",
|
||||||
"time_log_report_title": "Time log for {project}",
|
"time_log_report_title": "Time log for {project}",
|
||||||
"time_log_report_meta": "From {start} to {end}, grouped {granularity}",
|
"time_log_report_meta": "From {start} to {end}, grouped {granularity}",
|
||||||
"time_log_total_hours": "Total time spent",
|
"time_log_total_hours": "Total time spent",
|
||||||
"time_log_with_total": "Time log ({hours:.2f}h)",
|
"time_log_with_total": "Time log ({hours:.2f}h)",
|
||||||
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
"time_log_total_hours": "Total for day: {hours:.2f}h",
|
||||||
"title_key": "title",
|
"title_key": "title",
|
||||||
"update_time_entry": "Update time entry",
|
"update_time_entry": "Update time entry",
|
||||||
"time_report_total": "Total: {hours:.2f} hours",
|
"time_report_total": "Total: {hours:.2f} hours",
|
||||||
"no_report_title": "No report",
|
"no_report_title": "No report",
|
||||||
"no_report_message": "Please run a report before exporting.",
|
"no_report_message": "Please run a report before exporting.",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"export_csv": "Export CSV",
|
"export_csv": "Export CSV",
|
||||||
"export_csv_error_title": "Export failed",
|
"export_csv_error_title": "Export failed",
|
||||||
"export_csv_error_message": "Could not write CSV file:\n{error}",
|
"export_csv_error_message": "Could not write CSV file:\n{error}",
|
||||||
"export_pdf": "Export PDF",
|
"export_pdf": "Export PDF",
|
||||||
"export_pdf_error_title": "PDF export failed",
|
"export_pdf_error_title": "PDF export failed",
|
||||||
"export_pdf_error_message": "Could not write PDF file:\n{error}"
|
"export_pdf_error_message": "Could not write PDF file:\n{error}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,9 +181,6 @@ class TimeLogDialog(QDialog):
|
||||||
self._db = db
|
self._db = db
|
||||||
self._date_iso = date_iso
|
self._date_iso = date_iso
|
||||||
self._current_entry_id: Optional[int] = None
|
self._current_entry_id: Optional[int] = None
|
||||||
# Guard flag used when repopulating the table so we don’t treat
|
|
||||||
# programmatic item changes as user edits.
|
|
||||||
self._reloading_entries: bool = False
|
|
||||||
|
|
||||||
self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
|
self.setWindowTitle(strings._("time_log_for").format(date=date_iso))
|
||||||
self.resize(900, 600)
|
self.resize(900, 600)
|
||||||
|
|
@ -267,8 +264,6 @@ class TimeLogDialog(QDialog):
|
||||||
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
self.table.itemSelectionChanged.connect(self._on_row_selected)
|
self.table.itemSelectionChanged.connect(self._on_row_selected)
|
||||||
# When a cell is edited inline, commit the change back to the DB.
|
|
||||||
self.table.itemChanged.connect(self._on_table_item_changed)
|
|
||||||
root.addWidget(self.table, 1)
|
root.addWidget(self.table, 1)
|
||||||
|
|
||||||
# --- Close button
|
# --- Close button
|
||||||
|
|
@ -298,38 +293,28 @@ class TimeLogDialog(QDialog):
|
||||||
self.activity_edit.setCompleter(completer)
|
self.activity_edit.setCompleter(completer)
|
||||||
|
|
||||||
def _reload_entries(self) -> None:
|
def _reload_entries(self) -> None:
|
||||||
"""Reload the table from the database.
|
rows = self._db.time_log_for_date(self._date_iso)
|
||||||
|
self.table.setRowCount(len(rows))
|
||||||
|
for row_idx, r in enumerate(rows):
|
||||||
|
entry_id = r[0]
|
||||||
|
project_name = r[3]
|
||||||
|
activity_name = r[5]
|
||||||
|
note = r[7] or ""
|
||||||
|
minutes = r[6]
|
||||||
|
hours = minutes / 60.0
|
||||||
|
|
||||||
While we are repopulating the QTableWidget we temporarily disable the
|
item_proj = QTableWidgetItem(project_name)
|
||||||
itemChanged handler so that programmatic changes do not get written
|
item_act = QTableWidgetItem(activity_name)
|
||||||
back to the database.
|
item_note = QTableWidgetItem(note)
|
||||||
"""
|
item_hours = QTableWidgetItem(f"{hours:.2f}")
|
||||||
self._reloading_entries = True
|
|
||||||
try:
|
|
||||||
rows = self._db.time_log_for_date(self._date_iso)
|
|
||||||
self.table.setRowCount(len(rows))
|
|
||||||
for row_idx, r in enumerate(rows):
|
|
||||||
entry_id = r[0]
|
|
||||||
project_name = r[3]
|
|
||||||
activity_name = r[5]
|
|
||||||
note = r[7] or ""
|
|
||||||
minutes = r[6]
|
|
||||||
hours = minutes / 60.0
|
|
||||||
|
|
||||||
item_proj = QTableWidgetItem(project_name)
|
# store the entry id on the first column
|
||||||
item_act = QTableWidgetItem(activity_name)
|
item_proj.setData(Qt.ItemDataRole.UserRole, entry_id)
|
||||||
item_note = QTableWidgetItem(note)
|
|
||||||
item_hours = QTableWidgetItem(f"{hours:.2f}")
|
|
||||||
|
|
||||||
# store the entry id on the first column
|
self.table.setItem(row_idx, 0, item_proj)
|
||||||
item_proj.setData(Qt.ItemDataRole.UserRole, entry_id)
|
self.table.setItem(row_idx, 1, item_act)
|
||||||
|
self.table.setItem(row_idx, 2, item_note)
|
||||||
self.table.setItem(row_idx, 0, item_proj)
|
self.table.setItem(row_idx, 3, item_hours)
|
||||||
self.table.setItem(row_idx, 1, item_act)
|
|
||||||
self.table.setItem(row_idx, 2, item_note)
|
|
||||||
self.table.setItem(row_idx, 3, item_hours)
|
|
||||||
finally:
|
|
||||||
self._reloading_entries = False
|
|
||||||
|
|
||||||
self._current_entry_id = None
|
self._current_entry_id = None
|
||||||
self.delete_btn.setEnabled(False)
|
self.delete_btn.setEnabled(False)
|
||||||
|
|
@ -420,81 +405,6 @@ class TimeLogDialog(QDialog):
|
||||||
self.note.setText(note)
|
self.note.setText(note)
|
||||||
self.hours_spin.setValue(hours)
|
self.hours_spin.setValue(hours)
|
||||||
|
|
||||||
def _on_table_item_changed(self, item: QTableWidgetItem) -> None:
|
|
||||||
"""Commit inline edits in the table back to the database.
|
|
||||||
|
|
||||||
Editing a cell should behave like selecting that row and pressing
|
|
||||||
the Add/Update button, so we reuse the same validation and DB logic.
|
|
||||||
"""
|
|
||||||
if self._reloading_entries:
|
|
||||||
# Ignore changes that come from _reload_entries().
|
|
||||||
return
|
|
||||||
|
|
||||||
if item is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
row = item.row()
|
|
||||||
|
|
||||||
proj_item = self.table.item(row, 0)
|
|
||||||
act_item = self.table.item(row, 1)
|
|
||||||
note_item = self.table.item(row, 2)
|
|
||||||
hours_item = self.table.item(row, 3)
|
|
||||||
|
|
||||||
if proj_item is None or act_item is None or hours_item is None:
|
|
||||||
# Incomplete row – nothing to do.
|
|
||||||
return
|
|
||||||
|
|
||||||
# Recover the entry id from the hidden UserRole on the project cell
|
|
||||||
entry_id = proj_item.data(Qt.ItemDataRole.UserRole)
|
|
||||||
self._current_entry_id = int(entry_id) if entry_id is not None else None
|
|
||||||
|
|
||||||
# Push values into the editors (similar to _on_row_selected).
|
|
||||||
proj_name = proj_item.text()
|
|
||||||
act_name = act_item.text()
|
|
||||||
note_text = note_item.text() if note_item is not None else ""
|
|
||||||
hours_text = hours_item.text()
|
|
||||||
|
|
||||||
# Set project combo by name, creating a project on the fly if needed.
|
|
||||||
idx = self.project_combo.findText(proj_name, Qt.MatchFixedString)
|
|
||||||
if idx < 0 and proj_name:
|
|
||||||
# Allow creating a new project directly from the table.
|
|
||||||
proj_id = self._db.add_project(proj_name)
|
|
||||||
self._reload_projects()
|
|
||||||
idx = self.project_combo.findData(proj_id)
|
|
||||||
if idx >= 0:
|
|
||||||
self.project_combo.setCurrentIndex(idx)
|
|
||||||
else:
|
|
||||||
self.project_combo.setCurrentIndex(-1)
|
|
||||||
|
|
||||||
self.activity_edit.setText(act_name)
|
|
||||||
self.note.setText(note_text)
|
|
||||||
|
|
||||||
# Parse hours; if invalid, show the same style of warning as elsewhere.
|
|
||||||
try:
|
|
||||||
hours = float(hours_text)
|
|
||||||
except ValueError:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
strings._("invalid_time_title"),
|
|
||||||
strings._("invalid_time_message"),
|
|
||||||
)
|
|
||||||
# Reset table back to the last known-good state.
|
|
||||||
self._reload_entries()
|
|
||||||
return
|
|
||||||
|
|
||||||
self.hours_spin.setValue(hours)
|
|
||||||
|
|
||||||
# Mirror button state to reflect whether we're updating or adding.
|
|
||||||
if self._current_entry_id is None:
|
|
||||||
self.delete_btn.setEnabled(False)
|
|
||||||
self.add_update_btn.setText(strings._("add_time_entry"))
|
|
||||||
else:
|
|
||||||
self.delete_btn.setEnabled(True)
|
|
||||||
self.add_update_btn.setText(strings._("update_time_entry"))
|
|
||||||
|
|
||||||
# Finally, reuse the existing validation + DB logic.
|
|
||||||
self._on_add_or_update()
|
|
||||||
|
|
||||||
def _on_delete_entry(self) -> None:
|
def _on_delete_entry(self) -> None:
|
||||||
if self._current_entry_id is None:
|
if self._current_entry_id is None:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bouquin"
|
name = "bouquin"
|
||||||
version = "0.4.1"
|
version = "0.3.2"
|
||||||
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue