aa-structures 2.9.1__py3-none-any.whl → 2.11.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: aa-structures
3
- Version: 2.9.1
3
+ Version: 2.11.0
4
4
  Summary: App for managing Eve Online structures with Alliance Auth.
5
5
  Author-email: Erik Kalkoken <kalkoken87@gmail.com>
6
6
  Requires-Python: >=3.8
@@ -1,15 +1,15 @@
1
- structures/__init__.py,sha256=JDqVcTEi8eCaAdMSvOph_y7iv7PQbntaqVFeo0pFNS0,203
2
- structures/admin.py,sha256=cn6AtcSDI6bJROhBnvE2iQwOkm1t3NVabrpTVphxbTk,38662
3
- structures/app_settings.py,sha256=7BFWl1Q4tqTuj3CV6BmEctIFKJx9QIUfm2tjKzme2jM,6526
1
+ structures/__init__.py,sha256=73A8QmbMlzuQCd4B1PJ0x0-1eGmBUMeyKhZGFG2Oafk,204
2
+ structures/admin.py,sha256=TYWGPou4s-mgspoGaZSiZu2nxBeNzidL_DcJTv7JGJI,40068
3
+ structures/app_settings.py,sha256=7I_6VC7YecCxnSgtyNQa9h1Zdwb11qJhrNaBLgyA5m0,6527
4
4
  structures/apps.py,sha256=MNZH9l3qWCwuS7OGiKGkBVrDzKoOFlqwDdEgyEFzxVA,195
5
- structures/auth_hooks.py,sha256=JI9dCNpYdRAPmD2fbiFqhc3ODxqvvyrgQQSmDyrZ3Fk,915
5
+ structures/auth_hooks.py,sha256=nRbrixFkAE5gphDokB1E8xhH8FY2VtXVwu0XMmSGBAw,1013
6
6
  structures/constants.py,sha256=R7sC5esaWJayJpTDbug7dTKxkkXDKXQ-U6M9Qb0GH5s,967
7
7
  structures/forms.py,sha256=kXs-SGIIj-D9xtFCILKe_PMRU4eHl3-HLNWRhaZKmnQ,391
8
8
  structures/helpers.py,sha256=_dw7j7yobpcV70VwWxoQiptk69b3ksvaDlRQKHg8Kmg,2344
9
- structures/managers.py,sha256=Th_nPY8GNcdBPqqfqjtTriYdsZWFAWknbNqI8QG17vo,19225
9
+ structures/managers.py,sha256=5hejDjWN3ZPZqvSGNf9HXtbtpWX9d75Ygc9FXrO0Np4,19638
10
10
  structures/providers.py,sha256=9QYHd8X5HwBhrbgbX3LUrXXV1KGM3hFhcxK6qIJjJtg,317
11
11
  structures/tasks.py,sha256=INZ3soMYHYfxrQsyTO-ozoxbVX6lpMnF3AUo8ybWIoA,9083
12
- structures/urls.py,sha256=rBOv84nMX6WcZEnTZxASrSUhGaqubs4eRLyibDovrUs,1396
12
+ structures/urls.py,sha256=An5v27AD_NXMNL66cMVtQNci1C_Aqry32rrvDkMpgYo,1467
13
13
  structures/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  structures/core/notification_timers.py,sha256=G-QwpqP9HFv_Y7KsID9bLF-GluEJ1-duSJQN-zqJtM0,14977
15
15
  structures/core/notification_types.py,sha256=0SExh-VuImRz05jatnCu9lAfIFORUZ-kon9rjF4SXfI,12279
@@ -57,11 +57,12 @@ structures/migrations/0002_remove_eveuniverse_relation_names.py,sha256=yXOFSolB0
57
57
  structures/migrations/0003_add_localization_and_unique_key.py,sha256=ZxJMfUtgUH2W8Z0zbN-iqMzjC3-1pvsbywJFyFMxWGA,44042
58
58
  structures/migrations/0004_improve_localization.py,sha256=R29j2Ki9I_yOvEQTxEu_zMnZrR7JkSGpmvMTpp9XjqM,14014
59
59
  structures/migrations/0005_add_notification_types.py,sha256=aIgNZN4Gsvh9GiJ0i4XmXopT0qqjZTQrdVjZaVIJKP8,7087
60
+ structures/migrations/0006_add_ownercharacter_disabled.py,sha256=yfcuwWHdSoF7HTEzdtQ1bGhI-K78SxCeE3g3z6UjbqI,726
60
61
  structures/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
