Optimize table view and export for software lists added. excelexport fixed - first line

This commit is contained in:
2026-09-24 11:38:40 +02:00
parent 80f6136399
commit 8de15f223c
16 changed files with 320 additions and 24 deletions
+51 -10
View File
@@ -1950,9 +1950,8 @@ def _xlsx_response(filename: str, headers: list[str], rows: list[list]) -> Respo
workbook = Workbook()
sheet = workbook.active
sheet.title = "Export"
sheet.freeze_panes = "A2"
sheet.auto_filter.ref = f"A1:{sheet.cell(row=1, column=max(1, len(headers))).column_letter}1"
sheet.append(headers)
sheet.freeze_panes = "A2"
header_fill = PatternFill("solid", fgColor="D9EAF7")
for cell in sheet[1]:
cell.font = Font(bold=True)
@@ -1960,6 +1959,8 @@ def _xlsx_response(filename: str, headers: list[str], rows: list[list]) -> Respo
cell.alignment = Alignment(vertical="top")
for row in rows:
sheet.append([_display_value(value) for value in row])
last_column = sheet.cell(row=1, column=max(1, len(headers))).column_letter
sheet.auto_filter.ref = f"A1:{last_column}{max(1, sheet.max_row)}"
for column in sheet.columns:
max_length = min(max((len(str(cell.value or "")) for cell in column), default=10) + 2, 60)
sheet.column_dimensions[column[0].column_letter].width = max_length
@@ -4582,8 +4583,17 @@ async def status_options_save(request: Request, db: Session = Depends(get_db)):
name = (form.get(f"name_{option.id}") or "").strip()
if not name:
continue
old_name = option.name
if name != old_name:
# Asset.status stores the status name as text. Keep existing assets
# linked when an administrator renames a status option.
db.query(Asset).filter(Asset.status == old_name).update(
{Asset.status: name},
synchronize_session=False,
)
option.name = name
option.active = form.get(f"active_{option.id}") == "on"
option.show_in_software_list = form.get(f"software_list_{option.id}") == "on"
try:
option.sort_order = int(form.get(f"sort_{option.id}") or option.sort_order)
except ValueError:
@@ -4596,7 +4606,12 @@ async def status_options_save(request: Request, db: Session = Depends(get_db)):
new_name = (form.get("new_name") or "").strip()
if new_name:
max_order = max((item.sort_order for item in existing), default=-1)
db.add(StatusOption(name=new_name, sort_order=max_order + 1, active=True))
db.add(StatusOption(
name=new_name,
sort_order=max_order + 1,
active=True,
show_in_software_list=form.get("new_show_in_software_list") == "on",
))
try:
db.commit()
except IntegrityError as exc:
@@ -5080,6 +5095,27 @@ def software_callback_health():
}
def _software_list_query(db: Session, *entities):
"""Base query for global software lists and statistics.
Status options explicitly disabled for software lists are excluded.
Assets without a status, or with a legacy/unmatched status value, remain
visible so enabling this feature cannot silently hide existing inventory.
The software inventory on an individual asset remains unchanged.
"""
return (
db.query(*entities)
.join(Asset, InstalledSoftware.asset_id == Asset.id)
.outerjoin(StatusOption, StatusOption.name == Asset.status)
.filter(
or_(
StatusOption.id.is_(None),
StatusOption.show_in_software_list.is_(True),
)
)
)
def _software_overview_statistics(db: Session) -> dict[str, int]:
"""Return only the counters required by the software overview.
@@ -5087,7 +5123,8 @@ def _software_overview_statistics(db: Session) -> dict[str, int]:
PostgreSQL performs the counting and grouping and returns only scalar values.
"""
total_entries, initialized_assets = (
db.query(
_software_list_query(
db,
func.count(InstalledSoftware.id),
func.count(func.distinct(InstalledSoftware.asset_id)),
)
@@ -5098,7 +5135,8 @@ def _software_overview_statistics(db: Session) -> dict[str, int]:
# intentionally preserves the exact grouping semantics used by the existing
# detailed summary (Unicode casefold is stronger than SQL LOWER).
summary_rows = (
db.query(
_software_list_query(
db,
InstalledSoftware.name,
InstalledSoftware.publisher,
InstalledSoftware.platform,
@@ -5158,7 +5196,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
distinct(func.nullif(func.trim(func.coalesce(InstalledSoftware.version, "")), ""))
)
query_rows = (
db.query(
_software_list_query(
db,
name_key.label("name_key"),
platform_key.label("platform_key"),
func.min(func.coalesce(InstalledSoftware.name, "")).label("name"),
@@ -5198,7 +5237,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
if grouping == "version":
query_rows = (
db.query(
_software_list_query(
db,
*common_columns,
version_key.label("version_key"),
func.min(func.coalesce(InstalledSoftware.version, "")).label("version"),
@@ -5232,7 +5272,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
distinct(func.nullif(func.trim(func.coalesce(InstalledSoftware.version, "")), ""))
)
query_rows = (
db.query(
_software_list_query(
db,
*common_columns,
versions_array.label("versions"),
func.count(func.distinct(InstalledSoftware.asset_id)).label("asset_count"),
@@ -5266,7 +5307,8 @@ def _software_summary_rows(db: Session, grouping: str) -> list[dict[str, Any]]:
def _software_installation_rows(db: Session):
"""Return only columns rendered by the detailed installation table."""
return (
db.query(
_software_list_query(
db,
InstalledSoftware.id.label("id"),
InstalledSoftware.asset_id.label("asset_id"),
Asset.name.label("asset_name"),
@@ -5278,7 +5320,6 @@ def _software_installation_rows(db: Session):
InstalledSoftware.install_date.label("install_date"),
InstalledSoftware.last_seen_at.label("last_seen_at"),
)
.join(Asset, InstalledSoftware.asset_id == Asset.id)
.order_by(
InstalledSoftware.name.asc(),
Asset.name.asc(),