loko/streetup/assets/views/export.py
2026-07-22 14:48:40 +02:00

204 lines
6.4 KiB
Python

"""
Export views for assets.
"""
import csv
import json
import re
from io import BytesIO
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.dateformat import format as dj_format_date
from django.utils.timezone import localtime, now
from django.utils.translation import gettext as _
from django.views.decorators.http import require_POST
from assets.models import AbstractLocation
from assets.permissions import can_view_asset
def _get_asset_location(asset):
"""Return the parent location of an asset by inspecting FK fields."""
for field in asset._meta.get_fields():
related = getattr(field, 'related_model', None)
if related and issubclass(related, AbstractLocation):
try:
loc = getattr(asset, field.name)
if loc is not None:
return loc
except Exception:
pass
return None
def _get_asset_model_display(asset):
"""Return the display name of the asset's model (subtype), if any."""
try:
model_val = getattr(asset, 'model', None)
if model_val and hasattr(model_val, 'get_name'):
return model_val.get_name()
except Exception:
pass
return ''
def _fmt_date(dt):
if dt is None:
return ''
try:
return dj_format_date(dt, 'Y-m-d')
except Exception:
return str(dt)
def _get_row(model_name, asset):
location = _get_asset_location(asset)
loc_code = location.code if location else ''
loc_name = location.get_name() if location else ''
status_display = (
asset.get_status_display()
if hasattr(asset, 'get_status_display')
else (getattr(asset, 'status', '') or '')
)
obsolescence_display = (
asset.get_obsolescence_display()
if hasattr(asset, 'get_obsolescence_display')
else (getattr(asset, 'obsolescence', '') or '')
)
warranty = getattr(asset, 'warranty_duration', None)
return [
model_name,
asset.id,
asset.code or '',
asset.get_name() if hasattr(asset, 'get_name') else '',
asset.category.get_name() if getattr(asset, 'category', None) else '',
_get_asset_model_display(asset),
status_display,
obsolescence_display,
loc_code,
loc_name,
_fmt_date(getattr(asset, 'installation_date', None)),
_fmt_date(getattr(asset, 'uninstallation_date', None)),
getattr(asset, 'serial_number', '') or '',
warranty if warranty is not None else '',
getattr(asset, 'brand', '') or '',
getattr(asset, 'note', '') or '',
getattr(asset, 'lon', '') if getattr(asset, 'lon', None) is not None else '',
getattr(asset, 'lat', '') if getattr(asset, 'lat', None) is not None else '',
]
EXPORT_HEADERS = [
_("Type"),
_("ID"),
_("Code"),
_("Nom"),
_("Catégorie"),
_("Modèle"),
_("Statut"),
_("Obsolescence"),
_("Code localisation"),
_("Nom localisation"),
_("Date d'installation"),
_("Date de déinstallation"),
_("Numéro de série"),
_("Garantie (mois)"),
_("Marque"),
_("Note"),
_("Longitude"),
_("Latitude"),
]
@login_required
@require_POST
def export_assets(request):
"""Export selected assets as CSV or XLSX.
POST params:
- ids: JSON string of asset keys (e.g. ``'["streetlamp:1","pole:2"]'``)
- format: ``'csv'`` (default) or ``'xlsx'``
Only assets that the current user is allowed to view are included.
"""
ids_param = request.POST.get('ids', '[]')
fmt = (request.POST.get('format') or 'csv').lower()
try:
keys = json.loads(ids_param)
if not isinstance(keys, list):
raise ValueError
except (ValueError, TypeError, json.JSONDecodeError):
return HttpResponseBadRequest(_("Paramètre 'ids' invalide."))
if not keys:
return HttpResponseBadRequest(_("Aucun asset sélectionné."))
# Group keys by model name
groups: dict[str, list[int]] = {}
for key in keys:
try:
model_name, asset_id = str(key).rsplit(':', 1)
groups.setdefault(model_name.lower(), []).append(int(asset_id))
except (ValueError, TypeError):
continue
# Fetch assets, filtering by user view permission
items: list[tuple[str, object]] = []
for model_name, asset_ids in groups.items():
try:
ct = ContentType.objects.get(model=model_name)
model_class = ct.model_class()
if model_class is None:
continue
for asset in model_class.objects.filter(pk__in=asset_ids):
if can_view_asset(request.user, asset):
items.append((model_name, asset))
except ContentType.DoesNotExist:
continue
timestamp = localtime(now()).strftime('%Y%m%d_%H%M%S')
headers = [str(h) for h in EXPORT_HEADERS]
if fmt == 'xlsx':
try:
from openpyxl import Workbook
except ImportError:
return HttpResponseBadRequest(_("Export XLSX indisponible (dépendance manquante)."))
_illegal_re = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]')
def _sanitize(value):
return _illegal_re.sub('', value) if isinstance(value, str) else value
wb = Workbook()
ws = wb.active
ws.title = 'Assets'
ws.append(headers)
for model_name, asset in items:
ws.append([_sanitize(v) for v in _get_row(model_name, asset)])
output = BytesIO()
wb.save(output)
output.seek(0)
filename = f"assets_{timestamp}.xlsx"
resp = HttpResponse(
output.read(),
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
)
resp['Content-Disposition'] = f'attachment; filename="{filename}"'
return resp
# Default: CSV with semicolon separator (better EU Excel support)
filename = f"assets_{timestamp}.csv"
resp = HttpResponse(content_type='text/csv; charset=utf-8')
resp['Content-Disposition'] = f'attachment; filename="{filename}"'
# UTF-8 BOM for Excel auto-detection
resp.write('\ufeff')
writer = csv.writer(resp, delimiter=';')
writer.writerow(headers)
for model_name, asset in items:
writer.writerow(_get_row(model_name, asset))
return resp