62
  structures/models/__init__.py,sha256=Da0-Z4BtsZ_mlnI6XtyvoE0UtTuOA0psGCuVhVPVKrM,916
62
63
  structures/models/eveuniverse.py,sha256=vTIfzZL9guEDuZHQvRdG6pI4zSEdMqfVE-2SkHdqbRo,2213
63
64
  structures/models/notifications.py,sha256=tznJo2prNjhk9C7S4cDrQxftIvuLcidB6vSkvFfb4mw,35146
64
- structures/models/owners.py,sha256=8tw8mR2ti9MpPs5ajeV5AONY53cvO5XnucrfIOQXEA4,53953
65
+ structures/models/owners.py,sha256=8VDRL1fPep0TN6Nv5MJg1NC6_2V6bMJd6f-eda4-PJg,55697
65
66
  structures/models/structures_1.py,sha256=e1sI-2zMSKcPi2PyYA--fcc8rn-yQwODzlUHKjeWgLI,29778
66
67
  structures/models/structures_2.py,sha256=g5Pct5jNmZC-n7fnpLs5UyiP9JLrGdI1vjYucHTV_wQ,10371
67
68
  structures/static/structures/css/global.css,sha256=R4LEH9PwLoN77qkqBK7u2y_vzRC3fq8X_zKnAW7yhiA,1165
@@ -156,10 +157,10 @@ structures/templates/structures/templatetags/list_title.html,sha256=C2NWwo27TGfG
156
157
  structures/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
158
  structures/templatetags/structures.py,sha256=vSt5AnfYuo11q1SxiskT4oshSMsZ9ZgM0nYpYiMl8bA,1637
158
159
  structures/tests/__init__.py,sha256=9MrJzKr8DdsQY3-79v188pTpwqT4TDQ46vKnBJAGqic,75
159
- structures/tests/test_admin.py,sha256=_kV4giMVn-Pp77er0V5Pu10eBBohzT_oEdnl6D6q5bM,24333
160
+ structures/tests/test_admin.py,sha256=cb_AbOanp31k-oZhaUXCqnlkUu87EgNddq_VQKbB_Dk,26839
160
161
  structures/tests/test_helpers.py,sha256=BQC-4H-9-v5qW4nugqy0bkxuGlA3UO2clYY2lI2LjoY,4893
161
162
  structures/tests/test_managers_1.py,sha256=ruKq5VRYHdStDQe3uzVhcxF0seQQfmdudJEUYXzi8bY,34539
162
- structures/tests/test_managers_2.py,sha256=COF4DO67ejHueNwmVj5mtj9ps-FCq9uJQYTmWwpQW7Y,2942
163
+ structures/tests/test_managers_2.py,sha256=FISr_NT1Qnu9j1JYYTceNsUk8k9rfCTKwjKi5WUVbPs,3025
163
164
  structures/tests/test_tasks.py,sha256=reL1rPv_kvTgsC4lq5SXulkbEHaJrw5B1-hFI4tiDWY,17269
164
165
  structures/tests/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
165
166
  structures/tests/core/test_notification_structuretimers.py,sha256=OvldjgIHmRVsqoC9C6VJRqwS9WeRFMgpyutRc69PjAU,10857
@@ -172,26 +173,27 @@ structures/tests/core/notification_embeds/__init__.py,sha256=47DEQpj8HBSa-_TImW-
172
173
  structures/tests/core/notification_embeds/test_helpers.py,sha256=HG20TC8Bz7NqpaQWo6uvrsxfNIwTL2Zi_NRkh3UyN2Y,1776
173
174
  structures/tests/core/notification_embeds/test_main.py,sha256=vTDByjxxVCA6KMcr5XH4f4QsMgcDI_YDd-fjZfHztng,13808
174
175
  structures/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
175
- structures/tests/integration/test_tasks.py,sha256=nb2HjS31ArQfoD2FIjQN-QHGGR4Xcqs9J1ICshmJVDA,23588
176
- structures/tests/integration/test_views.py,sha256=iA3chzZXhrnZzyBGmpRm-rQsKa6ru7y34fdB7CBtKDw,4331
176
+ structures/tests/integration/test_tasks.py,sha256=s6hL5UBotDmhLq7NUiOad72W-S_18u_yAHiLWVryVi8,23617
177
+ structures/tests/integration/test_views.py,sha256=SkbIposTUjo0nujHL4WEaZO7nY1Bfuvm805H39XUv-g,4361
177
178
  structures/tests/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
