Skip to content

Models

bd_models.models

balls

balls: dict[int, Ball] = {}

economies

economies: dict[int, Economy] = {}

regimes

regimes: dict[int, Regime] = {}

specials

specials: dict[int, Special] = {}

Ball

Bases: Model

attack

attack = IntegerField(help_text='Ball attack stat')

cached_economy

cached_economy: Economy | None

cached_regime

cached_regime: Regime

capacity_description

capacity_description = CharField(max_length=256, help_text="Description of the countryball's capacity")

capacity_logic

capacity_logic = JSONField(help_text='Effect of this capacity', blank=True, default=dict)

capacity_name

capacity_name = CharField(max_length=64, help_text="Name of the countryball's capacity")

catch_names

catch_names = TextField(blank=True, null=True, help_text='Additional possible names for catching this ball, separated by semicolons')

collection_card

collection_card = ImageField(max_length=200, help_text='Image used when displaying balls')

country

country = CharField(unique=True, max_length=48, verbose_name='Name')

created_at

created_at = DateTimeField(blank=True, null=True, auto_now_add=True, editable=False)

credits

credits = CharField(max_length=64, help_text='Author of the collection artwork')

economy

economy = ForeignKey(Economy, on_delete=SET_NULL, blank=True, null=True, help_text='Economical regime of this country')

economy_id

economy_id: int | None

emoji_id

emoji_id = BigIntegerField(help_text='Emoji ID for this ball')

enabled

enabled = BooleanField(help_text='Enables spawning and show in completion', default=True)

enabled_objects

enabled_objects: EnabledManager[Self] = EnabledManager()

health

health = IntegerField(help_text='Ball health stat')

objects

objects: Manager[Self] = Manager()

rarity

rarity = FloatField(help_text='Rarity of this ball')

regime

regime = ForeignKey(Regime, on_delete=CASCADE, help_text='Political regime of this country')

regime_id

regime_id: int

short_name

short_name = CharField(max_length=24, blank=True, null=True, help_text='An alternative shorter name used only when generating the card, if the base name is too long.')

tradeable

tradeable = BooleanField(help_text='Whether this ball can be traded with others', default=True)

tradeable_objects

tradeable_objects: TradeableManager[Self] = TradeableManager()

translations

translations = TextField(blank=True, null=True)

wild_card

wild_card = ImageField(max_length=200, help_text='Image used when a new ball spawns in the wild')

Meta

db_table
db_table = 'ball'
managed
managed = True
verbose_name
verbose_name = collectible_name
verbose_name_plural
verbose_name_plural = plural_collectible_name

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return self.country

collection_image

collection_image() -> SafeText
Source code in admin_panel/bd_models/models.py
@admin.display(description="Current collection card")
def collection_image(self) -> SafeText:
    return image_display(str(self.collection_card))

save

save(force_insert: bool = False, force_update: bool = False, using: str | None = None, update_fields: Iterable[str] | None = None) -> None
Source code in admin_panel/bd_models/models.py
def save(
    self,
    force_insert: bool = False,
    force_update: bool = False,
    using: str | None = None,
    update_fields: Iterable[str] | None = None,
) -> None:
    def lower_catch_names(names: str | None) -> str | None:
        if names:
            return ";".join([x.strip() for x in names.split(";")]).lower()

    self.catch_names = lower_catch_names(self.catch_names)
    self.translations = lower_catch_names(self.translations)

    return super().save(force_insert, force_update, using, update_fields)

spawn_image

spawn_image() -> SafeText
Source code in admin_panel/bd_models/models.py
@admin.display(description="Current spawn asset")
def spawn_image(self) -> SafeText:
    return image_display(str(self.wild_card))

BallInstance

Bases: Model

attack

attack: int

attack_bonus

attack_bonus = IntegerField(default=0)

ball

ball = ForeignKey(Ball, on_delete=CASCADE)

ball_id

ball_id: int

catch_date

catch_date = DateTimeField(auto_now_add=True)

countryball

countryball: Ball

extra_data

extra_data = JSONField(blank=True, default=dict)

favorite

favorite = BooleanField(default=False)

health

health: int

health_bonus

health_bonus = IntegerField(default=0)

is_tradeable

is_tradeable: bool

locked

locked = DateTimeField(null=True, help_text='If the instance was locked for a trade and when', default=None)

objects

objects: Manager[Self] = Manager()

player

player = ForeignKey(Player, on_delete=CASCADE, related_name='balls')

