diff --git a/migrations/migrate_to_repository_and_group_urls.py b/migrations/migrate_to_repository_and_group_urls.py new file mode 100644 index 0000000..f1ba4c4 --- /dev/null +++ b/migrations/migrate_to_repository_and_group_urls.py @@ -0,0 +1,24 @@ +# https://docs.peewee-orm.com/en/latest/peewee/playhouse.html#schema-migrations + +import peewee +import playhouse.migrate + +import pycamp_bot.models + + +my_db = peewee.SqliteDatabase('pycamp_projects.db') +migrator = playhouse.migrate.SqliteMigrator(my_db) + + +playhouse.migrate.migrate( + migrator.add_column( + pycamp_bot.models.Project._meta.table_name, + 'repository_url', + pycamp_bot.models.Project.repository_url, + ), + migrator.add_column( + pycamp_bot.models.Project._meta.table_name, + 'group_url', + pycamp_bot.models.Project.group_url, + ), +) diff --git a/src/pycamp_bot/commands/base.py b/src/pycamp_bot/commands/base.py index fc3badb..bac80c5 100644 --- a/src/pycamp_bot/commands/base.py +++ b/src/pycamp_bot/commands/base.py @@ -2,13 +2,16 @@ from pycamp_bot.commands.help_msg import get_help from pycamp_bot.logger import logger +import os + async def msg_to_active_pycamp_chat(bot, text): - chat_id = -1001404878013 # Prueba - await bot.send_message( - chat_id=chat_id, - text=text - ) + if 'TEST_CHAT_ID' in os.environ: + chat_id = -1001404878013 # Prueba + await bot.send_message( + chat_id=os.environ['TEST_CHAT_ID'], + text=text + ) async def start(update, context): diff --git a/src/pycamp_bot/commands/manage_pycamp.py b/src/pycamp_bot/commands/manage_pycamp.py index 61f2e2d..4df2be9 100644 --- a/src/pycamp_bot/commands/manage_pycamp.py +++ b/src/pycamp_bot/commands/manage_pycamp.py @@ -152,6 +152,7 @@ async def define_duration(update, context): chat_id=update.message.chat_id, text=msg ) + return ConversationHandler.END async def cancel(update, context): diff --git a/src/pycamp_bot/commands/projects.py b/src/pycamp_bot/commands/projects.py index 035f508..dc2f484 100644 --- a/src/pycamp_bot/commands/projects.py +++ b/src/pycamp_bot/commands/projects.py @@ -1,6 +1,9 @@ import logging +import textwrap + import peewee -from telegram.ext import ConversationHandler, CommandHandler, MessageHandler, filters +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, LinkPreviewOptions +from telegram.ext import CallbackQueryHandler, CommandHandler, ConversationHandler, MessageHandler, filters from pycamp_bot.models import Pycampista, Project, Slot, Vote from pycamp_bot.commands.base import msg_to_active_pycamp_chat from pycamp_bot.commands.manage_pycamp import active_needed, get_active_pycamp @@ -8,9 +11,19 @@ from pycamp_bot.commands.schedule import DIAS from pycamp_bot.utils import escape_markdown - current_projects = {} -NOMBRE, DIFICULTAD, TOPIC = ["nombre", "dificultad", "topic"] + +NOMBRE = "nombre" +DIFICULTAD = "dificultad" +TOPIC = "topic" +CHECK_REPOSITORIO = "check_repositorio" +REPOSITORIO = "repositorio" +CHECK_GRUPO = "check_grupo" +GRUPO = "grupo" + +REPO_EXISTS_PATTERN = 'repoexists' +PROJECT_NAME_PATTERN = 'projectname' +GROUP_EXISTS_PATTERN = 'groupexists' logger = logging.getLogger(__name__) @@ -38,15 +51,11 @@ async def load_project(update, context): logger.info("Adding project") username = update.message.from_user.username - logger.info("Load autorized. Starting dialog") - await context.bot.send_message( - chat_id=update.message.chat_id, - text="Usuario: " + username - ) + logger.info("Load authorized. Starting dialog") await context.bot.send_message( chat_id=update.message.chat_id, - text="Ingresá el Nombre del Proyecto a proponer", + text="¿Cuál es el nombre del proyecto?", ) return NOMBRE @@ -57,23 +66,22 @@ async def naming_project(update, context): username = update.message.from_user.username name = update.message.text + user = Pycampista.get_or_create(username=username, chat_id=update.message.chat_id)[0] + new_project = Project(name=name) + new_project.owner = user + current_projects[username] = new_project await context.bot.send_message( chat_id=update.message.chat_id, - text="Estamos cargando tu proyecto: {}!".format(username) - ) - await context.bot.send_message( - chat_id=update.message.chat_id, - text="Tu proyecto se llama: {}".format(name) - ) - await context.bot.send_message( - chat_id=update.message.chat_id, - text="""Cual es el nivel de dificultad? - 1 = newbie friendly - 2 = intermedio - 3 = python avanzado""" + text=textwrap.dedent(""" + ¿Cuál es el nivel de dificultad del proyecto? + + 1: newbie friendly + 2: intermedio + 3: python avanzado""" + ) ) return DIFICULTAD @@ -89,17 +97,16 @@ async def project_level(update, context): await context.bot.send_message( chat_id=update.message.chat_id, - text="Ok! Tu proyecto es nivel: {}".format(text) - ) - await context.bot.send_message( - chat_id=update.message.chat_id, - text="""Ahora necesitamos que nos digas la temática de tu proyecto. - Algunos ejemplos pueden ser: - - flask - - django - - telegram - - inteligencia artificial - - recreativo""" + text=textwrap.dedent(""" + ¿Cuál es la temática del proyecto? + + Ejemplos: + - flask + - django + - telegram + - inteligencia artificial + - recreativo""" + ) ) return TOPIC else: @@ -118,33 +125,227 @@ async def project_topic(update, context): new_project = current_projects[username] new_project.topic = text - chat_id = update.message.chat_id - user = Pycampista.get_or_create(username=username, chat_id=chat_id)[0] + keyboard = [ + [ + InlineKeyboardButton("Sí", callback_data=f"{REPO_EXISTS_PATTERN}:si"), + InlineKeyboardButton("No", callback_data=f"{REPO_EXISTS_PATTERN}:no"), + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + + await context.bot.send_message( + chat_id=update.message.chat_id, + text="¿El proyecto tiene un repositorio?", + reply_markup=reply_markup, + ) + + return CHECK_REPOSITORIO + + +async def save_project(username, chat_id, context): + '''Save project to database''' + new_project = current_projects[username] - new_project.owner = user try: new_project.save() except peewee.IntegrityError: await context.bot.send_message( - chat_id=update.message.chat_id, + chat_id=chat_id, text="Ups ese proyecto ya fue cargado" ) + else: + await context.bot.send_message( + chat_id=chat_id, + text="Proyecto cargado" + ) + + +async def present_group_inline_keyboard(chat_id, context): + keyboard = [ + [ + InlineKeyboardButton("Sí", callback_data=f"{GROUP_EXISTS_PATTERN}:si"), + InlineKeyboardButton("No", callback_data=f"{GROUP_EXISTS_PATTERN}:no"), + ] + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await context.bot.send_message( + chat_id=chat_id, + text="¿Tu proyecto tiene un grupo de Telegram?", + reply_markup=reply_markup, + ) + + +async def ask_if_repository_exists(update, context): + '''Dialog to ask if a repository exists''' + callback_query = update.callback_query + chat = callback_query.message.chat + + if callback_query.data.split(':')[1] == "si": + await context.bot.send_message( + chat_id=chat.id, + text="¿Cuál es la URL del repositorio del proyecto?", + ) + return REPOSITORIO + else: + await context.bot.send_message( + chat_id=chat.id, + text="Si creás un repositorio, podés agregarlo con /agregar_repositorio." + ) + + await present_group_inline_keyboard( + chat_id=chat.id, + context=context, + ) + + return CHECK_GRUPO + + +async def ask_if_group_exists(update, context): + '''Dialog to ask if a group exists''' + callback_query = update.callback_query + chat = callback_query.message.chat + + if callback_query.data.split(':')[1] == "si": + await context.bot.send_message( + chat_id=chat.id, + text="¿Cuál es la URL del grupo del proyecto?", + ) + return GRUPO + else: + await context.bot.send_message( + chat_id=chat.id, + text="Si creás un grupo, podés agregarlo con /agregar_grupo." + ) + await save_project(callback_query.from_user.username, chat.id, context) + return ConversationHandler.END + + +async def project_repository(update, context): + '''Dialog to set project repository''' + username = update.message.from_user.username + text = update.message.text + + new_project = current_projects[username] + new_project.repository_url = text + + await present_group_inline_keyboard( + chat_id=update.message.chat_id, + context=context, + ) + + return CHECK_GRUPO + + +async def project_group(update, context): + '''Dialog to set project group''' + username = update.message.from_user.username + text = update.message.text + + new_project = current_projects[username] + new_project.group_url = text + + await save_project(username, update.message.chat_id, context) + return ConversationHandler.END + + +async def cancel(update, context): + await context.bot.send_message( + chat_id=update.message.chat_id, + text="Has cancelado la carga del proyecto") + return ConversationHandler.END + + +@active_needed +async def ask_project_name(update, context): + '''Command to start the agregar_repositorio/agregar_grupo dialogs''' + username = update.message.from_user.username + + projects = Project.select().join(Pycampista).where(Pycampista.username == username) + + if not projects: + await context.bot.send_message( + chat_id=update.message.chat_id, + text="No cargaste ningún proyecto", + ) return ConversationHandler.END + keyboard = [] + for project in projects: + keyboard.append([InlineKeyboardButton(project.name, callback_data=f"{PROJECT_NAME_PATTERN}:{project.name}")]) + reply_markup = InlineKeyboardMarkup(keyboard) + await context.bot.send_message( chat_id=update.message.chat_id, - text="Excelente {}! La temática de tu proyecto es: {}.".format(username, text)) + text="¿Qué proyecto querés modificar?", + reply_markup=reply_markup, + ) + + return 1 + + +async def ask_repository_url(update, context): + '''Dialog to set project name''' + callback_query = update.callback_query + chat = callback_query.message.chat + + username = callback_query.from_user.username + + current_projects[username] = callback_query.data.split(':')[1] + + await context.bot.send_message( + chat_id=chat.id, + text="¿Cuál es la URL del repositorio?", + ) + return 2 + + +async def ask_group_url(update, context): + '''Dialog to set project name''' + callback_query = update.callback_query + chat = callback_query.message.chat + + username = callback_query.from_user.username + + current_projects[username] = callback_query.data.split(':')[1] + + await context.bot.send_message( + chat_id=chat.id, + text="¿Cuál es la URL del grupo?", + ) + return 2 + + +async def add_repository(update, context): + '''Dialog to set repository''' + username = update.message.from_user.username + text = update.message.text + + project = Project.select().where(Project.name == current_projects[username]).get() + + project.repository_url = text + project.save() + await context.bot.send_message( chat_id=update.message.chat_id, - text="Tu proyecto ha sido cargado" + text=f'Repositorio agregado', ) return ConversationHandler.END -async def cancel(update, context): +async def add_group(update, context): + '''Dialog to set group''' + username = update.message.from_user.username + text = update.message.text + + project = Project.select().where(Project.name == current_projects[username]).get() + + project.group_url = text + project.save() + await context.bot.send_message( chat_id=update.message.chat_id, - text="Has cancelado la carga del proyecto") + text=f'Grupo agregado', + ) return ConversationHandler.END @@ -158,7 +359,7 @@ async def start_project_load(update, context): pycamp.project_load_authorized = True pycamp.save() - await update.message.reply_text("Autorizadx \nCarga de proyectos Abierta") + await update.message.reply_text("Carga de proyectos Abierta") await msg_to_active_pycamp_chat(context.bot, "Carga de proyectos Abierta") else: await update.message.reply_text("La carga de proyectos ya estaba abierta") @@ -186,7 +387,12 @@ async def end_project_load(update, context): states={ NOMBRE: [MessageHandler(filters.TEXT, naming_project)], DIFICULTAD: [MessageHandler(filters.TEXT, project_level)], - TOPIC: [MessageHandler(filters.TEXT, project_topic)]}, + TOPIC: [MessageHandler(filters.TEXT, project_topic)], + CHECK_REPOSITORIO: [CallbackQueryHandler(ask_if_repository_exists, pattern=f'{REPO_EXISTS_PATTERN}:')], + REPOSITORIO: [MessageHandler(filters.TEXT, project_repository)], + CHECK_GRUPO: [CallbackQueryHandler(ask_if_group_exists, pattern=f'{GROUP_EXISTS_PATTERN}:')], + GRUPO: [MessageHandler(filters.TEXT, project_group)], + }, fallbacks=[CommandHandler('cancel', cancel)]) @@ -236,11 +442,13 @@ async def show_projects(update, context): projects = Project.select() text = [] for project in projects: - project_text = "{} \n Owner: {} \n Temática: {} \n Nivel: {}".format( + project_text = "{}\n Owner: @{}\n Temática: {}\n Nivel: {}\n Repositorio: {}\n Grupo de Telegram: {}".format( project.name, project.owner.username, project.topic, - project.difficult_level + project.difficult_level, + project.repository_url or '(ninguno)', + project.group_url or '(ninguno)', ) participants_count = Vote.select().where( (Vote.project == project) & (Vote.interest)).count() @@ -253,7 +461,7 @@ async def show_projects(update, context): else: text = "Todavía no hay ningún proyecto cargado" - await update.message.reply_text(text) + await update.message.reply_text(text, link_preview_options=LinkPreviewOptions(is_disabled=True)) @@ -336,7 +544,35 @@ async def show_my_projects(update, context): await update.message.reply_text(text, parse_mode='MarkdownV2') def set_handlers(application): + add_repository_handler = ConversationHandler( + entry_points=[ + CommandHandler('agregar_repositorio', ask_project_name), + ], + states={ + 1: [CallbackQueryHandler(ask_repository_url, pattern=f'{PROJECT_NAME_PATTERN}:')], + 2: [MessageHandler(filters.TEXT, add_repository)], + }, + fallbacks=[ + CommandHandler('cancel', cancel), + ], + ) + add_group_handler = ConversationHandler( + entry_points=[ + CommandHandler('agregar_grupo', ask_project_name), + ], + states={ + 1: [CallbackQueryHandler(ask_group_url, pattern=f'{PROJECT_NAME_PATTERN}:')], + 2: [MessageHandler(filters.TEXT, add_group)], + }, + fallbacks=[ + CommandHandler('cancel', cancel), + ], + ) + application.add_handler(load_project_handler) + application.add_handler(add_repository_handler) + application.add_handler(add_group_handler) + application.add_handler( CommandHandler('empezar_carga_proyectos', start_project_load)) application.add_handler( diff --git a/src/pycamp_bot/commands/voting.py b/src/pycamp_bot/commands/voting.py index fb90647..db8c143 100644 --- a/src/pycamp_bot/commands/voting.py +++ b/src/pycamp_bot/commands/voting.py @@ -9,6 +9,9 @@ from pycamp_bot.logger import logger +VOTE_PATTERN = 'vote' + + def vote_authorized(func): @functools.wraps(func) async def wrap(*args): @@ -61,7 +64,7 @@ async def button(update, context): # Save vote in the database and confirm the chosen proyects. - if query.data == "si": + if query.data.split(':')[1] == "si": result = f"✅ Sumade a {project_name}!" new_vote.interest = True else: @@ -96,8 +99,8 @@ async def vote(update, context): # ask user for each project in the database for project in Project.select(): - keyboard = [[InlineKeyboardButton("Me Sumo!", callback_data="si"), - InlineKeyboardButton("Paso", callback_data="no")]] + keyboard = [[InlineKeyboardButton("Me Sumo!", callback_data=f"{VOTE_PATTERN}:si"), + InlineKeyboardButton("Paso", callback_data=f"{VOTE_PATTERN}:no")]] reply_markup = InlineKeyboardMarkup(keyboard) @@ -129,7 +132,7 @@ async def vote_count(update, context): def set_handlers(application): application.add_handler( - CallbackQueryHandler(button)) + CallbackQueryHandler(button, pattern=f'{VOTE_PATTERN}:')) application.add_handler( CommandHandler('empezar_votacion_proyectos', start_voting)) application.add_handler( diff --git a/src/pycamp_bot/models.py b/src/pycamp_bot/models.py index b5e2bb2..a931da1 100644 --- a/src/pycamp_bot/models.py +++ b/src/pycamp_bot/models.py @@ -152,12 +152,16 @@ class Project(BaseModel): topic: string comma separated with the pertinences slot: ForeignKey with the slot asigned owner: ForeignKey with the pycamp user asigned + repository_url: URL of the repository of the project + group_url: URL of the Telegram group of the project ''' name = pw.CharField(unique=True) difficult_level = pw.IntegerField(default=1) topic = pw.CharField(null=True) slot = pw.ForeignKeyField(Slot, null=True) owner = pw.ForeignKeyField(Pycampista) + repository_url = pw.CharField(null=True) + group_url = pw.CharField(null=True) class Vote(BaseModel):