178
179
  structures/tests/models/test_eveuniverse.py,sha256=bQ1upOLfoivfaa9KCrzjWvSH_EvL2STuvfFfZ9saIMY,1623
179
180
  structures/tests/models/test_notifications_1.py,sha256=xVX4an54sZM8WLUdsAGUG2Nb4Rl7W5FBzhI0hBhJo5g,30300
180
181
  structures/tests/models/test_notifications_2.py,sha256=wBoB6lHMK4yrd63DSkZ570yXW6coevDtI0pl1ttgB1o,31989
181
182
  structures/tests/models/test_notifications_3.py,sha256=u11s0k9EmtFgL3kyb4zDyLVliQLOps701ZgEA2T2i0Y,7257
182
183
  structures/tests/models/test_notifications_discord.py,sha256=9_42lcrb2lUUMRqgdRJAub9cJ4QZI71OyoORwK0LEvg,5551
183
- structures/tests/models/test_owners_1.py,sha256=Xpac4P0xpJUmU8Jz5Ebxu0oSvhGcaW9a6UI7Jxhgmz4,22600
184
+ structures/tests/models/test_owners_1.py,sha256=o8AJasQ0-Pdb0FRFZcmNcMGjaIk8fH3OZIZw4paJiDw,24433
184
185
  structures/tests/models/test_owners_2.py,sha256=gkTXpSibPWs1XfZtPvyeEvVebyYZcKERD9GQSYSClYk,21911
185
186
  structures/tests/models/test_owners_3.py,sha256=cpDkfFkVvC1v4uuXha1vGUmWZ163huYTUDuZyed9Rdc,16947
186
- structures/tests/models/test_owners_4.py,sha256=1tLHxeQTE4RacEdckwwQCocWyy0tVYuDDrHnHtQTNlA,18831
187
+ structures/tests/models/test_owners_4.py,sha256=_VP4QsPYGwBnn_bVN4aNX2ZVeJzMAzWObZrgCJkOPgY,19496
187
188
  structures/tests/models/test_owners_5.py,sha256=lTSfVY3rPOLyF-IhlKPT4mbRqvuN_NA-kyU0Vzzp9qY,31607
189
+ structures/tests/models/test_owners_6.py,sha256=R9UE2QGwiGTWLHzzOZwGQPWJrAPwIIl7UwrHH-ki4Ak,940
188
190
  structures/tests/models/test_structures.py,sha256=6HktwBOsmRtRq5MnRlZnvNixUsWUpRni_XnPjNpMN0Y,41469
189
191
  structures/tests/testdata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
190
192
  structures/tests/testdata/create_eveuniverse.py,sha256=E-Fh-dmuHmLZcqgK2U6mwt9W6dI7H99bG7DaETfcVQY,2129
191
193
  structures/tests/testdata/entities.json,sha256=kmcmmbHG_igHwvkpAJGHQEztEXjlo8McDuojpEBmgqU,32147
192
194
  structures/tests/testdata/esi_data.json,sha256=DX47oSlRZIPxlPueMfiNElbZbAip2cp3LAUmv8GAPJ0,13864
193
195
  structures/tests/testdata/eveuniverse.json,sha256=Z8_EXOF8XNvls9RfBYIHhoNX0rj0NqcjInTU6tZuwhI,993284
194
- structures/tests/testdata/factories.py,sha256=I-NQI9c6yakWzvYWg48QyX3-6h2bxoymcvPM0RyiEJw,24718
196
+ structures/tests/testdata/factories.py,sha256=KNXxOad44DKD0luQ7QS0DepnCr1S_Z6qwfiXnNDy10I,24774
195
197
  structures/tests/testdata/generate_notifications.py,sha256=nE077mVHVmOVudKIGMtHUfBIcV5Ch5Z0IngD1gp5yQk,5746
196
198
  structures/tests/testdata/generate_notifications_2.py,sha256=mdoGXd9vgy_opmrwTr9MfGe-QieHV4BHtRA7VFzxFKM,1413
197
199
  structures/tests/testdata/generate_structures.py,sha256=4p2ypDj-goniBnraTP2KfWzEX3YLj0qhKPNTfCXy05g,9134
@@ -201,14 +203,16 @@ structures/tests/testdata/tasks_loadtest.py,sha256=Hn0UVeaMfdi5S4W12lsb6g_xwjQcN
201
203
  structures/tests/testdata/test_generate_structures.py,sha256=vpReGRROduZsGB99Dq7yPKNKSkSLFdLM_esdf5TTnnI,414
202
204
  structures/tests/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