player_id

player_id: int

server_id

server_id = BigIntegerField(null=True, help_text='Discord server ID where this ball was caught')

spawned_time

spawned_time = DateTimeField(null=True)

special

special = ForeignKey(Special, on_delete=SET_NULL, null=True)

special_card

special_card: str | None

special_id

special_id: int | None

specialcard

specialcard: Special | None

trade_player

trade_player = ForeignKey(Player, on_delete=SET_NULL, related_name='ballinstance_trade_player_set', null=True)

trade_player_id

trade_player_id: int | None

tradeable

tradeable = BooleanField(default=True)

tradeable_objects

tradeable_objects: TradeableManager[Self] = TradeableManager()

Meta

db_table
db_table = 'ballinstance'
indexes
indexes = (Index(fields=('ball_id',)), Index(fields=('player_id',)), Index(fields=('special_id',)))
managed
managed = True
unique_together
unique_together = (('player', 'id'),)
verbose_name
verbose_name = f'{collectible_name} instance'

__getattribute__

__getattribute__(name: str) -> Any
Source code in admin_panel/bd_models/models.py
def __getattribute__(self, name: str) -> Any:
    if name == "ball":
        balls = cast(list[Ball], cache.get_or_set("balls", Ball.objects.all(), timeout=30))
        for ball in balls:
            if ball.pk == self.ball_id:
                return ball
    return super().__getattribute__(name)

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return self.short_description()

admin_description

admin_description() -> SafeText
Source code in admin_panel/bd_models/models.py
@admin.display(description="Countryball")
def admin_description(self) -> SafeText:
    text = str(self)
    emoji = f'<img src="https://cdn.discordapp.com/emojis/{self.ball.emoji_id}.png?size=20" />'
    return mark_safe(f"{emoji} {text} ATK:{self.attack_bonus:+d}% HP:{self.health_bonus:+d}%")

catch_time

catch_time()
Source code in admin_panel/bd_models/models.py
@admin.display(description="Time to catch")
def catch_time(self):
    if self.spawned_time:
        return str(self.catch_date - self.spawned_time)
    return "-"

description

description(*, short: bool = False, include_emoji: bool = False, bot: 'BallsDexBot | None' = None, is_trade: bool = False) -> str
Source code in admin_panel/bd_models/models.py
def description(
    self,
    *,
    short: bool = False,
    include_emoji: bool = False,
    bot: "BallsDexBot | None" = None,
    is_trade: bool = False,
) -> str:
    text = self.short_description(is_trade=is_trade)
    if not short:
        text += f" ATK:{self.attack_bonus:+d}% HP:{self.health_bonus:+d}%"
    if include_emoji:
        if not bot:
            raise TypeError("You need to provide the bot argument when using with include_emoji=True")
        if isinstance(self.countryball, Ball):
            emoji = bot.get_emoji(self.countryball.emoji_id)
            if emoji:
                text = f"{emoji} {text}"
    return text

draw_card

draw_card() -> BytesIO
Source code in admin_panel/bd_models/models.py
def draw_card(self) -> BytesIO:
    image, kwargs = draw_card(self)
    buffer = BytesIO()
    image.save(buffer, **kwargs)
    buffer.seek(0)
    image.close()
    return buffer

is_locked

is_locked()
Source code in admin_panel/bd_models/models.py
async def is_locked(self):
    await self.arefresh_from_db(fields=["locked"])
    self.locked
    return self.locked is not None and (self.locked + timedelta(minutes=30)) > timezone.now()

lock_for_trade

lock_for_trade()
Source code in admin_panel/bd_models/models.py
async def lock_for_trade(self):
    self.locked = timezone.now()
    await self.asave(update_fields=("locked",))

prepare_for_message

