Source code for allianceauth.eveonline.models

import logging
from datetime import datetime, timezone
from typing import TYPE_CHECKING, ClassVar

from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.translation import gettext_lazy as _

from esi.exceptions import HTTPNotModified
from esi.models import Token

if TYPE_CHECKING:
    from allianceauth.authentication.models import CharacterOwnership
    from esi.stubs import CorporationID

from allianceauth.eveonline.evelinks import eveimageserver
from allianceauth.eveonline.managers import (
    EveAllianceManager, EveCharacterManager, EveCorporationManager,
    EveFactionManager,
)
from allianceauth.eveonline.providers import open_api_provider
from allianceauth.notifications import notify

logger = logging.getLogger(__name__)

_DEFAULT_IMAGE_SIZE = 32
DOOMHEIM_CORPORATION_ID = 1000001


[docs] class EveFactionInfo(models.Model): """A faction in Eve Online.""" # There exists some soft FKs in this model as otherwise the Corp<>Faction relation is circular. # This cannot be fully resolved with reverse fields, as militia things? faction_id = models.PositiveIntegerField(unique=True, db_index=True) corporation_id = models.PositiveIntegerField(unique=True, blank=True, null=True) # Soft FK description = models.TextField(blank=True, default="") # is_unique boolean required # skipped militia_corporation_id = models.PositiveIntegerField(unique=True, blank=True, null=True) # Soft FK faction_name = models.CharField(max_length=254, unique=True) size_factor = models.PositiveSmallIntegerField(blank=True, null=True, default=None) solar_system_id = models.PositiveIntegerField(blank=True, null=True, default=None) station_count = models.PositiveIntegerField(blank=True, null=True, default=None) station_system_count = models.PositiveIntegerField(blank=True, null=True, default=None) last_updated = models.DateTimeField( default=None, blank=True, null=True, help_text="Last time the faction's details were updated, (GetUniverseFactions), 24 hr Cache") objects: ClassVar[EveFactionManager] = EveFactionManager() # pyright: ignore[reportIncompatibleVariableOverride] class Meta: default_permissions = () verbose_name = _("Faction") verbose_name_plural = _("Factions") indexes = [ models.Index(fields=['corporation_id',]), models.Index(fields=['militia_corporation_id',]), models.Index(fields=['faction_name',]) ] def __str__(self) -> str: return self.faction_name
[docs] @staticmethod def generic_logo_url(faction_id: int, size: int = _DEFAULT_IMAGE_SIZE) -> str: """image URL for the given faction ID""" return eveimageserver.corporation_logo_url(faction_id, size)
[docs] def logo_url(self, size: int = _DEFAULT_IMAGE_SIZE) -> str: """image URL of this faction""" return self.generic_logo_url(self.faction_id, size)
@property def logo_url_32(self) -> str: """image URL for this faction""" return self.logo_url(32) @property def logo_url_64(self) -> str: """image URL for this faction""" return self.logo_url(64) @property def logo_url_128(self) -> str: """image URL for this faction""" return self.logo_url(128) @property def logo_url_256(self) -> str: """image URL for this faction""" return self.logo_url(256) @property def name(self) -> str: # This is the literal ESI field return self.faction_name @property def militia_corporation(self) -> "EveCorporationInfo | None": """Return militia corporation of this faction or None if not found.""" try: return EveCorporationInfo.objects.get(corporation_id=self.militia_corporation_id) except EveCorporationInfo.DoesNotExist: return None @property def corporation(self) -> "EveCorporationInfo | None": """Return corporation of this faction or None if not found.""" try: return EveCorporationInfo.objects.get(corporation_id=self.corporation_id) except EveCorporationInfo.DoesNotExist: return None
[docs] class EveAllianceInfo(models.Model): """An alliance in Eve Online.""" # There exists some soft FKs in this model as otherwise the Corp<>Alliance relation is circular. # Executor and Creator are not reverse fields on Corp. alliance_id = models.PositiveIntegerField(unique=True) creator_corporation_id = models.PositiveIntegerField( # Soft FK help_text="Alliance's creator corporation ID", blank=True, null=True, default=None) creator_id = models.PositiveIntegerField( help_text="Alliance's creator ID", blank=True, null=True, default=None) date_founded = models.DateField( help_text="Alliance's founding date", blank=True, null=True, default=None) executor_corp_id = models.PositiveIntegerField( # Soft FK help_text="Alliance's executor corporation ID", blank=True, null=True, default=None) faction = models.ForeignKey( "EveFactionInfo", verbose_name=_("Faction"), related_name="alliance", help_text="Alliance's faction ID", on_delete=models.SET_NULL, blank=True, null=True, default=None) alliance_name = models.CharField( help_text="Alliance's name", max_length=254) alliance_ticker = models.CharField( help_text="Alliance's ticker", max_length=254) last_updated = models.DateTimeField( default=None, blank=True, null=True, help_text="Last time the alliance's details were updated, (GetAlliancesAllianceId), 1 hr Cache") objects: ClassVar[EveAllianceManager] = EveAllianceManager() # pyright: ignore[reportIncompatibleVariableOverride] class Meta: default_permissions = () verbose_name = _("alliance") verbose_name_plural = _("alliances") indexes = [ models.Index(fields=['creator_corporation_id',]), models.Index(fields=['alliance_name',]), models.Index(fields=['executor_corp_id',]), ] def __str__(self) -> str: return self.alliance_name
[docs] @staticmethod def generic_logo_url( alliance_id: int, size: int = _DEFAULT_IMAGE_SIZE ) -> str: """image URL for the given alliance ID""" return eveimageserver.alliance_logo_url(alliance_id, size)
[docs] def logo_url(self, size: int = _DEFAULT_IMAGE_SIZE) -> str: """image URL of this alliance""" return self.generic_logo_url(self.alliance_id, size)
@property def logo_url_32(self) -> str: """image URL for this alliance""" return self.logo_url(32) @property def logo_url_64(self) -> str: """image URL for this alliance""" return self.logo_url(64) @property def logo_url_128(self) -> str: """image URL for this alliance""" return self.logo_url(128) @property def logo_url_256(self) -> str: """image URL for this alliance""" return self.logo_url(256) @property def creator_corporation(self) -> "EveCorporationInfo | None": """Return creator corporation of this alliance or None if not found.""" try: return EveCorporationInfo.objects.get(corporation_id=self.creator_corporation_id) except EveCorporationInfo.DoesNotExist: return None @property def name(self) -> str: # This is the literal ESI field return self.alliance_name @property def ticker(self) -> str: # This is the literal ESI field return self.alliance_ticker @property def executor_corporation(self) -> "EveCorporationInfo | None": """Return executor corporation of this alliance or None if not found.""" if self.executor_corp_id is None: return None try: return EveCorporationInfo.objects.get(corporation_id=self.executor_corp_id) except EveCorporationInfo.DoesNotExist: return None @property def executor_corporation_id(self) -> "CorporationID | None": """Return executor corporation of this alliance or None if not found.""" return self.executor_corp_id if self.executor_corp_id else None def populate_alliance(self, force_refresh: bool = False) -> "EveAllianceInfo": try: corp_ids = open_api_provider.get_alliance_corps( self.alliance_id, use_etag=not force_refresh, force_refresh=force_refresh) except HTTPNotModified: # nothing to update return self for corp_id in corp_ids: if not EveCorporationInfo.objects.filter(corporation_id=corp_id).exists(): EveCorporationInfo.objects.create_corporation(corporation_id=corp_id) EveCorporationInfo.objects.filter( corporation_id__in=corp_ids ).update( alliance=self ) EveCorporationInfo.objects.filter(alliance=self).exclude( corporation_id__in=corp_ids ).update( alliance=None ) return self def update_alliance(self, force_refresh: bool = False) -> "EveAllianceInfo": try: alliance, response = open_api_provider.get_alliance( alliance_id=self.alliance_id, last_modified=self.last_updated if not force_refresh else None, use_etag=not force_refresh, force_refresh=force_refresh) except HTTPNotModified: # nothing to update return self if alliance.faction_id: try: self.faction = EveFactionInfo.objects.get(faction_id=alliance.faction_id) except EveFactionInfo.DoesNotExist: self.faction = EveFactionInfo.objects.create_faction(faction_id=alliance.faction_id) else: self.faction = None self.creator_corporation_id = alliance.creator_corporation_id if alliance.creator_corporation_id else None self.creator_id = alliance.creator_id if alliance.creator_id else None self.date_founded = alliance.date_founded self.executor_corp_id = alliance.executor_corporation_id if alliance.executor_corporation_id else None self.alliance_name = alliance.name self.alliance_ticker = alliance.ticker self.last_updated = datetime.strptime(response.headers.get("Last-Modified"), "%a, %d %b %Y %H:%M:%S GMT").replace(tzinfo=timezone.utc) self.save() return self
[docs] class EveCorporationInfo(models.Model): """A corporation in Eve Online.""" corporation_id = models.PositiveIntegerField(unique=True) alliance = models.ForeignKey( EveAllianceInfo, blank=True, null=True, on_delete=models.SET_NULL ) ceo_id = models.PositiveIntegerField( # CEO_ID = 1 for closed corps help_text="Corporation's CEO ID", blank=True, null=True, default=None) creator_id = models.PositiveIntegerField( help_text="Corporation's creator ID", blank=True, null=True, default=None) date_founded = models.DateField( help_text="Corporation's founding date", blank=True, null=True, default=None) description = models.TextField( help_text="Corporation's description", blank=True, default="") faction = models.ForeignKey( "EveFactionInfo", blank=True, null=True, on_delete=models.SET_NULL) home_station_id = models.PositiveIntegerField( help_text="Corporation's home station ID", blank=True, null=True, default=None) member_count = models.PositiveIntegerField( help_text="Corporation's member count", blank=True, null=True, default=None) corporation_name = models.CharField( help_text="Corporation's name", max_length=254) shares = models.PositiveBigIntegerField( help_text="Corporation's shares", blank=True, null=True, default=None) tax_rate = models.FloatField( help_text="Corporation's tax rate", blank=True, null=True, default=None) corporation_ticker = models.CharField( help_text="Corporation's short name", max_length=254) url = models.URLField( help_text="Corporation's URL", max_length=2048, blank=True, default="") war_eligible = models.BooleanField( help_text="Corporation's war eligibility", blank=True, null=True, default=None) last_updated = models.DateTimeField( default=None, blank=True, null=True, help_text="Last time the corporation's details were updated, (GetCorporationsCorporationId), 1 hr Cache") objects: ClassVar[EveCorporationManager] = EveCorporationManager() # pyright: ignore[reportIncompatibleVariableOverride] class Meta: indexes = [ models.Index(fields=['ceo_id',]), models.Index(fields=['corporation_name',]), ] verbose_name = _("corporation") verbose_name_plural = _("corporations") def __str__(self) -> str: return self.corporation_name
[docs] @staticmethod def generic_logo_url( corporation_id: int, size: int = _DEFAULT_IMAGE_SIZE ) -> str: """image URL for the given corporation ID""" return eveimageserver.corporation_logo_url(corporation_id, size)
[docs] def logo_url(self, size: int = _DEFAULT_IMAGE_SIZE) -> str: """image URL for this corporation""" return self.generic_logo_url(self.corporation_id, size)
@property def logo_url_32(self) -> str: """image URL for this corporation""" return self.logo_url(32) @property def logo_url_64(self) -> str: """image URL for this corporation""" return self.logo_url(64) @property def logo_url_128(self) -> str: """image URL for this corporation""" return self.logo_url(128) @property def logo_url_256(self) -> str: """image URL for this corporation""" return self.logo_url(256) @property def name(self) -> str: # This is the literal ESI field return self.corporation_name @property def ticker(self) -> str: # This is the literal ESI field return self.corporation_ticker def update_corporation(self, force_refresh: bool = False) -> "EveCorporationInfo": try: corporation, response = open_api_provider.get_corporation( corporation_id=self.corporation_id, last_modified=self.last_updated if not force_refresh else None, use_etag=not force_refresh, force_refresh=force_refresh) except HTTPNotModified: # nothing to update return self if corporation.alliance_id: try: self.alliance = EveAllianceInfo.objects.get(alliance_id=corporation.alliance_id) except EveAllianceInfo.DoesNotExist: self.alliance = EveAllianceInfo.objects.create_alliance(alliance_id=corporation.alliance_id) else: self.alliance = None if corporation.faction_id: try: self.faction = EveFactionInfo.objects.get(faction_id=corporation.faction_id) except EveFactionInfo.DoesNotExist: self.faction = EveFactionInfo.objects.create_faction(faction_id=corporation.faction_id) else: self.faction = None self.ceo_id = corporation.ceo_id self.creator_id = corporation.creator_id self.date_founded = corporation.date_founded self.description = corporation.description if corporation.description else "" self.home_station_id = corporation.home_station_id if corporation.home_station_id else None self.member_count = corporation.member_count if corporation.member_count else None self.corporation_name = corporation.name self.shares = corporation.shares if corporation.shares else None self.tax_rate = corporation.tax_rate if corporation.tax_rate else None self.corporation_ticker = corporation.ticker self.url = corporation.url if corporation.url and len(corporation.url) <= 2048 else "" self.war_eligible = corporation.war_eligible if corporation.war_eligible is not None else None self.last_updated = datetime.strptime(response.headers.get("Last-Modified"), "%a, %d %b %Y %H:%M:%S GMT").replace(tzinfo=timezone.utc) self.save() return self
[docs] class EveCharacter(models.Model): """A character in Eve Online."""
[docs] class Gender(models.TextChoices): male = "Male" female = "Female"
character_id = models.PositiveIntegerField(unique=True) alliance_id = models.PositiveIntegerField( help_text="Character's alliance ID", blank=True, null=True, default=None) birthday = models.DateField( help_text="Character's creation date", blank=True, null=True, default=None) bloodline_id = models.PositiveSmallIntegerField( help_text="Character's bloodline ID", blank=True, null=True, default=None) corporation_id = models.PositiveIntegerField( help_text="Character's corporation ID", blank=False, null=False, default=DOOMHEIM_CORPORATION_ID) description = models.TextField( help_text="Character's description (biography)", blank=True, default="") faction_id = models.PositiveIntegerField( help_text="Character's faction ID", blank=True, null=True, default=None) gender = models.CharField( help_text="Character's gender", max_length=10, choices=Gender, blank=True, default="") character_name = models.CharField( help_text="Character's name", max_length=254) race_id = models.PositiveSmallIntegerField( help_text="Character's race ID", blank=True, null=True, default=None) security_status = models.FloatField( help_text="Character's security status", blank=True, null=True, default=0.0) title = models.CharField( help_text="Character's title", max_length=254, blank=True, default="") corporation_name = models.CharField(max_length=254, blank=True, default="") # Cached attribute corporation_ticker = models.CharField(max_length=5, blank=True, default="") alliance_name = models.CharField(max_length=254, blank=True, default="") alliance_ticker = models.CharField(max_length=5, blank=True, default="") faction_name = models.CharField(max_length=254, blank=True, default="") last_updated_affiliations = models.DateTimeField( default=None, blank=True, null=True, help_text="Last time the character's affiliations were updated (PostCharactersAffiliation, 1 hr Cache)") last_updated_other = models.DateTimeField( default=None, blank=True, null=True, help_text="Last time the character's details were updated, (GetCharactersCharacterId), 24 hr Cache") objects: ClassVar[EveCharacterManager] = EveCharacterManager() # pyright: ignore[reportIncompatibleVariableOverride] character_ownership: models.OneToOneField["CharacterOwnership"] class Meta: indexes = [ models.Index(fields=['character_name',]), models.Index(fields=['corporation_id',]), models.Index(fields=['alliance_id',]), models.Index(fields=['corporation_name',]), models.Index(fields=['alliance_name',]), models.Index(fields=['faction_id',]), ] verbose_name = _("character") verbose_name_plural = _("characters") def __str__(self) -> str: return self.character_name @property def is_biomassed(self) -> bool: """Whether this character is dead or not.""" return self.corporation_id == DOOMHEIM_CORPORATION_ID @property def alliance(self) -> EveAllianceInfo | None: """ Pseudo foreign key from alliance_id to EveAllianceInfo :raises: EveAllianceInfo.DoesNotExist :return: EveAllianceInfo or None """ if self.alliance_id is None: return None return EveAllianceInfo.objects.get(alliance_id=self.alliance_id) @property def corporation(self) -> EveCorporationInfo: """ Pseudo foreign key from corporation_id to EveCorporationInfo :raises: EveCorporationInfo.DoesNotExist :return: EveCorporationInfo """ return EveCorporationInfo.objects.get(corporation_id=self.corporation_id) @property def faction(self) -> EveFactionInfo | None: """ Pseudo foreign key from faction_id to EveFactionInfo :raises: EveFactionInfo.DoesNotExist :return: EveFactionInfo """ if self.faction_id is None: return None return EveFactionInfo.objects.get(faction_id=self.faction_id)
[docs] def update_character(self) -> "EveCharacter": """Update only character's affiliation (alliance, corporation, faction)""" affiliation, response = open_api_provider.get_affiliations(character_ids=[self.character_id]) affiliation = affiliation[0] # This is the important affiliation data, update this first to ensure we dont fail on any of the less important models. self.corporation_id = affiliation.corporation_id self.alliance_id = getattr(affiliation, "alliance_id", None) self.faction_id = getattr(affiliation, "faction_id", None) self.last_updated_affiliations = datetime.strptime(response.headers.get("Date"), "%a, %d %b %Y %H:%M:%S GMT").replace(tzinfo=timezone.utc) self.save(update_fields=["corporation_id", "alliance_id", "faction_id", "last_updated_affiliations"]) if self.is_biomassed: self._remove_tokens_of_biomassed_character() # Attempt to populate the Corporation, Alliance, Faction models try: corporation_obj = EveCorporationInfo.objects.get(corporation_id=affiliation.corporation_id) except EveCorporationInfo.DoesNotExist: corporation_obj = EveCorporationInfo.objects.create_corporation(corporation_id=affiliation.corporation_id) if affiliation.alliance_id: try: alliance_obj = EveAllianceInfo.objects.get(alliance_id=affiliation.alliance_id) except EveAllianceInfo.DoesNotExist: alliance_obj = EveAllianceInfo.objects.create_alliance(alliance_id=affiliation.alliance_id) else: alliance_obj = None if affiliation.faction_id: try: faction_obj = EveFactionInfo.objects.get(faction_id=affiliation.faction_id) except EveFactionInfo.DoesNotExist: faction_obj = EveFactionInfo.objects.create_faction(faction_id=affiliation.faction_id) else: faction_obj = None # populate cached name/ticker fields, legacy AA kinda self.alliance_name = alliance_obj.alliance_name if alliance_obj else "" self.alliance_ticker = alliance_obj.alliance_ticker if alliance_obj else "" self.corporation_name = corporation_obj.corporation_name self.corporation_ticker = corporation_obj.corporation_ticker self.faction_name = faction_obj.faction_name if faction_obj else "" self.save(update_fields=["alliance_name", "alliance_ticker", "corporation_name", "corporation_ticker", "faction_name"]) return self
[docs] def update_character_other(self, force_refresh: bool = False) -> "EveCharacter": ''' Update a characters full data from ESI, with GetCharactersCharacterId, 24 hour cache. This function doesnt touch Affiliations as thats on a 1hr cache. _we could compare caches_ but i think thats fraught with danger ''' try: character, response = open_api_provider.get_character( character_id=self.character_id, last_modified=self.last_updated_other if not force_refresh else None, use_etag=not force_refresh, force_refresh=force_refresh) except HTTPNotModified: # nothing to update return self self.birthday = character.birthday self.bloodline_id = character.bloodline_id self.description = character.description if character.description else "" self.gender = character.gender self.character_name = character.name self.race_id = character.race_id self.security_status = character.security_status if character.security_status else 0.0 self.title = character.title if character.title else "" self.last_updated_other = datetime.strptime(response.headers.get("Last-Modified"), "%a, %d %b %Y %H:%M:%S GMT").replace(tzinfo=timezone.utc) self.save() if self.is_biomassed: self._remove_tokens_of_biomassed_character() return self
def _remove_tokens_of_biomassed_character(self) -> None: """Remove tokens of this biomassed character.""" try: user = self.character_ownership.user except ObjectDoesNotExist: return tokens_to_delete = Token.objects.filter(character_id=self.character_id) tokens_count = tokens_to_delete.count() if not tokens_count: return tokens_to_delete.delete() logger.info( "%d tokens from user %s for biomassed character %s [id:%s] deleted.", tokens_count, user, self, self.character_id, ) notify( user=user, title=f"Character {self} biomassed", message=( f"Your former character {self} has been biomassed " "and has been removed from the list of your alts." ) )
[docs] @staticmethod def generic_portrait_url( character_id: int, size: int = _DEFAULT_IMAGE_SIZE ) -> str: """image URL for the given character ID""" return eveimageserver.character_portrait_url(character_id, size)
[docs] def portrait_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: """image URL for this character""" return self.generic_portrait_url(self.character_id, size)
@property def portrait_url_32(self) -> str: """image URL for this character""" return self.portrait_url(32) @property def portrait_url_64(self) -> str: """image URL for this character""" return self.portrait_url(64) @property def portrait_url_128(self) -> str: """image URL for this character""" return self.portrait_url(128) @property def portrait_url_256(self) -> str: """image URL for this character""" return self.portrait_url(256)
[docs] def corporation_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: """image URL for corporation of this character""" return EveCorporationInfo.generic_logo_url(self.corporation_id, size)
@property def corporation_logo_url_32(self) -> str: """image URL for corporation of this character""" return self.corporation_logo_url(32) @property def corporation_logo_url_64(self) -> str: """image URL for corporation of this character""" return self.corporation_logo_url(64) @property def corporation_logo_url_128(self) -> str: """image URL for corporation of this character""" return self.corporation_logo_url(128) @property def corporation_logo_url_256(self) -> str: """image URL for corporation of this character""" return self.corporation_logo_url(256)
[docs] def alliance_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: """image URL for alliance of this character or empty string""" if self.alliance_id: return EveAllianceInfo.generic_logo_url(self.alliance_id, size) else: return ''
@property def alliance_logo_url_32(self) -> str: """image URL for alliance of this character or empty string""" return self.alliance_logo_url(32) @property def alliance_logo_url_64(self) -> str: """image URL for alliance of this character or empty string""" return self.alliance_logo_url(64) @property def alliance_logo_url_128(self) -> str: """image URL for alliance of this character or empty string""" return self.alliance_logo_url(128) @property def alliance_logo_url_256(self) -> str: """image URL for alliance of this character or empty string""" return self.alliance_logo_url(256)
[docs] def faction_logo_url(self, size=_DEFAULT_IMAGE_SIZE) -> str: """image URL for alliance of this character or empty string""" if self.faction_id: return EveFactionInfo.generic_logo_url(self.faction_id, size) else: return ''
@property def faction_logo_url_32(self) -> str: """image URL for alliance of this character or empty string""" return self.faction_logo_url(32) @property def faction_logo_url_64(self) -> str: """image URL for alliance of this character or empty string""" return self.faction_logo_url(64) @property def faction_logo_url_128(self) -> str: """image URL for alliance of this character or empty string""" return self.faction_logo_url(128) @property def faction_logo_url_256(self) -> str: """image URL for alliance of this character or empty string""" return self.faction_logo_url(256)