From eefc83b5406f6bbcb63413575b697b33b880cef5 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 00:02:05 -0300 Subject: [PATCH 1/8] Refactor set_as_only_active method to prevent unnecessary bulk updates - Changed the implementation of the set_as_only_active method to convert the active selection into a list before processing. - Added a conditional check to ensure bulk updates are only called if there are active instances, improving efficiency. --- src/pycamp_bot/models.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pycamp_bot/models.py b/src/pycamp_bot/models.py index 935c770..5ddaf99 100644 --- a/src/pycamp_bot/models.py +++ b/src/pycamp_bot/models.py @@ -86,10 +86,11 @@ def __str__(self): return rv_str def set_as_only_active(self): - active = Pycamp.select().where(Pycamp.active) + active = list(Pycamp.select().where(Pycamp.active)) for p in active: p.active = False - Pycamp.bulk_update(active, fields=[Pycamp.active]) + if active: + Pycamp.bulk_update(active, fields=[Pycamp.active]) self.active = True self.save() From 42d6914c3902f0a145c2e69c7a04e84d7274cc01 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 01:11:28 -0300 Subject: [PATCH 2/8] Refactor voting command to improve user data retrieval - Updated the method of retrieving the username from the callback query to use `query.from_user.username` for accuracy. - Adjusted the project name retrieval to use `query.message.text` for consistency. - Enhanced project creation logic to associate the project with the user who initiated the vote, ensuring proper ownership in the database. --- src/pycamp_bot/commands/voting.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pycamp_bot/commands/voting.py b/src/pycamp_bot/commands/voting.py index db8c143..748fb8e 100644 --- a/src/pycamp_bot/commands/voting.py +++ b/src/pycamp_bot/commands/voting.py @@ -47,10 +47,10 @@ async def start_voting(update, context): async def button(update, context): '''Save user vote in the database''' query = update.callback_query - username = query.message['chat']['username'] + username = query.from_user.username chat_id = query.message.chat_id user = Pycampista.get_or_create(username=username, chat_id=chat_id)[0] - project_name = query.message['text'] + project_name = query.message.text # Get project from the database project = Project.get(Project.name == project_name) @@ -95,7 +95,11 @@ async def vote(update, context): # if there is not project in the database, create a new project if not Project.select().exists(): - Project.create(name='PROYECTO DE PRUEBA') + user = Pycampista.get_or_create( + username=update.message.from_user.username, + chat_id=str(update.message.chat_id), + )[0] + Project.create(name='PROYECTO DE PRUEBA', owner=user) # ask user for each project in the database for project in Project.select(): From ad60178fcf6be09ac6399648f54a1e49ca78a191 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 10:31:41 -0300 Subject: [PATCH 3/8] Fix logging level in announcements command to use 'warning' instead of 'warn' --- src/pycamp_bot/commands/announcements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycamp_bot/commands/announcements.py b/src/pycamp_bot/commands/announcements.py index 69a0236..c13e2c0 100644 --- a/src/pycamp_bot/commands/announcements.py +++ b/src/pycamp_bot/commands/announcements.py @@ -50,7 +50,7 @@ async def announce(update: Update, context: CallbackContext) -> str: chat_id=update.message.chat_id, text=ERROR_MESSAGES["no_admin"], ) - logger.warn(f"Pycampista {state.username} no contiene proyectos creados.") + logger.warning(f"Pycampista {state.username} no contiene proyectos creados.") return ConversationHandler.END else: state.projects = Project.select() From 668c545605ea9eed35644584453e19de990062e6 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 11:00:13 -0300 Subject: [PATCH 4/8] fix(projects): show votes in /mis_proyectos even when schedule is not set The query used INNER JOIN with Slot, so only projects with an assigned slot were returned. Before running the schedule step all projects have slot=NULL and every vote was excluded. Switched to LEFT OUTER JOIN with Slot and handle the no-slot case by showing a "Sin asignar" section with project name and owner. --- src/pycamp_bot/commands/projects.py | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pycamp_bot/commands/projects.py b/src/pycamp_bot/commands/projects.py index 3d80c4b..d59527e 100644 --- a/src/pycamp_bot/commands/projects.py +++ b/src/pycamp_bot/commands/projects.py @@ -2,6 +2,7 @@ import textwrap import peewee +from peewee import JOIN from telegram import InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions from telegram.ext import CallbackQueryHandler, CommandHandler, ConversationHandler, MessageHandler, filters from pycamp_bot.models import Pycampista, Project, Slot, Vote @@ -528,9 +529,9 @@ async def show_my_projects(update, context): ) votes = ( Vote - .select(Project, Slot) + .select(Vote, Project, Slot) .join(Project) - .join(Slot) + .join(Slot, join_type=JOIN.LEFT_OUTER) .where( (Vote.pycampista == user) & Vote.interest @@ -544,17 +545,28 @@ async def show_my_projects(update, context): prev_slot_day_code = None for vote in votes: - slot_day_code = vote.project.slot.code[0] - slot_day_name = get_slot_weekday_name(slot_day_code) + slot = vote.project.slot + if slot is None: + slot_day_code = None + slot_day_name = "Sin asignar" + else: + slot_day_code = slot.code[0] + slot_day_name = get_slot_weekday_name(slot_day_code) if slot_day_code != prev_slot_day_code: text_chunks.append(f'*{slot_day_name}*') - project_lines = [ - f'{vote.project.slot.start}:00', - escape_markdown(vote.project.name), - f'Owner: @{escape_markdown(vote.project.owner.username)}', - ] + if slot is None: + project_lines = [ + escape_markdown(vote.project.name), + f'Owner: @{escape_markdown(vote.project.owner.username)}', + ] + else: + project_lines = [ + f'{slot.start}:00', + escape_markdown(vote.project.name), + f'Owner: @{escape_markdown(vote.project.owner.username)}', + ] text_chunks.append('\n'.join(project_lines)) From ad5785177a0d5577187b7b24c08b1f93beedb912 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 11:28:43 -0300 Subject: [PATCH 5/8] fix(schedule): await make_schedule in create_slot function Updated the create_slot function to await the make_schedule call, ensuring proper asynchronous execution and preventing potential issues with scheduling operations. --- src/pycamp_bot/commands/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycamp_bot/commands/schedule.py b/src/pycamp_bot/commands/schedule.py index 19c9c51..1ac1551 100644 --- a/src/pycamp_bot/commands/schedule.py +++ b/src/pycamp_bot/commands/schedule.py @@ -117,7 +117,7 @@ async def create_slot(update, context): chat_id=update.message.chat_id, text="Genial! Slots Asignados" ) - make_schedule(update, context) + await make_schedule(update, context) return ConversationHandler.END From 5205dc2754d0c8c1d03cdf6e78bbe6739326d063 Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 11:29:04 -0300 Subject: [PATCH 6/8] fix(wizard): correct error message formatting in schedule_wizards function Updated the error message in the schedule_wizards function to include the username of the admin when a BadRequest occurs, ensuring clearer logging and debugging information. --- src/pycamp_bot/commands/wizard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pycamp_bot/commands/wizard.py b/src/pycamp_bot/commands/wizard.py index e8e9c17..93a1e98 100644 --- a/src/pycamp_bot/commands/wizard.py +++ b/src/pycamp_bot/commands/wizard.py @@ -240,7 +240,7 @@ async def schedule_wizards(update, context, pycamp=None): parse_mode="MarkdownV2" ) except BadRequest as e: - m = "Coulnd't return the Wizards list to the admin. ".format(update.message.from_user.username) + m = "Couldn't return the Wizards list to the admin ({}).".format(update.message.from_user.username) if len(msg) >= MSG_MAX_LEN: m += "The message is too long. Check the data in the DB ;-)" logger.exception(m) From 017f05c5c1f8408b1380c9f08b18011d9f15439a Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 11:44:10 -0300 Subject: [PATCH 7/8] feat(voting): add vote_count command with admin restriction Introduced a new command, vote_count, to tally votes and restricted access to admins using the @admin_needed decorator. This enhances the voting functionality by allowing authorized users to view the total votes cast. --- src/pycamp_bot/commands/voting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pycamp_bot/commands/voting.py b/src/pycamp_bot/commands/voting.py index 748fb8e..f0b6f54 100644 --- a/src/pycamp_bot/commands/voting.py +++ b/src/pycamp_bot/commands/voting.py @@ -125,6 +125,8 @@ async def end_voting(update, context): await update.message.reply_text("Selección cerrada") await msg_to_active_pycamp_chat(context.bot, "La selección de proyectos ha finalizado.") + +@admin_needed async def vote_count(update, context): votes = [vote.pycampista_id for vote in Vote.select()] vote_count = len(set(votes)) From b7a96b9e7b9b81f2186945e504680444e56c2dfe Mon Sep 17 00:00:00 2001 From: botON Date: Tue, 17 Feb 2026 11:52:07 -0300 Subject: [PATCH 8/8] fix(scheduler): avoid division by zero and empty neighbours in hill climbing - Use max(1, total_participants) when computing most_voted_cost to prevent ZeroDivisionError when there are no participants. - Return current_state when neighboors is empty so hill_climbing does not call max() on an empty sequence (e.g. initial state with no projects). --- src/pycamp_bot/scheduler/schedule_calculator.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pycamp_bot/scheduler/schedule_calculator.py b/src/pycamp_bot/scheduler/schedule_calculator.py index bae190f..0013570 100644 --- a/src/pycamp_bot/scheduler/schedule_calculator.py +++ b/src/pycamp_bot/scheduler/schedule_calculator.py @@ -128,7 +128,8 @@ def value(self, state): # were at the begining vote_quantity = sum([len(self.data.projects[project].votes) for project in slot_projects]) - most_voted_cost += (slot_number * vote_quantity) / self.total_participants + denom = max(1, self.total_participants) + most_voted_cost += (slot_number * vote_quantity) / denom for project, slot in state: project_data = self.data.projects[project] @@ -221,6 +222,8 @@ def hill_climbing(problem, initial_state): while True: neighboors = [(n, problem.value(n)) for n in problem.neighboors(current_state)] + if not neighboors: + return current_state best_neighbour, best_value = max(neighboors, key=itemgetter(1)) if best_value > current_value: