Coverage for src/history/views.py: 0%
194 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-14 10:02 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-14 10:02 +0000
1import json
2from typing import TYPE_CHECKING, Any, Callable
3from urllib.parse import urlencode
5from django.core.paginator import Paginator
6from django.db import transaction
7from django.db.models import F
8from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
9from django.http.response import HttpResponseBadRequest
10from django.urls import reverse, reverse_lazy
11from django.utils import timezone
12from django.views import View
13from django.views.generic import TemplateView
14from django.views.generic.base import ContextMixin
15from django.views.generic.detail import BaseDetailView
16from django.views.generic.edit import DeleteView, UpdateView
17from opentelemetry import trace
18from ptf import views as ptf_views
19from ptf.exceptions import ServerUnderMaintenance
20from ptf.models.classes.article import Article
21from ptf.models.classes.collection import Collection
22from requests.exceptions import Timeout
24from history.model_data import HistoryEventDict, HistoryEventStatus, HistoryEventType
25from history.models import HistoryEvent
26from history.utils import (
27 get_history_error_warning_counts,
28 get_history_last_event_by,
29 insert_history_event,
30)
32if TYPE_CHECKING:
33 from django import http
34 from ptf.models.classes.resource import Resource
36tracer = trace.get_tracer(__name__)
39def manage_exceptions(
40 event: HistoryEventDict,
41 exception: BaseException,
42):
43 if type(exception).__name__ == "ServerUnderMaintenance":
44 message = " - ".join([str(exception), "Please try again later"])
45 else:
46 message = " ".join([str(exception)])
48 event["message"] = message
50 insert_history_event(event)
53class HistoryEventUpdateView(UpdateView):
54 model = HistoryEvent
55 fields = ["status", "message"]
56 success_url = reverse_lazy("history")
59def matching_decorator(func: Callable[[Any, str], str], is_article):
60 def inner(*args, **kwargs):
61 article: Article
63 if is_article:
64 article = args[0]
65 else:
66 article = args[0].resource.cast()
68 event: HistoryEventDict = {
69 "type": "matching",
70 "pid": article.pid,
71 "col": article.get_top_collection(),
72 "status": HistoryEventStatus.OK,
73 }
74 # Merge matching ids (can be zbl, numdam...)
75 try:
76 id_value = func(*args, **kwargs)
78 insert_history_event(event)
79 return id_value
81 except Timeout as exception:
82 """
83 Exception caused by the requests module: store it as a warning
84 """
85 event["status"] = HistoryEventStatus.WARNING
86 manage_exceptions(event, exception)
87 raise exception
89 except ServerUnderMaintenance as exception:
90 event["status"] = HistoryEventStatus.ERROR
91 event["type"] = "deploy"
92 manage_exceptions(event, exception)
93 raise exception
95 except Exception as exception:
96 event["status"] = HistoryEventStatus.ERROR
97 manage_exceptions(event, exception)
98 raise exception
100 return inner
103def execute_and_record_func(
104 type: HistoryEventType,
105 pid,
106 colid,
107 func,
108 message="",
109 record_error_only=False,
110 user=None,
111 type_error=None,
112 *func_args,
113 **func_kwargs,
114):
115 status = 200
116 func_result = None
117 collection = None
118 if colid not in ["ALL", "numdam"]:
119 collection = Collection.objects.get(pid=colid)
120 event: HistoryEventDict = {
121 "type": type,
122 "pid": pid,
123 "col": collection,
124 "status": HistoryEventStatus.OK,
125 "message": message,
126 }
128 try:
129 func_result = func(*func_args, **func_kwargs)
131 if type_error:
132 event["type_error"] = type_error
134 if not record_error_only:
135 insert_history_event(event)
136 except Timeout as exception:
137 """
138 Exception caused by the requests module: store it as a warning
139 """
140 event["status"] = HistoryEventStatus.WARNING
142 event["message"] = message
144 manage_exceptions(event, exception)
145 raise exception
146 except Exception as exception:
147 event["status"] = HistoryEventStatus.ERROR
149 event["message"] = message
150 manage_exceptions(event, exception)
151 raise exception
152 return func_result, status, message
155def edit_decorator(func):
156 def inner(self, action, *args, **kwargs):
157 resource_obj: Resource = self.resource.cast()
158 pid = resource_obj.pid
159 colid = resource_obj.get_top_collection().pid
161 message = ""
162 if hasattr(self, "obj"):
163 # Edit 1 item (ExtId or BibItemId)
164 obj = self.obj
165 if hasattr(self, "parent"):
166 parent = self.parent
167 if parent:
168 message += "[" + str(parent.sequence) + "] "
169 list_ = obj.id_type.split("-")
170 id_type = obj.id_type if len(list_) == 0 else list_[0]
171 message += id_type + ":" + obj.id_value + " " + action
172 else:
173 message += "All " + action
175 args = (self, action) + args
177 execute_and_record_func(
178 "edit", pid, colid, func, message, False, None, None, *args, **kwargs
179 )
181 return inner
184ptf_views.UpdateExtIdView.update_obj = edit_decorator(ptf_views.UpdateExtIdView.update_obj)
187def getLastHistoryImport(pid):
188 data = get_history_last_event_by(type="import", pid=pid)
189 return data
192class HistoryContextMixin(ContextMixin):
193 def get_context_data(self, **kwargs):
194 context = super().get_context_data(**kwargs)
195 error_count, warning_count = get_history_error_warning_counts()
196 context["warning_count"] = warning_count
197 context["error_count"] = error_count
199 # if isinstance(last_clockss_event, datetime):
200 # now = timezone.now()
201 # td = now - last_clockss_event['created_on']
202 # context['last_clockss_event'] = td.days
203 return context
206class HistoryView(TemplateView, HistoryContextMixin):
207 template_name = "history.html"
208 accepted_params = ["type", "col", "status", "month"]
210 def get_context_data(self, **kwargs):
211 context = super().get_context_data(**kwargs)
213 filters = {}
215 urlparams = {k: v[0] if isinstance(v, list) else v for k, v in self.request.GET.items()}
216 if "page" in urlparams:
217 del urlparams["page"]
218 if "search" in urlparams:
219 del urlparams["search"]
221 for filter in self.accepted_params:
222 value = self.request.GET.get(filter, None)
223 if value:
224 filters[filter] = value
226 # Get current URL params without current filterurlpatterns
227 _params = {**urlparams}
228 if filter in _params:
229 del _params[filter]
231 context[filter + "_link"] = "?" + urlencode(_params)
233 # filter_by_month = False # Ignore months for Now
234 # filters.setdefault("status", Q(status="ERROR") | Q(status="WARNING"))
236 # if filter_by_month:
237 # today = datetime.datetime.today()
238 # filters["created_on__year"] = today.year
239 # filters["created_on__month"] = today.month
240 if "col" in filters:
241 filters["col__pid"] = filters["col"]
242 del filters["col"]
243 qs = HistoryEvent.objects.filter(**filters).order_by("-created_on")
245 paginator = Paginator(qs, 50)
246 try:
247 page = int(self.request.GET.get("page", 1))
248 except ValueError:
249 page = 1
251 if page > paginator.num_pages:
252 page = paginator.num_pages
254 context["element_count"] = HistoryEvent.objects.all().count()
255 context["page_obj"] = paginator.get_page(page)
256 context["paginator"] = {
257 "page_range": paginator.get_elided_page_range(number=page, on_each_side=2, on_ends=1),
258 }
260 context["now"] = timezone.now()
262 if "col__pid" in filters:
263 del filters["col__pid"]
264 context["collections"] = (
265 HistoryEvent.objects.filter(col__isnull=False, **filters)
266 .values(colid=F("col__pid"), name=F("col__title_html"))
267 .distinct("col__title_html")
268 .order_by("col__title_html")
269 )
270 return context
273class HistoryClearView(TemplateView):
274 model = HistoryEvent
275 http_method_names = ["post"]
276 template_name = "blocks/history/historyevent_confirm_clear.html"
277 success_url = reverse_lazy("history")
279 @transaction.atomic
280 def post(self, *args):
281 HistoryEvent.objects.all().delete()
282 return HttpResponseRedirect(reverse("history"))
285class HistoryEventDeleteView(DeleteView):
286 model = HistoryEvent
287 success_url = reverse_lazy("history")
290class HistoryAPIView(BaseDetailView):
291 model = HistoryEvent
293 http_method_names = ["get"]
295 @tracer.start_as_current_span("HistoryAPIView.get")
296 def get(self, request: "http.HttpRequest", pk: int, datatype: str):
297 object: HistoryEvent = self.get_object()
298 if datatype == "message":
299 return HttpResponse(object.message)
300 if datatype == "table":
301 data = []
302 for a in object.children.all().prefetch_related("resource"):
303 entry = {
304 "type": a.type,
305 "status": a.status,
306 "status_message": a.status_message,
307 "message": a.message,
308 "score": a.score,
309 "url": a.url,
310 }
311 if a.resource:
312 entry["resource_pid"] = a.resource.pid
313 data.append(entry)
314 return JsonResponse(
315 {
316 "data": data,
317 "headers": [
318 "resource_pid",
319 "type",
320 "status",
321 "status_message",
322 "message",
323 "score",
324 "url",
325 ],
326 "source": object.source,
327 "pid": object.pid,
328 "col": object.col.pid if object.col else None,
329 "date": object.created_on,
330 }
331 )
332 return HttpResponseBadRequest()
335class HistoryEventInsert(View):
336 def post(self, request: "http.HttpRequest"):
337 # Todo : Sanitarization ?
338 event = json.loads(request.body.decode("utf-8"))
339 if "colid" in event:
340 colid = event["colid"]
341 del event["colid"]
342 collection = Collection.objects.get(pid=colid)
343 event["col"] = collection
344 insert_history_event(event)
345 response = HttpResponse()
346 response.status_code = 201
347 return response