|
|
import secrets |
|
|
import string |
|
|
from datetime import date, datetime, timedelta, timezone |
|
|
|
|
|
from flask import current_app |
|
|
from flask_login import UserMixin |
|
|
|
|
|
from app import db, login_manager |
|
|
|
|
|
|
|
|
@login_manager.user_loader |
|
|
def load_user(user_id): |
|
|
return User.query.get(int(user_id)) |
|
|
|
|
|
|
|
|
class AppSettings(db.Model): |
|
|
"""Model for storing application settings that can be modified from admin panel""" |
|
|
|
|
|
__tablename__ = "app_settings" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
key = db.Column(db.String(100), unique=True, nullable=False) |
|
|
value = db.Column(db.Text, nullable=True) |
|
|
description = db.Column(db.String(255), nullable=True) |
|
|
updated_at = db.Column( |
|
|
db.DateTime, |
|
|
default=lambda: datetime.now(timezone.utc), |
|
|
onupdate=lambda: datetime.now(timezone.utc), |
|
|
) |
|
|
|
|
|
@staticmethod |
|
|
def get_setting(key, default=None): |
|
|
"""Get a setting value by key""" |
|
|
setting = AppSettings.query.filter_by(key=key).first() |
|
|
return setting.value if setting else default |
|
|
|
|
|
@staticmethod |
|
|
def set_setting(key, value, description=None): |
|
|
"""Set a setting value""" |
|
|
setting = AppSettings.query.filter_by(key=key).first() |
|
|
if setting: |
|
|
setting.value = value |
|
|
if description: |
|
|
setting.description = description |
|
|
else: |
|
|
setting = AppSettings(key=key, value=value, description=description) |
|
|
db.session.add(setting) |
|
|
db.session.commit() |
|
|
return setting |
|
|
|
|
|
@staticmethod |
|
|
def get_app_name(): |
|
|
"""Get the application name""" |
|
|
return AppSettings.get_setting("app_name", "Apex Ores") |
|
|
|
|
|
@staticmethod |
|
|
def get_app_logo(): |
|
|
"""Get the application logo URL""" |
|
|
return AppSettings.get_setting("app_logo", None) |
|
|
|
|
|
@staticmethod |
|
|
def get_fake_user_count(): |
|
|
"""Get the fake user count added by admin for display purposes""" |
|
|
value = AppSettings.get_setting("fake_user_count", "0") |
|
|
try: |
|
|
return int(value) |
|
|
except (ValueError, TypeError): |
|
|
return 0 |
|
|
|
|
|
@staticmethod |
|
|
def set_fake_user_count(count): |
|
|
"""Set the fake user count""" |
|
|
AppSettings.set_setting( |
|
|
"fake_user_count", |
|
|
str(count), |
|
|
"Nombre d'utilisateurs fictifs ajoutés pour l'affichage", |
|
|
) |
|
|
|
|
|
@staticmethod |
|
|
def get_total_displayed_users(): |
|
|
"""Get total users to display (real + fake)""" |
|
|
real_users = User.query.count() |
|
|
fake_users = AppSettings.get_fake_user_count() |
|
|
return real_users + fake_users |
|
|
|
|
|
|
|
|
class User(UserMixin, db.Model): |
|
|
__tablename__ = "users" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
name = db.Column(db.String(100), nullable=False, default="Utilisateur") |
|
|
phone = db.Column(db.String(20), unique=True, nullable=False) |
|
|
country_code = db.Column(db.String(5), nullable=False) |
|
|
password_hash = db.Column(db.String(255), nullable=False) |
|
|
referral_code = db.Column(db.String(10), unique=True, nullable=False) |
|
|
referred_by_code = db.Column(db.String(10), nullable=True) |
|
|
|
|
|
balance = db.Column(db.Float, default=0.0) |
|
|
bonus_balance = db.Column(db.Float, default=0.0) |
|
|
total_gains = db.Column(db.Float, default=0.0) |
|
|
|
|
|
|
|
|
referral_earnings = db.Column(db.Float, default=0.0) |
|
|
|
|
|
|
|
|
registration_bonus = db.Column(db.Float, default=0.0) |
|
|
registration_bonus_unlocked = db.Column(db.Boolean, default=False) |
|
|
|
|
|
|
|
|
has_seen_welcome_popup = db.Column(db.Boolean, default=False) |
|
|
|
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
last_login = db.Column(db.DateTime, nullable=True) |
|
|
last_daily_bonus = db.Column(db.Date, nullable=True) |
|
|
|
|
|
account_active = db.Column(db.Boolean, default=True) |
|
|
is_admin = db.Column(db.Boolean, default=False) |
|
|
|
|
|
metals = db.relationship( |
|
|
"UserMetal", backref="user", lazy=True, cascade="all, delete-orphan" |
|
|
) |
|
|
transactions = db.relationship( |
|
|
"Transaction", backref="user", lazy=True, cascade="all, delete-orphan" |
|
|
) |
|
|
notifications = db.relationship( |
|
|
"Notification", backref="user", lazy=True, cascade="all, delete-orphan" |
|
|
) |
|
|
|
|
|
def __init__(self, phone, country_code, password, name="Utilisateur"): |
|
|
self.name = name |
|
|
self.phone = phone |
|
|
self.country_code = country_code |
|
|
self.password_hash = password |
|
|
self.referral_code = self.generate_referral_code() |
|
|
|
|
|
def generate_referral_code(self): |
|
|
while True: |
|
|
code = "".join( |
|
|
secrets.choice(string.ascii_uppercase + string.digits) for _ in range(6) |
|
|
) |
|
|
if not User.query.filter_by(referral_code=code).first(): |
|
|
return code |
|
|
|
|
|
@property |
|
|
def has_active_subscription(self): |
|
|
"""Check if user has ever purchased a subscription plan""" |
|
|
return UserMetal.query.filter_by(user_id=self.id).count() > 0 |
|
|
|
|
|
@property |
|
|
def display_balance(self): |
|
|
"""Total balance displayed to user (includes locked bonus)""" |
|
|
return self.balance + ( |
|
|
self.registration_bonus if not self.registration_bonus_unlocked else 0 |
|
|
) |
|
|
|
|
|
@property |
|
|
def withdrawable_balance(self): |
|
|
"""Balance available for withdrawal (excludes locked bonus)""" |
|
|
return self.balance |
|
|
|
|
|
@property |
|
|
def locked_bonus(self): |
|
|
"""Amount of registration bonus still locked""" |
|
|
if self.registration_bonus_unlocked: |
|
|
return 0 |
|
|
return self.registration_bonus |
|
|
|
|
|
def unlock_registration_bonus(self): |
|
|
"""Unlock the registration bonus and add it to main balance""" |
|
|
if not self.registration_bonus_unlocked and self.registration_bonus > 0: |
|
|
self.balance += self.registration_bonus |
|
|
self.registration_bonus_unlocked = True |
|
|
return True |
|
|
return False |
|
|
|
|
|
def get_referrer(self): |
|
|
"""Get the user who referred this user""" |
|
|
if self.referred_by_code: |
|
|
return User.query.filter_by(referral_code=self.referred_by_code).first() |
|
|
return None |
|
|
|
|
|
def get_referrals(self): |
|
|
"""Get all users referred by this user""" |
|
|
return User.query.filter_by(referred_by_code=self.referral_code).all() |
|
|
|
|
|
def get_referral_count(self): |
|
|
"""Get count of users referred by this user""" |
|
|
return User.query.filter_by(referred_by_code=self.referral_code).count() |
|
|
|
|
|
|
|
|
class Metal(db.Model): |
|
|
__tablename__ = "metals" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
name = db.Column(db.String(100), nullable=False) |
|
|
description = db.Column(db.Text) |
|
|
price = db.Column(db.Float, nullable=False) |
|
|
daily_gain = db.Column(db.Float, nullable=False) |
|
|
cycle_days = db.Column(db.Integer, nullable=False) |
|
|
image_url = db.Column(db.String(255)) |
|
|
|
|
|
is_active = db.Column(db.Boolean, default=True) |
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
|
|
|
user_metals = db.relationship( |
|
|
"UserMetal", backref="metal", lazy=True, cascade="all, delete-orphan" |
|
|
) |
|
|
|
|
|
@property |
|
|
def total_return(self): |
|
|
return self.daily_gain * self.cycle_days |
|
|
|
|
|
|
|
|
class UserMetal(db.Model): |
|
|
__tablename__ = "user_metals" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) |
|
|
metal_id = db.Column(db.Integer, db.ForeignKey("metals.id"), nullable=False) |
|
|
|
|
|
purchase_date = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
expiry_date = db.Column(db.DateTime, nullable=False) |
|
|
last_gain_date = db.Column(db.Date, default=date.today) |
|
|
is_active = db.Column(db.Boolean, default=True) |
|
|
|
|
|
purchase_price = db.Column(db.Float, nullable=False) |
|
|
|
|
|
|
|
|
class Transaction(db.Model): |
|
|
__tablename__ = "transactions" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) |
|
|
|
|
|
type = db.Column(db.String(20), nullable=False) |
|
|
amount = db.Column(db.Float, nullable=False) |
|
|
description = db.Column(db.String(255)) |
|
|
status = db.Column(db.String(20), default="pending") |
|
|
|
|
|
|
|
|
gross_amount = db.Column(db.Float, nullable=True) |
|
|
fee_amount = db.Column(db.Float, nullable=True) |
|
|
net_amount = db.Column( |
|
|
db.Float, nullable=True |
|
|
) |
|
|
|
|
|
|
|
|
scheduled_process_time = db.Column( |
|
|
db.DateTime, nullable=True |
|
|
) |
|
|
admin_action = db.Column( |
|
|
db.String(20), nullable=True |
|
|
) |
|
|
admin_action_time = db.Column(db.DateTime, nullable=True) |
|
|
|
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
processed_at = db.Column(db.DateTime, nullable=True) |
|
|
|
|
|
@staticmethod |
|
|
def get_withdrawal_fee_percentage(): |
|
|
"""Get withdrawal fee percentage from config""" |
|
|
try: |
|
|
return current_app.config.get("WITHDRAWAL_FEE_PERCENTAGE", 0.15) |
|
|
except RuntimeError: |
|
|
|
|
|
return 0.15 |
|
|
|
|
|
@staticmethod |
|
|
def get_withdrawal_delay_hours(): |
|
|
"""Get withdrawal delay in hours from config""" |
|
|
try: |
|
|
return current_app.config.get("WITHDRAWAL_DELAY_HOURS", 24) |
|
|
except RuntimeError: |
|
|
return 24 |
|
|
|
|
|
@staticmethod |
|
|
def calculate_withdrawal_fee(amount): |
|
|
"""Calculate withdrawal fee based on config percentage""" |
|
|
fee_percentage = Transaction.get_withdrawal_fee_percentage() |
|
|
fee = amount * fee_percentage |
|
|
net = amount - fee |
|
|
return { |
|
|
"gross": amount, |
|
|
"fee": fee, |
|
|
"net": net, |
|
|
"fee_percentage": fee_percentage * 100, |
|
|
} |
|
|
|
|
|
@staticmethod |
|
|
def calculate_scheduled_process_time(from_time=None): |
|
|
""" |
|
|
Calculate when a withdrawal should be processed (configurable delay, business days only) |
|
|
If withdrawal is on Friday, Saturday, or Sunday, schedule for Monday + delay |
|
|
""" |
|
|
if from_time is None: |
|
|
from_time = datetime.now(timezone.utc) |
|
|
|
|
|
delay_hours = Transaction.get_withdrawal_delay_hours() |
|
|
|
|
|
|
|
|
process_time = from_time + timedelta(hours=delay_hours) |
|
|
|
|
|
|
|
|
while process_time.weekday() in [5, 6]: |
|
|
|
|
|
process_time += timedelta(days=1) |
|
|
|
|
|
return process_time |
|
|
|
|
|
def can_be_auto_processed(self): |
|
|
"""Check if withdrawal can be auto-processed (delay passed and no admin action)""" |
|
|
if self.type != "withdrawal" or self.status != "pending": |
|
|
return False |
|
|
if self.admin_action is not None: |
|
|
return False |
|
|
if self.scheduled_process_time is None: |
|
|
return False |
|
|
|
|
|
now = datetime.now(timezone.utc) |
|
|
|
|
|
if now.weekday() in [5, 6]: |
|
|
return False |
|
|
|
|
|
return now >= self.scheduled_process_time |
|
|
|
|
|
@staticmethod |
|
|
def get_total_withdrawal_fees(): |
|
|
"""Get total fees collected from all completed withdrawals""" |
|
|
total = ( |
|
|
db.session.query(db.func.sum(Transaction.fee_amount)) |
|
|
.filter( |
|
|
Transaction.type == "withdrawal", |
|
|
Transaction.status.in_(["approved", "completed"]), |
|
|
Transaction.fee_amount.isnot(None), |
|
|
) |
|
|
.scalar() |
|
|
) |
|
|
return total or 0 |
|
|
|
|
|
@staticmethod |
|
|
def get_total_withdrawals_amount(): |
|
|
"""Get total amount of all completed withdrawals (gross amount)""" |
|
|
total = ( |
|
|
db.session.query(db.func.sum(Transaction.gross_amount)) |
|
|
.filter( |
|
|
Transaction.type == "withdrawal", |
|
|
Transaction.status.in_(["approved", "completed"]), |
|
|
Transaction.gross_amount.isnot(None), |
|
|
) |
|
|
.scalar() |
|
|
) |
|
|
return total or 0 |
|
|
|
|
|
|
|
|
class ReferralCommission(db.Model): |
|
|
__tablename__ = "referral_commissions" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
referrer_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) |
|
|
referred_user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) |
|
|
|
|
|
level = db.Column(db.Integer, nullable=False) |
|
|
commission_type = db.Column( |
|
|
db.String(20), nullable=False |
|
|
) |
|
|
commission_percentage = db.Column(db.Float, nullable=False) |
|
|
commission_amount = db.Column(db.Float, nullable=False) |
|
|
|
|
|
purchase_amount = db.Column(db.Float, nullable=True) |
|
|
gain_amount = db.Column(db.Float, nullable=True) |
|
|
|
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
|
|
|
referrer = db.relationship( |
|
|
"User", foreign_keys=[referrer_id], backref="commissions_earned" |
|
|
) |
|
|
referred_user = db.relationship( |
|
|
"User", foreign_keys=[referred_user_id], backref="commissions_generated" |
|
|
) |
|
|
|
|
|
@staticmethod |
|
|
def get_purchase_commission_rate(): |
|
|
"""Get purchase commission rate from config (default 15%)""" |
|
|
try: |
|
|
return current_app.config.get("REFERRAL_PURCHASE_COMMISSION", 0.15) |
|
|
except RuntimeError: |
|
|
|
|
|
return 0.15 |
|
|
|
|
|
@staticmethod |
|
|
def get_daily_gain_commission_rate(): |
|
|
"""Get daily gain commission rate from config (default 3%)""" |
|
|
try: |
|
|
return current_app.config.get("REFERRAL_DAILY_GAIN_COMMISSION", 0.03) |
|
|
except RuntimeError: |
|
|
|
|
|
return 0.03 |
|
|
|
|
|
@staticmethod |
|
|
def calculate_purchase_commission(purchase_amount): |
|
|
"""Calculate commission on plan purchase using config rate""" |
|
|
rate = ReferralCommission.get_purchase_commission_rate() |
|
|
return purchase_amount * rate |
|
|
|
|
|
@staticmethod |
|
|
def calculate_daily_gain_commission(gain_amount): |
|
|
"""Calculate commission on daily gains using config rate""" |
|
|
rate = ReferralCommission.get_daily_gain_commission_rate() |
|
|
return gain_amount * rate |
|
|
|
|
|
@staticmethod |
|
|
def create_purchase_commission(referrer, referred_user, purchase_amount): |
|
|
"""Create a commission entry for a plan purchase""" |
|
|
rate = ReferralCommission.get_purchase_commission_rate() |
|
|
commission_amount = purchase_amount * rate |
|
|
|
|
|
commission = ReferralCommission( |
|
|
referrer_id=referrer.id, |
|
|
referred_user_id=referred_user.id, |
|
|
level=1, |
|
|
commission_type="purchase", |
|
|
commission_percentage=rate * 100, |
|
|
commission_amount=commission_amount, |
|
|
purchase_amount=purchase_amount, |
|
|
) |
|
|
|
|
|
|
|
|
referrer.balance += commission_amount |
|
|
referrer.referral_earnings = ( |
|
|
referrer.referral_earnings or 0 |
|
|
) + commission_amount |
|
|
|
|
|
db.session.add(commission) |
|
|
return commission |
|
|
|
|
|
@staticmethod |
|
|
def create_daily_gain_commission(referrer, referred_user, gain_amount): |
|
|
"""Create a commission entry for daily gains""" |
|
|
rate = ReferralCommission.get_daily_gain_commission_rate() |
|
|
commission_amount = gain_amount * rate |
|
|
|
|
|
commission = ReferralCommission( |
|
|
referrer_id=referrer.id, |
|
|
referred_user_id=referred_user.id, |
|
|
level=1, |
|
|
commission_type="daily_gain", |
|
|
commission_percentage=rate * 100, |
|
|
commission_amount=commission_amount, |
|
|
gain_amount=gain_amount, |
|
|
) |
|
|
|
|
|
|
|
|
referrer.balance += commission_amount |
|
|
referrer.referral_earnings = ( |
|
|
referrer.referral_earnings or 0 |
|
|
) + commission_amount |
|
|
|
|
|
db.session.add(commission) |
|
|
return commission |
|
|
|
|
|
|
|
|
class Notification(db.Model): |
|
|
__tablename__ = "notifications" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) |
|
|
|
|
|
title = db.Column(db.String(100), nullable=False) |
|
|
message = db.Column(db.Text, nullable=False) |
|
|
type = db.Column(db.String(20), default="info") |
|
|
|
|
|
is_read = db.Column(db.Boolean, default=False) |
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
|
|
|
|
|
|
class ScheduledTask(db.Model): |
|
|
"""Model for database-based scheduled tasks - replaces cron jobs""" |
|
|
|
|
|
__tablename__ = "scheduled_tasks" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
name = db.Column(db.String(100), unique=True, nullable=False) |
|
|
description = db.Column(db.String(255)) |
|
|
|
|
|
|
|
|
schedule_type = db.Column(db.String(20), default="daily") |
|
|
schedule_time = db.Column(db.Time, nullable=True) |
|
|
interval_minutes = db.Column(db.Integer, nullable=True) |
|
|
|
|
|
|
|
|
is_active = db.Column(db.Boolean, default=True) |
|
|
last_run_at = db.Column(db.DateTime, nullable=True) |
|
|
next_run_at = db.Column(db.DateTime, nullable=True) |
|
|
last_status = db.Column( |
|
|
db.String(20), default="pending" |
|
|
) |
|
|
last_error = db.Column(db.Text, nullable=True) |
|
|
run_count = db.Column(db.Integer, default=0) |
|
|
fail_count = db.Column(db.Integer, default=0) |
|
|
|
|
|
|
|
|
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
updated_at = db.Column( |
|
|
db.DateTime, |
|
|
default=lambda: datetime.now(timezone.utc), |
|
|
onupdate=lambda: datetime.now(timezone.utc), |
|
|
) |
|
|
|
|
|
def __repr__(self): |
|
|
return f"<ScheduledTask {self.name}>" |
|
|
|
|
|
def should_run(self): |
|
|
"""Vérifie si la tâche doit s'exécuter maintenant""" |
|
|
from datetime import timezone |
|
|
|
|
|
if not self.is_active: |
|
|
return False |
|
|
|
|
|
if not self.next_run_at: |
|
|
self.calculate_next_run() |
|
|
return True |
|
|
|
|
|
now = datetime.now(timezone.utc) |
|
|
next_run = self.next_run_at |
|
|
|
|
|
if next_run.tzinfo is None: |
|
|
next_run = next_run.replace(tzinfo=timezone.utc) |
|
|
|
|
|
return now >= next_run |
|
|
|
|
|
def calculate_next_run(self): |
|
|
"""Calcule la prochaine exécution""" |
|
|
from datetime import date, time, timedelta |
|
|
|
|
|
now = datetime.now(timezone.utc) |
|
|
|
|
|
if self.schedule_type == "daily" and self.schedule_time: |
|
|
|
|
|
next_run = datetime.combine(date.today(), self.schedule_time) |
|
|
next_run = next_run.replace(tzinfo=timezone.utc) |
|
|
if next_run <= now: |
|
|
next_run += timedelta(days=1) |
|
|
self.next_run_at = next_run |
|
|
|
|
|
elif self.schedule_type == "hourly": |
|
|
|
|
|
self.next_run_at = now.replace( |
|
|
minute=0, second=0, microsecond=0 |
|
|
) + timedelta(hours=1) |
|
|
|
|
|
elif self.schedule_type == "interval" and self.interval_minutes: |
|
|
|
|
|
if self.last_run_at: |
|
|
self.next_run_at = self.last_run_at + timedelta( |
|
|
minutes=self.interval_minutes |
|
|
) |
|
|
else: |
|
|
self.next_run_at = now |
|
|
|
|
|
def mark_running(self): |
|
|
"""Marque la tâche comme en cours""" |
|
|
self.last_status = "running" |
|
|
db.session.commit() |
|
|
|
|
|
def mark_success(self): |
|
|
"""Marque la tâche comme réussie""" |
|
|
self.last_run_at = datetime.now(timezone.utc) |
|
|
self.run_count += 1 |
|
|
self.last_status = "success" |
|
|
self.last_error = None |
|
|
self.calculate_next_run() |
|
|
db.session.commit() |
|
|
|
|
|
def mark_failed(self, error_message): |
|
|
"""Marque la tâche comme échouée""" |
|
|
self.last_run_at = datetime.now(timezone.utc) |
|
|
self.fail_count += 1 |
|
|
self.last_status = "failed" |
|
|
self.last_error = str(error_message)[:500] |
|
|
self.calculate_next_run() |
|
|
db.session.commit() |
|
|
|
|
|
|
|
|
class TaskExecutionLog(db.Model): |
|
|
"""Log d'exécution des tâches planifiées""" |
|
|
|
|
|
__tablename__ = "task_execution_logs" |
|
|
|
|
|
id = db.Column(db.Integer, primary_key=True) |
|
|
task_id = db.Column(db.Integer, db.ForeignKey("scheduled_tasks.id"), nullable=False) |
|
|
task_name = db.Column(db.String(100), nullable=False) |
|
|
|
|
|
started_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc)) |
|
|
completed_at = db.Column(db.DateTime, nullable=True) |
|
|
status = db.Column(db.String(20), default="running") |
|
|
error_message = db.Column(db.Text, nullable=True) |
|
|
output_log = db.Column(db.Text, nullable=True) |
|
|
|
|
|
|
|
|
server_hostname = db.Column(db.String(100), nullable=True) |
|
|
process_id = db.Column(db.Integer, nullable=True) |
|
|
|
|
|
task = db.relationship( |
|
|
"ScheduledTask", backref=db.backref("execution_logs", lazy="dynamic") |
|
|
) |
|
|
|
|
|
def __repr__(self): |
|
|
return f"<TaskExecutionLog {self.task_name} {self.status}>" |
|
|
|