prepare_for_message(interaction: Interaction['BallsDexBot']) -> tuple[str, File, View]
Source code in admin_panel/bd_models/models.py
async def prepare_for_message(
    self, interaction: discord.Interaction["BallsDexBot"]
) -> tuple[str, discord.File, discord.ui.View]:
    # message content
    trade_content = ""
    if self.trade_player:
        original_player = None
        # we want to avoid calling fetch_user if possible (heavily rate-limited call)
        if interaction.guild:
            try:
                original_player = await interaction.guild.fetch_member(int(self.trade_player.discord_id))
            except discord.NotFound:
                pass
        elif original_player is None:  # try again if not found in guild
            try:
                original_player = await interaction.client.fetch_user(int(self.trade_player.discord_id))
            except discord.NotFound:
                pass

        original_player_name = (
            original_player.name if original_player else f"user with ID {self.trade_player.discord_id}"
        )
        trade_content = f"Obtained by trade with {original_player_name}.\n"
    content = (
        f"ID: `#{self.pk:0X}`\n"
        f"Caught on {format_dt(self.catch_date)} ({format_dt(self.catch_date, style='R')}).\n"
        f"{trade_content}\n"
        f"ATK: {self.attack} ({self.attack_bonus:+d}%)\n"
        f"HP: {self.health} ({self.health_bonus:+d}%)"
    )

    # draw image
    with ThreadPoolExecutor() as pool:
        buffer = await interaction.client.loop.run_in_executor(pool, self.draw_card)

    view = discord.ui.View()
    return content, discord.File(buffer, "card.webp"), view

short_description

short_description(*, is_trade: bool = False) -> str

Return a short string representation. Similar to str(x) without arguments.

Source code in admin_panel/bd_models/models.py
def short_description(self, *, is_trade: bool = False) -> str:
    """
    Return a short string representation. Similar to str(x) without arguments.
    """
    text = ""
    if not is_trade and self.locked and self.locked > now() - timedelta(minutes=30):
        text += "🔒"
    if self.favorite:
        text += settings.favorited_collectible_emoji
    if text:
        text += " "
    if self.specialcard:
        text += self.specialcard.emoji or ""
    return f"{text}#{self.pk:0X} {self.countryball.country}"

unlock

unlock()
Source code in admin_panel/bd_models/models.py
async def unlock(self):
    self.locked = None  # type: ignore
    await self.asave(update_fields=("locked",))

BlacklistHistory

Bases: Model

action_type

action_type = CharField(max_length=64, default='blacklist')

date

date = DateTimeField(auto_now_add=True, editable=False)

discord_id

discord_id = BigIntegerField(help_text='Discord ID')

id_type

id_type = CharField(max_length=64, default='user')

moderator_id

moderator_id = BigIntegerField(help_text='Discord Moderator ID')

objects

objects: Manager[Self] = Manager()

reason

reason = TextField(blank=True, null=True)

Meta

db_table
db_table = 'blacklisthistory'
managed
managed = True
verbose_name_plural
verbose_name_plural = 'blacklisthistories'

BlacklistedGuild

Bases: Model

date

date = DateTimeField(blank=True, null=True, auto_now_add=True)

discord_id

discord_id = BigIntegerField(unique=True, help_text='Discord Guild ID')

moderator_id

moderator_id = BigIntegerField(blank=True, null=True, default=None)

objects

objects: Manager[Self] = Manager()

reason

reason = TextField(blank=True, null=True, default=None)

Meta

db_table
db_table = 'blacklistedguild'
managed
managed = True

BlacklistedID

Bases: Model

date

date = DateTimeField(blank=True, null=True, auto_now_add=True)

discord_id

discord_id = BigIntegerField(unique=True, help_text='Discord user ID')

moderator_id

moderator_id = BigIntegerField(blank=True, null=True, default=None)

objects

objects: Manager[Self] = Manager()

reason

reason = TextField(blank=True, null=True, default=None)

Meta

db_table
db_table = 'blacklistedid'
managed
managed = True

Block

Bases: Model

date

date = DateTimeField(auto_now_add=True, editable=False)

objects

objects: Manager[Self] = Manager()

player1

player1 = ForeignKey(Player, on_delete=CASCADE)

player1_id

player1_id: int

player2

player2 = ForeignKey(Player, on_delete=CASCADE, related_name='block_player2_set')

player2_id

player2_id: int

Meta

db_table
db_table = 'block'
managed
managed = True

Economy

Bases: Model

icon

icon = ImageField(max_length=200, help_text='512x512 PNG image')

name

name = CharField(max_length=64)

objects

objects: Manager[Self] = Manager()

Meta

db_table
db_table = 'economy'
managed
managed = True
verbose_name_plural
verbose_name_plural = 'economies'

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return self.name

EnabledManager

Bases: Manager[T]

get_queryset

get_queryset() -> QuerySet[T]
Source code in admin_panel/bd_models/models.py
def get_queryset(self) -> models.QuerySet[T]:
    return super().get_queryset().filter(enabled=True)

Friendship

Bases: Model

objects

objects: Manager[Self] = Manager()

player1

player1 = ForeignKey(Player, on_delete=CASCADE)

