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 = [] subtask_name = latest_metric.subtask_name or "No active print" # Look up active PrintJob for a better display name (cloud design_title) job_display_name = subtask_name if latest_metric.subtask_name: active_job = ( PrintJob.objects.filter( device=printer_device, project_name=latest_metric.subtask_name, end_time__isnull=True, ).select_related('cloud_task').first() or PrintJob.objects.filter( device=printer_device, project_name=latest_metric.subtask_name, ).select_related('cloud_task').order_by('-start_time').first() ) if active_job: job_display_name = active_job.display_name 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": subtask_name, "job_display_name": job_display_name, "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, using cloud design_title when available.""" if not metrics: return [] # Build a lookup: subtask_name -> display_name from PrintJobs in this time window window_start = metrics[0].timestamp window_end = metrics[-1].timestamp device = metrics[0].device jobs_qs = PrintJob.objects.filter( device=device, start_time__gte=window_start - timedelta(minutes=5), start_time__lte=window_end + timedelta(minutes=5), ).select_related('cloud_task') # Map project_name (= subtask_name) -> best display name subtask_to_display = {} for job in jobs_qs: subtask_to_display[job.project_name] = job.display_name 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: display = subtask_to_display.get(subtask, subtask) markers.append({ 'type': 'start', 'index': idx, 'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(), 'project_name': display, }) 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']: display = subtask_to_display.get(current_job, current_job) markers.append({ 'type': 'end', 'index': idx, 'timestamp': metric.timestamp.astimezone(timezone_info).isoformat(), 'project_name': display, }) 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__cloud_task').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)