aa-fleetfinder 0.1.0a12__py3-none-any.whl → 3.0.0b2__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.
Files changed (89) hide show
  1. aa_fleetfinder-3.0.0b2.dist-info/METADATA +820 -0
  2. aa_fleetfinder-3.0.0b2.dist-info/RECORD +86 -0
  3. {aa_fleetfinder-0.1.0a12.dist-info → aa_fleetfinder-3.0.0b2.dist-info}/WHEEL +1 -2
  4. fleetfinder/__init__.py +19 -0
  5. fleetfinder/app_settings.py +20 -0
  6. fleetfinder/apps.py +22 -0
  7. fleetfinder/auth_hooks.py +58 -0
  8. fleetfinder/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
  9. fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +296 -0
  10. fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
  11. fleetfinder/locale/de/LC_MESSAGES/django.po +306 -0
  12. fleetfinder/locale/django.pot +303 -0
  13. fleetfinder/locale/es/LC_MESSAGES/django.mo +0 -0
  14. fleetfinder/locale/es/LC_MESSAGES/django.po +319 -0
  15. fleetfinder/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
  16. fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +314 -0
  17. fleetfinder/locale/it_IT/LC_MESSAGES/django.mo +0 -0
  18. fleetfinder/locale/it_IT/LC_MESSAGES/django.po +294 -0
  19. fleetfinder/locale/ja/LC_MESSAGES/django.mo +0 -0
  20. fleetfinder/locale/ja/LC_MESSAGES/django.po +303 -0
  21. fleetfinder/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
  22. fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +337 -0
  23. fleetfinder/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
  24. fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +294 -0
  25. fleetfinder/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
  26. fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +298 -0
  27. fleetfinder/locale/ru/LC_MESSAGES/django.mo +0 -0
  28. fleetfinder/locale/ru/LC_MESSAGES/django.po +319 -0
  29. fleetfinder/locale/sk/LC_MESSAGES/django.mo +0 -0
  30. fleetfinder/locale/sk/LC_MESSAGES/django.po +294 -0
  31. fleetfinder/locale/uk/LC_MESSAGES/django.mo +0 -0
  32. fleetfinder/locale/uk/LC_MESSAGES/django.po +310 -0
  33. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
  34. fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +319 -0
  35. fleetfinder/migrations/0001_initial.py +72 -0
  36. fleetfinder/migrations/0002_esi_error_handling_and_verbose_names.py +92 -0
  37. fleetfinder/migrations/0003_alter_fleet_fleet_commander_alter_fleet_groups_and_more.py +46 -0
  38. fleetfinder/migrations/__init__.py +0 -0
  39. fleetfinder/models.py +95 -0
  40. fleetfinder/providers.py +32 -0
  41. fleetfinder/static/fleetfinder/css/fleetfinder.css +31 -0
  42. fleetfinder/static/fleetfinder/css/fleetfinder.min.css +2 -0
  43. fleetfinder/static/fleetfinder/css/fleetfinder.min.css.map +1 -0
  44. fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.js +86 -0
  45. fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.min.js +2 -0
  46. fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.min.js.map +1 -0
  47. fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.js +154 -0
  48. fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.min.js +2 -0
  49. fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.min.js.map +1 -0
  50. fleetfinder/static/fleetfinder/js/fleetfinder.js +23 -0
  51. fleetfinder/static/fleetfinder/js/fleetfinder.min.js +2 -0
  52. fleetfinder/static/fleetfinder/js/fleetfinder.min.js.map +1 -0
  53. fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.css +477 -0
  54. fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.min.css +2 -0
  55. fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.min.css.map +1 -0
  56. fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/js/slimselect.min.js +1 -0
  57. fleetfinder/tasks.py +554 -0
  58. fleetfinder/templates/fleetfinder/base.html +43 -0
  59. fleetfinder/templates/fleetfinder/bundles/css/fleetfinder-css.html +3 -0
  60. fleetfinder/templates/fleetfinder/bundles/css/slim-select-css.html +3 -0
  61. fleetfinder/templates/fleetfinder/bundles/js/fleetfinder-js.html +9 -0
  62. fleetfinder/templates/fleetfinder/bundles/js/slim-select-js.html +3 -0
  63. fleetfinder/templates/fleetfinder/create-fleet.html +42 -0
  64. fleetfinder/templates/fleetfinder/dashboard.html +53 -0
  65. fleetfinder/templates/fleetfinder/edit-fleet.html +42 -0
  66. fleetfinder/templates/fleetfinder/fleet-details.html +102 -0
  67. fleetfinder/templates/fleetfinder/join-fleet.html +68 -0
  68. fleetfinder/templates/fleetfinder/modals/kick-fleet-member.html +46 -0
  69. fleetfinder/templates/fleetfinder/partials/body/form-fleet-details.html +50 -0
  70. fleetfinder/templates/fleetfinder/partials/footer/app-translation-footer.html +11 -0
  71. fleetfinder/templates/fleetfinder/partials/header/header-nav-left.html +9 -0
  72. fleetfinder/templates/fleetfinder/partials/header/header-nav-right.html +18 -0
  73. fleetfinder/templatetags/__init__.py +3 -0
  74. fleetfinder/templatetags/fleetfinder.py +33 -0
  75. fleetfinder/tests/__init__.py +41 -0
  76. fleetfinder/tests/test_access.py +74 -0
  77. fleetfinder/tests/test_auth_hooks.py +79 -0
  78. fleetfinder/tests/test_settings.py +38 -0
  79. fleetfinder/tests/test_tasks.py +1116 -0
  80. fleetfinder/tests/test_templatetags.py +65 -0
  81. fleetfinder/tests/test_user_agent.py +88 -0
  82. fleetfinder/tests/test_views.py +1184 -0
  83. fleetfinder/tests/utils.py +58 -0
  84. fleetfinder/urls.py +45 -0
  85. fleetfinder/views.py +631 -0
  86. aa_fleetfinder-0.1.0a12.dist-info/METADATA +0 -50
  87. aa_fleetfinder-0.1.0a12.dist-info/RECORD +0 -5
  88. aa_fleetfinder-0.1.0a12.dist-info/top_level.txt +0 -1
  89. {aa_fleetfinder-0.1.0a12.dist-info → aa_fleetfinder-3.0.0b2.dist-info/licenses}/LICENSE +0 -0
