mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
Fix archived files counted as printed in project statistics (#630)
Files added to a project from the archive (status="archived") were incorrectly counted in completed_prints and parts_progress stats. Only status="completed" (actually printed) now counts toward completion.
This commit is contained in:
@@ -13,6 +13,7 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Print Queue Scheduler Diagnostics** ([#616](https://github.com/maziggy/bambuddy/issues/616)) — Added diagnostic logging to the print queue scheduler to help diagnose why queued prints aren't starting. After each queue check, the scheduler now logs a skip summary (how many items were skipped due to manual_start, scheduled_time, etc.) and for each busy printer, logs the exact state preventing it from being considered idle (connected status, printer state, plate_cleared flag). Previously the scheduler only logged "found N pending items" with no visibility into why items were skipped.
|
||||
|
||||
### Fixed
|
||||
- **Project Statistics Count Archived Files as Printed** ([#630](https://github.com/maziggy/bambuddy/issues/630)) — Files added to a project from the archive were counted in project statistics (completed prints, parts progress) as if they had already been printed. Only files with `status="completed"` (actually printed via a printer) now count toward completion stats. Files with `status="archived"` (stored but not yet printed) are no longer included. Reported by @SebSeifert.
|
||||
- **Python 3.10 Compatibility** — Bambuddy failed to start on Python 3.10 with `ImportError: cannot import name 'StrEnum' from 'enum'` because `enum.StrEnum` was added in Python 3.11. Added a compatibility shim that falls back to `(str, Enum)` on Python < 3.11, matching the documented requirement of Python 3.10+.
|
||||
- **Bug Report Bubble Overlapping Toasts** — Moved toast notifications and upload progress up so they stack above the bug report bubble instead of overlapping on top of each other.
|
||||
- **Virtual Printer: Bind-TLS Proxy Handshake Failure on OpenSSL 3.x** — The TLS proxy connecting to the printer's bind port (3002) failed with `SSLV3_ALERT_HANDSHAKE_FAILURE` on systems with OpenSSL 3.x (e.g. Python 3.12+) because the default cipher set excludes plain RSA key exchange, which is the only mode Bambu printers support. Added `AES256-GCM-SHA384` and `AES128-GCM-SHA256` to the client SSL context's cipher list.
|
||||
|
||||
@@ -97,11 +97,11 @@ async def compute_project_stats(
|
||||
)
|
||||
in_progress_prints = in_progress_result.scalar() or 0
|
||||
|
||||
# Sum completed items (parts) - sum of quantities for successful prints
|
||||
# Sum completed items (parts) - sum of quantities for actually printed jobs
|
||||
completed_items_result = await db.execute(
|
||||
select(func.coalesce(func.sum(PrintArchive.quantity), 0)).where(
|
||||
PrintArchive.project_id == project_id,
|
||||
PrintArchive.status.in_(["completed", "archived"]),
|
||||
PrintArchive.status == "completed",
|
||||
)
|
||||
)
|
||||
completed_items = int(completed_items_result.scalar() or 0)
|
||||
@@ -192,11 +192,11 @@ async def list_projects(
|
||||
)
|
||||
queue_count = queue_count_result.scalar() or 0
|
||||
|
||||
# Sum completed parts (quantities) - includes "archived" as successful
|
||||
# Sum completed parts (quantities) - only actually printed jobs
|
||||
completed_result = await db.execute(
|
||||
select(func.coalesce(func.sum(PrintArchive.quantity), 0)).where(
|
||||
PrintArchive.project_id == project.id,
|
||||
PrintArchive.status.in_(["completed", "archived"]),
|
||||
PrintArchive.status == "completed",
|
||||
)
|
||||
)
|
||||
completed_count = int(completed_result.scalar() or 0)
|
||||
|
||||
@@ -262,6 +262,121 @@ class TestProjectPartsTracking:
|
||||
assert data["stats"]["parts_progress_percent"] == 40.0 # parts: 10/25
|
||||
|
||||
|
||||
class TestProjectArchivedStatusNotCounted:
|
||||
"""Tests for bug #630: archived files added to a project should not count as printed."""
|
||||
|
||||
@pytest.fixture
|
||||
async def project_factory(self, db_session):
|
||||
"""Factory to create test projects."""
|
||||
|
||||
async def _create_project(**kwargs):
|
||||
from backend.app.models.project import Project
|
||||
|
||||
defaults = {
|
||||
"name": "Archived Status Test",
|
||||
"description": "Test project",
|
||||
"color": "#FF0000",
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
project = Project(**defaults)
|
||||
db_session.add(project)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(project)
|
||||
return project
|
||||
|
||||
return _create_project
|
||||
|
||||
@pytest.fixture
|
||||
async def archive_factory(self, db_session):
|
||||
"""Factory to create test archives."""
|
||||
|
||||
async def _create_archive(**kwargs):
|
||||
from backend.app.models.archive import PrintArchive
|
||||
|
||||
defaults = {
|
||||
"filename": "test.3mf",
|
||||
"file_path": "test/test.3mf",
|
||||
"file_size": 1000,
|
||||
"print_name": "Test Print",
|
||||
"status": "completed",
|
||||
"quantity": 1,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
|
||||
archive = PrintArchive(**defaults)
|
||||
db_session.add(archive)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(archive)
|
||||
return archive
|
||||
|
||||
return _create_archive
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archived_files_not_counted_as_completed(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Archived files added to a project should not count in completed_prints stats."""
|
||||
project = await project_factory(target_parts_count=20)
|
||||
|
||||
# 2 actually printed (completed), 3 just archived (not printed yet)
|
||||
await archive_factory(project_id=project.id, quantity=2, status="completed")
|
||||
await archive_factory(project_id=project.id, quantity=3, status="archived")
|
||||
await archive_factory(project_id=project.id, quantity=5, status="archived")
|
||||
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Only the completed archive should count
|
||||
assert data["stats"]["completed_prints"] == 2
|
||||
assert data["stats"]["parts_progress_percent"] == 10.0 # 2/20 = 10%
|
||||
assert data["stats"]["remaining_parts"] == 18
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_archived_files_not_counted_in_project_list(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Project list endpoint should not count archived files as completed."""
|
||||
project = await project_factory(name="List Archived Test", target_parts_count=50)
|
||||
|
||||
await archive_factory(project_id=project.id, quantity=4, status="completed")
|
||||
await archive_factory(project_id=project.id, quantity=6, status="archived")
|
||||
|
||||
response = await async_client.get("/api/v1/projects/")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
our_project = next((p for p in data if p["name"] == "List Archived Test"), None)
|
||||
assert our_project is not None
|
||||
assert our_project["completed_count"] == 4 # Only completed, not archived
|
||||
assert our_project["archive_count"] == 2 # Both archives exist as plates
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_only_completed_status_counts(
|
||||
self, async_client: AsyncClient, project_factory, archive_factory, db_session
|
||||
):
|
||||
"""Only 'completed' status should count in stats, not archived/failed/etc."""
|
||||
project = await project_factory(target_parts_count=100)
|
||||
|
||||
await archive_factory(project_id=project.id, quantity=10, status="completed")
|
||||
await archive_factory(project_id=project.id, quantity=5, status="archived")
|
||||
await archive_factory(project_id=project.id, quantity=3, status="failed")
|
||||
await archive_factory(project_id=project.id, quantity=2, status="aborted")
|
||||
|
||||
response = await async_client.get(f"/api/v1/projects/{project.id}")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
assert data["stats"]["completed_prints"] == 10 # Only "completed"
|
||||
assert data["stats"]["failed_prints"] == 2 # failed + aborted (count of archives, not sum)
|
||||
assert data["stats"]["total_archives"] == 4 # All archives
|
||||
assert data["stats"]["total_items"] == 20 # Sum of all quantities
|
||||
|
||||
|
||||
class TestProjectArchivesAPI:
|
||||
"""Tests for project-archive relationships."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user