Various fixes on tree and removing a member
This commit is contained in:
Binary file not shown.
Binary file not shown.
BIN
arbre.png
BIN
arbre.png
Binary file not shown.
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 664 KiB |
47
bot.py
47
bot.py
@ -173,35 +173,32 @@ async def couple(interaction: discord.Interaction, partenaire: discord.Member):
|
|||||||
ephemeral=True
|
ephemeral=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@bot.tree.command(name="renier", description="Renier un enfant")
|
@bot.tree.command(name="renier", description="Renier et supprimer un enfant")
|
||||||
async def renier(interaction: discord.Interaction, enfant: discord.Member):
|
async def renier(interaction: discord.Interaction, enfant: discord.Member):
|
||||||
parent = interaction.user
|
parent = interaction.user
|
||||||
success = family.remove_child(str(parent.id), str(enfant.id))
|
|
||||||
|
# Supprime d'abord le lien parental
|
||||||
if success:
|
family.remove_child(str(parent.id), str(enfant.id))
|
||||||
# Retire le rôle
|
|
||||||
generation = family.get_generation(str(enfant.id))
|
# Vérifie si l'enfant n'a plus aucun parent
|
||||||
role_id = config["ROLE_IDS"].get(
|
parents_restants = family.get_parents(str(enfant.id))
|
||||||
"ENFANT" if generation == 1 else
|
|
||||||
"PETIT_ENFANT" if generation == 2 else
|
if not parents_restants:
|
||||||
"ARRIERE_PETIT_ENFANT"
|
# Suppression complète
|
||||||
)
|
if family.remove_member(str(enfant.id)):
|
||||||
|
await interaction.response.send_message(
|
||||||
if role_id:
|
f"❌ {parent.mention} a renié et supprimé {enfant.mention} de la famille !"
|
||||||
role = interaction.guild.get_role(role_id)
|
)
|
||||||
if role:
|
else:
|
||||||
try:
|
await interaction.response.send_message(
|
||||||
await enfant.remove_roles(role)
|
"❌ Erreur lors de la suppression",
|
||||||
except discord.Forbidden:
|
ephemeral=True
|
||||||
pass # On continue même si échec du retrait de rôle
|
)
|
||||||
|
|
||||||
await interaction.response.send_message(
|
|
||||||
f"❌ {parent.mention} a renié {enfant.mention} !"
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
|
# Cas où l'enfant a d'autres parents
|
||||||
await interaction.response.send_message(
|
await interaction.response.send_message(
|
||||||
"❌ Aucun lien parental trouvé.",
|
f"⚠️ {enfant.mention} a encore des parents ({len(parents_restants)}). "
|
||||||
ephemeral=True
|
f"Seul le lien avec {parent.mention} a été rompu."
|
||||||
)
|
)
|
||||||
|
|
||||||
@bot.tree.command(name="arbre", description="Afficher l'arbre généalogique")
|
@bot.tree.command(name="arbre", description="Afficher l'arbre généalogique")
|
||||||
|
@ -132,11 +132,6 @@
|
|||||||
"avatar": "https://cdn.discordapp.com/avatars/582633859967877133/89c60709a5e4c47ee16001844f0d51ad.png?size=1024",
|
"avatar": "https://cdn.discordapp.com/avatars/582633859967877133/89c60709a5e4c47ee16001844f0d51ad.png?size=1024",
|
||||||
"parents": []
|
"parents": []
|
||||||
},
|
},
|
||||||
"1334641803059793940": {
|
|
||||||
"name": "Epic The Ticketal",
|
|
||||||
"avatar": "https://cdn.discordapp.com/avatars/1334641803059793940/e36465f33fa4c2cd35a9de212e6832fc.png?size=1024",
|
|
||||||
"parents": []
|
|
||||||
},
|
|
||||||
"689487017557360662": {
|
"689487017557360662": {
|
||||||
"name": "Ama-teramisu godess of Tiramisu",
|
"name": "Ama-teramisu godess of Tiramisu",
|
||||||
"avatar": "https://cdn.discordapp.com/avatars/689487017557360662/8bf7af66b2700f25a926d459f36f5b49.png?size=1024",
|
"avatar": "https://cdn.discordapp.com/avatars/689487017557360662/8bf7af66b2700f25a926d459f36f5b49.png?size=1024",
|
||||||
@ -180,4 +175,4 @@
|
|||||||
"582633859967877133"
|
"582633859967877133"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
25
family.py
25
family.py
@ -192,3 +192,28 @@ class FamilyManager:
|
|||||||
"""Liste toutes les racines"""
|
"""Liste toutes les racines"""
|
||||||
data = self._load()
|
data = self._load()
|
||||||
return data.get("roots", [])
|
return data.get("roots", [])
|
||||||
|
|
||||||
|
def remove_member(self, member_id: str) -> bool:
|
||||||
|
"""Supprime complètement un membre de la famille"""
|
||||||
|
data = self._load()
|
||||||
|
member_id = str(member_id)
|
||||||
|
|
||||||
|
if member_id not in data["members"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Supprime des couples
|
||||||
|
data["couples"] = [c for c in data["couples"] if member_id not in c]
|
||||||
|
|
||||||
|
# Supprime des racines
|
||||||
|
if "roots" in data and member_id in data["roots"]:
|
||||||
|
data["roots"].remove(member_id)
|
||||||
|
|
||||||
|
# Supprime le membre
|
||||||
|
del data["members"][member_id]
|
||||||
|
|
||||||
|
# Nettoie les références parentales
|
||||||
|
for m in data["members"].values():
|
||||||
|
m["parents"] = [p for p in m["parents"] if p["id"] != member_id]
|
||||||
|
|
||||||
|
self._save(data)
|
||||||
|
return True
|
||||||
|
94
tree.py
94
tree.py
@ -6,49 +6,37 @@ from PIL import Image, ImageDraw, ImageFont, ImageOps
|
|||||||
def create_avatar_with_name(user_id, name, avatar_path, output_path):
|
def create_avatar_with_name(user_id, name, avatar_path, output_path):
|
||||||
"""Crée une image combinant avatar rond et pseudo en dessous"""
|
"""Crée une image combinant avatar rond et pseudo en dessous"""
|
||||||
try:
|
try:
|
||||||
# Charge l'avatar original
|
|
||||||
avatar = Image.open(avatar_path).convert("RGBA")
|
avatar = Image.open(avatar_path).convert("RGBA")
|
||||||
|
|
||||||
# Redimensionne et arrondit l'avatar
|
|
||||||
avatar = avatar.resize((150, 150))
|
avatar = avatar.resize((150, 150))
|
||||||
|
|
||||||
mask = Image.new('L', (150, 150), 0)
|
mask = Image.new('L', (150, 150), 0)
|
||||||
draw = ImageDraw.Draw(mask)
|
draw = ImageDraw.Draw(mask)
|
||||||
draw.ellipse((0, 0, 150, 150), fill=255)
|
draw.ellipse((0, 0, 150, 150), fill=255)
|
||||||
rounded_avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5))
|
rounded_avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5))
|
||||||
rounded_avatar.putalpha(mask)
|
rounded_avatar.putalpha(mask)
|
||||||
|
|
||||||
# Crée une nouvelle image avec espace pour le texte
|
|
||||||
combined = Image.new('RGBA', (200, 200), (0, 0, 0, 0))
|
combined = Image.new('RGBA', (200, 200), (0, 0, 0, 0))
|
||||||
combined.paste(rounded_avatar, (25, 0), rounded_avatar)
|
combined.paste(rounded_avatar, (25, 0), rounded_avatar)
|
||||||
|
|
||||||
# Ajoute le texte
|
|
||||||
draw = ImageDraw.Draw(combined)
|
draw = ImageDraw.Draw(combined)
|
||||||
try:
|
try:
|
||||||
font = ImageFont.truetype("arial.ttf", 14)
|
font = ImageFont.truetype("arial.ttf", 14)
|
||||||
except:
|
except:
|
||||||
font = ImageFont.load_default()
|
font = ImageFont.load_default()
|
||||||
|
|
||||||
text_width = draw.textlength(name, font=font)
|
text_width = draw.textlength(name, font=font)
|
||||||
draw.text(((200 - text_width) / 2, 160), name, font=font, fill="white")
|
draw.text(((200 - text_width) / 2, 160), name, font=font, fill="white")
|
||||||
|
|
||||||
combined.save(output_path)
|
combined.save(output_path)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Erreur création avatar+texte {user_id}: {e}")
|
print(f"Erreur création avatar+texte {user_id}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def generate_tree(input_file, output_file, root_id=None): # root_id devient optionnel
|
def generate_tree(input_file, output_file, root_id=None):
|
||||||
with open(input_file) as f:
|
with open(input_file) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
roots = data.get("roots", [root_id] if root_id else [])
|
|
||||||
if not roots:
|
|
||||||
roots = [mid for mid, m in data["members"].items() if not m.get("parents")]
|
|
||||||
|
|
||||||
with open(input_file) as f:
|
|
||||||
data = json.load(f)
|
|
||||||
|
|
||||||
# Configuration du graphe
|
|
||||||
G = pgv.AGraph(
|
G = pgv.AGraph(
|
||||||
directed=True,
|
directed=True,
|
||||||
rankdir="TB",
|
rankdir="TB",
|
||||||
@ -58,7 +46,6 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt
|
|||||||
splines="true"
|
splines="true"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Style minimal pour les nœuds
|
|
||||||
node_style = {
|
node_style = {
|
||||||
"shape": "none",
|
"shape": "none",
|
||||||
"imagescale": "false",
|
"imagescale": "false",
|
||||||
@ -68,21 +55,19 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt
|
|||||||
"fixedsize": "true"
|
"fixedsize": "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Style des flèches
|
|
||||||
edge_style = {
|
edge_style = {
|
||||||
"color": "#FFFFFF",
|
"color": "#FFFFFF",
|
||||||
"penwidth": "3",
|
"penwidth": "3",
|
||||||
"arrowsize": "1.2"
|
"arrowsize": "1.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Dossier temporaire pour les images combinées
|
|
||||||
os.makedirs("temp_avatars", exist_ok=True)
|
os.makedirs("temp_avatars", exist_ok=True)
|
||||||
|
|
||||||
# Traitement de tous les membres
|
# Traitement de tous les membres
|
||||||
for user_id, info in data["members"].items():
|
for user_id, info in data["members"].items():
|
||||||
original_avatar = f"avatars/{user_id}.png"
|
original_avatar = f"avatars/{user_id}.png"
|
||||||
combined_avatar = f"temp_avatars/combined_{user_id}.png"
|
combined_avatar = f"temp_avatars/combined_{user_id}.png"
|
||||||
|
|
||||||
if os.path.exists(original_avatar):
|
if os.path.exists(original_avatar):
|
||||||
create_avatar_with_name(
|
create_avatar_with_name(
|
||||||
user_id,
|
user_id,
|
||||||
@ -96,37 +81,53 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt
|
|||||||
**node_style
|
**node_style
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Fallback si pas d'avatar
|
|
||||||
G.add_node(
|
G.add_node(
|
||||||
user_id,
|
user_id,
|
||||||
label=info["name"],
|
label=info["name"],
|
||||||
**{**node_style, "fontcolor": "white"}
|
**{**node_style, "fontcolor": "white"}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Organisation hiérarchique
|
# Nouveau : Dictionnaire pour forcer les rangs des couples
|
||||||
levels = {0: [root_id], 1: [], 2: []}
|
forced_ranks = {}
|
||||||
|
|
||||||
for user_id, info in data["members"].items():
|
|
||||||
if user_id == root_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parents = [p["id"] for p in info.get("parents", [])]
|
|
||||||
if root_id in parents:
|
|
||||||
levels[1].append(user_id)
|
|
||||||
elif any(p in levels[1] for p in parents):
|
|
||||||
levels[2].append(user_id)
|
|
||||||
|
|
||||||
# Ajout des partenaires au bon niveau
|
# Traitement des couples
|
||||||
for couple in data.get("couples", []):
|
for couple in data.get("couples", []):
|
||||||
if all(m in data["members"] for m in couple):
|
if all(m in data["members"] for m in couple):
|
||||||
for member in couple:
|
member1, member2 = couple
|
||||||
if member not in levels[1] and any(m in levels[1] for m in couple):
|
|
||||||
levels[1].append(member)
|
# Style spécial pour les liens de couple
|
||||||
|
G.add_edge(member1, member2,
|
||||||
|
style="dashed",
|
||||||
|
color="#FF69B4",
|
||||||
|
penwidth="2.5",
|
||||||
|
dir="none")
|
||||||
|
|
||||||
|
# Force les partenaires au même rang
|
||||||
|
partner_group = f"couple_{min(couple)}_{max(couple)}"
|
||||||
|
G.add_subgraph(couple, name=partner_group, rank="same")
|
||||||
|
forced_ranks[member1] = partner_group
|
||||||
|
forced_ranks[member2] = partner_group
|
||||||
|
|
||||||
|
# Organisation hiérarchique améliorée
|
||||||
|
if root_id:
|
||||||
|
roots = [root_id]
|
||||||
|
else:
|
||||||
|
roots = data.get("roots", [k for k,v in data["members"].items() if not v.get("parents")])
|
||||||
|
|
||||||
|
# Nouvelle approche pour les niveaux
|
||||||
|
levels = {}
|
||||||
|
for user_id in data["members"]:
|
||||||
|
gen = data["members"][user_id].get("generation",
|
||||||
|
len(data["members"][user_id].get("parents", [])))
|
||||||
|
if gen not in levels:
|
||||||
|
levels[gen] = []
|
||||||
|
levels[gen].append(user_id)
|
||||||
|
|
||||||
# Création des sous-graphes par niveau
|
# Création des sous-graphes par niveau
|
||||||
for level, members in levels.items():
|
for level, members in levels.items():
|
||||||
if members:
|
filtered_members = [m for m in members if m not in forced_ranks]
|
||||||
G.add_subgraph(members, name=f"rank{level}", rank="same")
|
if filtered_members:
|
||||||
|
G.add_subgraph(filtered_members, name=f"rank{level}", rank="same")
|
||||||
|
|
||||||
# Connexions parent-enfant
|
# Connexions parent-enfant
|
||||||
for user_id, info in data["members"].items():
|
for user_id, info in data["members"].items():
|
||||||
@ -134,18 +135,9 @@ def generate_tree(input_file, output_file, root_id=None): # root_id devient opt
|
|||||||
if parent["id"] in data["members"]:
|
if parent["id"] in data["members"]:
|
||||||
G.add_edge(parent["id"], user_id, **edge_style)
|
G.add_edge(parent["id"], user_id, **edge_style)
|
||||||
|
|
||||||
# Connexions des couples
|
|
||||||
for couple in data.get("couples", []):
|
|
||||||
if all(m in data["members"] for m in couple):
|
|
||||||
G.add_edge(couple[0], couple[1],
|
|
||||||
style="dashed",
|
|
||||||
color="#FF69B4",
|
|
||||||
penwidth="2.5",
|
|
||||||
dir="none")
|
|
||||||
|
|
||||||
G.layout(prog="dot")
|
G.layout(prog="dot")
|
||||||
G.draw(output_file)
|
G.draw(output_file)
|
||||||
|
|
||||||
# Nettoyage des fichiers temporaires
|
# Nettoyage
|
||||||
for f in os.listdir("temp_avatars"):
|
for f in os.listdir("temp_avatars"):
|
||||||
os.remove(f"temp_avatars/{f}")
|
os.remove(f"temp_avatars/{f}")
|
||||||
|
Reference in New Issue
Block a user