player1_id

player1_id: int

player2

player2 = ForeignKey(Player, on_delete=CASCADE, related_name='friendship_player2_set')

player2_id

player2_id: int

since

since = DateTimeField(auto_now_add=True, editable=False)

Meta

db_table
db_table = 'friendship'
managed
managed = True

GuildConfig

Bases: Model

enabled

enabled = BooleanField(help_text='Whether the bot will spawn countryballs in this guild', default=True)

guild_id

guild_id = BigIntegerField(unique=True, help_text='Discord guild ID')

objects

objects: Manager[Self] = Manager()

silent

silent = BooleanField(help_text='Whether the responses of guesses get sent as ephemeral or not', default=False)

spawn_channel

spawn_channel = BigIntegerField(null=True, help_text='Discord channel ID where balls will spawn')

Meta

db_table
db_table = 'guildconfig'
managed
managed = True

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return str(self.guild_id)

Manager

Bases: from_queryset(QuerySet)

Player

Bases: Model

balls

balls: QuerySet[BallInstance]

can_be_mentioned

can_be_mentioned: bool

discord_id

discord_id = BigIntegerField(unique=True, help_text='Discord user ID')

donation_policy

donation_policy = SmallIntegerField(choices=choices, help_text='How you want to handle donations', default=ALWAYS_ACCEPT)

extra_data

extra_data = JSONField(blank=True, default=dict)

friend_policy

friend_policy = SmallIntegerField(choices=choices, help_text='Open or close your friend requests', default=ALLOW)

mention_policy

mention_policy = SmallIntegerField(choices=choices, help_text="Control the bot's mentions", default=ALLOW)

money

money = PositiveBigIntegerField(help_text='Money posessed by the player', default=0)

objects

objects: Manager[Self] = Manager()

privacy_policy

privacy_policy = SmallIntegerField(choices=choices, help_text='How you want to handle inventory privacy', default=DENY)

trade_cooldown_policy

trade_cooldown_policy = SmallIntegerField(choices=choices, help_text='To bypass or not the trade cooldown')

Meta

db_table
db_table = 'player'
managed
managed = True

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return f"{'\N{NO MOBILE PHONES} ' if self.is_blacklisted() else ''}#{self.pk} ({self.discord_id})"

add_money

add_money(amount: int) -> int
Source code in admin_panel/bd_models/models.py
async def add_money(self, amount: int) -> int:
    if amount <= 0:
        raise ValueError("Amount to add must be positive")
    self.money += amount
    await self.asave(update_fields=("money",))
    return self.money

can_afford

can_afford(amount: int) -> bool
Source code in admin_panel/bd_models/models.py
def can_afford(self, amount: int) -> bool:
    return self.money >= amount

is_blacklisted

is_blacklisted() -> bool
Source code in admin_panel/bd_models/models.py
def is_blacklisted(self) -> bool:
    # this should only be used for the admin panel
    if "startbot" in sys.argv:
        return False

    blacklist = cast(
        list[int],
        cache.get_or_set(
            "blacklist", BlacklistedID.objects.all().values_list("discord_id", flat=True), timeout=300
        ),
    )
    return self.discord_id in blacklist

is_blocked

is_blocked(other_player: 'Player') -> bool
Source code in admin_panel/bd_models/models.py
async def is_blocked(self, other_player: "Player") -> bool:
    return await Block.objects.filter((Q(player1=self) & Q(player2=other_player))).aexists()

is_friend

is_friend(other_player: 'Player') -> bool
Source code in admin_panel/bd_models/models.py
async def is_friend(self, other_player: "Player") -> bool:
    return await Friendship.objects.filter(
        (Q(player1=self) & Q(player2=other_player)) | (Q(player1=other_player) & Q(player2=self))
    ).aexists()

remove_money

remove_money(amount: int) -> None
Source code in admin_panel/bd_models/models.py
async def remove_money(self, amount: int) -> None:
    if self.money < amount:
        raise ValueError("Not enough money")
    self.money -= amount
    await self.asave(update_fields=("money",))

QuerySet

Bases: QuerySet[T]

aall

aall() -> list[T]
Source code in admin_panel/bd_models/models.py
async def aall(self) -> list[T]:
    return [x async for x in super().all()]

aget_or_none

aget_or_none(*args: Any, **kwargs: Any) -> T | None
Source code in admin_panel/bd_models/models.py
async def aget_or_none(self, *args: Any, **kwargs: Any) -> T | None:
    try:
        return await super().aget(*args, **kwargs)
    except self.model.DoesNotExist:
        return None

