aa-fleetfinder 2.6.0__py3-none-any.whl → 2.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aa-fleetfinder might be problematic. Click here for more details.

Files changed (40) hide show
  1. {aa_fleetfinder-2.6.0.dist-info → aa_fleetfinder-2.7.0.dist-info}/METADATA +1 -1
  2. {aa_fleetfinder-2.6.0.dist-info → aa_fleetfinder-2.7.0.dist-info}/RECORD +40 -33
  3. fleetfinder/__init__.py +1 -1
  4. fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +94 -17
  5. fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
  6. fleetfinder/locale/de/LC_MESSAGES/django.po +108 -20
  7. fleetfinder/locale/django.pot +96 -18
  8. fleetfinder/locale/es/LC_MESSAGES/django.po +108 -18
  9. fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +98 -17
  10. fleetfinder/locale/it_IT/LC_MESSAGES/django.po +91 -17
  11. fleetfinder/locale/ja/LC_MESSAGES/django.po +95 -17
  12. fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +108 -18
  13. fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +91 -17
  14. fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +93 -17
  15. fleetfinder/locale/ru/LC_MESSAGES/django.mo +0 -0
  16. fleetfinder/locale/ru/LC_MESSAGES/django.po +112 -22
  17. fleetfinder/locale/sk/LC_MESSAGES/django.po +91 -17
  18. fleetfinder/locale/uk/LC_MESSAGES/django.mo +0 -0
  19. fleetfinder/locale/uk/LC_MESSAGES/django.po +112 -22
  20. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  21. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +112 -22
  22. fleetfinder/static/fleetfinder/css/fleetfinder.css +21 -0
  23. fleetfinder/static/fleetfinder/css/fleetfinder.min.css +1 -1
  24. fleetfinder/static/fleetfinder/css/fleetfinder.min.css.map +1 -1
  25. fleetfinder/static/fleetfinder/js/fleetfinder.js +23 -0
  26. fleetfinder/static/fleetfinder/js/fleetfinder.min.js +2 -0
  27. fleetfinder/static/fleetfinder/js/fleetfinder.min.js.map +1 -0
  28. fleetfinder/tasks.py +137 -150
  29. fleetfinder/templates/fleetfinder/bundles/js/fleetfinder-js.html +3 -0
  30. fleetfinder/templates/fleetfinder/dashboard.html +38 -63
  31. fleetfinder/templates/fleetfinder/fleet-details.html +124 -33
  32. fleetfinder/templates/fleetfinder/join-fleet.html +11 -1
  33. fleetfinder/templates/fleetfinder/modals/kick-fleet-member.html +46 -0
  34. fleetfinder/templates/fleetfinder/partials/body/form-fleet-details.html +8 -8
  35. fleetfinder/tests/test_tasks.py +140 -0
  36. fleetfinder/tests/test_views.py +473 -0
  37. fleetfinder/urls.py +5 -0
  38. fleetfinder/views.py +316 -103
  39. {aa_fleetfinder-2.6.0.dist-info → aa_fleetfinder-2.7.0.dist-info}/WHEEL +0 -0
  40. {aa_fleetfinder-2.6.0.dist-info → aa_fleetfinder-2.7.0.dist-info}/licenses/LICENSE +0 -0
fleetfinder/views.py CHANGED
@@ -2,25 +2,33 @@
2
2
  Views
