Coverage for sm / views.py: 49%
144 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-24 12:43 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-24 12:43 +0000
1from django.views.generic import TemplateView
2from django.contrib.auth.mixins import LoginRequiredMixin
3from django.contrib import messages
4from server.models import Model as Server
5from cluster.models import Model as Cluster
6from vendor.models import Model as Vendor
7from operatingsystem.models import Model as OS
8from django.db.models import Count, Q
9from django.core.exceptions import ObjectDoesNotExist
10from django.apps import apps
11from django.http import Http404, HttpResponseRedirect
13from django.db.models import ProtectedError
14from django.utils.translation import gettext as _
15from django.shortcuts import redirect
16from django.views import View
17from typing import Any, List
18from .utils_starterpack import import_starter_pack
21class SafeDeleteMixin:
22 """
23 Mixin to catch ProtectedError during deletion and offer
24 reassignment or bulk deletion.
25 """
27 def get_context_data(self, **kwargs: Any) -> Any:
28 context = super().get_context_data(**kwargs) # type: ignore
29 if hasattr(self, "protected_error") and self.protected_error: # type: ignore
30 context["protected_error"] = True
31 # Exclude our object from reassign list
32 context["all_objects"] = self.model.objects.exclude(
33 pk=self.object.pk # type: ignore
34 )
36 # Re-collect protected objects properly
37 try:
38 self.object.delete() # type: ignore
39 except ProtectedError as e:
40 context["protected_objects"] = e.protected_objects
41 context["protected_count"] = len(e.protected_objects)
42 return context
44 def form_valid(self, form: Any) -> Any:
45 success_url = self.get_success_url() # type: ignore
46 try:
47 # Try normal deletion first
48 obj_name = str(self.object) # type: ignore
49 self.object.delete() # type: ignore
50 if hasattr(self, "success_message") and self.success_message: # type: ignore # noqa: E501
51 messages.success(
52 # type: ignore
53 self.request,
54 self.success_message % self.object.__dict__,
55 )
56 else:
57 messages.success(self.request, _("Successfully deleted %s") % obj_name)
58 return HttpResponseRedirect(success_url)
59 except ProtectedError as e:
60 action = self.request.POST.get("protected_action")
61 if action == "reassign":
62 new_obj_id = self.request.POST.get("new_target")
63 if new_obj_id:
64 new_obj = self.model.objects.get(pk=new_obj_id) # type: ignore
65 # This part is tricky as we don't know the field name on
66 # the remote side without inspecting the protected objects.
67 for protected in e.protected_objects:
68 # Find the FK field that points to our object
69 for field in protected._meta.fields:
70 # type: ignore
71 if field.is_relation and field.related_model == self.model:
72 setattr(protected, field.name, new_obj)
73 protected.save()
75 self.object.delete() # type: ignore
76 messages.success(
77 self.request,
78 _("Successfully reassigned dependencies and deleted %s")
79 % self.object, # type: ignore
80 )
81 return HttpResponseRedirect(success_url)
83 elif action == "delete_all":
84 for protected in e.protected_objects:
85 protected.delete()
86 self.object.delete() # type: ignore
87 messages.success(
88 self.request,
89 _("Successfully deleted %s and all dependent objects")
90 % self.object, # type: ignore
91 )
92 return HttpResponseRedirect(success_url)
94 self.protected_error = True # type: ignore
95 return self.render_to_response(
96 self.get_context_data(object=self.object) # type: ignore
97 )
100class DashboardView(LoginRequiredMixin, TemplateView):
101 template_name = "dashboard.html"
103 def get_queryset_filtered(self, model: Any) -> Any:
104 if self.request.user.is_superuser:
105 return model.objects.all()
106 user_groups = self.request.user.groups.all()
107 if hasattr(model, "group"):
108 return model.objects.filter(
109 Q(group__in=user_groups) | Q(group__isnull=True)
110 )
111 return model.objects.all()
113 def get_context_data(self, **kwargs: Any) -> Any:
114 context = super().get_context_data(**kwargs)
116 # Basic Stats (Filtered)
117 context["server_count"] = self.get_queryset_filtered(Server).count()
118 context["cluster_count"] = self.get_queryset_filtered(Cluster).count()
119 context["vendor_count"] = Vendor.objects.count()
120 context["os_count"] = OS.objects.count()
122 # Data for Charts (Filtered)
123 # OS Distribution
124 os_dist = (
125 self.get_queryset_filtered(Server)
126 .values("operatingsystem__vendor__name", "operatingsystem__version")
127 .annotate(count=Count("id"))
128 .order_by("-count")[:5]
129 )
131 context["os_labels"] = [
132 f"{item['operatingsystem__vendor__name']} "
133 f"{item['operatingsystem__version']}"
134 for item in os_dist
135 ]
136 context["os_data"] = [item["count"] for item in os_dist]
138 # Status Distribution
139 status_dist = (
140 self.get_queryset_filtered(Server)
141 .values("status__name")
142 .annotate(count=Count("id"))
143 .order_by("-count")
144 )
146 context["status_labels"] = [item["status__name"] for item in status_dist]
147 context["status_data"] = [item["count"] for item in status_dist]
149 # Recent Activity (Filtered)
150 context["recent_servers"] = (
151 self.get_queryset_filtered(Server).all().order_by("-id")[:5]
152 )
154 return context
157class SearchView(LoginRequiredMixin, TemplateView):
158 template_name = "search.html"
160 def get_queryset_filtered(self, model: Any) -> Any:
161 if self.request.user.is_superuser:
162 return model.objects.all()
163 user_groups = self.request.user.groups.all()
164 if hasattr(model, "group"):
165 return model.objects.filter(
166 Q(group__in=user_groups) | Q(group__isnull=True)
167 )
168 return model.objects.all()
170 def get_template_names(self) -> List[str]:
171 if self.request.GET.get("ajax"):
172 return ["search_results_ajax.html"]
173 return [self.template_name]
175 def get_context_data(self, **kwargs: Any) -> Any:
176 context = super().get_context_data(**kwargs)
177 query = self.request.GET.get("q", "").lower()
178 context["query"] = query
180 # Navigation Quick Jumps
181 nav_targets = [
182 {"name": _("Dashboard"), "url": "/", "icon": "fa-gauge-high"},
183 {
184 "name": _("Group Management"),
185 "url": "/group/members/",
186 "icon": "fa-users",
187 },
188 {"name": _("Servers"), "url": "/server/", "icon": "fa-server"},
189 {"name": _("Server Models"), "url": "/servermodel/", "icon": "fa-cubes"},
190 {"name": _("Vendors"), "url": "/vendor/", "icon": "fa-industry"},
191 {"name": _("Clusters"), "url": "/cluster/", "icon": "fa-th-large"},
192 {
193 "name": _("Operating Systems"),
194 "url": "/operatingsystem/",
195 "icon": "fa-laptop",
196 },
197 {"name": _("Statuses"), "url": "/status/", "icon": "fa-tag"},
198 {"name": _("Locations"), "url": "/location/", "icon": "fa-map-marker-alt"},
199 {"name": _("Domains"), "url": "/domain/", "icon": "fa-globe"},
200 {"name": _("Patch Times"), "url": "/patchtime/", "icon": "fa-calendar"},
201 {
202 "name": _("Cluster Software"),
203 "url": "/clustersoftware/",
204 "icon": "fa-shield-halved",
205 },
206 {
207 "name": _("Cluster Packages"),
208 "url": "/clusterpackage/",
209 "icon": "fa-archive",
210 },
211 {
212 "name": _("API Documentation"),
213 "url": "/api/schema/swagger-ui/",
214 "icon": "fa-book",
215 },
216 ]
218 if len(query) >= 2:
219 # Filter navigation targets
220 context["nav_results"] = [
221 item for item in nav_targets if query in (item["name"].lower())
222 ]
224 context["servers"] = self.get_queryset_filtered(Server).filter(
225 hostname__icontains=query
226 )[:10]
227 context["vendors"] = Vendor.objects.filter(name__icontains=query)[:10]
228 context["clusters"] = self.get_queryset_filtered(Cluster).filter(
229 name__icontains=query
230 )[:10]
232 # Simple check if anything was found
233 context["has_results"] = any(
234 [
235 context["nav_results"],
236 context["servers"].exists(),
237 context["vendors"].exists(),
238 context["clusters"].exists(),
239 ]
240 )
241 else:
242 context["has_results"] = False
243 context["query_too_short"] = True
245 return context
248class HistoryDiffView(LoginRequiredMixin, TemplateView):
249 template_name = "history_diff.html"
251 def get_context_data(self, **kwargs: Any) -> Any:
252 context = super().get_context_data(**kwargs)
253 app_label = kwargs.get("app_label")
254 history_id = kwargs.get("history_id")
256 try:
257 model = apps.get_model(app_label, "Model")
258 record = model.history.get(history_id=history_id) # type: ignore
259 except (LookupError, ObjectDoesNotExist):
260 raise Http404("History record not found")
262 context["record"] = record
263 context["instance"] = record.instance
264 context["app_label"] = app_label
266 if record.prev_record:
267 context["diff"] = record.diff_against(record.prev_record)
268 else:
269 context["diff"] = None
271 return context
274class ImportStarterPackView(LoginRequiredMixin, View):
275 def post(self, request: Any, *args: Any, **kwargs: Any) -> Any:
276 user_groups = request.user.groups.all()
277 if not user_groups.exists():
278 messages.error(request, _("You are not assigned to any group."))
279 return redirect("dashboard")
281 group = user_groups.first()
282 results = import_starter_pack(group)
284 messages.success(
285 request,
286 _("Imported %d vendors and %d operating systems into group %s.")
287 % (results["vendors"], results["os"], group.name),
288 )
289 return redirect("vendor:index")
292class TermsView(TemplateView):
293 template_name = "legal/terms.html"
296class PrivacyView(TemplateView):
297 template_name = "legal/privacy.html"
300class ImpressumView(TemplateView):
301 template_name = "legal/impressum.html"