aa-structures 2.10.0__py3-none-any.whl → 2.12.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.
- {aa_structures-2.10.0.dist-info → aa_structures-2.12.0.dist-info}/METADATA +1 -1
- {aa_structures-2.10.0.dist-info → aa_structures-2.12.0.dist-info}/RECORD +22 -20
- structures/__init__.py +1 -1
- structures/admin.py +43 -4
- structures/app_settings.py +1 -1
- structures/core/notification_embeds/main.py +18 -0
- structures/managers.py +17 -4
- structures/migrations/0006_add_ownercharacter_disabled.py +27 -0
- structures/models/notifications.py +5 -1
- structures/models/owners.py +104 -51
- structures/tests/core/notification_embeds/test_main.py +20 -0
- structures/tests/models/test_notifications_discord.py +21 -0
- structures/tests/models/test_owners_1.py +95 -52
- structures/tests/models/test_owners_4.py +21 -5
- structures/tests/models/test_owners_6.py +31 -0
- structures/tests/test_admin.py +65 -0
- structures/tests/test_managers_2.py +3 -2
- structures/tests/testdata/factories.py +3 -4
- structures/tests/views/test_structures.py +58 -5
- structures/views/structures.py +2 -2
- {aa_structures-2.10.0.dist-info → aa_structures-2.12.0.dist-info}/LICENSE +0 -0
- {aa_structures-2.10.0.dist-info → aa_structures-2.12.0.dist-info}/WHEEL +0 -0
@@ -1,12 +1,12 @@
|
|
1
|
-
structures/__init__.py,sha256=
|
2
|
-
structures/admin.py,sha256=
|
3
|
-
structures/app_settings.py,sha256=
|
1
|
+
structures/__init__.py,sha256=CxjO-itYCGXfwByFQ-cKfhmKWBvz2Lygf1fchGS8_nU,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
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=
|
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
12
|
structures/urls.py,sha256=An5v27AD_NXMNL66cMVtQNci1C_Aqry32rrvDkMpgYo,1467
|
@@ -20,7 +20,7 @@ structures/core/notification_embeds/__init__.py,sha256=30cpjKLZ5_OFtXDACDcjvWQAY
|
|
20
20
|
structures/core/notification_embeds/billing_embeds.py,sha256=d9XkTyYH7U3UDZaqXu0WoN0wvlrT2S68Rfn2jEpVtM4,5164
|
21
21
|
structures/core/notification_embeds/corporate_embeds.py,sha256=mfm6dDrgzupyRZ9TZGXML-UGoY_PymNj9LuR8EthdDA,6098
|
22
22
|
structures/core/notification_embeds/helpers.py,sha256=0SlrLLFyV0IrdOYfIOLQs0xZgzK8bO7Iroqh7dZnm98,2974
|
23
|
-
structures/core/notification_embeds/main.py,sha256=
|
23
|
+
structures/core/notification_embeds/main.py,sha256=P8Ajvd9j4SUBpZde2IOejFUV5bEXMUWU4lolNjkrxWQ,16048
|
24
24
|
structures/core/notification_embeds/moonmining_embeds.py,sha256=5OJtfwoLE18mB62iwyHTcMo9eOPBC2vQFSjQlDnTcTY,7130
|
25
25
|
structures/core/notification_embeds/orbital_embeds.py,sha256=ovAnTqrc2X0s-HhQS-UleI8J5-gJEO361HYJzcoAdeQ,2864
|
26
26
|
structures/core/notification_embeds/sov_embeds.py,sha256=_r4U5zN3MfEag9FCeN0my2d4qfMBUwYUGWW-oGftjn8,8901
|
@@ -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
|
-
structures/models/notifications.py,sha256=
|
64
|
-
structures/models/owners.py,sha256=
|
64
|
+
structures/models/notifications.py,sha256=HwQc-ChdP0ULuJgxUym9_CBsCWCmcHT7whlk9tB2vzw,35364
|
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=
|
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=
|
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
|
@@ -170,7 +171,7 @@ structures/tests/core/test_sovereignty.py,sha256=SGgNw6NxmaU3jV-EDhKJRLfHnRnqDCw
|
|
170
171
|
structures/tests/core/test_starbases.py,sha256=Q3EZVIKZGzxUUvvn0TvZYOL9YQ-baiTRTT83xWqTAhQ,3186
|
171
172
|
structures/tests/core/notification_embeds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
172
173
|
structures/tests/core/notification_embeds/test_helpers.py,sha256=HG20TC8Bz7NqpaQWo6uvrsxfNIwTL2Zi_NRkh3UyN2Y,1776
|
173
|
-
structures/tests/core/notification_embeds/test_main.py,sha256=
|
174
|
+
structures/tests/core/notification_embeds/test_main.py,sha256=rSUm8dGsddjnX_OUwtCh-wd5UhJ6ttTsfxc1soAoDv8,14667
|
174
175
|
structures/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
175
176
|
structures/tests/integration/test_tasks.py,sha256=s6hL5UBotDmhLq7NUiOad72W-S_18u_yAHiLWVryVi8,23617
|
176
177
|
structures/tests/integration/test_views.py,sha256=SkbIposTUjo0nujHL4WEaZO7nY1Bfuvm805H39XUv-g,4361
|
@@ -179,19 +180,20 @@ structures/tests/models/test_eveuniverse.py,sha256=bQ1upOLfoivfaa9KCrzjWvSH_EvL2
|
|
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
|
-
structures/tests/models/test_notifications_discord.py,sha256=
|
183
|
-
structures/tests/models/test_owners_1.py,sha256=
|
183
|
+
structures/tests/models/test_notifications_discord.py,sha256=6maDXjl2C5yH2XRgsvMtb3P-tfOdZLQDaksffiqS8OM,6312
|
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=
|
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=
|
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
|
@@ -203,14 +205,14 @@ structures/tests/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
|
|
203
205
|
structures/tests/views/test_public.py,sha256=FTaT3VUOnoT4t9rCCTCGyDrberpa8CRJHsXuqAw2PQE,1372
|
204
206
|
structures/tests/views/test_service_status.py,sha256=mWkP13HnKRa3ltsZ2FKJuZRJjvYic9wjgQ1J3qA8n-k,4197
|
205
207
|
structures/tests/views/test_statistics.py,sha256=MHt31AIX9_jt8-yP8XsbsiRRUKEtY8mQJbk11saFYZc,2626
|
206
|
-
structures/tests/views/test_structures.py,sha256=
|
208
|
+
structures/tests/views/test_structures.py,sha256=4DrURs32GvbMnXXJm1dC1ZhsDVhJ50iKyiqTRyyRe70,28284
|
207
209
|
structures/tests/views/utils.py,sha256=0ALPjL8d0vcIWa_Pl4_gDl1qtBp71oDqyZLJyvDj-Uc,247
|
208
210
|
structures/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
209
211
|
structures/views/common.py,sha256=Z_hcpEpkGrhwFdMAAMON_DqsfQ_lMyRwOMl5ROo_aSk,818
|
210
212
|
structures/views/public.py,sha256=-Tel-poYKZTy8eQllhsWUxVf7HYBuCfeL9JWlbKhs7s,3035
|
211
213
|
structures/views/statistics.py,sha256=7jj8b9ATsYwE7Cg6gMp-bYx29nV43GdWYun9WBggGkM,4709
|
212
214
|
structures/views/status.py,sha256=gcahbk6dPIZDqkaNHDAsEHyDWLzicTK18Fom0A6xx3c,718
|
213
|
-
structures/views/structures.py,sha256=
|
215
|
+
structures/views/structures.py,sha256=E1ssnqr2zp0rLQ3txIa6XkcX3NStNjMkuQM4iNjt6yw,22560
|
214
216
|
structures/webhooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
215
217
|
structures/webhooks/core.py,sha256=mP25MbQG8Yv2YayDFcR6x2V30eqXhR2q5u4SRefblf4,6573
|
216
218
|
structures/webhooks/managers.py,sha256=L3G3AmsyDeif_lfpWshmAxQ61UGJ9w8i9lZaF2jbOtQ,1117
|
@@ -218,7 +220,7 @@ structures/webhooks/models.py,sha256=kUkt9rnRQIJIrU9Bjcs34rvkb-TMbUubHdn-kny08kI
|
|
218
220
|
structures/webhooks/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
219
221
|
structures/webhooks/tests/test_core.py,sha256=4NcEAQgK2KhQkFOxYh2ad0S-qUWh1DNGDmLo5Mo5opI,6762
|
220
222
|
structures/webhooks/tests/test_utils.py,sha256=ekADFv0JOEtXeqdiejbeqrABO__Q1flJHzVieQ7L9e0,459
|
221
|
-
aa_structures-2.
|
222
|
-
aa_structures-2.
|
223
|
-
aa_structures-2.
|
224
|
-
aa_structures-2.
|
223
|
+
aa_structures-2.12.0.dist-info/LICENSE,sha256=XZiwB_S_40_HhnvLg5xvtBb3g1oGjPrk0rpFwk8iInE,1070
|
224
|
+
aa_structures-2.12.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
225
|
+
aa_structures-2.12.0.dist-info/METADATA,sha256=E2IAO0tGX_bRqM9r1ryldZsIGyv22TLHn8tMs8zQ2zs,5972
|
226
|
+
aa_structures-2.12.0.dist-info/RECORD,,
|
structures/__init__.py
CHANGED
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="
|
489
|
-
def _characters(self, obj: Owner) ->
|
490
|
-
|
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.
|
673
|
+
@admin.display(description=_("structures Count"))
|
635
674
|
def _structures_count(self, obj: Owner) -> int:
|
636
675
|
return obj.structures.count()
|
637
676
|
|
structures/app_settings.py
CHANGED
@@ -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
|
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
|
|
@@ -10,6 +10,8 @@ import dhooks_lite
|
|
10
10
|
from django.conf import settings
|
11
11
|
from django.utils.translation import gettext as _
|
12
12
|
|
13
|
+
from allianceauth.services.hooks import get_extension_logger
|
14
|
+
from app_utils.logging import LoggerAddTag
|
13
15
|
from app_utils.urls import reverse_absolute, static_file_absolute_url
|
14
16
|
|
15
17
|
from structures import __title__
|
@@ -19,6 +21,8 @@ from structures.models.notifications import Notification, NotificationBase, Webh
|
|
19
21
|
|
20
22
|
from .helpers import target_datetime_formatted
|
21
23
|
|
24
|
+
logger = LoggerAddTag(get_extension_logger(__name__), __title__)
|
25
|
+
|
22
26
|
|
23
27
|
class NotificationBaseEmbed:
|
24
28
|
"""Base class for all notification embeds.
|
@@ -141,6 +145,20 @@ class NotificationBaseEmbed:
|
|
141
145
|
footer_text += f" #{my_text}"
|
142
146
|
footer_icon_url = footer_icon_url if is_absolute_url(footer_icon_url) else None
|
143
147
|
footer = dhooks_lite.Footer(text=footer_text, icon_url=footer_icon_url)
|
148
|
+
max_description = dhooks_lite.Embed.MAX_DESCRIPTION
|
149
|
+
if self._description and len(self._description) > max_description:
|
150
|
+
logger.warning(
|
151
|
+
"%s: Description of notification is too long: %s",
|
152
|
+
self,
|
153
|
+
self._description,
|
154
|
+
)
|
155
|
+
self._description = self._description[:max_description]
|
156
|
+
max_title = dhooks_lite.Embed.MAX_TITLE
|
157
|
+
if self._title and len(self._title) > max_title:
|
158
|
+
logger.warning(
|
159
|
+
"%s: Title of notification is too long: %s", self, self._title
|
160
|
+
)
|
161
|
+
self._title = self._title[:max_title]
|
144
162
|
return dhooks_lite.Embed(
|
145
163
|
author=author,
|
146
164
|
color=self._color,
|
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
|
185
|
-
|
186
|
-
|
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(
|
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
|
+
]
|
@@ -410,11 +410,15 @@ class NotificationBase(models.Model):
|
|
410
410
|
try:
|
411
411
|
embed, ping_type = self._generate_embed(webhook.language_code)
|
412
412
|
except OSError as ex:
|
413
|
-
logger.
|
413
|
+
logger.error("%s: Failed to generate embed: %s", self, ex, exc_info=True)
|
414
414
|
return False
|
415
415
|
|
416
416
|
content = self._create_content_with_pings(webhook, ping_type)
|
417
417
|
content += self._add_discord_group_pings(webhook)
|
418
|
+
max_content = dhooks_lite.Webhook.MAX_CHARACTERS
|
419
|
+
if content and len(content) > max_content:
|
420
|
+
logger.error("%s: Content of notification is too long: %s", self, content)
|
421
|
+
return False
|
418
422
|
|
419
423
|
username, avatar_url = self._gen_avatar()
|
420
424
|
new_queue_size = webhook.send_message(
|
structures/models/owners.py
CHANGED
@@ -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
|
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
|
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
|
357
|
+
def disable_character(
|
355
358
|
self,
|
356
359
|
character: "OwnerCharacter",
|
357
|
-
|
358
|
-
level: str = "warning",
|
360
|
+
reason: str,
|
359
361
|
max_allowed_errors: int = 0,
|
360
362
|
) -> None:
|
361
|
-
"""
|
363
|
+
"""Disable character and notify it's owner and admins about it.
|
362
364
|
|
363
365
|
Args:
|
364
|
-
- character: Character
|
365
|
-
-
|
366
|
-
-
|
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
|
-
|
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
|
-
|
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}: {
|
388
|
-
"
|
389
|
-
"
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
828
|
-
character
|
829
|
-
|
830
|
-
|
831
|
-
|
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"])
|
@@ -240,6 +240,26 @@ class TestNotificationEmbedsGenerate(TestCase):
|
|
240
240
|
self.assertEqual(discord_embed.footer.text, "Structures")
|
241
241
|
self.assertIn("structures_logo.png", discord_embed.footer.icon_url)
|
242
242
|
|
243
|
+
def test_should_not_break_with_too_large_description(self):
|
244
|
+
# given
|
245
|
+
notification = Notification.objects.get(notification_id=1000000403)
|
246
|
+
notification_embed = NotificationBaseEmbed.create(notification)
|
247
|
+
notification_embed._description = "x" * 2049
|
248
|
+
# when
|
249
|
+
discord_embed = notification_embed.generate_embed()
|
250
|
+
# then
|
251
|
+
self.assertIsInstance(discord_embed, dhooks_lite.Embed)
|
252
|
+
|
253
|
+
def test_should_not_break_with_too_large_title(self):
|
254
|
+
# given
|
255
|
+
notification = Notification.objects.get(notification_id=1000000403)
|
256
|
+
notification_embed = NotificationBaseEmbed.create(notification)
|
257
|
+
notification_embed._title = "x" * 257
|
258
|
+
# when
|
259
|
+
discord_embed = notification_embed.generate_embed()
|
260
|
+
# then
|
261
|
+
self.assertIsInstance(discord_embed, dhooks_lite.Embed)
|
262
|
+
|
243
263
|
|
244
264
|
class TestNotificationEmbedsClasses(NoSocketsTestCase):
|
245
265
|
@classmethod
|
@@ -33,6 +33,7 @@ if "discord" in app_labels():
|
|
33
33
|
super().setUpClass()
|
34
34
|
load_eveuniverse()
|
35
35
|
load_eve_entities()
|
36
|
+
|
36
37
|
cls.group_1 = Group.objects.create(name="Dummy Group 1")
|
37
38
|
cls.group_2 = Group.objects.create(name="Dummy Group 2")
|
38
39
|
cls.owner = OwnerFactory()
|
@@ -140,3 +141,23 @@ if "discord" in app_labels():
|
|
140
141
|
self.assertTrue(mock_import_discord.called)
|
141
142
|
_, kwargs = mock_send_message.call_args
|
142
143
|
self.assertFalse(re.search(r"(<@&\d+>)", kwargs["content"]))
|
144
|
+
|
145
|
+
def test_should_abort_when_content_is_too_large(
|
146
|
+
self, mock_send_message, mock_import_discord
|
147
|
+
):
|
148
|
+
# given
|
149
|
+
mock_send_message.return_value = 1
|
150
|
+
mock_import_discord.return_value.objects.group_to_role.side_effect = (
|
151
|
+
self._my_group_to_role
|
152
|
+
)
|
153
|
+
webhook = WebhookFactory()
|
154
|
+
for i in range(286):
|
155
|
+
group = Group.objects.create(name=f"Group {i+1}")
|
156
|
+
webhook.ping_groups.add(group)
|
157
|
+
obj = clone_notification(
|
158
|
+
Notification.objects.get(notification_id=1000000509)
|
159
|
+
)
|
160
|
+
# when
|
161
|
+
result = obj.send_to_webhook(webhook)
|
162
|
+
# then
|
163
|
+
self.assertFalse(result)
|
@@ -318,20 +318,21 @@ class TestOwnerFetchToken(NoSocketsTestCase):
|
|
318
318
|
self.assertTrue(mock_notify.called)
|
319
319
|
self.assertEqual(owner.characters.count(), 0)
|
320
320
|
|
321
|
-
def
|
321
|
+
def test_raise_error_when_no_valid_token_found_and_disable_character(
|
322
322
|
self, mock_notify_admins, mock_notify
|
323
323
|
):
|
324
324
|
# given
|
325
|
-
|
326
|
-
user = UserMainDefaultOwnerFactory(main_character__character=
|
327
|
-
owner = OwnerFactory(user=user, characters=[
|
328
|
-
user.token_set.first().scopes.clear()
|
325
|
+
eve_character = EveCharacterFactory()
|
326
|
+
user = UserMainDefaultOwnerFactory(main_character__character=eve_character)
|
327
|
+
owner = OwnerFactory(user=user, characters=[eve_character])
|
328
|
+
user.token_set.first().scopes.clear() # token no longer valid
|
329
329
|
# when/then
|
330
330
|
with self.assertRaises(TokenError):
|
331
331
|
owner.fetch_token()
|
332
|
+
character = owner.characters.first()
|
333
|
+
self.assertFalse(character.is_enabled)
|
332
334
|
self.assertTrue(mock_notify_admins.called)
|
333
335
|
self.assertTrue(mock_notify.called)
|
334
|
-
self.assertEqual(owner.characters.count(), 0)
|
335
336
|
|
336
337
|
def test_raise_error_when_character_no_longer_a_corporation_member_and_delete_it(
|
337
338
|
self, mock_notify_admins, mock_notify
|
@@ -351,37 +352,7 @@ class TestOwnerFetchToken(NoSocketsTestCase):
|
|
351
352
|
self.assertTrue(mock_notify.called)
|
352
353
|
self.assertEqual(owner.characters.count(), 0)
|
353
354
|
|
354
|
-
def
|
355
|
-
self, mock_notify_admins, mock_notify
|
356
|
-
):
|
357
|
-
# given
|
358
|
-
character_1 = EveCharacterFactory()
|
359
|
-
user = UserMainDefaultOwnerFactory(main_character__character=character_1)
|
360
|
-
owner = OwnerFactory(
|
361
|
-
user=user,
|
362
|
-
characters=[character_1],
|
363
|
-
characters__notifications_last_used_at=dt.datetime(
|
364
|
-
2021, 1, 1, 1, 2, tzinfo=utc
|
365
|
-
),
|
366
|
-
)
|
367
|
-
character_2 = EveCharacterFactory()
|
368
|
-
OwnerCharacterFactory(
|
369
|
-
owner=owner,
|
370
|
-
eve_character=character_2,
|
371
|
-
notifications_last_used_at=dt.datetime(2021, 1, 1, 1, 1, tzinfo=utc),
|
372
|
-
)
|
373
|
-
|
374
|
-
# when
|
375
|
-
token = owner.fetch_token()
|
376
|
-
|
377
|
-
# then
|
378
|
-
self.assertIsInstance(token, Token)
|
379
|
-
self.assertEqual(token.user, user)
|
380
|
-
self.assertTrue(mock_notify_admins.called)
|
381
|
-
self.assertTrue(mock_notify.called)
|
382
|
-
self.assertEqual(owner.characters.count(), 1)
|
383
|
-
|
384
|
-
def test_should_rotate_through_characters_for_notification(
|
355
|
+
def test_should_rotate_through_enabled_characters_for_notification(
|
385
356
|
self, mock_notify_admins, mock_notify
|
386
357
|
):
|
387
358
|
# given
|
@@ -398,6 +369,9 @@ class TestOwnerFetchToken(NoSocketsTestCase):
|
|
398
369
|
owner=owner,
|
399
370
|
notifications_last_used_at=dt.datetime(2021, 1, 1, 3, 0, tzinfo=utc),
|
400
371
|
)
|
372
|
+
OwnerCharacterFactory(
|
373
|
+
owner=owner, is_enabled=False
|
374
|
+
) # this one should be ignore
|
401
375
|
tokens_received = []
|
402
376
|
|
403
377
|
# when
|
@@ -493,6 +467,36 @@ class TestOwnerFetchToken(NoSocketsTestCase):
|
|
493
467
|
],
|
494
468
|
)
|
495
469
|
|
470
|
+
def test_should_delete_invalid_characters_and_return_token_from_valid_char(
|
471
|
+
self, mock_notify_admins, mock_notify
|
472
|
+
):
|
473
|
+
# given
|
474
|
+
character_1 = EveCharacterFactory()
|
475
|
+
user = UserMainDefaultOwnerFactory(main_character__character=character_1)
|
476
|
+
owner = OwnerFactory(
|
477
|
+
user=user,
|
478
|
+
characters=[character_1],
|
479
|
+
characters__notifications_last_used_at=dt.datetime(
|
480
|
+
2021, 1, 1, 1, 2, tzinfo=utc
|
481
|
+
),
|
482
|
+
)
|
483
|
+
character_2 = EveCharacterFactory() # invalid, because of different corporation
|
484
|
+
OwnerCharacterFactory(
|
485
|
+
owner=owner,
|
486
|
+
eve_character=character_2,
|
487
|
+
notifications_last_used_at=dt.datetime(2021, 1, 1, 1, 1, tzinfo=utc),
|
488
|
+
)
|
489
|
+
|
490
|
+
# when
|
491
|
+
token = owner.fetch_token()
|
492
|
+
|
493
|
+
# then
|
494
|
+
self.assertIsInstance(token, Token)
|
495
|
+
self.assertEqual(token.user, user)
|
496
|
+
self.assertTrue(mock_notify_admins.called)
|
497
|
+
self.assertTrue(mock_notify.called)
|
498
|
+
self.assertEqual(owner.characters.count(), 1)
|
499
|
+
|
496
500
|
|
497
501
|
class TestOwnerCharacters(NoSocketsTestCase):
|
498
502
|
@classmethod
|
@@ -500,12 +504,6 @@ class TestOwnerCharacters(NoSocketsTestCase):
|
|
500
504
|
super().setUpClass()
|
501
505
|
cls.owner = OwnerFactory()
|
502
506
|
|
503
|
-
def test_should_return_str(self):
|
504
|
-
# given
|
505
|
-
character = OwnerCharacterFactory(owner=self.owner)
|
506
|
-
# when/then
|
507
|
-
self.assertTrue(str(character))
|
508
|
-
|
509
507
|
def test_should_add_new_character(self):
|
510
508
|
# given
|
511
509
|
character = EveCharacterFactory(corporation=self.owner.corporation)
|
@@ -546,22 +544,35 @@ class TestOwnerCharacters(NoSocketsTestCase):
|
|
546
544
|
with self.assertRaises(ValueError):
|
547
545
|
self.owner.add_character(character_ownership)
|
548
546
|
|
549
|
-
def
|
547
|
+
def test_should_count_enabled_characters_only(self):
|
550
548
|
# given
|
551
|
-
OwnerCharacterFactory(owner=self.owner)
|
549
|
+
OwnerCharacterFactory(owner=self.owner, is_enabled=False)
|
552
550
|
# when
|
553
|
-
result = self.owner.
|
551
|
+
result = self.owner.valid_characters_count()
|
554
552
|
# then
|
555
|
-
self.assertEqual(result,
|
553
|
+
self.assertEqual(result, 1)
|
556
554
|
|
557
555
|
def test_should_count_characters_when_empty(self):
|
558
556
|
# given
|
559
557
|
owner = OwnerFactory(characters=False)
|
560
558
|
# when
|
561
|
-
result = owner.
|
559
|
+
result = owner.valid_characters_count()
|
562
560
|
# then
|
563
561
|
self.assertEqual(result, 0)
|
564
562
|
|
563
|
+
def test_should_reset_character_when_re_adding(self):
|
564
|
+
# given
|
565
|
+
character: OwnerCharacter = self.owner.characters.first()
|
566
|
+
character.is_enabled = False
|
567
|
+
character.disabled_reason = "some reason"
|
568
|
+
character.save()
|
569
|
+
# when
|
570
|
+
self.owner.add_character(character.character_ownership)
|
571
|
+
# then
|
572
|
+
character.refresh_from_db()
|
573
|
+
self.assertTrue(character.is_enabled)
|
574
|
+
self.assertFalse(character.disabled_reason)
|
575
|
+
|
565
576
|
|
566
577
|
@patch(MODULE_PATH + ".notify", spec=True)
|
567
578
|
@patch(MODULE_PATH + ".notify_admins", spec=True)
|
@@ -577,7 +588,7 @@ class TestOwnerDeleteCharacter(NoSocketsTestCase):
|
|
577
588
|
user = character.character_ownership.user
|
578
589
|
|
579
590
|
# when
|
580
|
-
self.owner.delete_character(character=character,
|
591
|
+
self.owner.delete_character(character=character, reason="dummy error")
|
581
592
|
|
582
593
|
# then
|
583
594
|
self.assertEqual(self.owner.characters.count(), 0)
|
@@ -591,18 +602,50 @@ class TestOwnerDeleteCharacter(NoSocketsTestCase):
|
|
591
602
|
self.assertEqual(kwargs["user"], user)
|
592
603
|
self.assertEqual(kwargs["level"], "warning")
|
593
604
|
|
594
|
-
|
605
|
+
|
606
|
+
@patch(MODULE_PATH + ".notify", spec=True)
|
607
|
+
@patch(MODULE_PATH + ".notify_admins", spec=True)
|
608
|
+
class TestOwnerDisableCharacters(NoSocketsTestCase):
|
609
|
+
@classmethod
|
610
|
+
def setUpClass(cls):
|
611
|
+
super().setUpClass()
|
612
|
+
cls.owner = OwnerFactory(characters=False)
|
613
|
+
|
614
|
+
def test_should_disable_character_and_notify(self, mock_notify_admins, mock_notify):
|
615
|
+
# given
|
616
|
+
character = OwnerCharacterFactory(owner=self.owner)
|
617
|
+
user = character.character_ownership.user
|
618
|
+
|
619
|
+
# when
|
620
|
+
self.owner.disable_character(character=character, reason="dummy error")
|
621
|
+
|
622
|
+
# then
|
623
|
+
character.refresh_from_db()
|
624
|
+
self.assertFalse(character.is_enabled)
|
625
|
+
self.assertTrue(character.disabled_reason)
|
626
|
+
self.assertTrue(mock_notify_admins.called)
|
627
|
+
_, kwargs = mock_notify_admins.call_args
|
628
|
+
self.assertIn("dummy error", kwargs["message"])
|
629
|
+
self.assertEqual(kwargs["level"], "danger")
|
630
|
+
self.assertTrue(mock_notify.called)
|
631
|
+
_, kwargs = mock_notify.call_args
|
632
|
+
self.assertIn("dummy error", kwargs["message"])
|
633
|
+
self.assertEqual(kwargs["user"], user)
|
634
|
+
self.assertEqual(kwargs["level"], "warning")
|
635
|
+
|
636
|
+
def test_should_not_disable_when_error_counter_above_zero(
|
595
637
|
self, mock_notify_admins, mock_notify
|
596
638
|
):
|
597
639
|
# given
|
598
640
|
character = OwnerCharacterFactory(owner=self.owner)
|
599
641
|
|
600
642
|
# when
|
601
|
-
self.owner.
|
602
|
-
character=character,
|
643
|
+
self.owner.disable_character(
|
644
|
+
character=character, reason="dummy error", max_allowed_errors=1
|
603
645
|
)
|
604
646
|
# then
|
605
647
|
character.refresh_from_db()
|
648
|
+
self.assertTrue(character.is_enabled)
|
606
649
|
self.assertEqual(character.error_count, 1)
|
607
650
|
self.assertFalse(mock_notify_admins.called)
|
608
651
|
self.assertFalse(mock_notify.called)
|
@@ -7,7 +7,7 @@ from app_utils.esi_testing import EsiClientStub, EsiEndpoint
|
|
7
7
|
from app_utils.testing import NoSocketsTestCase
|
8
8
|
|
9
9
|
from structures.constants import EveCorporationId
|
10
|
-
from structures.models import StarbaseDetail, Structure
|
10
|
+
from structures.models import OwnerCharacter, StarbaseDetail, Structure
|
11
11
|
from structures.tests.testdata.factories import (
|
12
12
|
EveEntityCorporationFactory,
|
13
13
|
OwnerFactory,
|
@@ -353,12 +353,13 @@ class TestUpdateStarbasesEsi(NoSocketsTestCase):
|
|
353
353
|
owner.refresh_from_db()
|
354
354
|
self.assertFalse(owner.is_structure_sync_fresh)
|
355
355
|
self.assertTrue(mock_notify)
|
356
|
-
character = owner.characters.first()
|
356
|
+
character: OwnerCharacter = owner.characters.first()
|
357
357
|
self.assertEqual(character.error_count, 1)
|
358
|
+
self.assertTrue(character.is_enabled)
|
358
359
|
|
359
360
|
@patch(MODULE_PATH + ".STRUCTURES_ESI_DIRECTOR_ERROR_MAX_RETRIES", 3)
|
360
361
|
@patch(MODULE_PATH + ".notify", spec=True)
|
361
|
-
def
|
362
|
+
def test_should_disable_character_when_not_director_while_updating_starbases(
|
362
363
|
self, mock_notify, mock_esi
|
363
364
|
):
|
364
365
|
# given
|
@@ -369,7 +370,7 @@ class TestUpdateStarbasesEsi(NoSocketsTestCase):
|
|
369
370
|
)
|
370
371
|
mock_esi.client = self.esi_client_stub.replace_endpoints([new_endpoint])
|
371
372
|
owner = OwnerFactory(user=self.user, structures_last_update_at=None)
|
372
|
-
character = owner.characters.first()
|
373
|
+
character: OwnerCharacter = owner.characters.first()
|
373
374
|
character.error_count = 3
|
374
375
|
character.save()
|
375
376
|
# when
|
@@ -378,7 +379,22 @@ class TestUpdateStarbasesEsi(NoSocketsTestCase):
|
|
378
379
|
owner.refresh_from_db()
|
379
380
|
self.assertFalse(owner.is_structure_sync_fresh)
|
380
381
|
self.assertTrue(mock_notify)
|
381
|
-
|
382
|
+
character.refresh_from_db()
|
383
|
+
self.assertFalse(character.is_enabled)
|
384
|
+
|
385
|
+
def test_should_reset_error_count_for_character_when_successful(self, mock_esi):
|
386
|
+
# given
|
387
|
+
mock_esi.client = self.esi_client_stub
|
388
|
+
owner = OwnerFactory(user=self.user, structures_last_update_at=None)
|
389
|
+
character: OwnerCharacter = owner.characters.first()
|
390
|
+
character.error_count = 3
|
391
|
+
character.save()
|
392
|
+
# when
|
393
|
+
owner.update_structures_esi()
|
394
|
+
# then
|
395
|
+
character.refresh_from_db()
|
396
|
+
self.assertTrue(character.is_enabled)
|
397
|
+
self.assertEqual(character.error_count, 0)
|
382
398
|
|
383
399
|
def test_should_remove_old_starbases(self, mock_esi):
|
384
400
|
# given
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from app_utils.testing import NoSocketsTestCase
|
2
|
+
|
3
|
+
from structures.tests.testdata.factories import OwnerCharacterFactory, OwnerFactory
|
4
|
+
|
5
|
+
MODULE_PATH = "structures.models.owners"
|
6
|
+
|
7
|
+
|
8
|
+
class TestOwnerCharacter(NoSocketsTestCase):
|
9
|
+
@classmethod
|
10
|
+
def setUpClass(cls):
|
11
|
+
super().setUpClass()
|
12
|
+
cls.owner = OwnerFactory()
|
13
|
+
|
14
|
+
def test_should_return_str(self):
|
15
|
+
# given
|
16
|
+
character = OwnerCharacterFactory(owner=self.owner)
|
17
|
+
# when/then
|
18
|
+
self.assertTrue(str(character))
|
19
|
+
|
20
|
+
def test_can_reset_character(self):
|
21
|
+
# given
|
22
|
+
character = OwnerCharacterFactory(
|
23
|
+
owner=self.owner, is_enabled=False, disabled_reason="reason", error_count=42
|
24
|
+
)
|
25
|
+
# when
|
26
|
+
character.reset()
|
27
|
+
# then
|
28
|
+
character.refresh_from_db()
|
29
|
+
self.assertTrue(character.is_enabled)
|
30
|
+
self.assertFalse(character.disabled_reason)
|
31
|
+
self.assertFalse(character.error_count)
|
structures/tests/test_admin.py
CHANGED
@@ -32,6 +32,7 @@ from .testdata.factories import (
|
|
32
32
|
EveCorporationInfoFactory,
|
33
33
|
FuelAlertConfigFactory,
|
34
34
|
NotificationFactory,
|
35
|
+
OwnerCharacterFactory,
|
35
36
|
OwnerFactory,
|
36
37
|
PocoFactory,
|
37
38
|
StarbaseFactory,
|
@@ -261,6 +262,7 @@ class TestNotificationAdmin(TestCase):
|
|
261
262
|
)
|
262
263
|
# when
|
263
264
|
result = self.modeladmin._structures(obj)
|
265
|
+
# then
|
264
266
|
self.assertIsNone(result)
|
265
267
|
|
266
268
|
# FIXME: Does not seam to work with special prefetch list
|
@@ -356,6 +358,52 @@ class TestNotificationAdmin(TestCase):
|
|
356
358
|
self.assertSetEqual(set(queryset), set(expected))
|
357
359
|
|
358
360
|
|
361
|
+
class TestNotificationAdminWebhooks(TestCase):
|
362
|
+
@classmethod
|
363
|
+
def setUpClass(cls):
|
364
|
+
super().setUpClass()
|
365
|
+
cls.factory = RequestFactory()
|
366
|
+
load_eveuniverse()
|
367
|
+
cls.modeladmin = NotificationAdmin(model=Notification, admin_site=AdminSite())
|
368
|
+
cls.user = SuperuserFactory()
|
369
|
+
|
370
|
+
def test_should_return_name_of_owner_webhook(self):
|
371
|
+
# given
|
372
|
+
owner = OwnerFactory(webhooks__name="Alpha")
|
373
|
+
obj = NotificationFactory(
|
374
|
+
owner=owner, notif_type=NotificationType.STRUCTURE_LOST_SHIELD
|
375
|
+
)
|
376
|
+
obj2 = self.modeladmin.get_queryset(MockRequest(user=self.user)).get(pk=obj.pk)
|
377
|
+
# when
|
378
|
+
result = self.modeladmin._webhooks(obj2)
|
379
|
+
# then
|
380
|
+
self.assertEqual("Alpha", result)
|
381
|
+
|
382
|
+
def test_should_report_missing_webhook(self):
|
383
|
+
# given
|
384
|
+
owner = OwnerFactory(webhooks=False)
|
385
|
+
obj = NotificationFactory(
|
386
|
+
owner=owner, notif_type=NotificationType.STRUCTURE_LOST_SHIELD
|
387
|
+
)
|
388
|
+
obj2 = self.modeladmin.get_queryset(MockRequest(user=self.user)).get(pk=obj.pk)
|
389
|
+
# when
|
390
|
+
result = self.modeladmin._webhooks(obj2)
|
391
|
+
# then
|
392
|
+
self.assertIn("Not configured", result)
|
393
|
+
|
394
|
+
def test_should_report_when_webhooks_not_configured_for_this_notif_type(self):
|
395
|
+
# given
|
396
|
+
owner = OwnerFactory()
|
397
|
+
obj = NotificationFactory(
|
398
|
+
owner=owner, notif_type=NotificationType.SOV_ENTOSIS_CAPTURE_STARTED
|
399
|
+
)
|
400
|
+
obj2 = self.modeladmin.get_queryset(MockRequest(user=self.user)).get(pk=obj.pk)
|
401
|
+
# when
|
402
|
+
result = self.modeladmin._webhooks(obj2)
|
403
|
+
# then
|
404
|
+
self.assertIn("Not configured", result)
|
405
|
+
|
406
|
+
|
359
407
|
class TestOwnerAdmin(TestCase):
|
360
408
|
@classmethod
|
361
409
|
def setUpClass(cls):
|
@@ -420,6 +468,23 @@ class TestOwnerAdmin(TestCase):
|
|
420
468
|
self.assertEqual(mock_task.delay.call_count, 1)
|
421
469
|
self.assertTrue(mock_message_user.called)
|
422
470
|
|
471
|
+
@patch(MODULE_PATH + ".OwnerAdmin.message_user", spec=True)
|
472
|
+
def test_action_reset_characters(self, mock_message_user):
|
473
|
+
# given
|
474
|
+
owner_1 = OwnerFactory(characters=False)
|
475
|
+
character_1 = OwnerCharacterFactory(owner=owner_1, is_enabled=False)
|
476
|
+
owner_2 = OwnerFactory(characters=False)
|
477
|
+
character_2 = OwnerCharacterFactory(owner=owner_2, is_enabled=False)
|
478
|
+
# when
|
479
|
+
queryset = Owner.objects.filter(pk=owner_1.pk)
|
480
|
+
self.modeladmin.reset_characters(MockRequest(self.user), queryset)
|
481
|
+
# then
|
482
|
+
self.assertTrue(mock_message_user.called)
|
483
|
+
character_1.refresh_from_db()
|
484
|
+
self.assertTrue(character_1.is_enabled)
|
485
|
+
character_2.refresh_from_db()
|
486
|
+
self.assertFalse(character_2.is_enabled)
|
487
|
+
|
423
488
|
def test_should_return_empty_turnaround_times(self):
|
424
489
|
# given
|
425
490
|
obj = OwnerFactory()
|
@@ -60,12 +60,13 @@ class TestOwnerManager(NoSocketsTestCase):
|
|
60
60
|
def test_should_annotate_characters_count(self):
|
61
61
|
# given
|
62
62
|
owner = OwnerFactory() # 1st character automatically created
|
63
|
-
OwnerCharacterFactory(owner=owner) # 2nd character added
|
63
|
+
OwnerCharacterFactory(owner=owner, is_enabled=False) # 2nd character added
|
64
64
|
# when
|
65
65
|
result = Owner.objects.annotate_characters_count()
|
66
66
|
# then
|
67
67
|
obj = result.get(pk=owner.pk)
|
68
|
-
self.assertEqual(obj.
|
68
|
+
self.assertEqual(obj.characters_enabled_count, 1)
|
69
|
+
self.assertEqual(obj.characters_disabled_count, 1)
|
69
70
|
|
70
71
|
def test_should_return_when_structures_where_last_updated_for_several_owners(self):
|
71
72
|
# given
|
@@ -203,9 +203,7 @@ class OwnerFactory(factory.django.DjangoModelFactory, metaclass=BaseMetaFactory[
|
|
203
203
|
def characters(
|
204
204
|
obj, create: bool, extracted: Optional[List[EveCharacter]], **kwargs
|
205
205
|
):
|
206
|
-
|
207
|
-
Set extracted to False to skip creating characters.
|
208
|
-
"""
|
206
|
+
# Set characters=False to skip creating characters.
|
209
207
|
if not create or extracted is False:
|
210
208
|
return
|
211
209
|
|
@@ -222,7 +220,8 @@ class OwnerFactory(factory.django.DjangoModelFactory, metaclass=BaseMetaFactory[
|
|
222
220
|
|
223
221
|
@factory.post_generation
|
224
222
|
def webhooks(obj, create, extracted, **kwargs):
|
225
|
-
|
223
|
+
# Set webhooks=False to skip creating characters.
|
224
|
+
if not create or extracted is False:
|
226
225
|
return
|
227
226
|
|
228
227
|
if extracted:
|
@@ -8,14 +8,13 @@ from django.urls import reverse
|
|
8
8
|
from django.utils.dateparse import parse_datetime
|
9
9
|
from django.utils.timezone import now
|
10
10
|
|
11
|
+
from allianceauth.eveonline.models import EveCharacter
|
11
12
|
from app_utils.testdata_factories import UserMainFactory
|
12
13
|
from app_utils.testing import json_response_to_python
|
13
14
|
|
14
15
|
import structures.views.status
|
15
16
|
from structures.models import Owner, Structure
|
16
|
-
from structures.
|
17
|
-
|
18
|
-
from ..testdata.factories import (
|
17
|
+
from structures.tests.testdata.factories import (
|
19
18
|
EveCharacterFactory,
|
20
19
|
JumpGateFactory,
|
21
20
|
OwnerFactory,
|
@@ -27,7 +26,9 @@ from ..testdata.factories import (
|
|
27
26
|
UserMainDefaultOwnerFactory,
|
28
27
|
WebhookFactory,
|
29
28
|
)
|
30
|
-
from
|
29
|
+
from structures.tests.testdata.load_eveuniverse import load_eveuniverse
|
30
|
+
from structures.views import structures
|
31
|
+
|
31
32
|
from .utils import json_response_to_dict
|
32
33
|
|
33
34
|
VIEWS_PATH = "structures.views.structures"
|
@@ -527,7 +528,7 @@ class TestAddStructureOwner(TestCase):
|
|
527
528
|
cls.factory = RequestFactory()
|
528
529
|
load_eveuniverse()
|
529
530
|
cls.user = UserMainDefaultOwnerFactory()
|
530
|
-
cls.character = cls.user.profile.main_character
|
531
|
+
cls.character: EveCharacter = cls.user.profile.main_character
|
531
532
|
cls.character_ownership = cls.character.character_ownership
|
532
533
|
|
533
534
|
def _add_structure_owner_view(self, token=None, user=None):
|
@@ -656,6 +657,58 @@ class TestAddStructureOwner(TestCase):
|
|
656
657
|
)
|
657
658
|
self.assertTrue(owner.is_active)
|
658
659
|
|
660
|
+
@patch(VIEWS_PATH + ".STRUCTURES_ADMIN_NOTIFICATIONS_ENABLED", False)
|
661
|
+
@patch(VIEWS_PATH + ".tasks.update_all_for_owner")
|
662
|
+
@patch(VIEWS_PATH + ".notify_admins")
|
663
|
+
@patch(VIEWS_PATH + ".messages")
|
664
|
+
def test_can_readd_same_character(
|
665
|
+
self, mock_messages, mock_notify_admins, mock_update_all_for_owner
|
666
|
+
):
|
667
|
+
# given
|
668
|
+
owner = OwnerFactory(characters=[self.character])
|
669
|
+
owner_character = owner.characters.first()
|
670
|
+
# when
|
671
|
+
response = self._add_structure_owner_view(user=self.user)
|
672
|
+
# then
|
673
|
+
self.assertEqual(response.status_code, 302)
|
674
|
+
self.assertEqual(response.url, reverse("structures:index"))
|
675
|
+
owner.refresh_from_db()
|
676
|
+
character_ownerships = set(
|
677
|
+
owner.characters.values_list("character_ownership", flat=True)
|
678
|
+
)
|
679
|
+
self.assertSetEqual(
|
680
|
+
{self.character_ownership.pk, owner_character.character_ownership.pk},
|
681
|
+
character_ownerships,
|
682
|
+
)
|
683
|
+
|
684
|
+
# @patch(VIEWS_PATH + ".STRUCTURES_ADMIN_NOTIFICATIONS_ENABLED", False)
|
685
|
+
# @patch(VIEWS_PATH + ".tasks.update_all_for_owner")
|
686
|
+
# @patch(VIEWS_PATH + ".notify_admins")
|
687
|
+
# @patch(VIEWS_PATH + ".messages")
|
688
|
+
# def test_should_reenable_character_when_re_adding(
|
689
|
+
# self, mock_messages, mock_notify_admins, mock_update_all_for_owner
|
690
|
+
# ):
|
691
|
+
# # given
|
692
|
+
# owner = OwnerFactory(characters=[self.character])
|
693
|
+
# owner_character: OwnerCharacter = owner.characters.first()
|
694
|
+
# owner_character.is_enabled = False
|
695
|
+
# owner_character.save()
|
696
|
+
# # when
|
697
|
+
# response = self._add_structure_owner_view(user=self.user)
|
698
|
+
# # then
|
699
|
+
# self.assertEqual(response.status_code, 302)
|
700
|
+
# self.assertEqual(response.url, reverse("structures:index"))
|
701
|
+
# owner.refresh_from_db()
|
702
|
+
# character_ownerships = set(
|
703
|
+
# owner.characters.values_list("character_ownership", flat=True)
|
704
|
+
# )
|
705
|
+
# self.assertSetEqual(
|
706
|
+
# {self.character_ownership.pk, owner_character.character_ownership.pk},
|
707
|
+
# character_ownerships,
|
708
|
+
# )
|
709
|
+
# owner_character.refresh_from_db()
|
710
|
+
# self.assertTrue(owner_character.is_enabled)
|
711
|
+
|
659
712
|
|
660
713
|
class TestStructureFittingModal(TestCase):
|
661
714
|
@classmethod
|
structures/views/structures.py
CHANGED
@@ -630,7 +630,7 @@ def add_structure_owner(request: HttpRequest, token: Token):
|
|
630
630
|
% {
|
631
631
|
"corporation": owner,
|
632
632
|
"character": token_char,
|
633
|
-
"characters_count": owner.
|
633
|
+
"characters_count": owner.valid_characters_count(),
|
634
634
|
}
|
635
635
|
),
|
636
636
|
)
|
@@ -646,7 +646,7 @@ def add_structure_owner(request: HttpRequest, token: Token):
|
|
646
646
|
"character": token_char,
|
647
647
|
"corporation": owner,
|
648
648
|
"user": request.user.username,
|
649
|
-
"characters_count": owner.
|
649
|
+
"characters_count": owner.valid_characters_count(),
|
650
650
|
},
|
651
651
|
title=_("%s: Character added to: %s") % (__title__, owner),
|
652
652
|
)
|
File without changes
|
File without changes
|