mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-06-22 14:09:04 +01:00
* added db model is transparent and fixed PETG translucent showing black * js and filament form for transparent color * bumped version to v0.1.2
799 lines
33 KiB
Python
799 lines
33 KiB
Python
from datetime import timedelta, datetime
|
|
from django.views.generic import TemplateView, View, ListView, CreateView, UpdateView, DetailView, DeleteView
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.utils import timezone
|
|
from django.http import JsonResponse
|
|
from django.urls import reverse_lazy
|
|
from django.contrib import messages
|
|
from django.db.models import Q, Sum
|
|
import json
|
|
import zoneinfo
|
|
|
|
from .conf import app_settings
|
|
from .models import Printer, PrinterMetrics, Filament, FilamentColor, FilamentType, FilamentSnapshot, PrintJob, FilamentUsage
|
|
from .forms import FilamentForm, FilamentColorForm, FilamentTypeForm
|
|
|
|
_METRICS_API_FIELDS = [
|
|
'id', 'device_id', 'timestamp',
|
|
'nozzle_temp', 'nozzle_target_temp',
|
|
'bed_temp', 'bed_target_temp',
|
|
'print_percent', 'cooling_fan_speed', 'heatbreak_fan_speed',
|
|
'wifi_signal_dbm', 'ams_humidity_raw', 'ams_temp',
|
|
'layer_num', 'total_layer_num',
|
|
'gcode_state', 'print_type', 'subtask_name',
|
|
'external_spool',
|
|
]
|
|
_MAX_CHART_POINTS = 3000
|
|
|
|
|
|
class PrinterDashboardView(LoginRequiredMixin, TemplateView):
|
|
template_name = "bambu_run/printer_dashboard.html"
|
|
|
|
def _get_date_range(self, request):
|
|
"""Return (start_dt, end_dt) for the dashboard query. Override for custom date logic."""
|
|
time_24h_ago = timezone.now() - timedelta(hours=24)
|
|
return time_24h_ago, None # None means "now"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
|
|
try:
|
|
printer_device = Printer.objects.filter(is_active=True).first()
|
|
if not printer_device:
|
|
context["error"] = (
|
|
"No 3D printer device found. Please run bambu_collector first."
|
|
)
|
|
return context
|
|
except Exception as e:
|
|
context["error"] = f"Error loading printer device: {str(e)}"
|
|
return context
|
|
|
|
tz = zoneinfo.ZoneInfo(app_settings.TIMEZONE)
|
|
|
|
# Get date range (overridable by subclasses)
|
|
start_dt, end_dt = self._get_date_range(self.request)
|
|
metrics = PrinterMetrics.objects.filter(
|
|
device=printer_device, timestamp__gte=start_dt
|
|
)
|
|
if end_dt:
|
|
metrics = metrics.filter(timestamp__lte=end_dt)
|
|
metrics = metrics.prefetch_related('filament_snapshots').order_by("timestamp")
|
|
|
|
latest_metric = metrics.last()
|
|
|
|
printer_data_json = {
|
|
"timestamps": [
|
|
m.timestamp.astimezone(tz).strftime("%H:%M") for m in metrics
|
|
],
|
|
"dates": [
|
|
m.timestamp.astimezone(tz).strftime("%Y-%m-%d") for m in metrics
|
|
],
|
|
"nozzle_temp": [
|
|
float(m.nozzle_temp) if m.nozzle_temp else None for m in metrics
|
|
],
|
|
"nozzle_target_temp": [
|
|
float(m.nozzle_target_temp) if m.nozzle_target_temp else None
|
|
for m in metrics
|
|
],
|
|
"bed_temp": [float(m.bed_temp) if m.bed_temp else None for m in metrics],
|
|
"bed_target_temp": [
|
|
float(m.bed_target_temp) if m.bed_target_temp else None for m in metrics
|
|
],
|
|
"print_percent": [
|
|
m.print_percent if m.print_percent else 0 for m in metrics
|
|
],
|
|
"print_type": [m.print_type for m in metrics],
|
|
"gcode_state": [m.gcode_state for m in metrics],
|
|
"cooling_fan_speed": [
|
|
m.cooling_fan_speed if m.cooling_fan_speed else 0 for m in metrics
|
|
],
|
|
"heatbreak_fan_speed": [
|
|
m.heatbreak_fan_speed if m.heatbreak_fan_speed else 0 for m in metrics
|
|
],
|
|
"wifi_signal_dbm": [
|
|
m.wifi_signal_dbm if m.wifi_signal_dbm else None for m in metrics
|
|
],
|
|
"ams_humidity_raw": [
|
|
m.ams_humidity_raw if m.ams_humidity_raw else None for m in metrics
|
|
],
|
|
"ams_temp": [
|
|
float(m.ams_temp) if m.ams_temp else None for m in metrics
|
|
],
|
|
"layer_num": [
|
|
m.layer_num if m.layer_num else 0 for m in metrics
|
|
],
|
|
"total_layer_num": [
|
|
m.total_layer_num if m.total_layer_num else 0 for m in metrics
|
|
],
|
|
"filament_timeline": self._prepare_filament_timeline(metrics),
|
|
}
|
|
|
|
stats = {}
|
|
if latest_metric:
|
|
filaments_list = []
|
|
try:
|
|
filament_snapshots = latest_metric.filament_snapshots.select_related('filament').all()
|
|
for snapshot in filament_snapshots:
|
|
filament_dict = {
|
|
'tray_id': snapshot.tray_id,
|
|
'type': snapshot.type or 'Unknown',
|
|
'brand': snapshot.sub_type or 'Unknown',
|
|
'color': snapshot.color or 'FFFFFFFF',
|
|
'remain_percent': snapshot.remain_percent or 0,
|
|
}
|
|
if snapshot.filament:
|
|
filament_dict['color_name'] = snapshot.filament.color
|
|
filament_dict['filament_pk'] = snapshot.filament.pk
|
|
filament_dict['is_transparent'] = snapshot.filament.is_transparent
|
|
filaments_list.append(filament_dict)
|
|
except Exception:
|
|
filaments_list = []
|
|
|
|
stats = {
|
|
"nozzle_temp": float(latest_metric.nozzle_temp) if latest_metric.nozzle_temp else 0,
|
|
"bed_temp": float(latest_metric.bed_temp) if latest_metric.bed_temp else 0,
|
|
"chamber_temp": float(latest_metric.chamber_temp) if latest_metric.chamber_temp else 0,
|
|
"print_percent": latest_metric.print_percent or 0,
|
|
"gcode_state": latest_metric.gcode_state or "Unknown",
|
|
"print_type": latest_metric.print_type or "idle",
|
|
"subtask_name": latest_metric.subtask_name or "No active print",
|
|
"chamber_light": latest_metric.chamber_light or "unknown",
|
|
"ams_temp": float(latest_metric.ams_temp) if latest_metric.ams_temp else None,
|
|
"ams_humidity": latest_metric.ams_humidity,
|
|
"filaments": filaments_list,
|
|
"external_spool": latest_metric.external_spool or {},
|
|
"timestamp": latest_metric.timestamp.astimezone(tz).strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
|
|
project_markers = self._calculate_project_markers(list(metrics), tz)
|
|
printer_data_json["project_markers"] = project_markers
|
|
|
|
context["printer_device"] = printer_device
|
|
context["device_name"] = printer_device.name
|
|
context["stats"] = stats
|
|
context["metrics_count"] = metrics.count()
|
|
context["printer_data_json"] = json.dumps(printer_data_json)
|
|
|
|
return context
|
|
|
|
def _calculate_project_markers(self, metrics, timezone_info):
|
|
"""Calculate where print jobs start and end"""
|
|
markers = []
|
|
current_job = None
|
|
last_state = None
|
|
|
|
for idx, metric in enumerate(metrics):
|
|
subtask = metric.subtask_name
|
|
gcode_state = metric.gcode_state
|
|
|
|
is_printing = gcode_state not in ['FINISH', 'IDLE', None, '']
|
|
|
|
if subtask and subtask != current_job and is_printing:
|
|
markers.append({
|
|
'type': 'start',
|
|
'index': idx,
|
|
'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(),
|
|
'project_name': subtask,
|
|
})
|
|
current_job = subtask
|
|
last_state = gcode_state
|
|
|
|
elif current_job and last_state and last_state not in ['FINISH', 'IDLE'] and gcode_state in ['FINISH', 'IDLE']:
|
|
markers.append({
|
|
'type': 'end',
|
|
'index': idx,
|
|
'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(),
|
|
'project_name': current_job,
|
|
})
|
|
current_job = None
|
|
|
|
last_state = gcode_state
|
|
|
|
return markers
|
|
|
|
def _prepare_filament_timeline(self, metrics):
|
|
"""Prepare filament data organized by unique filament configurations."""
|
|
filament_data = {}
|
|
total_points = len(metrics)
|
|
|
|
for idx, metric in enumerate(metrics):
|
|
try:
|
|
snapshots = metric.filament_snapshots.all()
|
|
except Exception:
|
|
snapshots = []
|
|
|
|
for snapshot in snapshots:
|
|
tray_id = snapshot.tray_id
|
|
fil_type = snapshot.type or 'Unknown'
|
|
fil_sub_type = snapshot.sub_type or 'Unknown'
|
|
fil_color = snapshot.color or 'FFFFFFFF'
|
|
|
|
unique_key = f"{tray_id}_{fil_type}_{fil_sub_type}_{fil_color}"
|
|
|
|
if unique_key not in filament_data:
|
|
filament_data[unique_key] = {
|
|
'tray_id': tray_id,
|
|
'type': fil_type,
|
|
'brand': fil_sub_type,
|
|
'color': fil_color,
|
|
'remain_data': [None] * total_points,
|
|
'start_idx': idx,
|
|
}
|
|
|
|
remain_percent = snapshot.remain_percent or 0
|
|
filament_data[unique_key]['remain_data'][idx] = remain_percent
|
|
|
|
for idx, metric in enumerate(metrics):
|
|
external = metric.external_spool or {}
|
|
if external.get('type'):
|
|
fil_type = external.get('type', 'Unknown')
|
|
fil_color = external.get('color', '161616FF')
|
|
unique_key = f"External_{fil_type}_{fil_color}"
|
|
|
|
if unique_key not in filament_data:
|
|
filament_data[unique_key] = {
|
|
'tray_id': 'External',
|
|
'type': fil_type,
|
|
'brand': 'External',
|
|
'color': fil_color,
|
|
'remain_data': [None] * total_points,
|
|
'start_idx': idx,
|
|
}
|
|
|
|
remain_percent = external.get('remain', 0)
|
|
filament_data[unique_key]['remain_data'][idx] = remain_percent
|
|
|
|
return filament_data
|
|
|
|
|
|
class PrinterDataAPIView(LoginRequiredMixin, View):
|
|
"""API endpoint for dynamic printer chart updates"""
|
|
|
|
def get(self, request):
|
|
start_date = request.GET.get("start_date")
|
|
end_date = request.GET.get("end_date")
|
|
start_time = request.GET.get("start_time", "00:00")
|
|
end_time = request.GET.get("end_time", "23:59")
|
|
|
|
try:
|
|
printer_device = Printer.objects.filter(is_active=True).first()
|
|
if not printer_device:
|
|
return JsonResponse({"error": "No printer device found"}, status=404)
|
|
|
|
tz = zoneinfo.ZoneInfo(app_settings.TIMEZONE)
|
|
|
|
# Stage A: only() + step calculation
|
|
query = (
|
|
PrinterMetrics.objects
|
|
.filter(device=printer_device)
|
|
.only(*_METRICS_API_FIELDS)
|
|
)
|
|
|
|
if start_date and start_time and end_date and end_time:
|
|
start_dt = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
|
end_dt = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
|
query = query.filter(timestamp__gte=start_dt, timestamp__lte=end_dt)
|
|
range_seconds = (end_dt - start_dt).total_seconds()
|
|
expected_count = max(1, int(range_seconds / 30))
|
|
elif start_date and start_time:
|
|
start_dt = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
|
query = query.filter(timestamp__gte=start_dt)
|
|
expected_count = _MAX_CHART_POINTS
|
|
elif end_date and end_time:
|
|
end_dt = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M").replace(tzinfo=tz)
|
|
query = query.filter(timestamp__lte=end_dt)
|
|
expected_count = _MAX_CHART_POINTS
|
|
else:
|
|
expected_count = _MAX_CHART_POINTS
|
|
|
|
step = max(1, expected_count // _MAX_CHART_POINTS)
|
|
|
|
# Stage B: single DB round-trip, downsample in Python
|
|
metrics_list = list(query.order_by("timestamp"))
|
|
if step > 1:
|
|
metrics_list = metrics_list[::step]
|
|
|
|
total_points = len(metrics_list)
|
|
|
|
# Stage C: targeted snapshot fetch (only sampled IDs)
|
|
snapshots_by_metric: dict = {}
|
|
if metrics_list:
|
|
sampled_ids = [m.id for m in metrics_list]
|
|
for snap in FilamentSnapshot.objects.filter(printer_metric_id__in=sampled_ids):
|
|
snapshots_by_metric.setdefault(snap.printer_metric_id, []).append(snap)
|
|
|
|
# Stage D: single-pass serialization
|
|
timestamps = []
|
|
timestamps_iso = []
|
|
dates = []
|
|
nozzle_temp = []
|
|
nozzle_target_temp = []
|
|
bed_temp = []
|
|
bed_target_temp = []
|
|
print_percent = []
|
|
cooling_fan_speed = []
|
|
heatbreak_fan_speed = []
|
|
wifi_signal_dbm = []
|
|
ams_humidity_raw = []
|
|
ams_temp = []
|
|
layer_num = []
|
|
total_layer_num = []
|
|
gcode_state = []
|
|
print_type = []
|
|
subtask_name = []
|
|
|
|
project_markers = []
|
|
current_job = None
|
|
last_state = None
|
|
|
|
filament_data = {}
|
|
|
|
for idx, m in enumerate(metrics_list):
|
|
ts = m.timestamp.astimezone(tz)
|
|
timestamps.append(ts.strftime('%H:%M'))
|
|
timestamps_iso.append(ts.isoformat())
|
|
dates.append(ts.strftime('%Y-%m-%d'))
|
|
nozzle_temp.append(float(m.nozzle_temp) if m.nozzle_temp else None)
|
|
nozzle_target_temp.append(float(m.nozzle_target_temp) if m.nozzle_target_temp else None)
|
|
bed_temp.append(float(m.bed_temp) if m.bed_temp else None)
|
|
bed_target_temp.append(float(m.bed_target_temp) if m.bed_target_temp else None)
|
|
print_percent.append(m.print_percent if m.print_percent else 0)
|
|
cooling_fan_speed.append(m.cooling_fan_speed if m.cooling_fan_speed else 0)
|
|
heatbreak_fan_speed.append(m.heatbreak_fan_speed if m.heatbreak_fan_speed else 0)
|
|
wifi_signal_dbm.append(m.wifi_signal_dbm if m.wifi_signal_dbm else None)
|
|
ams_humidity_raw.append(m.ams_humidity_raw if m.ams_humidity_raw else None)
|
|
ams_temp.append(float(m.ams_temp) if m.ams_temp else None)
|
|
layer_num.append(m.layer_num if m.layer_num else 0)
|
|
total_layer_num.append(m.total_layer_num if m.total_layer_num else 0)
|
|
gcode_state.append(m.gcode_state)
|
|
print_type.append(m.print_type)
|
|
subtask_name.append(m.subtask_name)
|
|
|
|
# Project marker detection (inline)
|
|
subtask = m.subtask_name
|
|
gs = m.gcode_state
|
|
is_printing = gs not in ['FINISH', 'IDLE', None, '']
|
|
if subtask and subtask != current_job and is_printing:
|
|
project_markers.append({
|
|
'type': 'start',
|
|
'index': idx,
|
|
'timestamp': ts.isoformat(),
|
|
'project_name': subtask,
|
|
})
|
|
current_job = subtask
|
|
last_state = gs
|
|
elif current_job and last_state and last_state not in ['FINISH', 'IDLE'] and gs in ['FINISH', 'IDLE']:
|
|
project_markers.append({
|
|
'type': 'end',
|
|
'index': idx,
|
|
'timestamp': ts.isoformat(),
|
|
'project_name': current_job,
|
|
})
|
|
current_job = None
|
|
last_state = gs
|
|
|
|
# Filament timeline (inline)
|
|
for snap in snapshots_by_metric.get(m.id, []):
|
|
tray_id = snap.tray_id
|
|
fil_type = snap.type or 'Unknown'
|
|
fil_sub_type = snap.sub_type or 'Unknown'
|
|
fil_color = snap.color or 'FFFFFFFF'
|
|
unique_key = f"{tray_id}_{fil_type}_{fil_sub_type}_{fil_color}"
|
|
if unique_key not in filament_data:
|
|
filament_data[unique_key] = {
|
|
'tray_id': tray_id,
|
|
'type': fil_type,
|
|
'brand': fil_sub_type,
|
|
'color': fil_color,
|
|
'remain_data': [None] * total_points,
|
|
'start_idx': idx,
|
|
}
|
|
filament_data[unique_key]['remain_data'][idx] = snap.remain_percent or 0
|
|
|
|
external = m.external_spool or {}
|
|
if external.get('type'):
|
|
fil_type = external.get('type', 'Unknown')
|
|
fil_color = external.get('color', '161616FF')
|
|
unique_key = f"External_{fil_type}_{fil_color}"
|
|
if unique_key not in filament_data:
|
|
filament_data[unique_key] = {
|
|
'tray_id': 'External',
|
|
'type': fil_type,
|
|
'brand': 'External',
|
|
'color': fil_color,
|
|
'remain_data': [None] * total_points,
|
|
'start_idx': idx,
|
|
}
|
|
filament_data[unique_key]['remain_data'][idx] = external.get('remain', 0)
|
|
|
|
data = {
|
|
"timestamps": timestamps,
|
|
"timestamps_iso": timestamps_iso,
|
|
"dates": dates,
|
|
"nozzle_temp": nozzle_temp,
|
|
"nozzle_target_temp": nozzle_target_temp,
|
|
"bed_temp": bed_temp,
|
|
"bed_target_temp": bed_target_temp,
|
|
"print_percent": print_percent,
|
|
"cooling_fan_speed": cooling_fan_speed,
|
|
"heatbreak_fan_speed": heatbreak_fan_speed,
|
|
"wifi_signal_dbm": wifi_signal_dbm,
|
|
"ams_humidity_raw": ams_humidity_raw,
|
|
"ams_temp": ams_temp,
|
|
"layer_num": layer_num,
|
|
"total_layer_num": total_layer_num,
|
|
"gcode_state": gcode_state,
|
|
"print_type": print_type,
|
|
"subtask_name": subtask_name,
|
|
"project_markers": project_markers,
|
|
"filament_timeline": filament_data,
|
|
}
|
|
|
|
return JsonResponse(data)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return JsonResponse({"error": str(e)}, status=500)
|
|
|
|
|
|
class FilamentUsageDataAPIView(LoginRequiredMixin, View):
|
|
"""API endpoint for filament usage history with date/time filtering"""
|
|
|
|
def get(self, request, pk):
|
|
start_date = request.GET.get("start_date")
|
|
end_date = request.GET.get("end_date")
|
|
start_time = request.GET.get("start_time", "00:00")
|
|
end_time = request.GET.get("end_time", "23:59")
|
|
|
|
try:
|
|
filament = Filament.objects.get(pk=pk)
|
|
tz = zoneinfo.ZoneInfo(app_settings.TIMEZONE)
|
|
query = filament.usage_snapshots.select_related('printer_metric')
|
|
|
|
if start_date and start_time:
|
|
from datetime import datetime
|
|
start_dt_naive = datetime.strptime(f"{start_date} {start_time}", "%Y-%m-%d %H:%M")
|
|
start_dt = start_dt_naive.replace(tzinfo=tz)
|
|
query = query.filter(printer_metric__timestamp__gte=start_dt)
|
|
|
|
if end_date and end_time:
|
|
from datetime import datetime
|
|
end_dt_naive = datetime.strptime(f"{end_date} {end_time}", "%Y-%m-%d %H:%M")
|
|
end_dt = end_dt_naive.replace(tzinfo=tz)
|
|
query = query.filter(printer_metric__timestamp__lte=end_dt)
|
|
|
|
fallback_used = False
|
|
if not start_date and not end_date:
|
|
time_24h_ago = timezone.now() - timedelta(hours=24)
|
|
default_query = query.filter(printer_metric__timestamp__gte=time_24h_ago)
|
|
if default_query.exists():
|
|
snapshots = default_query.order_by('printer_metric__timestamp')
|
|
else:
|
|
# Fallback: show 24h window ending at the most recent available snapshot
|
|
last_snapshot = query.order_by('-printer_metric__timestamp').first()
|
|
if last_snapshot:
|
|
last_ts = last_snapshot.printer_metric.timestamp
|
|
fallback_start = last_ts - timedelta(hours=24)
|
|
snapshots = query.filter(
|
|
printer_metric__timestamp__gte=fallback_start,
|
|
printer_metric__timestamp__lte=last_ts
|
|
).order_by('printer_metric__timestamp')
|
|
fallback_used = True
|
|
else:
|
|
snapshots = query.none()
|
|
else:
|
|
snapshots = query.order_by('printer_metric__timestamp')
|
|
|
|
data = {
|
|
"timestamps": [s.printer_metric.timestamp.astimezone(tz).strftime('%Y-%m-%d %H:%M') for s in snapshots],
|
|
"remaining": [s.remain_percent for s in snapshots],
|
|
"fallback_used": fallback_used,
|
|
}
|
|
|
|
return JsonResponse(data)
|
|
|
|
except Filament.DoesNotExist:
|
|
return JsonResponse({"error": "Filament not found"}, status=404)
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return JsonResponse({"error": str(e)}, status=500)
|
|
|
|
|
|
# ==================== Filament CRUD Views ====================
|
|
|
|
class FilamentListView(LoginRequiredMixin, ListView):
|
|
model = Filament
|
|
template_name = 'bambu_run/filament_list.html'
|
|
context_object_name = 'filaments'
|
|
paginate_by = 20
|
|
|
|
def get_queryset(self):
|
|
queryset = Filament.objects.all()
|
|
|
|
filament_type = self.request.GET.get('type')
|
|
if filament_type:
|
|
queryset = queryset.filter(type=filament_type)
|
|
|
|
loaded = self.request.GET.get('loaded')
|
|
if loaded == 'yes':
|
|
queryset = queryset.filter(is_loaded_in_ams=True)
|
|
elif loaded == 'no':
|
|
queryset = queryset.filter(is_loaded_in_ams=False)
|
|
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(brand__icontains=search) |
|
|
Q(color__icontains=search) |
|
|
Q(type__icontains=search)
|
|
)
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['total_spools'] = Filament.objects.count()
|
|
context['loaded_spools'] = Filament.objects.filter(is_loaded_in_ams=True).count()
|
|
context['low_filaments'] = Filament.objects.filter(remaining_percent__lt=20).count()
|
|
context['filament_types'] = sorted(
|
|
set(Filament.objects.exclude(type__isnull=True).exclude(type='').values_list('type', flat=True))
|
|
)
|
|
return context
|
|
|
|
|
|
def _filament_type_map():
|
|
"""Return a JSON-serialisable dict mapping FilamentType pk → {type, sub_type, brand}."""
|
|
return {
|
|
str(ft.pk): {'type': ft.type, 'sub_type': ft.sub_type or '', 'brand': ft.brand}
|
|
for ft in FilamentType.objects.all()
|
|
}
|
|
|
|
|
|
class FilamentCreateView(LoginRequiredMixin, CreateView):
|
|
model = Filament
|
|
form_class = FilamentForm
|
|
template_name = 'bambu_run/filament_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['filament_type_map'] = json.dumps(_filament_type_map())
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, f'Filament spool "{form.instance}" added successfully!')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class FilamentUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = Filament
|
|
form_class = FilamentForm
|
|
template_name = 'bambu_run/filament_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['filament_type_map'] = json.dumps(_filament_type_map())
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, f'Filament spool "{form.instance}" updated successfully!')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class FilamentDeleteView(LoginRequiredMixin, DeleteView):
|
|
model = Filament
|
|
template_name = 'bambu_run/filament_confirm_delete.html'
|
|
success_url = reverse_lazy('bambu_run:filament_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
return context
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
filament = self.get_object()
|
|
messages.success(self.request, f'Filament spool "{filament}" has been deleted.')
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
|
|
class FilamentDetailView(LoginRequiredMixin, DetailView):
|
|
model = Filament
|
|
template_name = 'bambu_run/filament_detail.html'
|
|
context_object_name = 'filament'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
filament = self.object
|
|
|
|
context['print_usages'] = filament.print_usages.select_related('print_job').order_by('-print_job__start_time')[:20]
|
|
|
|
total_consumed = filament.print_usages.aggregate(
|
|
total=Sum('consumed_percent')
|
|
)['total'] or 0
|
|
context['total_consumed_percent'] = total_consumed
|
|
|
|
return context
|
|
|
|
|
|
# ==================== FilamentColor Views ====================
|
|
|
|
class FilamentColorListView(LoginRequiredMixin, ListView):
|
|
model = FilamentColor
|
|
template_name = 'bambu_run/filament_color_list.html'
|
|
context_object_name = 'colors'
|
|
paginate_by = 50
|
|
|
|
def get_queryset(self):
|
|
return FilamentColor.objects.all().order_by('filament_type', 'filament_sub_type', 'color_name')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['total_colors'] = FilamentColor.objects.count()
|
|
return context
|
|
|
|
|
|
class FilamentColorCreateView(LoginRequiredMixin, CreateView):
|
|
model = FilamentColor
|
|
form_class = FilamentColorForm
|
|
template_name = 'bambu_run/filament_color_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_color_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
self._update_matching_filaments(self.object)
|
|
return response
|
|
|
|
def _update_matching_filaments(self, filament_color):
|
|
from .utils import match_and_update_filament_color
|
|
updated_count = match_and_update_filament_color(filament_color)
|
|
if updated_count > 0:
|
|
messages.success(
|
|
self.request,
|
|
f"Color '{filament_color.color_name}' created! "
|
|
f"Updated {updated_count} matching filament spool(s)."
|
|
)
|
|
|
|
|
|
class FilamentColorUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = FilamentColor
|
|
form_class = FilamentColorForm
|
|
template_name = 'bambu_run/filament_color_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_color_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
self._update_matching_filaments(self.object)
|
|
return response
|
|
|
|
def _update_matching_filaments(self, filament_color):
|
|
from .utils import match_and_update_filament_color
|
|
updated_count = match_and_update_filament_color(filament_color)
|
|
if updated_count > 0:
|
|
messages.success(
|
|
self.request,
|
|
f"Color '{filament_color.color_name}' updated! "
|
|
f"Updated {updated_count} matching filament spool(s)."
|
|
)
|
|
|
|
|
|
class FilamentColorDeleteView(LoginRequiredMixin, DeleteView):
|
|
model = FilamentColor
|
|
template_name = 'bambu_run/filament_color_confirm_delete.html'
|
|
success_url = reverse_lazy('bambu_run:filament_color_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
return context
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
messages.success(request, f"Color '{self.get_object().color_name}' deleted successfully!")
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
|
|
# ==================== FilamentType Views ====================
|
|
|
|
class FilamentTypeListView(LoginRequiredMixin, ListView):
|
|
model = FilamentType
|
|
template_name = 'bambu_run/filament_type_list.html'
|
|
context_object_name = 'types'
|
|
paginate_by = 50
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['total_types'] = FilamentType.objects.count()
|
|
return context
|
|
|
|
|
|
class FilamentTypeCreateView(LoginRequiredMixin, CreateView):
|
|
model = FilamentType
|
|
form_class = FilamentTypeForm
|
|
template_name = 'bambu_run/filament_type_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_type_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['existing_types'] = list(
|
|
FilamentType.objects.values_list('type', flat=True).distinct().order_by('type')
|
|
)
|
|
context['existing_sub_types'] = list(
|
|
FilamentType.objects.exclude(sub_type__isnull=True).exclude(sub_type='')
|
|
.values_list('sub_type', flat=True).distinct().order_by('sub_type')
|
|
)
|
|
context['existing_brands'] = list(
|
|
FilamentType.objects.values_list('brand', flat=True).distinct().order_by('brand')
|
|
)
|
|
context['preset_types'] = FilamentTypeForm.PRESET_TYPES
|
|
context['preset_sub_types'] = FilamentTypeForm.PRESET_SUB_TYPES
|
|
context['preset_brands'] = FilamentTypeForm.PRESET_BRANDS
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, f'Filament type "{form.instance}" added successfully!')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class FilamentTypeUpdateView(LoginRequiredMixin, UpdateView):
|
|
model = FilamentType
|
|
form_class = FilamentTypeForm
|
|
template_name = 'bambu_run/filament_type_form.html'
|
|
success_url = reverse_lazy('bambu_run:filament_type_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
context['existing_types'] = list(
|
|
FilamentType.objects.values_list('type', flat=True).distinct().order_by('type')
|
|
)
|
|
context['existing_sub_types'] = list(
|
|
FilamentType.objects.exclude(sub_type__isnull=True).exclude(sub_type='')
|
|
.values_list('sub_type', flat=True).distinct().order_by('sub_type')
|
|
)
|
|
context['existing_brands'] = list(
|
|
FilamentType.objects.values_list('brand', flat=True).distinct().order_by('brand')
|
|
)
|
|
context['preset_types'] = FilamentTypeForm.PRESET_TYPES
|
|
context['preset_sub_types'] = FilamentTypeForm.PRESET_SUB_TYPES
|
|
context['preset_brands'] = FilamentTypeForm.PRESET_BRANDS
|
|
return context
|
|
|
|
def form_valid(self, form):
|
|
messages.success(self.request, f'Filament type "{form.instance}" updated successfully!')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class FilamentTypeDeleteView(LoginRequiredMixin, DeleteView):
|
|
model = FilamentType
|
|
template_name = 'bambu_run/filament_type_confirm_delete.html'
|
|
success_url = reverse_lazy('bambu_run:filament_type_list')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['bambu_run_base_template'] = app_settings.BASE_TEMPLATE
|
|
return context
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
messages.success(request, f"Filament type '{self.get_object()}' deleted successfully!")
|
|
return super().delete(request, *args, **kwargs)
|