feat(teams): add opt-in /teamsync reposition to tidy existing team roles
Creation-time placement only positions NEW team roles. Team roles made before divider placement existed (sitting at the bottom of the role list) stayed put. Add `reposition_team_roles(guild)` and expose it via `/teamsync reposition:true` (off by default, so the hourly task never reorders roles on its own). It moves each EXISTING team role under its game's divider, sheet-driven: - a role's game/divider comes from sheets.get_game_for_team; - roles whose team is no longer in the sheet (or game unknown) are left alone; - idempotent: a role already within its section band (below its own divider, above the next divider down) is skipped, so re-runs move nothing. Adds RepositionSummary, TEAMSYNC_UI/OPT strings, and tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6JZkyszyDFuFtk25WBbcR
This commit is contained in:
@@ -83,6 +83,15 @@ class TeamSyncSummary:
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RepositionSummary:
|
||||
"""Aggregate outcome of the opt-in existing-team-role reposition pass."""
|
||||
scanned: int = 0 # existing team roles considered
|
||||
moved: int = 0 # roles relocated under their divider
|
||||
moves: list[str] = field(default_factory=list) # "role -> game" lines
|
||||
errors: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _format_nickname(full_name: str) -> str:
|
||||
"""Format a nickname from a full name: first name + last name initial.
|
||||
|
||||
@@ -373,6 +382,69 @@ async def sync_all_team_roles(
|
||||
return summary
|
||||
|
||||
|
||||
def _resolve_dividers(guild: discord.Guild) -> dict[str, discord.Role]:
|
||||
"""Map each configured game code to its divider role present in the guild."""
|
||||
dividers: dict[str, discord.Role] = {}
|
||||
for game, rid in config.TEAM_DIVIDER_ROLE_IDS.items():
|
||||
if not rid:
|
||||
continue
|
||||
role = guild.get_role(rid)
|
||||
if role is not None:
|
||||
dividers[game] = role
|
||||
return dividers
|
||||
|
||||
|
||||
async def reposition_team_roles(
|
||||
guild: discord.Guild,
|
||||
log: logging.Logger = log,
|
||||
) -> RepositionSummary:
|
||||
"""Move EXISTING team roles under their game's divider (opt-in cleanup).
|
||||
|
||||
Unlike creation-time placement this also relocates team roles that were made
|
||||
before divider placement existed and are sitting at the bottom of the list.
|
||||
Sheet-driven: a role's game (and thus its divider) comes from
|
||||
``sheets.get_game_for_team``; roles whose team is no longer in the sheet, or
|
||||
whose game/divider is unknown, are left untouched.
|
||||
|
||||
Idempotent: a role already within its section band (below its own divider and
|
||||
above the next divider down) is skipped, so re-running moves nothing.
|
||||
"""
|
||||
summary = RepositionSummary()
|
||||
teams = sheets.all_team_names()
|
||||
dividers = _resolve_dividers(guild)
|
||||
if not teams or not dividers:
|
||||
return summary
|
||||
|
||||
for role in list(guild.roles):
|
||||
if role.name not in teams:
|
||||
continue
|
||||
summary.scanned += 1
|
||||
game = sheets.get_game_for_team(role.name)
|
||||
divider = dividers.get(game) if game else None
|
||||
if divider is None:
|
||||
continue # unknown game / divider not in guild -> leave in place
|
||||
|
||||
# Section band = (highest divider below this one, this divider). A role
|
||||
# already inside it is grouped correctly; only relocate outliers.
|
||||
lower = max(
|
||||
(d.position for d in dividers.values() if d.position < divider.position),
|
||||
default=0,
|
||||
)
|
||||
if lower < role.position < divider.position:
|
||||
continue
|
||||
|
||||
try:
|
||||
await role.edit(position=divider.position, reason="Team sync: reposition")
|
||||
summary.moved += 1
|
||||
summary.moves.append(f"{role.name} -> {game}")
|
||||
log.info("Repositioned team role %r under the %s divider", role.name, game)
|
||||
except discord.Forbidden:
|
||||
summary.errors.append(f"Tiimirolli '{role.name}' paigutamiseks puudub õigus")
|
||||
except discord.HTTPException as e:
|
||||
summary.errors.append(f"Tiimirolli '{role.name}' paigutamine ebaõnnestus: {e}")
|
||||
return summary
|
||||
|
||||
|
||||
async def announce_birthday(
|
||||
member: discord.Member,
|
||||
bot: discord.Client,
|
||||
|
||||
Reference in New Issue
Block a user