3
3
  """
4
4
 
5
+ # Standard Library
6
+ import json
7
+
5
8
  # Third Party
6
9
  from bravado.exception import HTTPNotFound
7
10
 
8
11
  # Django
9
12
  from django.contrib import messages
10
13
  from django.contrib.auth.decorators import login_required, permission_required
14
+ from django.core.handlers.wsgi import WSGIRequest
11
15
  from django.db.models import Q
12
16
  from django.http import JsonResponse
13
17
  from django.shortcuts import redirect, render
14
18
  from django.urls import reverse
19
+ from django.utils import timezone
20
+ from django.utils.functional import Promise
15
21
  from django.utils.safestring import mark_safe
16
22
  from django.utils.translation import gettext_lazy as _
17
23
 
18
24
  # Alliance Auth
19
25
  from allianceauth.eveonline.evelinks.eveimageserver import character_portrait_url
20
26
  from allianceauth.eveonline.models import EveCharacter
27
+ from allianceauth.framework.api.user import get_all_characters_from_user
21
28
  from allianceauth.groupmanagement.models import AuthGroup
22
29
  from allianceauth.services.hooks import get_extension_logger
23
30
  from esi.decorators import token_required
31
+ from esi.models import Token
24
32
 
25
33
  # Alliance Auth (External Libs)
26
34
  from app_utils.logging import LoggerAddTag
@@ -28,11 +36,45 @@ from app_utils.logging import LoggerAddTag
28
36
  # AA Fleet Finder
29
37
  from fleetfinder import __title__
30
38
  from fleetfinder.models import Fleet
31
- from fleetfinder.tasks import get_fleet_composition, open_fleet, send_fleet_invitation
39
+ from fleetfinder.providers import esi
40
+ from fleetfinder.tasks import get_fleet_composition, send_fleet_invitation
32
41
 
33
42
  logger = LoggerAddTag(my_logger=get_extension_logger(name=__name__), prefix=__title__)
34
43
 
35
44
 
45
+ @login_required()
46
+ @permission_required(perm="fleetfinder.access_fleetfinder")
47
+ def _get_and_validate_fleet(token: Token, character_id: int) -> tuple:
48
+ """
49
+ Get fleet information and validate fleet commander permissions
50
+
51
+ :param token: Token object containing the access token
52
+ :type token: Token
53
+ :param character_id: The character ID of the fleet commander
54
+ :type character_id: int
55
+ :return: Tuple containing the fleet result, fleet commander, and fleet ID
56
+ :rtype: tuple
57
+ """
58
+
59
+ fleet_result = esi.client.Fleets.get_characters_character_id_fleet(
60
+ character_id=token.character_id, token=token.valid_access_token()
61
+ ).result()
62
+
63
+ logger.debug(f"Fleet result: {fleet_result}")
64
+
65
+ fleet_commander = EveCharacter.objects.get(character_id=token.character_id)
66
+ fleet_id = fleet_result.get("fleet_id")
67
+ fleet_boss_id = fleet_result.get("fleet_boss_id")
68
+
69
+ if not fleet_id:
70
+ raise ValueError(f"No fleet found for {fleet_commander.character_name}")
71
+
72
+ if fleet_boss_id != character_id:
73
+ raise ValueError(f"{fleet_commander.character_name} is not the fleet boss")
74
+
75
+ return fleet_result
76
+
77
+
36
78
  @login_required()
37
79
  @permission_required(perm="fleetfinder.access_fleetfinder")
38
80
  def dashboard(request):
@@ -64,55 +106,100 @@ def ajax_dashboard(request) -> JsonResponse: # pylint: disable=too-many-locals
64
106
  :return:
65
107
  """
66
108
 
67
- data = []
68
- groups = request.user.groups.all()
69
- fleets = (
70
- Fleet.objects.filter(Q(groups__group__in=groups) | Q(groups__isnull=True))
71
- .distinct()
72
- .order_by("name")
73
- )
109
+ def _create_button_style_link(
110
+ url: str, fa_icon_class: str, btn_title: str | Promise, btn_modifier_class: str
111
+ ) -> str:
112
+ """
113
+ Helper function to create a button HTML string
114
+ This function generates an HTML anchor tag styled as a button with an icon.
115
+
116
+ :param url: The URL the button should link to
117
+ :type url: str
118
+ :param fa_icon_class: The Font Awesome class for the icon to be displayed
119
+ :type fa_icon_class: str
120
+ :param btn_title: The title attribute for the button, typically a translation string
121
+ :type btn_title: str | Promise
122
+ :param btn_modifier_class: The Bootstrap modifier class for the button styling
123
+ :type btn_modifier_class: str
124
+ :return: An HTML string representing the button
125
+ :rtype: str
126
+ """
127
+
128
+ return (
129
+ f'<a href="{url}" class="btn btn-sm {btn_modifier_class} ms-1" '
130
+ f'data-bs-tooltip="aa-fleetfinder" title="{btn_title}">'
131
+ f'<i class="{fa_icon_class}"></i></a>'
132
+ )
74
133
 