get_or_none

get_or_none(*args: Any, **kwargs: Any) -> T | None
Source code in admin_panel/bd_models/models.py
def get_or_none(self, *args: Any, **kwargs: Any) -> T | None:
    try:
        return super().get(*args, **kwargs)
    except self.model.DoesNotExist:
        return None

Regime

Bases: Model

background

background = ImageField(max_length=200, help_text='1428x2000 PNG image')

name

name = CharField(max_length=64)

objects

objects: Manager[Self] = Manager()

Meta

db_table
db_table = 'regime'
managed
managed = True

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return self.name

Special

Bases: Model

background

background = ImageField(max_length=200, blank=True, null=True, help_text='1428x2000 PNG image')

catch_phrase

catch_phrase = CharField(max_length=128, blank=True, null=True, help_text='Sentence sent in bonus when someone catches a special card')

credits

credits = CharField(max_length=64, help_text='Author of the special event artwork', null=True)

emoji

emoji = CharField(max_length=20, blank=True, null=True, help_text='Either a unicode character or a discord emoji ID')

enabled_objects

enabled_objects = SpecialEnabledManager()

end_date

end_date = DateTimeField(blank=True, null=True, help_text='End time of the event. If blank, the event is permanent')

hidden

hidden = BooleanField(help_text='Hides the event from user commands', default=False)

name

name = CharField(max_length=64)

objects

objects: Manager[Self] = Manager()

rarity

rarity = FloatField(help_text='Value between 0 and 1, chances of using this special background.')

start_date

start_date = DateTimeField(blank=True, null=True, help_text='Start time of the event. If blank, starts immediately')

tradeable

tradeable = BooleanField(help_text='Whether balls of this event can be traded', default=True)

Meta

db_table
db_table = 'special'
managed
managed = True

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return self.name

SpecialEnabledManager

Bases: Manager['Special']

get_queryset

get_queryset() -> QuerySet[Special]
Source code in admin_panel/bd_models/models.py
def get_queryset(self) -> models.QuerySet[Special]:
    return super().get_queryset().filter(hidden=False)

Trade

Bases: Model

date

date = DateTimeField(auto_now_add=True, editable=False)

objects

objects: Manager[Self] = Manager()

player1

player1 = ForeignKey(Player, on_delete=CASCADE)

player1_id

player1_id: int

player1_money

player1_money = PositiveBigIntegerField(default=0)

player2

player2 = ForeignKey(Player, on_delete=CASCADE, related_name='trade_player2_set')

player2_id

player2_id: int

player2_money

player2_money = PositiveBigIntegerField(default=0)

tradeobject_set

tradeobject_set: QuerySet[TradeObject]

Meta

db_table
db_table = 'trade'
indexes
indexes = (Index(fields=('player1_id',)), Index(fields=('player2_id',)))
managed
managed = True

__str__

__str__() -> str
Source code in admin_panel/bd_models/models.py
def __str__(self) -> str:
    return f"Trade #{self.pk:0X}"

TradeObject

Bases: Model

ballinstance

ballinstance = ForeignKey(BallInstance, on_delete=CASCADE)

ballinstance_id

ballinstance_id: int

objects

objects: Manager[Self] = Manager()

player

player = ForeignKey(Player, on_delete=CASCADE)

player_id

player_id: int

trade

trade = ForeignKey(Trade, on_delete=CASCADE)

trade_id

trade_id: int

Meta

db_table
db_table = 'tradeobject'
indexes
indexes = (Index(fields=('ballinstance_id',)), Index(fields=('player_id',)), Index(fields=('trade_id',)))
managed
managed = True

TradeableManager

Bases: Manager[T]

get_queryset

get_queryset() -> QuerySet[T]
Source code in admin_panel/bd_models/models.py
def get_queryset(self) -> models.QuerySet[T]:
    return super().get_queryset().filter(tradeable=True)

image_display

image_display(image_link: str) -> SafeText
Source code in admin_panel/bd_models/models.py
def image_display(image_link: str) -> SafeText:
    return mark_safe(f'<img src="/media/{transform_media(image_link)}" width="80%" />')

transform_media

transform_media(path: str) -> str
Source code in admin_panel/bd_models/models.py
def transform_media(path: str) -> str:
    return path.replace("/static/uploads/", "").replace("/ballsdex/core/image_generator/src/", "default/")