Five integration tests fail because three API v1 route groups call
`joinedload()` on relationships the underlying models never define,
producing AttributeError -> 500 on the first query.
The three bad references were all introduced in commit 579fc7af
("extract business logic to service layer", Nov 2025) and have been
quietly broken since. They surfaced only now because the v5.5.6
fixture refresh started exercising these endpoints more thoroughly.
FK enforcement (also added in v5.5.6) is a red herring here.
### Repairs
- `KanbanColumn` has no `project` relationship (only the `project_id`
FK column), and `KanbanColumn.to_dict()` never reads `self.project`.
The eager load is dead code in both the PUT/PATCH and DELETE
handlers — drop it.
- `ClientNote` has an `author` relationship, not `created_by_user`,
and `ClientNote.to_dict()` reads `self.author.username` /
`self.author.full_name`. The four call sites still need eager
loading; rename the attribute to the real one (`author`).
- `SavedFilter` has no `user` relationship (only the `user_id` FK
column), and `SavedFilter.to_dict()` never reads `self.user`. Like
KanbanColumn, the eager load is dead code in all four call sites —
drop it.
### Tests fixed
- tests/test_api_kanban_v1.py::test_kanban_columns
- tests/test_api_client_notes_v1.py::test_client_notes_crud
- tests/test_api_saved_filters_v1.py::test_saved_filters_crud
- tests/test_routes/test_api_v1_invoices_tasks_expenses_refactored.py::TestAPITasksRefactored::test_get_task_uses_eager_loading (transitively, via the same route module)
### Notes
The unused `from sqlalchemy.orm import joinedload` import in the
kanban/saved-filter handlers is left in place to keep the diff
surgical. It can be removed in a follow-up cleanup.
### Test plan
- [ ] pytest tests/test_api_kanban_v1.py::test_kanban_columns
- [ ] pytest tests/test_api_client_notes_v1.py::test_client_notes_crud
- [ ] pytest tests/test_api_saved_filters_v1.py::test_saved_filters_crud
- [ ] All three return 200 from CRUD operations; no AttributeError logged
- [ ] No regression on routes that already used joinedload correctly
Bump setup.py to 5.5.7 and document the invoice PDF designer layout fix,
designer-to-ReportLab template parity, and preview vs export alignment
for issue #622 in CHANGELOG.md.
Close the missing canvas-area wrapper in the invoice PDF designer so the properties panel sits in the third grid column beside the canvas instead of below it.
When saving template JSON from the designer, read items-table and expenses-table width, colors, and separator line settings from the Konva group children so exports match user edits. Scale column widths to the chosen table width and emit a style block for ReportLab.
In the ReportLab renderer, scale column widths to element.width, wrap tables in a two-column outer table to honor horizontal x offset relative to the left margin, and apply borderColor and borderWidth from template style.
Extend the JSON-to-HTML preview converter so table header text, row text, row background, and border width reflect the same style keys used on export (fixes preview vs PDF mismatch for issue #622).
- Honor AI_ENABLED across session AI, REST v1, LLM service, templates, and
context; add regression tests for the AI helper gate.
- Docker Compose: optional Ollama behind the ai profile; align env.example
and example compose with safe defaults.
- Add UNINSTALL.md with a dedicated AI teardown section; cross-link from
README, INSTALLATION, Getting Started, docs index, and Docker setup guide.
- Record 5.5.6 in CHANGELOG and sync version examples in BUILD_CONFIGURATION;
bump setup.py to 5.5.6.
The dashboard route now writes two cache entries per user
(dashboard:stats:<id> and dashboard💹<id>) instead of a single
dashboard:<id> blob, and today_hours moved into the nested
time_tracking payload. Update the caching tests to mirror this shape
and centralise the key tuple in a small helper.
The ProjectCost.updated_at column default captures datetime.utcnow at
class definition time, so patching app.models.project_cost.datetime
does not change the initial value written on insert. Assign
updated_at directly on the instance to get the deterministic
"before" timestamp the test relies on.
base.html now loads keyboard-shortcuts-advanced.js globally instead of
keyboard-shortcuts-enhanced.js, and the cheat-sheet CSS is only pulled
in on the dedicated settings page. Update the integration assertion
accordingly.
- Return lightweight SimpleNamespace snapshots from the sample_client
and sample_project fixtures so downstream tests can access .id and
.name without re-attaching the SQLAlchemy instance to a new session.
- In test_settings_currency_can_be_changed, persist the Settings row
when get_settings() returns a transient instance, and re-query for
verification instead of refresh() (which fails on transient objects).
WebhookDelivery.next_retry_at is a naive DateTime column. Strip the
tzinfo before passing it to mark_retrying() so the round-trip
comparison after commit/reload does not fail with a naive/aware
mismatch.
Supplier.stock_items is no longer a list-like relationship; assert
against the dynamic supplier_items relationship using .count() and
.first() instead.
With foreign keys enforced in tests, hard-coded client_id=1 and
user_id=1 references no longer work because no such rows exist in the
isolated test database. Create the required parent rows (Client for
inventory project tests, Client + User for the burndown/SavedFilter
smoke tests) before inserting the children.
Introduce a small set_client_portal_access helper that updates the
user row via a single UPDATE and expires the session, replacing the
scattered no_autoflush + safe_commit_with_retry + safe_get_user dance
in every test. Also auto-disable audit logging for this module to
avoid SQLite write-lock contention during portal tests, and fix a few
indentation bugs where assertions sat outside the app_context block.
- Turn on PRAGMA foreign_keys=ON for every SQLite connection so
ondelete="CASCADE" and other FK constraints are exercised by tests.
- Disable FK enforcement only for DROP TABLE statements, since the
schema has cyclic references (deals/leads/projects/quotes) and
drop_all() cannot order them cleanly.
- Seed admin/user/manager/subcontractor roles in the app fixture so
route tests that validate against the role table no longer need to
run the full permission seed command.
- Make TimeEntryFactory.end_time deterministic relative to start_time
so created entries always represent a valid 2h window.
The legacy api.py module exposes _get_client_id_from_session, which is
imported by client-portal tests via app.routes.api. Surface it through
the package __init__ so the import works regardless of which loader
path is taken.
Use request.get_json(silent=True) so a request with the wrong
Content-Type or a malformed body falls through to the existing
validation path and returns 400 instead of raising a 415/parse error.
Inspecting the engine directly opens a second connection, which under
SQLite can contend with a write lock held by the current flush and
cause spurious failures during tests. Bind the inspector to the
session's existing connection when available and fall back to the
engine otherwise.
Match the behaviour of the API v1 endpoint by marking the supplier as
inactive instead of hard-deleting it. This preserves referential
integrity with purchase orders, stock items and audit data and removes
the "cannot delete supplier with associated stock items" failure mode.
Add multi-select with Shift/Ctrl toggles and marquee selection on the invoice
and quote PDF template editors, plus a shared transformer, group nudge with
snap-aware arrow keys, multi copy/paste/duplicate, align-to-page or
align-to-selection, horizontal/vertical distribute, and Ctrl/Cmd+G group /
Ctrl/Cmd+Shift+G ungroup.
Expose horizontal (left/center/right/justify) and vertical (top/middle/bottom)
text controls in the properties panel; export verticalAlign, group_id,
locked, and hidden in template JSON where set. Skip hidden elements in
ReportLab rendering; draw justified text on canvas via Paragraph; honor
verticalAlign when a text box height is present.
Introduce app/static/js/pdf_editor helpers (loaded with dynamic import from
the templates), pdf_editor.css, a layers panel with visibility and lock toggles,
smart-guide snapping, and optional ruler chrome. Document behaviour and manual
QA in docs/PDF_LAYOUT_EDITOR.md; add tests/test_pdf_template_schema.py for
optional template fields.
Closes#619.
Security pytest:
- Run an explicit node list instead of -m security over the whole tests
tree, so collection stays small and nothing is spuriously deselected.
- Use a writable pytest cache under INSTALLATION_CONFIG_DIR and filter
the known Flask-SQLAlchemy SAWarning on metadata DROP ordering.
- Add scripts/ci/security-pytest.sh and wire Makefile, run-tests.sh/.bat,
and ci-comprehensive to call it for a single source of truth.
Safety:
- Write JSON to .test_installation_config/safety-report.json (with the
rest of local CI artifacts) instead of the repo root.
- Run scripts/ci/sanitize_safety_report.py after each scan so paths in
the report are workspace-relative for artifacts and reviews.
- Capture Safety exit codes so failures still print where the report was
written; use python -m safety in workflows where appropriate.
Release and legacy workflows pick up the new report path, sanitizer,
and a pinned Safety install where the CLI is invoked.
Safety 3.0.1 crashes on import with Typer >= 0.17 (AttributeError:
typer.rich_utils). Pin the test dependency to 3.7.0 so local CI and
workflows can run the Safety CLI reliably.
Drives ``mypy app/`` from 567 errors in 208 files to 0 errors across the
376 source files checked by ``./scripts/run-ci-local.sh code-quality``.
Configuration & dependencies
- pyproject.toml: enable implicit_optional (Flask-style ``x: str = None``
defaults), silence truthy-function/truthy-bool (legitimate import-guard
checks like ``KanbanColumn``), and disable warn_return_any (SQLAlchemy
1.x ``Query`` API returns Any pervasively). Add module overrides for
``app.models.*``, repositories, base CRUD service, and known
``joinedload`` / ``Query.paginate`` callers where mypy cannot model the
Flask-SQLAlchemy runtime API without a plugin.
- requirements-test.txt: pin ``types-requests``, ``types-bleach``,
``types-Markdown``, ``types-python-dateutil`` so mypy stops complaining
about missing stubs.
Latent bugs fixed while driving mypy to zero
- app/utils/logger.py, app/utils/datetime_utils.py: drop imports of
symbols that don't exist (``get_performance_metrics``,
``from_app_timezone``, ``to_app_timezone``) — these would have raised
at import time on first use.
- app/services/currency_service.py: ``from typing import Decimal`` was a
bug (typing has no Decimal); switch to ``decimal.Decimal`` and rename
the ``D`` alias.
- app/utils/env_validation.py, app/utils/role_migration.py: ``Dict[str,
any]`` → ``Dict[str, Any]`` (built-in ``any`` is not a type).
- app/utils/email.py: introduce ``send_template_email`` and update the
three callers (``client_approval_service``,
``client_notification_service``, ``workflow_engine``) that were
passing ``to=``/``template=``/etc. to ``send_email`` whose signature
doesn't accept them — calls would have raised TypeError at runtime.
- app/services/permission_service.py: rewrite ``grant_permission`` /
``revoke_permission`` to use the actual ``Role`` ↔ ``Permission``
many-to-many relationship; the old code referenced non-existent
``Permission.role_id`` / ``Permission.granted`` columns.
- app/services/gps_tracking_service.py: pass the required ``title`` and
``expense_date`` fields when creating mileage ``Expense`` rows.
- app/services/workflow_engine.py: ``_perform_action`` now forwards the
``rule`` argument to ``_action_log_time``, and ``_action_webhook``
short-circuits when ``url`` is missing.
- app/services/time_tracking_service.py: validate ``start_time`` /
``end_time`` before comparing them.
- app/services/export_service.py: build CSV in a ``StringIO`` then wrap
the bytes in ``BytesIO`` — ``csv.writer`` requires text I/O.
- app/integrations/peppol_smp.py: avoid attribute access on ``None`` in
the SMP ``href`` fallback.
- app/integrations/{github,gitlab,slack}.py: coerce query-string params
to strings so ``requests.get(params=...)`` matches the typed signature
(and is what the HTTP layer expects anyway).
- app/integrations/{xero,quickbooks}.py: guard ``get_access_token()``
returning ``None`` before calling private ``_api_request`` helpers.
Annotation-only changes
- Add ``Dict[str, Any]`` / ``list`` / ``Optional[...]`` annotations to
service dict-literals that mypy could not infer from heterogeneous
values (``ai_suggestion_service``, ``ai_categorization_service``,
``custom_report_service``, ``unpaid_hours_service``,
``integration_service``, ``invoice_service``, ``backup_service``,
``inventory_report_service``, ``analytics_service``, etc.).
- ``app/utils/event_bus.py``: ``emit_event`` accepts ``str |
WebhookEvent`` and normalizes to ``str`` so all call-sites type-check.
- ``app/utils/api_responses.py``: introduce ``ApiResponse`` alias for
``Response | tuple[Response, int] | tuple[str, int]``.
- ``app/utils/budget_forecasting.py``: forecasting helpers return
``Optional[Dict]`` (they already returned ``None`` when the project
was missing).
- ``app/utils/pdf_generator_reportlab.py``: ``_normalize_color`` is
``Optional[str]``.
- ``app/utils/pdfa3.py``: remove invalid ``force_version=None`` retry
call.
- Narrow ``type: ignore`` markers on optional-dependency fallbacks
(``redis``, ``bleach``, ``markdown``, ``babel``,
``powerpoint_export``) and on the documented ``requests.Session``
/ ``RotatingFileHandler`` typeshed limitations.
Pure formatting pass to satisfy ``./scripts/run-ci-local.sh code-quality``:
no behavioural changes, just consistent line wrapping, import ordering,
and trailing-newline normalization across routes, models, services, and
utility modules.
Introduce scripts/run-ci-local.sh and scripts/ci/lib.sh to run the same
checks as ci-comprehensive.yml. Ignore .test_installation_config/ where
the script writes INSTALLATION_CONFIG_DIR by default.
Remove obsolete skips across admin, calendar, invoices, payments, PDF,
uploads, and related tests. Use /login and passwords consistent with
conftest; fix team chat setup in silent-exception tests; assert expense
currency with locale-tolerant amount matching; align admin logo tests
with admin_authenticated_client credentials.
Switch to the listen + http2 on; form for modern nginx. Add Docker DNS
resolver and a variable-based proxy_pass so nginx can start before the
app container registers, and apply the same upstream to /socket.io/.
Wrap create flow in a single try/except with explicit step labels for
logging and diagnostics. Harden logging around current_user and keep
validation/flash behavior while making failures easier to trace.
Skip network OTLP export when TESTING is set unless OTEL_ENABLE_IN_TESTS=1.
Register shutdown once at exit, reset providers cleanly in tests, and
briefly silence OTEL loggers during shutdown to avoid noisy CI output.
Flask-SQLAlchemy exposes a scoped_session proxy; reading _flushing on the
proxy did not reflect the underlying session, so Settings.get_settings()
could run add/commit mid-flush and break tests. Unwrap to the real session
before checking _flushing and _warn_on_events.
Move shell layout fixes from [Unreleased] to CHANGELOG [5.5.5].
Sync BUILD_CONFIGURATION.md version code example and setup.py snippet
with setup.py (single source of truth).
Remove stray closing </div> tags from admin backups, admin API
tokens, and quote detail templates so content and modals stay
inside #mainContent and the footer aligns with the main column.
Drop max-w-7xl from the primary shell; wrap main + attribution in
a flex column so the scroll area fills width beside the sidebar.
Align the support banner inner row to full width.
Documented in CHANGELOG.md under [Unreleased].
Bump package version in setup.py to 5.5.4.
CHANGELOG: document 5.5.4 (full-database restore hardening and backup/restore documentation).
docs/BUILD_CONFIGURATION.md: refresh version-code example and setup.py snippet to 5.5.4 (50504).
Admin restore runs in a background thread; the finally block must not use current_app.logger outside an application context. Use the captured Flask app instance for safe_file_remove logging instead.
While restore_backup runs (extract through Alembic upgrade), set a per-app _database_restore_in_progress flag and expose is_database_restore_in_progress(). The client portal blueprint registers a global app_context_processor; get_current_client() now skips database access during restore and catches SQLAlchemy errors with session rollback so error pages and login can still render when the schema is briefly torn on PostgreSQL.
Documentation: add docs/admin/BACKUP_AND_RESTORE.md, link it from the admin index and import/export docs, cross-reference from DATABASE_RECOVERY.md, and extend IMPORT_EXPORT_GUIDE.md with concurrent-restore guidance.
CommonMark and GFM both define ~~text~~ as strikethrough, and Toast UI
emits it when the user toggles strikethrough in the WYSIWYG editor.
Python markdown's 'extra' extension does not implement strikethrough,
so the wrapping tildes leak through to the rendered HTML and the user
sees ~~text~~ instead of struck-through text on /tasks/<id>, notes,
client view, etc.
Add a regex pass to _normalize_toastui_markdown that converts
~~text~~ to <del>text</del> before markdown parsing. The bleach
sanitizer already permits <del> via the existing allowed_tags list,
so the rendered HTML survives the sanitization pass intact.
The regex is non-greedy and stays on a single line so multiple
strikethroughs on the same line each get their own pair.
Task and note descriptions saved through the Toast UI WYSIWYG editor
came back wrapped in CommonMark-style escapes that Python markdown
either does not honour (\, → literal \,) or honours in a way that
breaks rendering (line-leading \- prevents list parsing).
The visible symptom on /tasks/<id>: bullet lists rendered as a single
flat paragraph with literal backslashes peppered between words.
Add a normaliser run before _md.markdown(...) that:
- Restores line-leading bullets that the editor escaped (\-/\*/\+
followed by whitespace at start of line).
- Strips backslashes before punctuation Python markdown does not
recognise as a valid escape (commas, colons, semicolons, etc.).
This handles existing rows in the DB without any data migration. The
normaliser leaves alone backslashes before punctuation Python markdown
does handle natively (\. \( \) \+ \- mid-line, \* \_ \# etc.)
so author-intent escapes still render correctly.
Strikethrough (~~text~~) still does not render because the 'extra'
extension does not include it; that is a separate enhancement.
Same class as the projects/list.html and weekly_goals/index.html fix
shipped earlier. Both task templates had a stray </div> immediately
before the <style> block with no matching opener:
app/templates/tasks/edit.html:339
app/templates/tasks/create.html:192
The orphan close pushed the rest of the page outside the outer
.grid lg:grid-cols-3 wrapper, so the form and sidebar rendered
left-aligned and the 'Built by an independent developer' footer
floated up to the top right.
Verified by div-balance trace: both files now reach final balance 0
with no negative excursions.
The validator's last-ditch fallback (used when both project-specific
and global columns are missing) returned a hardcoded list that omitted
"on_hold". The function's own docstring on the same code path even
calls this out explicitly:
drops to globally-defined columns like "on_hold" come back as 400
"Invalid status".
The broader fix from PR #605 made the validator fall back to global
columns first, which fixes the common case. But the very last fallback
list — used during fresh migrations before the kanban_columns table is
seeded — still rejects "on_hold" tasks the user has already created.
Real installs that ship with on_hold columns enabled hit this on the
first request after a clean migration.
Add on_hold to the hardcoded list so it stays consistent with what
on_hold-enabled installs expect to validate.
Auto-lint reformatted the surrounding column declarations; the only
behavioral change is the addition of on_hold to the fallback list.
ClientApprovalStatus is defined with uppercase Python names and lowercase
string values (PENDING = "pending", etc.), but the Postgres enum type
clientapprovalstatus is defined with the lowercase values. SQLAlchemy
defaults to binding the enum *name*, so every query against the column
sent "PENDING" and Postgres rejected it with InvalidTextRepresentation.
This made get_pending_approvals_for_client raise on every client portal
request — the navbar context processor catches the exception and returns
0, but the stack trace was logged on every page load.
Pass values_callable to SQLEnum so SQLAlchemy uses the enum *value* (the
lowercase string PG actually stores).
The auto-lint hook reformatted the rest of the file; the only behavioral
change is the values_callable parameter on the status column.