203
205
  structures/tests/views/test_public.py,sha256=FTaT3VUOnoT4t9rCCTCGyDrberpa8CRJHsXuqAw2PQE,1372
206
+ structures/tests/views/test_service_status.py,sha256=mWkP13HnKRa3ltsZ2FKJuZRJjvYic9wjgQ1J3qA8n-k,4197
204
207
  structures/tests/views/test_statistics.py,sha256=MHt31AIX9_jt8-yP8XsbsiRRUKEtY8mQJbk11saFYZc,2626
205
- structures/tests/views/test_structures.py,sha256=foTvuaI8x36wwTLiwsfAE1ofVPTJVrnXuBT39sWLBOs,29004
208
+ structures/tests/views/test_structures.py,sha256=4DrURs32GvbMnXXJm1dC1ZhsDVhJ50iKyiqTRyyRe70,28284
206
209
  structures/tests/views/utils.py,sha256=0ALPjL8d0vcIWa_Pl4_gDl1qtBp71oDqyZLJyvDj-Uc,247
207
210
  structures/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
208
211
  structures/views/common.py,sha256=Z_hcpEpkGrhwFdMAAMON_DqsfQ_lMyRwOMl5ROo_aSk,818
209
212
  structures/views/public.py,sha256=-Tel-poYKZTy8eQllhsWUxVf7HYBuCfeL9JWlbKhs7s,3035
210
213
  structures/views/statistics.py,sha256=7jj8b9ATsYwE7Cg6gMp-bYx29nV43GdWYun9WBggGkM,4709
211
- structures/views/structures.py,sha256=XtQxYySOPqO9klPgF1o57pCejuo22EdU00h5LAdEVME,23167
214
+ structures/views/status.py,sha256=gcahbk6dPIZDqkaNHDAsEHyDWLzicTK18Fom0A6xx3c,718
215
+ structures/views/structures.py,sha256=E1ssnqr2zp0rLQ3txIa6XkcX3NStNjMkuQM4iNjt6yw,22560
212
216
  structures/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
213
217
  structures/webhooks/core.py,sha256=mP25MbQG8Yv2YayDFcR6x2V30eqXhR2q5u4SRefblf4,6573
214
218
  structures/webhooks/managers.py,sha256=L3G3AmsyDeif_lfpWshmAxQ61UGJ9w8i9lZaF2jbOtQ,1117
@@ -216,7 +220,7 @@ structures/webhooks/models.py,sha256=kUkt9rnRQIJIrU9Bjcs34rvkb-TMbUubHdn-kny08kI
216
220
  structures/webhooks/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
221
  structures/webhooks/tests/test_core.py,sha256=4NcEAQgK2KhQkFOxYh2ad0S-qUWh1DNGDmLo5Mo5opI,6762
218
222
  structures/webhooks/tests/test_utils.py,sha256=ekADFv0JOEtXeqdiejbeqrABO__Q1flJHzVieQ7L9e0,459
219
- aa_structures-2.9.1.dist-info/LICENSE,sha256=XZiwB_S_40_HhnvLg5xvtBb3g1oGjPrk0rpFwk8iInE,1070
220
- aa_structures-2.9.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
221
- aa_structures-2.9.1.dist-info/METADATA,sha256=yvzawtDxsWgorHAKcQRjx1WJFOc8GbVlvPQcYcL8q50,5971
222
- aa_structures-2.9.1.dist-info/RECORD,,
223
+ aa_structures-2.11.0.dist-info/LICENSE,sha256=XZiwB_S_40_HhnvLg5xvtBb3g1oGjPrk0rpFwk8iInE,1070
224
+ aa_structures-2.11.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
225
+ aa_structures-2.11.0.dist-info/METADATA,sha256=yuYKAElX7wpn9YDI8qwLn20gb_Vksa5A7mnjsnN5dBg,5972
226
+ aa_structures-2.11.0.dist-info/RECORD,,
structures/__init__.py CHANGED
@@ -3,5 +3,5 @@
3
3
  # pylint: disable = invalid-name
4
4
  default_app_config = "structures.apps.StructuresConfig"
5
5
 
6
- __version__ = "2.9.1"
6
+ __version__ = "2.11.0"
7
7
  __title__ = "Structures"
structures/admin.py CHANGED
@@ -348,6 +348,29 @@ class OwnerCharacterAdminInline(admin.TabularInline):
348
348
  return False
349
349
 
350
350
 
