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.
- aa_fleetfinder-3.0.0b2.dist-info/METADATA +820 -0
- aa_fleetfinder-3.0.0b2.dist-info/RECORD +86 -0
- {aa_fleetfinder-0.1.0a12.dist-info → aa_fleetfinder-3.0.0b2.dist-info}/WHEEL +1 -2
- fleetfinder/__init__.py +19 -0
- fleetfinder/app_settings.py +20 -0
- fleetfinder/apps.py +22 -0
- fleetfinder/auth_hooks.py +58 -0
- fleetfinder/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/cs_CZ/LC_MESSAGES/django.po +296 -0
- fleetfinder/locale/de/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/de/LC_MESSAGES/django.po +306 -0
- fleetfinder/locale/django.pot +303 -0
- fleetfinder/locale/es/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/es/LC_MESSAGES/django.po +319 -0
- fleetfinder/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/fr_FR/LC_MESSAGES/django.po +314 -0
- fleetfinder/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/it_IT/LC_MESSAGES/django.po +294 -0
- fleetfinder/locale/ja/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/ja/LC_MESSAGES/django.po +303 -0
- fleetfinder/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/ko_KR/LC_MESSAGES/django.po +337 -0
- fleetfinder/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/nl_NL/LC_MESSAGES/django.po +294 -0
- fleetfinder/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/pl_PL/LC_MESSAGES/django.po +298 -0
- fleetfinder/locale/ru/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/ru/LC_MESSAGES/django.po +319 -0
- fleetfinder/locale/sk/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/sk/LC_MESSAGES/django.po +294 -0
- fleetfinder/locale/uk/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/uk/LC_MESSAGES/django.po +310 -0
- fleetfinder/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- fleetfinder/locale/zh_Hans/LC_MESSAGES/django.po +319 -0
- fleetfinder/migrations/0001_initial.py +72 -0
- fleetfinder/migrations/0002_esi_error_handling_and_verbose_names.py +92 -0
- fleetfinder/migrations/0003_alter_fleet_fleet_commander_alter_fleet_groups_and_more.py +46 -0
- fleetfinder/migrations/__init__.py +0 -0
- fleetfinder/models.py +95 -0
- fleetfinder/providers.py +32 -0
- fleetfinder/static/fleetfinder/css/fleetfinder.css +31 -0
- fleetfinder/static/fleetfinder/css/fleetfinder.min.css +2 -0
- fleetfinder/static/fleetfinder/css/fleetfinder.min.css.map +1 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.js +86 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.min.js +2 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-dashboard.min.js.map +1 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.js +154 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.min.js +2 -0
- fleetfinder/static/fleetfinder/js/fleetfinder-fleet-details.min.js.map +1 -0
- fleetfinder/static/fleetfinder/js/fleetfinder.js +23 -0
- fleetfinder/static/fleetfinder/js/fleetfinder.min.js +2 -0
- fleetfinder/static/fleetfinder/js/fleetfinder.min.js.map +1 -0
- fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.css +477 -0
- fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.min.css +2 -0
- fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/css/slimselect.min.css.map +1 -0
- fleetfinder/static/fleetfinder/libs/slim-select/2.6.0/js/slimselect.min.js +1 -0
- fleetfinder/tasks.py +554 -0
- fleetfinder/templates/fleetfinder/base.html +43 -0
- fleetfinder/templates/fleetfinder/bundles/css/fleetfinder-css.html +3 -0
- fleetfinder/templates/fleetfinder/bundles/css/slim-select-css.html +3 -0
- fleetfinder/templates/fleetfinder/bundles/js/fleetfinder-js.html +9 -0
- fleetfinder/templates/fleetfinder/bundles/js/slim-select-js.html +3 -0
- fleetfinder/templates/fleetfinder/create-fleet.html +42 -0
- fleetfinder/templates/fleetfinder/dashboard.html +53 -0
- fleetfinder/templates/fleetfinder/edit-fleet.html +42 -0
- fleetfinder/templates/fleetfinder/fleet-details.html +102 -0
- fleetfinder/templates/fleetfinder/join-fleet.html +68 -0
- fleetfinder/templates/fleetfinder/modals/kick-fleet-member.html +46 -0
- fleetfinder/templates/fleetfinder/partials/body/form-fleet-details.html +50 -0
- fleetfinder/templates/fleetfinder/partials/footer/app-translation-footer.html +11 -0
- fleetfinder/templates/fleetfinder/partials/header/header-nav-left.html +9 -0
- fleetfinder/templates/fleetfinder/partials/header/header-nav-right.html +18 -0
- fleetfinder/templatetags/__init__.py +3 -0
- fleetfinder/templatetags/fleetfinder.py +33 -0
- fleetfinder/tests/__init__.py +41 -0
- fleetfinder/tests/test_access.py +74 -0
- fleetfinder/tests/test_auth_hooks.py +79 -0
- fleetfinder/tests/test_settings.py +38 -0
- fleetfinder/tests/test_tasks.py +1116 -0
- fleetfinder/tests/test_templatetags.py +65 -0
- fleetfinder/tests/test_user_agent.py +88 -0
- fleetfinder/tests/test_views.py +1184 -0
- fleetfinder/tests/utils.py +58 -0
- fleetfinder/urls.py +45 -0
- fleetfinder/views.py +631 -0
- aa_fleetfinder-0.1.0a12.dist-info/METADATA +0 -50
- aa_fleetfinder-0.1.0a12.dist-info/RECORD +0 -5
- aa_fleetfinder-0.1.0a12.dist-info/top_level.txt +0 -1
- {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,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 %}
|