aa-alumni 1.0.0b1__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.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: aa-alumni
3
+ Version: 1.0.0b1
4
+ Summary: Integration with Alliance Auths State System
5
+ Keywords: allianceauth,eveonline
6
+ Author-email: Joel Falknau <joel.falknau@gmail.com>
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Environment :: Web Environment
10
+ Classifier: Framework :: Celery
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: Implementation :: CPython
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
27
+ License-File: LICENSE
28
+ Requires-Dist: allianceauth>=4.6.4,<6
29
+ Requires-Dist: django-esi>=8,<9
30
+ Project-URL: Homepage, https://gitlab.com/tactical-supremacy/aa-alumni
31
+ Project-URL: Source, https://gitlab.com/tactical-supremacy/aa-alumni
32
+ Project-URL: Tracker, https://gitlab.com/tactical-supremacy/aa-alumni/-/issues
33
+
34
+ # Alliance Auth - Alumni
35
+
36
+ ## Features
37
+
38
+ - Integration with Alliance Auth's State System, creates and maintains an Alumni State for past members of an Alliance and/or Corporation.
39
+
40
+ ## Installation
41
+
42
+ ### Step 1 - Prepare Auth
43
+
44
+ Remove/Promote any state with a priority of `1`, Alumni is considered slightly better than the built in Guest State.
45
+
46
+ ### Step 2 - Install from pip
47
+
48
+ ```shell
49
+ pip install aa-alumni
50
+ ```
51
+
52
+ ### Step 3 - Configure Auth settings
53
+
54
+ Configure your Auth settings (`local.py`) as follows:
55
+
56
+ - Add `'alumni'` to `INSTALLED_APPS`
57
+ - Add below lines to your settings file:
58
+
59
+ ```python
60
+ ## Settings for AA-Alumni
61
+ # Tasks
62
+ CELERYBEAT_SCHEDULE['alumni_run_alumni_check_all'] = {
63
+ 'task': 'alumni.tasks.run_alumni_check_all',
64
+ 'schedule': crontab(minute="0", hour="0", day_of_week="4"),
65
+ 'apply_offset': True,
66
+ }
67
+ CELERYBEAT_SCHEDULE['alumni_run_update_all_models'] = {
68
+ 'task': 'alumni.tasks.update_all_models',
69
+ 'schedule': crontab(minute="0", hour="0", day_of_week="3"),
70
+ 'apply_offset': True,
71
+ }
72
+ ```
73
+
74
+ ### Step 4 - Update AA's State system
75
+
76
+ ```shell
77
+ python myauth/manage.py alumni_state
78
+ ```
79
+
80
+ ### Step 5 - Maintain Alliance Auth
81
+
82
+ - Run migrations `python manage.py migrate`
83
+ - Gather your staticfiles `python manage.py collectstatic`
84
+ - Restart your project `supervisorctl restart myauth:`
85
+
86
+ ### Step 6 - Configure Further
87
+
88
+ In the Admin interface, visit `alumni > config > add` or `<AUTH-URL>/admin/alumni/config/add/`
89
+ Select the Alliances and/or Corporations for which characters with historical membership are Alumni
90
+
91
+ ## Settings
92
+
93
+ | Name | Description | Default |
94
+ | --- | --- | --- |
95
+ |`ALUMNI_CHARACTERCORPORATION_RATELIMIT`| Celery Rate Limit _per worker_, 10 tasks * 10 Workers = 100 tasks/min | '10/m' |
96
+
97
+ ## Contributing
98
+
99
+ Make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at <https://developers.eveonline.com> before submitting any pull requests. All bug fixes or features must not include extra superfluous formatting changes.
100
+
@@ -0,0 +1,24 @@
1
+ alumni/__init__.py,sha256=oYGhAgysyvO0h-v434BQRXMOWqlEmjgjoamWPFb4Qys,293
2
+ alumni/admin.py,sha256=BiqnT1Gk_wJOJ1eeIFbtgQf1n2q5MLf8Wkc4CqSz5Vw,1271
3
+ alumni/app_settings.py,sha256=soF5EtRwxvWG2ARqzm7e7Qe5dpp-NCPZ-um781fENvs,383
4
+ alumni/apps.py,sha256=JNO5V_uldj3Y2OEJL0h4u86LoPZr5UOhzkcTFZ6VeNI,239
5
+ alumni/models.py,sha256=UPr_xGxiRwZrFB2nmhdMPXxCVCiN7RBKGeu-ULNjGd0,3036
6
+ alumni/providers.py,sha256=qUK2zX_DvGOwbgozn8dlgKXZ87e4Zpl4ZnYMEDLjjEY,1309
7
+ alumni/signals.py,sha256=WpeyRa8AGfcWAR9qvy3FgfdP2zNLBzqrh7xm1cH5Klg,835
8
+ alumni/tasks.py,sha256=r-y0M5ueWxlsdMgN-75MSU4b75zdLHXlX57xcL2Yw7Q,11991
9
+ alumni/locale/de/LC_MESSAGES/django.mo,sha256=hTZUh1Insgy-eZmz0-Owvgy9Mh6tNw3p2X9J6tD4TwE,1250
10
+ alumni/locale/de/LC_MESSAGES/django.po,sha256=aEAiaM_iPQFp3BCjD0-WEKRMrtLSIn2gp7INLJ5KbJY,1744
11
+ alumni/locale/en/LC_MESSAGES/django.mo,sha256=N1pb17IfLd0ASiKO8d68-B4ygSpDkhKOCs8YTzMXQo0,380
12
+ alumni/locale/en/LC_MESSAGES/django.po,sha256=dW_HNYdzscfIrGUoGA1-0PwV0LNn7-WdgXmdMU7pGm8,1250
13
+ alumni/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ alumni/management/commands/alumni_state.py,sha256=OyJKnkzk9eksk-vULSVBdGvMEhxZX_xa7eH5RXFjeJ8,759
15
+ alumni/migrations/0001_initial.py,sha256=C8R2k4y5KvpTLU_etBQ0J3jkGYd3HAgevAekUZvzxDw,3283
16
+ alumni/migrations/0002_auto_20211230_0147.py,sha256=p3sK1Ohdf6O20tDbMmuPJwC2vVFH-AR0kkXFy9H34_Y,626
17
+ alumni/migrations/0003_alter_alumnisetup_options_and_more.py,sha256=JyXQqo9vR-hRx7CHsNpXLxZEjEijSBLW3zX7Zmw3Y50,1012
18
+ alumni/migrations/0004_corporationupdatetimestamp_alter_alumnisetup_options_and_more.py,sha256=5jDhobyP_AfnBDnKlfAodriKbHrX4MyYMGf1A4oUk8w,2112
19
+ alumni/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ alumni/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ aa_alumni-1.0.0b1.dist-info/licenses/LICENSE,sha256=6TxpZBZY7OLGLrJXe6vREu--u7CjCfW19u4oXW_uZKs,1069
22
+ aa_alumni-1.0.0b1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
23
+ aa_alumni-1.0.0b1.dist-info/METADATA,sha256=oDf7ZDVPX1Hvo4mt7SaeDFxHpfE9W6K9SQ5v7urrVkg,3448
24
+ aa_alumni-1.0.0b1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Joel Falknau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
alumni/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Integration with Alliance Auths State System
3
+
4
+ Creates and maintains an Alumni State for past members of an Alliance and/or Corporation
5
+ """
6
+
7
+ __version__ = "1.0.0b1"
8
+ __title__ = "AAAlumni"
9
+ __url__ = "https://gitlab.com/tactical-supremacy/aa-alumni"
10
+ __esi_compatibility_date__ = "2025-11-06"
alumni/admin.py ADDED
@@ -0,0 +1,37 @@
1
+ from solo.admin import SingletonModelAdmin
2
+
3
+ from django.contrib import admin
4
+
5
+ from .models import (
6
+ AlumniSetup, CharacterCorporationHistory, CharacterUpdateTimestamp,
7
+ CorporationAllianceHistory, CorporationUpdateTimestamp,
8
+ )
9
+
10
+
11
+ @admin.register(AlumniSetup)
12
+ class AlumniSetupAdmin(SingletonModelAdmin):
13
+ filter_horizontal = ["alumni_corporations", "alumni_alliances"]
14
+
15
+
16
+ @admin.register(CorporationAllianceHistory)
17
+ class CorporationAllianceHistoryAdmin(admin.ModelAdmin):
18
+ search_fields = ['corporation_id', 'alliance_id']
19
+ list_display = ('corporation_id', 'alliance_id', 'record_id', 'start_date')
20
+
21
+
22
+ @admin.register(CharacterCorporationHistory)
23
+ class CharacterCorporationHistoryAdmin(admin.ModelAdmin):
24
+ search_fields = ['corporation_id', 'character']
25
+ list_display = ('corporation_id', 'character', 'record_id', 'start_date')
26
+
27
+
28
+ @admin.register(CharacterUpdateTimestamp)
29
+ class CharacterUpdateTimestampAdmin(admin.ModelAdmin):
30
+ search_fields = ['character__character_name', 'character__character_id']
31
+ list_display = ('character', 'last_updated')
32
+
33
+
34
+ @admin.register(CorporationUpdateTimestamp)
35
+ class CorporationUpdateTimestampAdmin(admin.ModelAdmin):
36
+ search_fields = ['corporation_id']
37
+ list_display = ('corporation_id', 'last_updated')
alumni/app_settings.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.conf import settings
2
+
3
+ ALUMNI_STATE_NAME = getattr(settings, 'ALUMNI_STATE_NAME', "Alumni")
4
+
5
+ ALUMNI_TASK_PRIORITY = getattr(settings, 'ALUMNI_TASK_PRIORITY', 7)
6
+
7
+ ALUMNI_CHARACTERCORPORATION_RATELIMIT = getattr(settings, 'ALUMNI_CHARACTERCORPORATION_RATELIMIT', '10/m') # 10*10workers = 100/300 per min
8
+
9
+ ALUMNI_TASK_JITTER = getattr(settings, 'ALUMNI_TASK_PRIORITY', 600)
alumni/apps.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+ from . import __version__
4
+
5
+
6
+ class AlumniConfig(AppConfig):
7
+ default_auto_field = 'django.db.models.BigAutoField'
8
+ name = "alumni"
9
+ label = "alumni"
10
+ verbose_name = f"AA Alumni v{__version__}"
Binary file
@@ -0,0 +1,53 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ # Translators:
7
+ # Peter Pfeufer, 2024
8
+ #
9
+ #, fuzzy
10
+ msgid ""
11
+ msgstr ""
12
+ "Project-Id-Version: PACKAGE VERSION\n"
13
+ "Report-Msgid-Bugs-To: \n"
14
+ "POT-Creation-Date: 2024-05-11 18:13+1000\n"
15
+ "PO-Revision-Date: 2024-05-11 10:42+0000\n"
16
+ "Last-Translator: Peter Pfeufer, 2024\n"
17
+ "Language-Team: German (https://app.transifex.com/alliance-auth/teams/107430/de/)\n"
18
+ "MIME-Version: 1.0\n"
19
+ "Content-Type: text/plain; charset=UTF-8\n"
20
+ "Content-Transfer-Encoding: 8bit\n"
21
+ "Language: de\n"
22
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
23
+
24
+ #: alumni/models.py:14
25
+ msgid ""
26
+ "Characters with these Corps in their History will be given Alumni Status"
27
+ msgstr ""
28
+ "Charaktere mit diesen Corps in ihrer Vergangenheit erhalten den Alumni-"
29
+ "Status"
30
+
31
+ #: alumni/models.py:18
32
+ msgid ""
33
+ "Characters with these Alliances in their History will be given Alumni Status"
34
+ msgstr ""
35
+ "Charaktere mit diesen Allianzen in ihrer Vergangenheit erhalten den Alumni-"
36
+ "Status"
37
+
38
+ #: alumni/models.py:21 alumni/models.py:24 alumni/models.py:25
39
+ msgid "Alumni Config"
40
+ msgstr "Alumni-Konfiguration"
41
+
42
+ #: alumni/models.py:37 alumni/models.py:52
43
+ msgid "True if the corporation has been deleted"
44
+ msgstr "Wahr, wenn die Corporation gelöscht wurde"
45
+
46
+ #: alumni/models.py:39 alumni/models.py:54
47
+ msgid ""
48
+ "An incrementing ID that can be used to canonically establish order of "
49
+ "records in cases where dates may be ambiguous"
50
+ msgstr ""
51
+ "Eine aufsteigende ID, die zur kanonischen Festlegung der Reihenfolge von "
52
+ "Datensätzen in Fällen verwendet werden kann, in denen Daten möglicherweise "
53
+ "nicht eindeutig sind"
Binary file
@@ -0,0 +1,43 @@
1
+ # SOME DESCRIPTIVE TITLE.
2
+ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+ # This file is distributed under the same license as the PACKAGE package.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PACKAGE VERSION\n"
10
+ "Report-Msgid-Bugs-To: \n"
11
+ "POT-Creation-Date: 2024-08-29 14:59+1000\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "Language: \n"
16
+ "MIME-Version: 1.0\n"
17
+ "Content-Type: text/plain; charset=UTF-8\n"
18
+ "Content-Transfer-Encoding: 8bit\n"
19
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
20
+
21
+ #: alumni/models.py:15
22
+ msgid ""
23
+ "Characters with these Corps in their History will be given Alumni Status"
24
+ msgstr ""
25
+
26
+ #: alumni/models.py:19
27
+ msgid ""
28
+ "Characters with these Alliances in their History will be given Alumni Status"
29
+ msgstr ""
30
+
31
+ #: alumni/models.py:22 alumni/models.py:25 alumni/models.py:26
32
+ msgid "Alumni Config"
33
+ msgstr ""
34
+
35
+ #: alumni/models.py:38 alumni/models.py:53
36
+ msgid "True if the corporation has been deleted"
37
+ msgstr ""
38
+
39
+ #: alumni/models.py:40 alumni/models.py:55
40
+ msgid ""
41
+ "An incrementing ID that can be used to canonically establish order of "
42
+ "records in cases where dates may be ambiguous"
43
+ msgstr ""
File without changes
@@ -0,0 +1,24 @@
1
+ from django.core.management.base import BaseCommand
2
+
3
+ from allianceauth.authentication.models import State
4
+
5
+ from alumni import app_settings
6
+
7
+
8
+ class Command(BaseCommand):
9
+ help = 'Setup/Reset/Fix the Alumni State for the Alumni Module'
10
+
11
+ def handle(self, *args, **options):
12
+ self.stdout.write("Creating/Reseting/Fixing the Alumni State for the Alumni Module")
13
+ alumni_name = app_settings.ALUMNI_STATE_NAME
14
+ priority = 1
15
+
16
+ created = State.objects.update_or_create(
17
+ name=alumni_name,
18
+ defaults={
19
+ "priority": priority, }
20
+ )
21
+ if created:
22
+ self.stdout.write("Success! Created Alumni State")
23
+ else:
24
+ self.stdout.write("Success! Updated Alumni State")
@@ -0,0 +1,69 @@
1
+ # Generated by Django 3.2.10 on 2021-12-29 11:41
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ('eveonline', '0015_factions'),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name='AlumniSetup',
18
+ fields=[
19
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20
+ ],
21
+ options={
22
+ 'verbose_name_plural': 'Alumni Config',
23
+ },
24
+ ),
25
+ migrations.CreateModel(
26
+ name='CharacterCorporationHistory',
27
+ fields=[
28
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29
+ ('corporation_id', models.PositiveIntegerField()),
30
+ ('is_deleted', models.BooleanField(default=False, help_text='True if the corporation has been deleted')),
31
+ ('record_id', models.IntegerField(help_text='An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous')),
32
+ ('start_date', models.DateTimeField()),
33
+ ],
34
+ ),
35
+ migrations.CreateModel(
36
+ name='CorporationAllianceHistory',
37
+ fields=[
38
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
39
+ ('corporation_id', models.PositiveIntegerField()),
40
+ ('alliance_id', models.PositiveIntegerField(blank=True, null=True)),
41
+ ('is_deleted', models.BooleanField(default=False, help_text='True if the corporation has been deleted')),
42
+ ('record_id', models.IntegerField(help_text='An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous')),
43
+ ('start_date', models.DateTimeField()),
44
+ ],
45
+ ),
46
+ migrations.AddConstraint(
47
+ model_name='corporationalliancehistory',
48
+ constraint=models.UniqueConstraint(fields=('corporation_id', 'record_id'), name='CorporationAllianceRecord'),
49
+ ),
50
+ migrations.AddField(
51
+ model_name='charactercorporationhistory',
52
+ name='character',
53
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecharacter'),
54
+ ),
55
+ migrations.AddField(
56
+ model_name='alumnisetup',
57
+ name='alumni_alliances',
58
+ field=models.ManyToManyField(blank=True, help_text='Characters with these Alliances in their History will be given Alumni Status', to='eveonline.EveAllianceInfo'),
59
+ ),
60
+ migrations.AddField(
61
+ model_name='alumnisetup',
62
+ name='alumni_corporations',
63
+ field=models.ManyToManyField(blank=True, help_text='Characters with these Corps in their History will be given Alumni Status', to='eveonline.EveCorporationInfo'),
64
+ ),
65
+ migrations.AddConstraint(
66
+ model_name='charactercorporationhistory',
67
+ constraint=models.UniqueConstraint(fields=('character', 'record_id'), name='CharacterCorporationRecord'),
68
+ ),
69
+ ]
@@ -0,0 +1,23 @@
1
+ # Generated by Django 3.2.10 on 2021-12-30 01:47
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('alumni', '0001_initial'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='corporationalliancehistory',
15
+ name='alliance_id',
16
+ field=models.PositiveIntegerField(blank=True, db_index=True, null=True),
17
+ ),
18
+ migrations.AlterField(
19
+ model_name='corporationalliancehistory',
20
+ name='corporation_id',
21
+ field=models.PositiveIntegerField(db_index=True),
22
+ ),
23
+ ]
@@ -0,0 +1,27 @@
1
+ # Generated by Django 4.2.10 on 2024-05-11 08:13
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('alumni', '0002_auto_20211230_0147'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterModelOptions(
14
+ name='alumnisetup',
15
+ options={'verbose_name': 'Alumni Config', 'verbose_name_plural': 'Alumni Config'},
16
+ ),
17
+ migrations.AlterField(
18
+ model_name='charactercorporationhistory',
19
+ name='record_id',
20
+ field=models.PositiveIntegerField(help_text='An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous'),
21
+ ),
22
+ migrations.AlterField(
23
+ model_name='corporationalliancehistory',
24
+ name='record_id',
25
+ field=models.PositiveIntegerField(help_text='An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous'),
26
+ ),
27
+ ]
@@ -0,0 +1,51 @@
1
+ # Generated by Django 4.2.26 on 2025-11-14 03:28
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('eveonline', '0017_alliance_and_corp_names_are_not_unique'),
11
+ ('alumni', '0003_alter_alumnisetup_options_and_more'),
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='CorporationUpdateTimestamp',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('corporation_id', models.PositiveIntegerField(unique=True)),
20
+ ('last_updated', models.DateTimeField(auto_now=True)),
21
+ ],
22
+ options={
23
+ 'verbose_name': 'Corporation Update Timestamp',
24
+ 'verbose_name_plural': 'Corporation Update Timestamps',
25
+ },
26
+ ),
27
+ migrations.AlterModelOptions(
28
+ name='alumnisetup',
29
+ options={'verbose_name': 'Alumni Config'},
30
+ ),
31
+ migrations.AlterModelOptions(
32
+ name='charactercorporationhistory',
33
+ options={'verbose_name': 'Character/Corporation History', 'verbose_name_plural': 'Character/Corporation Histories'},
34
+ ),
35
+ migrations.AlterModelOptions(
36
+ name='corporationalliancehistory',
37
+ options={'verbose_name': 'Corporation/Alliance History', 'verbose_name_plural': 'Corporation/Alliance Histories'},
38
+ ),
39
+ migrations.CreateModel(
40
+ name='CharacterUpdateTimestamp',
41
+ fields=[
42
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43
+ ('last_updated', models.DateTimeField(auto_now=True)),
44
+ ('character', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='eveonline.evecharacter')),
45
+ ],
46
+ options={
47
+ 'verbose_name': 'Character Update Timestamp',
48
+ 'verbose_name_plural': 'Character Update Timestamps',
49
+ },
50
+ ),
51
+ ]
File without changes
alumni/models.py ADDED
@@ -0,0 +1,80 @@
1
+ from solo.models import SingletonModel
2
+
3
+ from django.db import models
4
+ from django.utils.translation import gettext as _
5
+
6
+ from allianceauth.eveonline.models import (
7
+ EveAllianceInfo, EveCharacter, EveCorporationInfo,
8
+ )
9
+
10
+
11
+ class AlumniSetup(SingletonModel):
12
+ alumni_corporations = models.ManyToManyField(
13
+ EveCorporationInfo,
14
+ blank=True,
15
+ help_text=_("Characters with these Corps in their History will be given Alumni Status"))
16
+ alumni_alliances = models.ManyToManyField(
17
+ EveAllianceInfo,
18
+ blank=True,
19
+ help_text=_("Characters with these Alliances in their History will be given Alumni Status"))
20
+
21
+ def __str__(self) -> str:
22
+ return _("Alumni Config")
23
+
24
+ class Meta:
25
+ verbose_name = _("Alumni Config")
26
+
27
+
28
+ class CorporationUpdateTimestamp(models.Model):
29
+ corporation_id = models.PositiveIntegerField(unique=True)
30
+ last_updated = models.DateTimeField(auto_now=True)
31
+
32
+ class Meta:
33
+ verbose_name = _("Corporation Update Timestamp")
34
+ verbose_name_plural = _("Corporation Update Timestamps")
35
+
36
+
37
+ class CharacterUpdateTimestamp(models.Model):
38
+ character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE)
39
+ last_updated = models.DateTimeField(auto_now=True)
40
+
41
+ class Meta:
42
+ verbose_name = _("Character Update Timestamp")
43
+ verbose_name_plural = _("Character Update Timestamps")
44
+
45
+
46
+ class CorporationAllianceHistory(models.Model):
47
+ corporation_id = models.PositiveIntegerField(db_index=True)
48
+ alliance_id = models.PositiveIntegerField(blank=True, null=True, db_index=True)
49
+ is_deleted = models.BooleanField(
50
+ default=False,
51
+ help_text=_("True if the corporation has been deleted"))
52
+ record_id = models.PositiveIntegerField(
53
+ help_text=_("An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous"))
54
+ start_date = models.DateTimeField()
55
+
56
+ class Meta:
57
+ verbose_name = _("Corporation/Alliance History")
58
+ verbose_name_plural = _("Corporation/Alliance Histories")
59
+ constraints = [
60
+ models.UniqueConstraint(fields=['corporation_id', 'record_id'], name="CorporationAllianceRecord"),
61
+ ]
62
+
63
+
64
+ class CharacterCorporationHistory(models.Model):
65
+
66
+ character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE)
67
+ corporation_id = models.PositiveIntegerField()
68
+ is_deleted = models.BooleanField(
69
+ default=False,
70
+ help_text=_("True if the corporation has been deleted"))
71
+ record_id = models.PositiveIntegerField(
72
+ help_text=_("An incrementing ID that can be used to canonically establish order of records in cases where dates may be ambiguous"))
73
+ start_date = models.DateTimeField()
74
+
75
+ class Meta:
76
+ verbose_name = _("Character/Corporation History")
77
+ verbose_name_plural = _("Character/Corporation Histories")
78
+ constraints = [
79
+ models.UniqueConstraint(fields=['character', 'record_id'], name="CharacterCorporationRecord"),
80
+ ]
alumni/providers.py ADDED
@@ -0,0 +1,38 @@
1
+ import typing
2
+
3
+ from allianceauth.services.hooks import get_extension_logger
4
+ from esi.openapi_clients import ESIClientProvider
5
+
6
+ from . import __esi_compatibility_date__, __title__, __url__, __version__
7
+
8
+ if typing.TYPE_CHECKING:
9
+ from esi.stubs import (
10
+ CharacterID, CharactersCharacterIdCorporationhistoryGetItem,
11
+ CorporationID, CorporationsCorporationIdAlliancehistoryGetItem,
12
+ )
13
+
14
+ esi = ESIClientProvider(
15
+ compatibility_date=__esi_compatibility_date__,
16
+ ua_appname=__title__,
17
+ ua_version=__version__,
18
+ ua_url=__url__,
19
+ operations=[
20
+ "GetCorporationsCorporationIdAlliancehistory",
21
+ "GetCharactersCharacterIdCorporationhistory"]
22
+ )
23
+
24
+ logger = get_extension_logger(__name__)
25
+
26
+
27
+ def get_corporations_corporation_id_alliancehistory(corporation_id: "CorporationID") -> list["CorporationsCorporationIdAlliancehistoryGetItem"]:
28
+ result = esi.client.Corporation.GetCorporationsCorporationIdAlliancehistory(
29
+ corporation_id=corporation_id
30
+ ).results()
31
+ return result
32
+
33
+
34
+ def get_characters_character_id_corporationhistory(character_id: "CharacterID") -> list["CharactersCharacterIdCorporationhistoryGetItem"]:
35
+ result = esi.client.Character.GetCharactersCharacterIdCorporationhistory(
36
+ character_id=character_id
37
+ ).results()
38
+ return result
alumni/signals.py ADDED
@@ -0,0 +1,24 @@
1
+ import logging
2
+
3
+ from django.db.models.signals import post_save
4
+ from django.dispatch import receiver
5
+
6
+ from allianceauth.eveonline.models import EveCharacter
7
+
8
+ from alumni.models import AlumniSetup
9
+ from alumni.tasks import alumni_check_character, run_alumni_check_all
10
+
11
+ from .app_settings import ALUMNI_TASK_PRIORITY
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @receiver(post_save, sender=AlumniSetup)
17
+ def alumni_was_updated(sender, instance: AlumniSetup, *args, **kwargs) -> None:
18
+ run_alumni_check_all.apply_async(priority=ALUMNI_TASK_PRIORITY) # Type ignore
19
+
20
+
21
+ @receiver(post_save, sender=EveCharacter)
22
+ def character_added(sender, instance: EveCharacter, created, *args, **kwargs) -> None:
23
+ if created is True:
24
+ alumni_check_character.apply_async(args=[instance.character_id], priority=ALUMNI_TASK_PRIORITY - 1) # Type ignore
alumni/tasks.py ADDED
@@ -0,0 +1,288 @@
1
+ import typing
2
+ from datetime import datetime, timezone
3
+ from math import ceil
4
+ from random import randint
5
+
6
+ from celery import shared_task
7
+
8
+ from allianceauth.authentication.models import State
9
+ from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo
10
+ from allianceauth.services.hooks import get_extension_logger
11
+ from esi.decorators import esi_rate_limiter_bucketed
12
+ from esi.exceptions import (
13
+ ESIBucketLimitException, ESIErrorLimitException, HTTPNotModified,
14
+ )
15
+ from esi.rate_limiting import ESIRateLimitBucket
16
+
17
+ from .app_settings import (
18
+ ALUMNI_CHARACTERCORPORATION_RATELIMIT, ALUMNI_STATE_NAME,
19
+ ALUMNI_TASK_JITTER, ALUMNI_TASK_PRIORITY,
20
+ )
21
+ from .models import (
22
+ AlumniSetup, CharacterCorporationHistory, CharacterUpdateTimestamp,
23
+ CorporationAllianceHistory, CorporationUpdateTimestamp,
24
+ )
25
+ from .providers import (
26
+ get_characters_character_id_corporationhistory,
27
+ get_corporations_corporation_id_alliancehistory,
28
+ )
29
+
30
+ if typing.TYPE_CHECKING:
31
+ from esi.stubs import AllianceID, CharacterID, CorporationID
32
+
33
+ logger = get_extension_logger(__name__)
34
+
35
+
36
+ def char_alliance_datecompare(alliance_id: "AllianceID", character_id: "CharacterID") -> bool:
37
+ """Voodoo relating to checking start dates and _next_ start dates
38
+
39
+ Necessary to determine if a character was a member of a corp
40
+ WHILE it was in an alliance
41
+
42
+ Parameters
43
+ ----------
44
+ alliance_id: int
45
+ Should match an existing EveAllianceInfo model
46
+
47
+ character_id: int
48
+ Should match an existing EveCharacter model
49
+
50
+ Returns
51
+ -------
52
+ bool
53
+ Whether True"""
54
+
55
+ character = EveCharacter.objects.get(character_id=character_id)
56
+ char_corp_history = CharacterCorporationHistory.objects.filter(
57
+ character=character).order_by('record_id')
58
+
59
+ for index, char_corp_record in enumerate(char_corp_history):
60
+ # Corp Joins Alliance, Between Char Join/Leave Corp
61
+ try:
62
+ filter_end_date = char_corp_history[index + 1].start_date
63
+ except IndexError:
64
+ filter_end_date = datetime.now(timezone.utc)
65
+
66
+ if CorporationAllianceHistory.objects.filter(
67
+ corporation_id=char_corp_record.corporation_id,
68
+ alliance_id=alliance_id,
69
+ start_date__range=(
70
+ char_corp_record.start_date,
71
+ filter_end_date)).exists() is True:
72
+ return True
73
+
74
+ corp_alliance_history = CorporationAllianceHistory.objects.filter(
75
+ corporation_id=char_corp_record.corporation_id).order_by('record_id')
76
+
77
+ for index_2, corp_alliance_record in enumerate(corp_alliance_history):
78
+ # Needs to be unfiltered alliance id because we need _next_ start date
79
+ # but check if the alliance id matches before we run any logic
80
+ try:
81
+ if corp_alliance_record.alliance_id == alliance_id:
82
+ # Char Joins Corp, Between Corp Join/Leave
83
+ if corp_alliance_record.start_date < char_corp_record.start_date < corp_alliance_history[index_2 + 1].start_date:
84
+ return True
85
+ # Char Leaves Corp, Between Corp Join/Leave
86
+ elif corp_alliance_record.start_date < char_corp_history[index + 1].start_date < corp_alliance_history[index_2 + 1].start_date:
87
+ return True
88
+ # Corp Leaves Alliance in between Char Join/Leave Corp
89
+ elif char_corp_record.start_date < corp_alliance_history[index_2 + 1].start_date < char_corp_history[index + 1].start_date:
90
+ return True
91
+ else:
92
+ pass
93
+ else:
94
+ pass
95
+ except Exception as e:
96
+ # Need to actually add some IndexError handling to above tasks, but lets log this gracefully so as not to cactus up the whole thing.
97
+ logger.exception(e)
98
+ return False
99
+
100
+
101
+ @shared_task
102
+ def alumni_check_character(character_id: "CharacterID") -> bool:
103
+ """Check/Update a characters alumni status using the historical models
104
+
105
+ Parameters
106
+ ----------
107
+ character_id: int
108
+ Should match an existing EveCharacter model
109
+
110
+ Returns
111
+ -------
112
+ bool
113
+ Whether the user is an alumni or not **it is updated in this function as well**"""
114
+
115
+ alumni_setup = AlumniSetup.get_solo()
116
+ alumni_state = State.objects.get(name=ALUMNI_STATE_NAME)
117
+ character = EveCharacter.objects.get(character_id=character_id)
118
+
119
+ if character.corporation_id in alumni_setup.alumni_corporations.values_list('corporation_id', flat=True):
120
+ # Cheapo cop-out to end early
121
+ alumni_state.member_characters.add(character)
122
+ return True
123
+
124
+ if character.alliance_id in alumni_setup.alumni_alliances.values_list('alliance_id', flat=True):
125
+ # Cheapo cop-out to end early
126
+ alumni_state.member_characters.add(character)
127
+ return True
128
+
129
+ for char_corp in CharacterCorporationHistory.objects.filter(character=character):
130
+ if char_corp.corporation_id in alumni_setup.alumni_corporations.values_list('corporation_id', flat=True):
131
+ # Less Cheap, but ending here is still better than the next one.
132
+ alumni_state.member_characters.add(character)
133
+ return True
134
+
135
+ for alliance in alumni_setup.alumni_alliances.all():
136
+ if char_alliance_datecompare(alliance_id=alliance.alliance_id, character_id=character_id):
137
+ alumni_state.member_characters.add(character)
138
+ return True
139
+
140
+ # If we reach this point, we aren't an alumni
141
+ alumni_state.member_characters.remove(character)
142
+ return False
143
+
144
+
145
+ @shared_task
146
+ def run_alumni_check_all() -> None:
147
+ for character in EveCharacter.objects.all().values('character_id'):
148
+ alumni_check_character.apply_async(
149
+ args=[character['character_id']],
150
+ priority=ALUMNI_TASK_PRIORITY
151
+ ) # pyright: ignore[reportFunctionMemberAccess, reportCallIssue]
152
+
153
+
154
+ @shared_task(bind=True, rate_limit=ALUMNI_CHARACTERCORPORATION_RATELIMIT)
155
+ def update_corporationalliancehistory(self, corporation_id: "CorporationID") -> None:
156
+ """Update CorporationAllianceHistory models from ESI
157
+
158
+ Parameters
159
+ ----------
160
+ corporation_id: int """
161
+
162
+ if corporation_id <= 98000000: # NPC Corps don't have CCH
163
+ CorporationUpdateTimestamp.objects.update_or_create(
164
+ corporation_id=corporation_id,
165
+ defaults={'last_updated': datetime.now(timezone.utc)}
166
+ )
167
+
168
+ try:
169
+ for cah in get_corporations_corporation_id_alliancehistory(corporation_id):
170
+ CorporationAllianceHistory.objects.update_or_create(
171
+ record_id=cah.record_id,
172
+ corporation_id=corporation_id,
173
+ alliance_id=cah.alliance_id,
174
+ defaults={
175
+ 'is_deleted': True if cah.is_deleted == 'true' else False,
176
+ 'start_date': cah.start_date # This can get adjusted by CCP i think
177
+ }
178
+ )
179
+ CorporationUpdateTimestamp.objects.update_or_create(
180
+ corporation_id=corporation_id,
181
+ defaults={'last_updated': datetime.now(timezone.utc)}
182
+ )
183
+ except (ESIErrorLimitException, ESIBucketLimitException) as e:
184
+ raise self.retry(exc=e, countdown=e.reset + 1 if e.reset is not None else 61, max_retries=3)
185
+ except HTTPNotModified:
186
+ CorporationUpdateTimestamp.objects.update_or_create(
187
+ corporation_id=corporation_id,
188
+ defaults={'last_updated': datetime.now(timezone.utc)}
189
+ )
190
+ except Exception as e:
191
+ logger.exception(e)
192
+
193
+
194
+ @shared_task(bind=True, rate_limit=ALUMNI_CHARACTERCORPORATION_RATELIMIT)
195
+ @esi_rate_limiter_bucketed(bucket=ESIRateLimitBucket(*ESIRateLimitBucket.CHARACTER_CORPORATION_HISTORY))
196
+ def update_charactercorporationhistory(self, character_id: "CharacterID") -> None:
197
+ """Update CharacterCorporationHistory models from ESI
198
+
199
+ Parameters
200
+ ----------
201
+ character_id: int
202
+ Should match an existing EveCharacter model"""
203
+
204
+ try:
205
+ character = EveCharacter.objects.get(character_id=character_id)
206
+ except Exception as e:
207
+ logger.exception(e)
208
+ return
209
+ if character.character_id <= 90000000: # NPC Characters don't have CCH
210
+ CharacterUpdateTimestamp.objects.update_or_create(
211
+ character=character,
212
+ defaults={'last_updated': datetime.now(timezone.utc)}
213
+ )
214
+ try:
215
+ for cch in get_characters_character_id_corporationhistory(character_id):
216
+ CharacterCorporationHistory.objects.update_or_create(
217
+ character=character,
218
+ corporation_id=cch.corporation_id,
219
+ record_id=cch.record_id,
220
+ defaults={
221
+ 'is_deleted': True if cch.is_deleted == 'true' else False,
222
+ 'start_date': cch.start_date
223
+ }
224
+ )
225
+ CharacterUpdateTimestamp.objects.update_or_create(
226
+ character=character,
227
+ defaults={'last_updated': datetime.now(timezone.utc)}
228
+ )
229
+ except (ESIErrorLimitException, ESIBucketLimitException) as e:
230
+ raise self.retry(exc=e, countdown=e.reset + 1 if e.reset is not None else 61)
231
+ except HTTPNotModified:
232
+ CharacterUpdateTimestamp.objects.update_or_create(
233
+ character=character,
234
+ defaults={'last_updated': datetime.now(timezone.utc)}
235
+ )
236
+ except Exception as e:
237
+ logger.exception(e)
238
+
239
+
240
+ @shared_task
241
+ def update_models_subset(fraction: int = 14) -> None:
242
+ """
243
+ Update a subset of the CharacterCorporation history models from ESI.
244
+
245
+ This task operates on 1/fraction of the oldest CCH and CAH records.
246
+
247
+ At 1/14th, once a day this will update all Alumni records in two weeks.
248
+ """
249
+
250
+ # Force create all timestamps at zero if they don't exist
251
+ character_timestamps = [
252
+ CharacterUpdateTimestamp(
253
+ character_id=character.id,
254
+ last_updated=datetime.fromtimestamp(1, timezone.utc)) for character in EveCharacter.objects.all()]
255
+ CharacterUpdateTimestamp.objects.bulk_create(character_timestamps, ignore_conflicts=True, batch_size=500)
256
+
257
+ corporation_timestamps = [
258
+ CorporationUpdateTimestamp(
259
+ corporation_id=corp.corporation_id,
260
+ last_updated=datetime.fromtimestamp(1, timezone.utc)) for corp in EveCorporationInfo.objects.all()]
261
+ corporation_timestamps += [
262
+ CorporationUpdateTimestamp(
263
+ corporation_id=corp["corporation_id"],
264
+ last_updated=datetime.fromtimestamp(1, timezone.utc)) for corp in CharacterCorporationHistory.objects.values('corporation_id').distinct()]
265
+ CorporationUpdateTimestamp.objects.bulk_create(corporation_timestamps, ignore_conflicts=True, batch_size=500)
266
+
267
+ for character in CharacterUpdateTimestamp.objects.all(
268
+ ).order_by('last_updated')[:ceil(CharacterUpdateTimestamp.objects.count() / fraction)].values('character__character_id'):
269
+ update_charactercorporationhistory.apply_async(
270
+ args=[character['character__character_id']],
271
+ priority=ALUMNI_TASK_PRIORITY,
272
+ countdown=randint(1, ALUMNI_TASK_JITTER)
273
+ ) # pyright: ignore[reportFunctionMemberAccess, reportCallIssue]
274
+
275
+ for char_corp_record in CorporationUpdateTimestamp.objects.all(
276
+ ).order_by('last_updated')[:ceil(CorporationUpdateTimestamp.objects.count() / fraction)].values('corporation_id'):
277
+ update_corporationalliancehistory.apply_async(
278
+ args=[char_corp_record['corporation_id']],
279
+ priority=ALUMNI_TASK_PRIORITY,
280
+ countdown=randint(1, ALUMNI_TASK_JITTER)
281
+ ) # pyright: ignore[reportFunctionMemberAccess, reportCallIssue]
282
+
283
+
284
+ @shared_task()
285
+ def update_all_models() -> None:
286
+ """Update All CharacterCorporation history models from ESI"""
287
+
288
+ update_models_subset.apply_async(priority=ALUMNI_TASK_PRIORITY) # pyright: ignore[reportFunctionMemberAccess, reportCallIssue]
File without changes