351
+ class DisabledCharactersFilter(admin.SimpleListFilter):
352
+ title = _("has disabled characters")
353
+ parameter_name = "has_disabled_characters"
354
+
355
+ def lookups(self, request, model_admin):
356
+ return (
357
+ ("yes", _("yes")),
358
+ ("no", _("no")),
359
+ )
360
+
361
+ def queryset(self, request, queryset):
362
+ """Return the filtered queryset"""
363
+ if self.value() == "yes":
364
+ return queryset.annotate_characters_count().filter(
365
+ characters_disabled_count__gt=0
366
+ )
367
+ if self.value() == "no":
368
+ return queryset.annotate_characters_count().filter(
369
+ characters_disabled_count=0
370
+ )
371
+ return queryset
372
+
373
+
351
374
  @admin.register(Owner)
352
375
  class OwnerAdmin(admin.ModelAdmin):
353
376
  list_display = (
@@ -364,6 +387,7 @@ class OwnerAdmin(admin.ModelAdmin):
364
387
  list_filter = (
365
388
  "is_active",
366
389
  "is_up",
390
+ DisabledCharactersFilter,
367
391
  ("corporation__alliance", admin.RelatedOnlyFieldListFilter),
368
392
  "has_default_pings_enabled",
369
393
  "is_alliance_main",
@@ -376,6 +400,7 @@ class OwnerAdmin(admin.ModelAdmin):
376
400
  "fetch_notifications",
377
401
  "deactivate_owners",
378
402
  "activate_owners",
403
+ "reset_characters",
379
404
  )
380
405
  inlines = (OwnerCharacterAdminInline,)
381
406
  filter_horizontal = ("ping_groups", "webhooks")
@@ -485,9 +510,13 @@ class OwnerAdmin(admin.ModelAdmin):
485
510
  def has_add_permission(self, request):
486
511
  return False
487
512
 
488
- @admin.display(ordering="characters_count_2", description=_("characters"))
489
- def _characters(self, obj: Owner) -> int:
490
- return obj.characters_count_2
513
+ @admin.display(ordering="characters_enabled_count", description=_("characters"))
514
+ def _characters(self, obj: Owner) -> str:
515
+ enabled = obj.characters_enabled_count
516
+ disabled = obj.characters_disabled_count
517
+ if not disabled:
518
+ return enabled
519
+ return f"{enabled} ({disabled})"
491
520
 
492
521
  @admin.display(description=_("default pings"), boolean=True)
493
522
  def _has_default_pings_enabled(self, obj: Owner):
@@ -549,6 +578,16 @@ class OwnerAdmin(admin.ModelAdmin):
549
578
  queryset.update(is_active=False)
550
579
  self.message_user(request, _("Deactivated %d owners") % queryset.count())
551
580
 
581
+ @admin.action(description=_("Reset disabled characters for selected owners"))
582
+ def reset_characters(self, request, queryset):
583
+ owner_pks = queryset.values_list("pk", flat=True)
584
+ OwnerCharacter.objects.filter(
585
+ owner__pk__in=list(owner_pks), is_enabled=False
586
+ ).update(is_enabled=True, disabled_reason="", error_count=0)
587
+ self.message_user(
588
+ request, _("Characters have been reset for %d owners") % len(owner_pks)
589
+ )
590
+
552
591
  @admin.action(description=_("Update all from EVE server for selected owners"))
553
592
  def update_all(self, request, queryset):
554
593
  for obj in queryset:
@@ -631,7 +670,7 @@ class OwnerAdmin(admin.ModelAdmin):
631
670
  def _assets_last_update_fresh(self, obj: Owner) -> bool:
632
671
  return obj.is_assets_sync_fresh
633
672
 
634
- @admin.action(description=_("structures Count"))
673
+ @admin.display(description=_("structures Count"))
635
674
  def _structures_count(self, obj: Owner) -> int:
636
675
  return obj.structures.count()
637
676
 
@@ -55,7 +55,7 @@ STRUCTURES_FEATURE_STARBASES = clean_setting("STRUCTURES_FEATURE_STARBASES", Tru
55
55
  STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES = clean_setting(
56
56
  "STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES", 3
57
57
  )
58
- """Max retries before a character is deleted when ESI claims the character
58
+ """Max retries before a character is disabled when ESI claims the character
59
59
  is not a director (Since this sometimes is reported wrongly by ESI).
60
60
  """
61
61
 
structures/auth_hooks.py CHANGED
@@ -32,4 +32,9 @@ def register_menu():
32
32
 
33
33
  @hooks.register("url_hook")
34
34
  def register_urls():
35
- return UrlHook(urls, "structures", r"^structures/")
35
+ return UrlHook(
36
+ urls,
37
+ "structures",
38
+ r"^structures/",
39
+ excluded_views=["structures.views.status.service_status"],
40
+ )
structures/managers.py CHANGED
@@ -181,14 +181,27 @@ GeneratedNotificationManager = GeneratedNotificationManagerBase.from_queryset(
181
181
 
182
182
  class OwnerQuerySet(models.QuerySet):
183
183
  def annotate_characters_count(self) -> models.QuerySet:
184
- """Add character count annotation."""
185
- return self.annotate(
186
- characters_count_2=Count(
184
+ """Add character count annotations."""
185
+ qs = self.annotate(
186
+ characters_enabled_count=Count(
187
+ "characters",
188
+ filter=Q(
189
+ characters__character_ownership__isnull=False,
190
+ characters__is_enabled=True,
191
+ ),
192
+ distinct=True,
193
+ )
194
+ ).annotate(
195
+ characters_disabled_count=Count(
187
196
  "characters",
188
- filter=Q(characters__character_ownership__isnull=False),
197
+ filter=Q(
198
+ characters__character_ownership__isnull=False,
199
+ characters__is_enabled=False,
200
+ ),
189
201
  distinct=True,
190
202
  )
191
203
  )
204
+ return qs
192
205
 
193
206
  def structures_last_updated(self) -> Optional[dt.datetime]:
194
207
  """Date/time when structures were last updated for any of the active owners."""
@@ -0,0 +1,27 @@
1
+ # Generated by Django 4.0.10 on 2024-06-23 14:26
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("structures", "0005_add_notification_types"),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name="ownercharacter",
15
+ name="disabled_reason",
16
+ field=models.TextField(default=""),
17
+ ),
18
+ migrations.AddField(
19
+ model_name="ownercharacter",
20
+ name="is_enabled",
21
+ field=models.BooleanField(
22
+ default=True,
23
+ help_text="Disabled characters are not used for syncing owners",
24
+ verbose_name="is enabled",
25
+ ),
26
+ ),
27
+ ]
@@ -338,12 +338,15 @@ class Owner(models.Model):
338
338
  f"Character {character_ownership.character} does not belong "
339
339
  "to owner corporation."
340
340
  )
341
- obj, _ = self.characters.get_or_create(character_ownership=character_ownership)
341
+ obj: OwnerCharacter = self.characters.get_or_create(
342
+ character_ownership=character_ownership
343
+ )[0]
344
+ obj.reset()
342
345
  return obj
343
346
 
344
- def characters_count(self) -> int:
347
+ def valid_characters_count(self) -> int:
345
348
  """Count of valid owner characters."""
346
- return self.characters.count()
349
+ return self.characters.filter(is_enabled=True).count()
347
350
 
348
351
  def has_sov(self, eve_solar_system: EveSolarSystem) -> bool:
349
352
  """Determine whether this owner has sov in the given solar system."""
@@ -351,42 +354,31 @@ class Owner(models.Model):
351
354
  eve_solar_system=eve_solar_system, corporation=self.corporation
352
355
  )
353
356
 
354
- def delete_character(
357
+ def disable_character(
355
358
  self,
356
359
  character: "OwnerCharacter",
357
- error: str,
358
- level: str = "warning",
360
+ reason: str,
359
361
  max_allowed_errors: int = 0,
360
362
  ) -> None:
361
- """Delete character and notify it's owner and admin about the reason
363
+ """Disable character and notify it's owner and admins about it.
362
364
 
363
365
  Args:
364
- - character: Character this error refers to
365
- - error: Error text
366
- - level: context level for the notification
367
- - max_error: how many errors are permitted before character is deleted
366
+ - character: Character to disable
367
+ - reason: User friendly reason for the deletion
368
+ - max_allowed_errors: Maximum number of allowed errors for this type of error
368
369
  """
369
370
  if character.error_count < max_allowed_errors:
370
- logger.warning(
371
- (
372
- "%s: Character encountered an error and will be deleted "
373
- "if this occurs more often (%d/%d): %s"
374
- ),
375
- character,
376
- character.error_count + 1,
377
- max_allowed_errors,
378
- error,
379
- )
380
- with transaction.atomic():
381
- character.error_count = F("error_count") + 1
382
- character.save(update_fields=["error_count"])
371
+ character.increase_error_count()
383
372
  return
384
373
 
385
- title = f"{__title__}: {self}: Invalid character has been removed"
374
+ character.disable(reason)
375
+
376
+ title = f"{__title__}: {self}: Character has been disabled"
377
+ level = "warning"
386
378
  message = (
387
- f"{character.character_ownership}: {error}\n"
388
- "The character has been removed. "
389
- "Please add a new character to restore the previous service level."
379
+ f"{character.character_ownership}: {reason}\n"
380
+ "This character caused too many errors and has been disabled. "
381
+ "Administrator action is required to resolve this issue."
390
382
  )
391
383
  notify(
392
384
  user=character.character_ownership.user,
@@ -394,15 +386,44 @@ class Owner(models.Model):
394
386
  message=message,
395
387
  level=level,
396
388
  )
397
- if self.characters.count() == 1:
389
+ if not self.valid_characters_count():
398
390
  message += (
399
391
  " This owner has no configured characters anymore "
400
392
  "and it's services are now down."
401
393
  )
402
394
  level = "danger"
395
+
403
396
  notify_admins(title=f"FYI: {title}", message=message, level=level)
397
+
398
+ def delete_character(self, character: "OwnerCharacter", reason: str) -> None:
399
+ """Delete character and notify it's owner and admin about the reason
400
+
401
+ Args:
402
+ - character: Character this error refers to
403
+ - reason: User friendly reason for the deletion
404
+ """
404
405
  character.delete()
405
406
 
407
+ title = f"{__title__}: {self}: Invalid character has been removed"
408
+ level = "warning"
409
+ message = (
410
+ f"{character.character_ownership}: {reason}\n"
411
+ "Your character is no longer valid for syncing this owner and has been removed. "
412
+ )
413
+ notify(
414
+ user=character.character_ownership.user,
415
+ title=title,
416
+ message=message,
417
+ level=level,
418
+ )
419
+ if not self.valid_characters_count():
420
+ message += (
421
+ " This owner has no valid characters anymore "
422
+ "and it's services are now down."
423
+ )
424
+ level = "danger"
425
+ notify_admins(title=f"FYI: {title}", message=message, level=level)
426
+
406
427
  def _rotate_character(
407
428
  self,
408
429
  character: "OwnerCharacter",
@@ -421,7 +442,7 @@ class Owner(models.Model):
421
442
  )
422
443
  try:
423
444
  minimum_time_between_rotations = max(
424
- rotate_characters.esi_cache_duration / self.characters.count(),
445
+ rotate_characters.esi_cache_duration / self.valid_characters_count(),
425
446
  60,
426
447
  )
427
448
  except ZeroDivisionError:
@@ -453,14 +474,18 @@ class Owner(models.Model):
453
474
  if rotate_characters
454
475
  else "notifications_last_used_at"
455
476
  )
456
- for character in self.characters.order_by(order_by_last_used):
477
+ enabled_characters: models.QuerySet[OwnerCharacter] = self.characters.filter(
478
+ is_enabled=True
479
+ ).order_by(order_by_last_used)
480
+ for character in enabled_characters:
457
481
  if (
458
482
  character.character_ownership.character.corporation_id
459
483
  != self.corporation.corporation_id
460
484
  ):
485
+ corporation_name = self.corporation.corporation_name
461
486
  self.delete_character(
462
487
  character=character,
463
- error="Character does no longer belong to the owner's corporation.",
488
+ reason=f"Character is no longer a member of {corporation_name}",
464
489
  )
465
490
  continue
466
491
 
@@ -469,17 +494,17 @@ class Owner(models.Model):
469
494
  ):