75
- for fleet in fleets:
76
- fleet_commander_name = fleet.fleet_commander.character_name
77
- fleet_commander_portrait_url = character_portrait_url(
134
+ def _get_fleet_commander_information(fleet: Fleet) -> tuple[str, str]:
135
+ """
136
+ Helper function to get the fleet commander's HTML representation
137
+ This function retrieves the fleet commander's name and portrait URL,
138
+ and returns an HTML string with the portrait image and name.
139
+
140
+ :param fleet: The Fleet object containing the fleet commander's information
141
+ :type fleet: Fleet
142
+ :return: A tuple containing the HTML string for the fleet commander and the name for sorting
143
+ :rtype: tuple[str, str]
144
+ """
145
+
146
+ commander_name = fleet.fleet_commander.character_name
147
+ portrait_url = character_portrait_url(
78
148
  character_id=fleet.fleet_commander.character_id, size=32
79
149
  )
80
- fleet_commander_portrait = (
150
+ portrait_img = (
81
151
  '<img class="rounded eve-character-portrait" '
82
- f'src="{fleet_commander_portrait_url}" '
83
- f'alt="{fleet_commander_name}" loading="lazy">'
152
+ f'src="{portrait_url}" alt="{commander_name}" loading="lazy">'
84
153
  )
85
- fleet_commander_html = fleet_commander_portrait + fleet_commander_name
86
154
 
87
- button_join_url = reverse(
88
- viewname="fleetfinder:join_fleet", args=[fleet.fleet_id]
89
- )
90
- button_join_text = _("Join fleet")
91
- button_join = (
92
- f'<a href="{button_join_url}" '
93
- f'class="btn btn-sm btn-primary">{button_join_text}</a>'
155
+ return portrait_img + commander_name, commander_name
156
+
157
+ data = []
158
+ groups = request.user.groups.all()
159
+ user_characters = get_all_characters_from_user(user=request.user)
160
+ fleets = (
161
+ Fleet.objects.filter(
162
+ Q(groups__group__in=groups)
163
+ | Q(groups__isnull=True)
164
+ | Q(fleet_commander__in=user_characters)
94
165
  )
166
+ .distinct()
167
+ .order_by("name")
168
+ )
95
169
 
96
- button_details = ""
97
- button_edit = ""
170
+ can_manage_fleets = request.user.has_perm("fleetfinder.manage_fleets")
98
171
 
99
- if request.user.has_perm(perm="fleetfinder.manage_fleets"):
100
- button_details_url = reverse(
101
- viewname="fleetfinder:fleet_details", args=[fleet.fleet_id]
102
- )
103
- button_details_text = _("View fleet details")
104
- button_details = (
105
- f'<a href="{button_details_url}" '
106
- f'class="btn btn-sm btn-primary">{button_details_text}</a>'
107
- )
172
+ for fleet in fleets:
173
+ fleet_commander_html, fleet_commander_name = _get_fleet_commander_information(
174
+ fleet
175
+ )
108
176
 
109
- button_edit_url = reverse(
110
- viewname="fleetfinder:edit_fleet", args=[fleet.fleet_id]
177
+ # Create buttons
178
+ buttons = [
179
+ _create_button_style_link(
180
+ reverse("fleetfinder:join_fleet", args=[fleet.fleet_id]),
181
+ "fa-solid fa-right-to-bracket",
182
+ _("Join fleet"),
183
+ "btn-success",
111
184
  )
112
- button_edit_text = _("Edit fleet advert")
113
- button_edit = (
114
- f'<a href="{button_edit_url}" '
115
- f'class="btn btn-sm btn-primary">{button_edit_text}</a>'
185
+ ]
186
+
187
+ if can_manage_fleets:
188
+ buttons.extend(
189
+ [
190
+ _create_button_style_link(
191
+ reverse("fleetfinder:fleet_details", args=[fleet.fleet_id]),
192
+ "fa-solid fa-eye",
193
+ _("View fleet details"),
194
+ "btn-info",
195
+ ),
196
+ _create_button_style_link(
197
+ reverse("fleetfinder:edit_fleet", args=[fleet.fleet_id]),
198
+ "fa-solid fa-pen-to-square",
199
+ _("Edit fleet advert"),
200
+ "btn-warning",
201
+ ),
202
+ ]
116
203
  )
117
204
 
118
205
  data.append(
@@ -123,9 +210,7 @@ def ajax_dashboard(request) -> JsonResponse: # pylint: disable=too-many-locals
123
210
  },
124
211
  "fleet_name": fleet.name,
125
212
  "created_at": fleet.created_at,
126
- "join": button_join,
127
- "details": button_details,
128
- "edit": button_edit,
213
+ "actions": "".join(buttons),
129
214
  }
130
215
  )
131
216
 
@@ -144,26 +229,30 @@ def create_fleet(request, token):
144
229
  :return:
145
230
  """
146
231
 
147
- context = {}
232
+ # Validate the token and check if the character is in a fleet and is the fleet boss
233
+ try:
234
+ _get_and_validate_fleet(token, token.character_id)
235
+ except (HTTPNotFound, ValueError) as ex:
236
+ logger.debug(f"Error during fleet creation: {ex}", exc_info=True)
148
237
 
149
- if request.method == "POST":
150
- auth_groups = AuthGroup.objects.filter(internal=False).all()
151
-
152
- if "modified_fleet_data" in request.session:
153
- context["motd"] = request.session["modified_fleet_data"].get("motd", "")
154
- context["name"] = request.session["modified_fleet_data"].get("name", "")
155
- context["groups"] = request.session["modified_fleet_data"].get("groups", "")
156
- context["is_free_move"] = request.session["modified_fleet_data"].get(
157
- "free_move", ""
158
- )
159
- context["character_id"] = token.character_id
160
- context["auth_groups"] = auth_groups
161
-
162
- del request.session["modified_fleet_data"]
238
+ if isinstance(ex, HTTPNotFound):
239
+ error_detail = ex.swagger_result["error"]
163
240
  else:
164
- context = {"character_id": token.character_id, "auth_groups": auth_groups}
241
+ error_detail = str(ex)
165
242
 
166
- logger.info(msg=f"Fleet created by {request.user}")
243
+ error_message = _(
244
+ f"<h4>Error!</h4><p>There was an error creating the fleet: {error_detail}</p>"
245
+ )
246
+
247
+ messages.error(request, mark_safe(error_message))
248
+
249
+ return redirect("fleetfinder:dashboard")
250
+
251
+ if request.method != "POST":
252
+ return redirect("fleetfinder:dashboard")
253
+
254
+ auth_groups = AuthGroup.objects.filter(internal=False)
255
+ context = {"character_id": token.character_id, "auth_groups": auth_groups}
167
256
 
168
257
  return render(
169
258
  request=request,
@@ -183,7 +272,22 @@ def edit_fleet(request, fleet_id):
183
272
  :return:
184
273
  """
185
274
 
186
- fleet = Fleet.objects.get(fleet_id=fleet_id)
275
+ try:
276
+ fleet = Fleet.objects.get(fleet_id=fleet_id)
277
+ except Fleet.DoesNotExist:
278
+ logger.debug(f"Fleet with ID {fleet_id} does not exist.")
279
+
280
+ messages.error(
281
+ request,
282
+ mark_safe(
283
+ _(
284
+ "<h4>Error!</h4><p>Fleet does not exist or is no longer available.</p>"
285
+ )
286
+ ),
287
+ )
288
+
289
+ return redirect("fleetfinder:dashboard")
290
+
187
291
  auth_groups = AuthGroup.objects.filter(internal=False)
188
292
 
189
293
  context = {
@@ -192,6 +296,8 @@ def edit_fleet(request, fleet_id):
192
296
  "fleet": fleet,
193
297
  }
194
298
 
299
+ logger.debug("Context for fleet edit: %s", context)
300
+
195
301
  logger.info(msg=f"Fleet {fleet_id} edit view by {request.user}")
196
302
 
197
303
  return render(
@@ -252,46 +358,97 @@ def save_fleet(request):
252
358
  :return:
253
359
  """
254
360
 
255
- if request.method == "POST":
256
- free_move = request.POST.get(key="free_move", default=False)
257
-
258
- if free_move == "on":
259
- free_move = True
260
-
261
- motd = request.POST.get(key="motd", default="")
262
- name = request.POST.get(key="name", default="")
263
- groups = request.POST.getlist(key="groups", default=[])
264
-
265
- try:
266
- open_fleet(
267
- character_id=request.POST["character_id"],
268
- motd=motd,
269
- free_move=free_move,
270
- name=name,
271
- groups=groups,
272
- )
273
- except HTTPNotFound as ex:
274
- esi_error_message = ex.swagger_result["error"]
275
- error_message = _(
276
- f"<h4>Error!</h4><p>ESI returned the following error: {esi_error_message}</p>"
277
- )
361
+ def _edit_or_create_fleet(
362
+ character_id: int,
363
+ free_move: bool,
364
+ name: str,
365
+ groups: list,
366
+ motd: str = None, # pylint: disable=unused-argument
367
+ ) -> None:
368
+ """
369
+ Edit or create a fleet from a fleet in EVE Online
370
+
371
+ :param character_id: The character ID of the fleet commander
372
+ :type character_id: int
373
+ :param free_move: Whether the fleet is free move or not
374
+ :type free_move: bool
375
+ :param name: Name of the fleet
376
+ :type name: str
377
+ :param groups: Groups that are allowed to access the fleet
378
+ :type groups: list[AuthGroup]
379
+ :param motd: Message of the Day for the fleet
380
+ :type motd: str
381
+ :return: None
382
+ :rtype: None
383
+ """
384
+
385
+ required_scopes = ["esi-fleets.read_fleet.v1", "esi-fleets.write_fleet.v1"]
386
+ token = Token.get_token(character_id=character_id, scopes=required_scopes)
387
+
388
+ fleet_result = _get_and_validate_fleet(token, character_id)
389
+ fleet_commander = EveCharacter.objects.get(character_id=character_id)
390
+ fleet_id = fleet_result.get("fleet_id")
391
+
392
+ fleet, created = Fleet.objects.get_or_create(
393
+ fleet_id=fleet_id,
394
+ defaults={
395
+ "created_at": timezone.now(),
396
+ # "motd": motd,
397
+ "is_free_move": free_move,
398
+ "fleet_commander": fleet_commander,
399
+ "name": name,
400
+ },
401
+ )
402
+
403
+ if not created:
404
+ fleet.is_free_move = free_move
405
+ fleet.name = name
406
+ fleet.save()
407
+
408
+ fleet.groups.set(groups)
409
+
410
+ esi.client.Fleets.put_fleets_fleet_id(
411
+ fleet_id=fleet_id,
412
+ token=token.valid_access_token(),
413
+ # new_settings={"is_free_move": free_move, "motd": motd},
414
+ new_settings={"is_free_move": free_move},
415
+ ).result()
416
+
417
+ if request.method != "POST":
418
+ return redirect("fleetfinder:dashboard")
419
+
420
+ # Extract form data
421
+ form_data = {
422
+ "character_id": int(request.POST["character_id"]),
423
+ "free_move": request.POST.get("free_move") == "on",
424
+ # "motd": request.POST.get("motd", ""),
425
+ "name": request.POST.get("name", ""),
426
+ "groups": request.POST.getlist("groups", []),
427
+ }
278
428
 
279
- messages.error(request=request, message=mark_safe(s=error_message))
429
+ logger.debug(f"Form data for fleet creation: {form_data}")
280
430
 
281
- if request.POST.get(key="origin", default="") == "edit":
282
- return redirect(to="fleetfinder:dashboard")
431
+ try:
432
+ _edit_or_create_fleet(**form_data)
433
+ except HTTPNotFound as ex:
434
+ logger.debug(f"ESI returned 404 for fleet creation: {ex}", exc_info=True)
283
435
 
284
- if request.POST.get(key="origin", default="") == "create":
285
- request.session["modified_fleet_data"] = {
286
- "motd": motd,
287
- "name": name,
288
- "free_move": free_move,
289
- "groups": groups,
290
- }
436
+ esi_error = ex.swagger_result.get("error", "Unknown error")
437
+ error_message = _(
438
+ f"<h4>Error!</h4><p>ESI returned the following error: {esi_error}</p>"
439
+ )
440
+
441
+ messages.error(request, mark_safe(error_message))
442
+ except ValueError as ex:
443
+ logger.debug(f"Value error during fleet creation: {ex}", exc_info=True)
291
444
 
292
- return redirect(to="fleetfinder:create_fleet")
445
+ error_message = _(
446
+ f"<h4>Error!</h4><p>There was an error creating the fleet: {ex}</p>"
447
+ )
293
448
 
294
- return redirect(to="fleetfinder:dashboard")
449
+ messages.error(request, mark_safe(error_message))
450
+
451
+ return redirect("fleetfinder:dashboard")
295
452
 
296
453
 
297
454
  @login_required()
@@ -319,8 +476,7 @@ def fleet_details(request, fleet_id):
319
476
  @login_required()
320
477
  @permission_required(perm="fleetfinder.manage_fleets")
321
478
  def ajax_fleet_details(
322
- request, # pylint: disable=unused-argument
323
- fleet_id,
479
+ request, fleet_id # pylint: disable=unused-argument
324
480
  ) -> JsonResponse:
325
481
  """
326
482
  Ajax :: Fleet Details
@@ -331,12 +487,69 @@ def ajax_fleet_details(
331
487
 
332
488
  fleet = get_fleet_composition(fleet_id)
333
489
 
334
- data = {"fleet_member": [], "fleet_composition": []}
490
+ data = {
491
+ "fleet_member": list(fleet.fleet),
492
+ "fleet_composition": [
493
+ {"ship_type_name": ship, "number": number}
494
+ for ship, number in fleet.aggregate.items()
495
+ ],
496
+ }
335
497
 
336
- for member in fleet.fleet:
337
- data["fleet_member"].append(member)
498
+ return JsonResponse(data=data, safe=False)
338
499
 
339
- for ship, number in fleet.aggregate.items():
340
- data["fleet_composition"].append({"ship_type_name": ship, "number": number})
341
500
 
342
- return JsonResponse(data=data, safe=False)
501
+ @login_required()
502
+ @permission_required(perm="fleetfinder.manage_fleets")
503
+ def ajax_fleet_kick_member(request: WSGIRequest, fleet_id: int) -> JsonResponse:
504
+ """
505
+ Ajax :: Kick member from fleet
506
+
507
+ :param request: WSGIRequest object containing the request data
508
+ :type request: WSGIRequest
509
+ :param fleet_id: The ID of the fleet from which to kick a member
510
+ :type fleet_id: int
511
+ :return: JsonResponse indicating success or failure of the operation
512
+ :rtype: JsonResponse
513
+ """
514
+
515
+ if request.method != "POST":
516
+ return JsonResponse(
517
+ data={"success": False, "error": _("Method not allowed")}, status=405
518
+ )
519
+
520
+ try:
521
+ fleet = Fleet.objects.get(fleet_id=fleet_id)
522
+ data = json.loads(request.body)
523
+ member_id = data.get("memberId")
524
+
525
+ if not member_id:
526
+ return JsonResponse(
527
+ data={"success": False, "error": _("Member ID required")}, status=400
528
+ )
529
+
530
+ logger.debug(f"Request data for kicking member: {data}")
531
+
532
+ token = Token.get_token(
533
+ character_id=fleet.fleet_commander.character_id,
534
+ scopes=["esi-fleets.write_fleet.v1"],
535
+ )
536
+
537
+ esi.client.Fleets.delete_fleets_fleet_id_members_member_id(
538
+ fleet_id=fleet_id,
539
+ member_id=member_id,
540
+ token=token.valid_access_token(),
541
+ ).result()
542
+
543
+ return JsonResponse(data={"success": True}, status=200)
544
+ except Fleet.DoesNotExist:
545
+ return JsonResponse(
546
+ data={"success": False, "error": _("Fleet not found")}, status=404
547
+ )
548
+ except (json.JSONDecodeError, ValueError):
549
+ return JsonResponse(
550
+ data={"success": False, "error": _("Invalid request data")}, status=400
551
+ )
552
+ except HTTPNotFound:
553
+ return JsonResponse(
554
+ data={"success": False, "error": _("Member not found in fleet")}, status=404
555
+ )