fleetfinder/tasks.py ADDED
@@ -0,0 +1,554 @@
1
+ """
2
+ Tasks
3
+ """
4
+
5
+ # Standard Library
6
+ from collections.abc import Iterable
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from datetime import timedelta
9
+
10
+ # Third Party
11
+ from aiopenapi3 import ContentTypeError
12
+ from celery import shared_task
13
+
14
+ # Django
15
+ from django.utils import timezone
16
+
17
+ # Alliance Auth
18
+ from allianceauth.services.hooks import get_extension_logger
19
+ from allianceauth.services.tasks import QueueOnce
20
+ from esi.exceptions import HTTPClientError
21
+ from esi.models import Token
22
+
23
+ # Alliance Auth (External Libs)
24
+ from app_utils.logging import LoggerAddTag
25
+
26
+ # AA Fleet Finder
27
+ from fleetfinder import __title__
28
+ from fleetfinder.models import Fleet
29
+ from fleetfinder.providers import esi
30
+
31
+ logger = LoggerAddTag(my_logger=get_extension_logger(name=__name__), prefix=__title__)
32
+
33
+
34
+ ESI_ERROR_LIMIT = 50
35
+ ESI_TIMEOUT_ONCE_ERROR_LIMIT_REACHED = 60
36
+ ESI_MAX_RETRIES = 3
37
+ ESI_MAX_ERROR_COUNT = 3
38
+ ESI_ERROR_GRACE_TIME = 75
39
+
40
+ TASK_TIME_LIMIT = 120 # Stop after 2 minutes
41
+
42
+ # Params for all tasks
43
+ TASK_DEFAULT_KWARGS = {"time_limit": TASK_TIME_LIMIT, "max_retries": ESI_MAX_RETRIES}
44
+
45
+
46
+ class FleetViewAggregate: # pylint: disable=too-few-public-methods
47
+ """
48
+ A helper class to encapsulate fleet data and its aggregate information.
49
+
50
+ This class is used to store and return the fleet view along with its aggregated data.
51
+ """
52
+
53
+ def __init__(self, fleet: list, aggregate: dict) -> None:
54
+ """
55
+ Initialize the FleetViewAggregate object.
56
+
57
+ :param fleet: A list of fleet members or fleet-related data.
58
+ :type fleet: list
59
+ :param aggregate: A dictionary containing aggregated data about the fleet.
60
+ :type aggregate: dict
61
+ """
62
+
63
+ self.fleet = fleet
64
+ self.aggregate = aggregate
65
+
66
+
67
+ @shared_task
68
+ def _send_invitation(
69
+ character_id: int, fleet_commander_token: Token, fleet_id: int
70
+ ) -> None:
71
+ """
72
+ Sends a fleet invitation to a character in the EVE Online client.
73
+
74
+ This task uses the ESI API to open the fleet invite window for a specific character,
75
+ assigning them the role of "squad_member" in the specified fleet.
76
+
77
+ :param character_id: The ID of the character to invite to the fleet.
78
+ :type character_id: int
79
+ :param fleet_commander_token: The ESI token of the fleet commander, used for authentication.
80
+ :type fleet_commander_token: str
81
+ :param fleet_id: The ID of the fleet to which the character is being invited.
82
+ :type fleet_id: int
83
+ :return: None
84
+ :rtype: None
85
+ """
86
+
87
+ # Define the invitation payload with the character ID and role
88
+ invitation = {"character_id": character_id, "role": "squad_member"}
89
+
90
+ # Send the invitation using the ESI API
91
+ esi.client.Fleets.PostFleetsFleetIdMembers(
92
+ fleet_id=fleet_id, token=fleet_commander_token, body=invitation
93
+ ).result(force_refresh=True)
94
+
95
+
96
+ def _close_esi_fleet(fleet: Fleet, reason: str) -> None:
97
+ """
98
+ Close a registered fleet and log the reason for closure.
99
+
100
+ This function deletes the specified fleet from the database and logs
101
+ the closure event with the provided reason.
102
+
103
+ :param fleet: The fleet object to be closed.
104
+ :type fleet: Fleet
105
+ :param reason: The reason for closing the fleet.
106
+ :type reason: str
107
+ :return: None
108
+ :rtype: None
109
+ """
110
+
111
+ logger.info(
112
+ msg=(
113
+ f'Fleet "{fleet.name}" of {fleet.fleet_commander} (ESI ID: {fleet.fleet_id}) » '
114
+ f"Closing: {reason}"
115
+ )
116
+ )
117
+
118
+ fleet.delete()
119
+
120
+
121
+ def _esi_fleet_error_handling(fleet: Fleet, error_key: str) -> None:
122
+ """
123
+ Handle errors related to ESI (EVE Swagger Interface) fleet operations.
124
+
125
+ This function manages error handling for a fleet by checking the error count
126
+ and the time of the last error. If the error count exceeds the maximum allowed
127
+ within the grace period, the fleet is closed. Otherwise, the error count is updated
128
+ and logged.
129
+
130
+ :param fleet: The fleet object associated with the error.
131
+ :type fleet: Fleet
132
+ :param error_key: A key representing the specific error encountered.
133
+ :type error_key: str
134
+ :return: None
135
+ :rtype: None
136
+ """
137
+
138
+ time_now = timezone.now()
139
+
140
+ # Close ESI fleet if the consecutive error count is too high
141
+ if (
142
+ fleet.last_esi_error == error_key
143
+ and fleet.last_esi_error_time
144
+ >= (time_now - timedelta(seconds=ESI_ERROR_GRACE_TIME))
145
+ and fleet.esi_error_count >= ESI_MAX_ERROR_COUNT
146
+ ):
147
+ _close_esi_fleet(fleet=fleet, reason=error_key.label)
148
+
149
+ return
150
+
151
+ # Increment the error count or reset it if the error is new or outside the grace period
152
+ error_count = (
153
+ fleet.esi_error_count + 1
154
+ if fleet.last_esi_error == error_key
155
+ and fleet.last_esi_error_time
156
+ >= (time_now - timedelta(seconds=ESI_ERROR_GRACE_TIME))
157
+ else 1
158
+ )
159
+
160
+ # Log the error details
161
+ logger.info(
162
+ f'Fleet "{fleet.name}" of {fleet.fleet_commander} (ESI ID: {fleet.fleet_id}) » '
163
+ f'Error: "{error_key.label}" ({error_count} of {ESI_MAX_ERROR_COUNT}).'
164
+ )
165
+
166
+ # Update the fleet object with the new error details
167
+ fleet.esi_error_count = error_count
168
+ fleet.last_esi_error = error_key
169
+ fleet.last_esi_error_time = time_now
170
+ fleet.save()
171
+
172
+
173
+ @shared_task
174
+ def _get_fleet_aggregate(fleet_infos: list) -> dict:
175
+ """
176
+ Calculate the composition of a fleet based on ship types.
177
+
178
+ This function processes a list of fleet members and counts the occurrences
179
+ of each ship type. The result is a dictionary where the keys are ship type names
180
+ and the values are the counts of those ship types.
181
+
182
+ :param fleet_infos: A list of dictionaries containing fleet member information.
183
+ Each dictionary is expected to have a "ship_type_name" key.
184
+ :type fleet_infos: list
185
+ :return: A dictionary with ship type names as keys and their counts as values.
186
+ :rtype: dict
187
+ """
188
+
189
+ counts = {}
190
+
191
+ logger.debug(f"Fleet infos for aggregation: {fleet_infos}")
192
+
193
+ for member in fleet_infos:
194
+ logger.debug(f"Processing member for aggregation: {member}")
195
+
196
+ # Extract the ship type name from the member information
197
+ type_ = member.get("ship_type_name")
198
+
199
+ # Check if the ship type name is valid and normalize it
200
+ if type_ and isinstance(type_, str) and type_.strip():
201
+ type_ = type_.strip() # Normalize ship type name
202
+
203
+ # Increment the count for the ship type or initialize it
204
+ if type_ in counts:
205
+ counts[type_] += 1
206
+ else:
207
+ counts[type_] = 1
208
+
209
+ return counts
210
+
211
+
212
+ def _check_for_esi_fleet(fleet: Fleet) -> dict | bool:
213
+ """
214
+ Check if a fleet exists and retrieve its ESI (EVE Swagger Interface) data.
215
+
216
+ This function verifies the existence of a fleet by checking the required ESI scopes
217
+ and retrieving the fleet data using the fleet commander's token. If the fleet is not found
218
+ or an error occurs, appropriate error handling is performed.
219
+
220
+ :param fleet: The fleet object to check.
221
+ :type fleet: Fleet
222
+ :return: A dictionary containing the fleet data and the ESI token if successful, or False otherwise.
223
+ :rtype: dict | bool
224
+ """
225
+
226
+ required_scopes = ["esi-fleets.read_fleet.v1"]
227
+
228
+ # Check if there is a fleet
229
+ try:
230
+ fleet_commander_id = fleet.fleet_commander.character_id
231
+ esi_token = Token.get_token(fleet_commander_id, required_scopes)
232
+
233
+ fleet_from_esi = esi.client.Fleets.GetCharactersCharacterIdFleet(
234
+ character_id=fleet_commander_id, token=esi_token
235
+ ).result(force_refresh=True)
236
+
237
+ return {"fleet": fleet_from_esi, "token": esi_token}
238
+ except ContentTypeError:
239
+ logger.debug(
240
+ f'ESI returned gibberish for fleet "{fleet.name}" of {fleet.fleet_commander} '
241
+ f"(ESI ID: {fleet.fleet_id}), skipping update."
242
+ )
243
+
244
+ return False
245
+ except HTTPClientError as ex:
246
+ # Handle the case where the fleet is not found
247
+ if ex.status_code == 404:
248
+ _esi_fleet_error_handling(
249
+ error_key=Fleet.EsiError.NOT_IN_FLEET, fleet=fleet
250
+ )
251
+ else: # 400, 401, 402, 403 ?
252
+ _esi_fleet_error_handling(error_key=Fleet.EsiError.NO_FLEET, fleet=fleet)
253
+ except Exception: # pylint: disable=broad-exception-caught
254
+ # Handle any other errors that occur
255
+ _esi_fleet_error_handling(error_key=Fleet.EsiError.NO_FLEET, fleet=fleet)
256
+
257
+ return False
258
+
259
+
260
+ def _process_fleet(fleet: Fleet) -> None:
261
+ """
262
+ Process a fleet and handle its state based on ESI (EVE Swagger Interface) data.
263
+
264
+ This function retrieves the fleet's ESI data, verifies its consistency, and performs
265
+ appropriate error handling if discrepancies are found. It also checks if the current
266
+ user is the fleet boss.
267
+
268
+ :param fleet: The fleet object to process.
269
+ :type fleet: Fleet
270
+ :return: None
271
+ :rtype: None
272
+ """
273
+
274
+ # Log the start of fleet processing
275
+ logger.info(
276
+ f'Processing information for fleet "{fleet.name}" '
277
+ f"of {fleet.fleet_commander} (ESI ID: {fleet.fleet_id})"
278
+ )
279
+
280
+ # Check if the fleet exists in ESI
281
+ esi_fleet = _check_for_esi_fleet(fleet=fleet)
282
+
283
+ # Exit if the fleet does not exist
284
+ if not esi_fleet:
285
+ return
286
+
287
+ # Handle the case where fleet IDs do not match, indicating the fleet commander changed fleets
288
+ if fleet.fleet_id != esi_fleet["fleet"].fleet_id:
289
+ _esi_fleet_error_handling(
290
+ fleet=fleet, error_key=Fleet.EsiError.FC_CHANGED_FLEET
291
+ )
292
+ return
293
+
294
+ # Verify if the current user is the fleet boss
295
+ try:
296
+ _ = esi.client.Fleets.GetFleetsFleetIdMembers(
297
+ fleet_id=fleet.fleet_id, token=esi_fleet["token"]
298
+ ).result(force_refresh=True)
299
+ except Exception: # pylint: disable=broad-exception-caught
300
+ # Handle the case where the user is not the fleet boss
301
+ _esi_fleet_error_handling(fleet=fleet, error_key=Fleet.EsiError.NOT_FLEETBOSS)
302
+
303
+
304
+ @shared_task
305
+ def send_fleet_invitation(fleet_id: int, character_ids: list) -> None:
306
+ """
307
+ Send fleet invitations to characters through ESI.
308
+
309
+ This task sends fleet invitations to a list of character IDs using the ESI API.
310
+ It retrieves the fleet and the fleet commander's token, then processes the invitations
311
+ concurrently using a thread pool.
312
+
313
+ :param fleet_id: The ID of the fleet to which invitations are sent.
314
+ :type fleet_id: int
315
+ :param character_ids: List of character IDs to invite to the fleet.
316
+ :type character_ids: list[int]
317
+ :return: None
318
+ :rtype: None
319
+ """
320
+
321
+ # Define the required ESI scopes for sending fleet invitations
322
+ required_scopes = ["esi-fleets.write_fleet.v1"]
323
+
324
+ # Retrieve the fleet object using the provided fleet ID
325
+ fleet = Fleet.objects.get(fleet_id=fleet_id)
326
+
327
+ # Retrieve the fleet commander's token for authentication
328
+ fleet_commander_token = Token.get_token(
329
+ character_id=fleet.fleet_commander.character_id, scopes=required_scopes
330
+ )
331
+
332
+ # Use a thread pool to send invitations concurrently
333
+ with ThreadPoolExecutor(max_workers=50) as ex:
334
+ # Create a list of futures for sending invitations
335
+ futures = [
336
+ ex.submit(
337
+ _send_invitation,
338
+ character_id=character_id,
339
+ fleet_commander_token=fleet_commander_token,
340
+ fleet_id=fleet_id,
341
+ )
342
+ for character_id in character_ids
343
+ ]
344
+
345
+ # Wait for all futures to complete and raise any exceptions that occurred
346
+ for future in as_completed(futures):
347
+ future.result() # This will raise any exceptions that occurred
348
+
349
+
350
+ @shared_task(**{**TASK_DEFAULT_KWARGS}, **{"base": QueueOnce})
351
+ def check_fleet_adverts() -> None:
352
+ """
353
+ Check all registered fleets and process them.
354
+
355
+ This task retrieves all registered fleets from the database and processes each fleet
356
+ individually. It first checks if there are any fleets to process and logs the count.
357
+ If no fleets are found, it logs a message and exits. Before processing, it ensures
358
+ that the ESI (EVE Swagger Interface) service is available. If ESI is offline or
359
+ above the error limit, the task aborts. Each fleet is then processed using the
360
+ `_process_fleet` function.
361
+
362
+ :return: None
363
+ :rtype: None
364
+ """
365
+
366
+ # Retrieve all registered fleets from the database
367
+ fleets = Fleet.objects.all()
368
+
369
+ # Check if there are any fleets to process
370
+ if not fleets.exists():
371
+ logger.info("No registered fleets found. Nothing to do...")
372
+ return
373
+
374
+ # Log the number of fleets to be processed
375
+ logger.info(f"Processing {fleets.count()} registered fleets...")
376
+
377
+ # Process each fleet individually
378
+ for fleet in fleets:
379
+ _process_fleet(fleet=fleet)
380
+
381
+
382
+ def _fetch_chunk(ids: list) -> list:
383
+ """
384
+ Fetch names for a list of IDs using the ESI API.
385
+
386
+ This function sends a request to the ESI API to retrieve names for a given list of IDs.
387
+ If the request fails and the list contains more than one ID, the list is split into two
388
+ halves, and the function is called recursively for each half. If the list contains only
389
+ one ID, the ID is dropped, and a warning is logged.
390
+
391
+ :param ids: A list of IDs to fetch names for.
392
+ :type ids: list
393
+ :return: A list of results containing the names corresponding to the provided IDs.
394
+ :rtype: list
395
+ """
396
+
397
+ try:
398
+ result = esi.client.Universe.PostUniverseNames(body=ids).result(
399
+ force_refresh=True
400
+ )
401
+
402
+ logger.debug(f"Fetched {len(result)} names for {len(ids)} IDs.")
403
+ logger.debug(f"Result: {result}")
404
+
405
+ return result
406
+ except Exception: # pylint: disable=broad-exception-caught
407
+ if len(ids) == 1:
408
+ logger.warning(f"Dropping ID {ids[0]}: failed to fetch name.")
409
+
410
+ return []
411
+
412
+ mid = len(ids) // 2
413
+
414
+ return _fetch_chunk(ids[:mid]) + _fetch_chunk(ids[mid:])
415
+
416
+
417
+ def _make_name_lookup(ids_to_name: Iterable) -> dict:
418
+ """
419
+ Create a lookup dictionary mapping IDs to names.
420
+
421
+ Build a mapping of id -> name from a sequence that may contain either
422
+ dicts like {'id': ..., 'name': ...} or objects with .id and .name attributes.
423
+
424
+ :param ids_to_name:
425
+ :type ids_to_name:
426
+ :return:
427
+ :rtype:
428
+ """
429
+
430
+ lookup = {}
431
+
432
+ if not ids_to_name:
433
+ return lookup
434
+
435
+ for item in ids_to_name:
436
+ if item is None:
437
+ continue
438
+
439
+ if isinstance(item, dict):
440
+ id_ = item.get("id")
441
+ name = item.get("name")
442
+ else:
443
+ id_ = getattr(item, "id", None)
444
+ name = getattr(item, "name", None)
445
+
446
+ if id_ is not None and name is not None:
447
+ lookup[id_] = name
448
+
449
+ return lookup
450
+
451
+
452
+ @shared_task
453
+ def get_fleet_composition(fleet_id: int) -> FleetViewAggregate | None:
454
+ """
455
+ Retrieve the composition of a fleet by its ESI ID.
456
+
457
+ This task fetches the fleet composition, including detailed information about its members,
458
+ using the EVE Swagger Interface (ESI). It processes the fleet data, retrieves names for
459
+ associated IDs, and aggregates the fleet composition based on ship types.
460
+
461
+ :param fleet_id: The ESI ID of the fleet to retrieve.
462
+ :type fleet_id: int
463
+ :return: A FleetViewAggregate object containing fleet members and aggregate data, or None if an error occurs.
464
+ :rtype: FleetViewAggregate | None
465
+ """
466
+
467
+ try:
468
+ # Retrieve the fleet object from the database
469
+ fleet = Fleet.objects.get(fleet_id=fleet_id)
470
+ except Fleet.DoesNotExist as exc:
471
+ # Log and raise an error if the fleet does not exist
472
+ logger.error(f"Fleet with ID {fleet_id} not found")
473
+
474
+ raise Fleet.DoesNotExist(f"Fleet with ID {fleet_id} not found.") from exc
475
+
476
+ # Log the start of fleet composition retrieval
477
+ logger.info(
478
+ f'Getting fleet composition for fleet "{fleet.name}" '
479
+ f"of {fleet.fleet_commander.character_name} (ESI ID: {fleet_id})"
480
+ )
481
+
482
+ try:
483
+ # Retrieve the fleet commander's token for authentication
484
+ token = Token.get_token(
485
+ character_id=fleet.fleet_commander.character_id,
486
+ scopes=["esi-fleets.read_fleet.v1"],
487
+ )
488
+
489
+ # Fetch fleet member information from the ESI API
490
+ fleet_infos = esi.client.Fleets.GetFleetsFleetIdMembers(
491
+ fleet_id=fleet_id, token=token
492
+ ).result(force_refresh=True)
493
+
494
+ logger.debug(f"Fleet infos: {fleet_infos}")
495
+
496
+ # Extract all unique IDs (character, solar system, and ship type) for name resolution
497
+ all_ids = {
498
+ item_id
499
+ for member in fleet_infos
500
+ for item_id in [
501
+ member.character_id,
502
+ member.solar_system_id,
503
+ member.ship_type_id,
504
+ ]
505
+ }
506
+
507
+ logger.debug(
508
+ f"Found {len(all_ids)} unique IDs to fetch names for in fleet {fleet_id}"
509
+ )
510
+
511
+ # Process IDs in chunks to avoid exceeding ESI limits
512
+ chunk_size = 1000
513
+ all_ids_list = list(all_ids)
514
+ ids_to_name = []
515
+
516
+ for start in range(0, len(all_ids_list), chunk_size):
517
+ chunk = all_ids_list[start : start + chunk_size]
518
+ results = _fetch_chunk(chunk)
519
+ ids_to_name.extend(results)
520
+
521
+ logger.debug(f"Fetched names for {len(ids_to_name)} IDs.")
522
+
523
+ # Create a lookup dictionary for resolving names
524
+ name_lookup = _make_name_lookup(ids_to_name)
525
+
526
+ logger.debug(f"Name lookup: {name_lookup}")
527
+
528
+ # Add detailed information to each fleet member
529
+ member_in_fleet = [
530
+ {
531
+ **member.dict(),
532
+ "takes_fleet_warp": member.takes_fleet_warp,
533
+ "character_name": name_lookup[member.character_id],
534
+ "solar_system_name": name_lookup[member.solar_system_id],
535
+ "ship_type_name": name_lookup[member.ship_type_id],
536
+ "is_fleet_boss": member.character_id
537
+ == fleet.fleet_commander.character_id,
538
+ }
539
+ for member in fleet_infos
540
+ ]
541
+
542
+ logger.debug(f"Member in fleet after processing: {member_in_fleet}")
543
+
544
+ # Return the fleet composition and aggregate data
545
+ return FleetViewAggregate(
546
+ fleet=member_in_fleet,
547
+ aggregate=_get_fleet_aggregate(fleet_infos=member_in_fleet),
548
+ )
549
+
550
+ except Exception as exc: # pylint: disable=broad-exception-caught
551
+ # Log and raise an error if fleet composition retrieval fails
552
+ logger.error(f"Failed to get fleet composition for fleet {fleet_id}: {exc}")
553
+
554
+ raise RuntimeError(exc) from exc
@@ -0,0 +1,43 @@
1
+ {% extends "allianceauth/base-bs5.html" %}
2
+
3
+ {% load i18n %}
4
+ {% load aa_i18n %}
5
+
6
+ {% block page_title %}
7
+ {% translate "Fleet Finder" %}
8
+ {% endblock %}
9
+
10
+ {% block header_nav_brand %}
11
+ {% translate "Fleet Finder" %}
12
+ {% endblock %}
13
+
14
+ {% block header_nav_collapse_left %}
15
+ {% include "fleetfinder/partials/header/header-nav-left.html" %}
16
+ {% endblock %}
17
+
18
+
19
+ {% block header_nav_collapse_right %}
20
+ {% include "fleetfinder/partials/header/header-nav-right.html" %}
21
+ {% endblock %}
22
+
23
+ {% block content %}
24
+ <div class="aa-fleetfinder">
25
+ <div class="aa-fleetfinder-body">
26
+ {% get_datatables_language_static LANGUAGE_CODE as DT_LANG_PATH %}
27
+
28
+ <script>
29
+ const aaFleetFinderSettings = {
30
+ dataTables: {
31
+ languageUrl: "{{ DT_LANG_PATH }}",
32
+ datetimeFormat: 'YYYY-MM-DD, HH:mm',
33
+ }
34
+ }
35
+ </script>
36
+ {% block aa_fleetfinder_body %}{% endblock %}
37
+ </div>
38
+
39
+ <div class="aa-fleetfinder-footer mt-3 pt-3 border-top border-light">
40
+ {% include "fleetfinder/partials/footer/app-translation-footer.html" %}
41
+ </div>
42
+ </div>
43
+ {% endblock %}
@@ -0,0 +1,3 @@
1
+ {% load sri %}
2
+
3
+ {% sri_static "fleetfinder/css/fleetfinder.min.css" %}
@@ -0,0 +1,3 @@
1
+ {% load sri %}
2
+
3
+ {% sri_static "fleetfinder/libs/slim-select/2.6.0/css/slimselect.min.css" %}
@@ -0,0 +1,9 @@
1
+ {% load sri %}
2
+
3
+ {% sri_static "fleetfinder/js/fleetfinder.min.js" %}
4
+
5
+ {% if view %}
6
+ {% with "fleetfinder/js/fleetfinder-"|add:view|add:".min.js" as script_path %}
7
+ {% sri_static script_path %}
8
+ {% endwith %}
9
+ {% endif %}
@@ -0,0 +1,3 @@
1
+ {% load sri %}
2
+
3
+ {% sri_static "fleetfinder/libs/slim-select/2.6.0/js/slimselect.min.js" %}
@@ -0,0 +1,42 @@
1
+ {% extends "fleetfinder/base.html" %}
2
+
3
+ {% load i18n %}
4
+
5
+ {% block page_title %}
6
+ {% translate "Create fleet" as page_title %}
7
+ {{ page_title|title }} » {% translate "Fleet Finder" %}
8
+ {% endblock %}
9
+
10
+ {% block aa_fleetfinder_body %}
11
+ <div class="card card-primary border-0">
12
+ <div class="card-header card-default">
13
+ <div class="card-title mb-0">
14
+ {% translate "Create fleet" %}
15
+ </div>
16
+ </div>
17
+
18
+ <div class="card-body container">
19
+ <div class="row">
20
+ <div class="align-self-center">
21
+ {% include "fleetfinder/partials/body/form-fleet-details.html" with name=name origin="create" groups=groups %}
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ {% endblock %}
27
+
28
+ {% block extra_css %}
29
+ {% include "fleetfinder/bundles/css/slim-select-css.html" %}
30
+ {% include "fleetfinder/bundles/css/fleetfinder-css.html" %}
31
+ {% endblock %}
32
+
33
+ {% block extra_javascript %}
34
+ {% include "fleetfinder/bundles/js/slim-select-js.html" %}
35
+
36
+ <script>
37
+ new SlimSelect({
38
+ select: '#groups',
39
+ hideSelectedOption: true
40
+ });
41
+ </script>
42
+ {% endblock %}