470
495
  self.delete_character(
471
496
  character=character,
472
- error="Character does not have sufficient permission to sync.",
497
+ reason="Character no longer has permission to sync",
473
498
  )
474
499
  continue
475
500
 
476
501
  token = character.valid_token()
477
502
  if not token:
478
- self.delete_character(
479
- character=character,
480
- error="Character has no valid token for sync.",
503
+ self.disable_character(
504
+ character=character, reason="No valid token found for character"
481
505
  )
482
506
  continue
507
+
483
508
  found_character = character
484
509
  break # leave the for loop if we have found a valid token
485
510
 
@@ -797,6 +822,13 @@ class Owner(models.Model):
797
822
 
798
823
  Return True when successful, else False.
799
824
  """
825
+ try:
826
+ character: OwnerCharacter = self.characters.get(
827
+ character_ownership__character__character_id=token.character_id
828
+ )
829
+ except ObjectDoesNotExist:
830
+ return False
831
+
800
832
  structures = []
801
833
  try:
802
834
  starbases_data = (
@@ -824,21 +856,11 @@ class Owner(models.Model):
824
856
  self._store_raw_data("starbases", structures)
825
857
 
826
858
  except HTTPForbidden:
827
- try:
828
- character = self.characters.get(
829
- character_ownership__character__character_id=token.character_id
830
- )
831
- except ObjectDoesNotExist:
832
- pass
833
- else:
834
- self.delete_character(
835
- character=character,
836
- error=(
837
- "Character is not a director or CEO and therefore "
838
- "can not fetch starbases."
839
- ),
840
- max_allowed_errors=STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES,
841
- )
859
+ self.disable_character(
860
+ character=character,
861
+ reason=("This character is not a director or CEO"),
862
+ max_allowed_errors=STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES,
863
+ )
842
864
  return False
843
865
 
844
866
  except OSError as ex:
@@ -849,6 +871,7 @@ class Owner(models.Model):
849
871
  structures_qs=self.structures.filter_starbases(),
850
872
  new_structures=structures,
851
873
  )
874
+ character.reset_error_counter()
852
875
  return True
853
876
 
854
877
  def _store_updates_for_starbases(self, token, structures):
@@ -1337,6 +1360,12 @@ class OwnerCharacter(models.Model):
1337
1360
  verbose_name=_("error count"),
1338
1361
  help_text="Count of ESI errors which happened with this character.",
1339
1362
  )
1363
+ is_enabled = models.BooleanField(
1364
+ default=True,
1365
+ verbose_name=_("is enabled"),
1366
+ help_text=_("Disabled characters are not used for syncing owners"),
1367
+ )
1368
+ disabled_reason = models.TextField(default="")
1340
1369
  created_at = models.DateTimeField(auto_now_add=True)
1341
1370
 
1342
1371
  class Meta:
@@ -1369,3 +1398,27 @@ class OwnerCharacter(models.Model):
1369
1398
  .require_valid()
1370
1399
  .first()
1371
1400
  )
1401
+
1402
+ def reset(self) -> None:
1403
+ """Resets a disabled owner character."""
1404
+ self.is_enabled = True
1405
+ self.disabled_reason = ""
1406
+ self.error_count = 0
1407
+ self.save()
1408
+
1409
+ def reset_error_counter(self) -> None:
1410
+ """Reset the error counter"""
1411
+ self.error_count = 0
1412
+ self.save(update_fields=["error_count"])
1413
+
1414
+ def disable(self, reason: str = "") -> None:
1415
+ """Disables a character."""
1416
+ self.is_enabled = False
1417
+ self.disabled_reason = reason
1418
+ self.save()
1419
+
1420
+ def increase_error_count(self):
1421
+ """Increase error count of this character by one."""
1422
+ with transaction.atomic():
1423
+ self.error_count = F("error_count") + 1
1424
+ self.save(update_fields=["error_count"])
@@ -16,8 +16,7 @@ from app_utils.esi_testing import EsiClientStub, EsiEndpoint
16
16
  from structures import tasks
17
17
  from structures.core.notification_types import NotificationType
18
18
  from structures.models import Structure
19
-
20
- from ..testdata.factories import (
19
+ from structures.tests.testdata.factories import (
21
20
  EveEntityAllianceFactory,
22
21
  EveEntityCorporationFactory,
23
22
  NotificationFactory,
@@ -28,7 +27,7 @@ from ..testdata.factories import (
28
27
  WebhookFactory,
29
28
  datetime_to_esi,
30
29
  )
31
- from ..testdata.load_eveuniverse import load_eveuniverse
30
+ from structures.tests.testdata.load_eveuniverse import load_eveuniverse
32
31
 
33
32
  if "structuretimers" in app_labels():
34
33
  from structuretimers.models import Timer as StructureTimer
@@ -5,7 +5,7 @@ from django.urls import reverse
5
5
 
6
6
  from app_utils.testing import add_character_to_user
7
7
 
8
- from ..testdata.factories import (
8
+ from structures.tests.testdata.factories import (
9
9
  EveCharacterFactory,
10
10
  JumpGateFactory,
11
11
  OwnerFactory,
@@ -15,7 +15,7 @@ from ..testdata.factories import (
15
15
  UserMainBasicFactory,
16
16
  UserMainDefaultFactory,
17
17
  )
18
- from ..testdata.load_eveuniverse import load_eveuniverse
18
+ from structures.tests.testdata.load_eveuniverse import load_eveuniverse
19
19
 
20
20
  STRUCTURES_PATH = "structures.views.structures"
21
21
  PUBLIC_PATH = "structures.views.public"