diff --git a/.gitignore b/.gitignore
index b823e313b4..caad338b9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ coverage.xml
# Django stuff:
*.log
local_settings.py
+decide/*/migrations
# Flask stuff:
instance/
@@ -101,3 +102,6 @@ ENV/
.mypy_cache/
.vagrant
+
+# Visual Studio Code settings
+.vscode
\ No newline at end of file
diff --git a/decide/decide/settings.py b/decide/decide/settings.py
index d67add849e..005681e78d 100644
--- a/decide/decide/settings.py
+++ b/decide/decide/settings.py
@@ -167,6 +167,9 @@
STATIC_URL = '/static/'
+#temporary link to visualizer page for bots (until hosted)
+VISUALIZER_VIEW="https://decide-full-tortuga-2.herokuapp.com/visualizer/"
+
# number of bits for the key, all auths should use the same number of bits
KEYBITS = 256
diff --git a/decide/visualizer/admin.py b/decide/visualizer/admin.py
index 8c38f3f3da..ea7783f5eb 100644
--- a/decide/visualizer/admin.py
+++ b/decide/visualizer/admin.py
@@ -1,3 +1,12 @@
from django.contrib import admin
+from django.http.response import HttpResponseRedirect
+from .models import TelegramBot
+from .urls import urlpatterns
-# Register your models here.
+class TelegramBotAdmin(admin.ModelAdmin):
+ list_display=('user_id', 'auto_msg')
+ list_filter=('auto_msg',)
+ ordering=('user_id', 'auto_msg')
+ change_list_template = "visualizer/telegram_admin.html"
+
+admin.site.register(TelegramBot, TelegramBotAdmin)
\ No newline at end of file
diff --git a/decide/visualizer/migrations/0001_initial.py b/decide/visualizer/migrations/0001_initial.py
new file mode 100644
index 0000000000..394118c9a1
--- /dev/null
+++ b/decide/visualizer/migrations/0001_initial.py
@@ -0,0 +1,21 @@
+# Generated by Django 2.0 on 2021-12-03 12:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TelegramBot',
+ fields=[
+ ('user_id', models.BigIntegerField(primary_key=True, serialize=False)),
+ ('auto_msg', models.BooleanField(default=False)),
+ ],
+ ),
+ ]
diff --git a/decide/visualizer/migrations/0002_auto_20211210_1800.py b/decide/visualizer/migrations/0002_auto_20211210_1800.py
new file mode 100644
index 0000000000..b772f6bc7a
--- /dev/null
+++ b/decide/visualizer/migrations/0002_auto_20211210_1800.py
@@ -0,0 +1,17 @@
+# Generated by Django 2.0 on 2021-12-10 18:00
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('visualizer', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='telegrambot',
+ options={'verbose_name': 'Telegram user'},
+ ),
+ ]
diff --git a/decide/visualizer/models.py b/decide/visualizer/models.py
index 71a8362390..1c622f6e5a 100644
--- a/decide/visualizer/models.py
+++ b/decide/visualizer/models.py
@@ -1,3 +1,11 @@
from django.db import models
-# Create your models here.
+class TelegramBot(models.Model):
+ user_id=models.BigIntegerField(primary_key=True)
+ auto_msg=models.BooleanField(default=False)
+
+ def __str__(self):
+ return '{}'.format(self.auto_msg)
+
+ class Meta:
+ verbose_name = 'Telegram user'
\ No newline at end of file
diff --git a/decide/visualizer/static/visualizer.css b/decide/visualizer/static/visualizer.css
new file mode 100644
index 0000000000..135d61f328
--- /dev/null
+++ b/decide/visualizer/static/visualizer.css
@@ -0,0 +1,31 @@
+#bots-footer {
+ position: fixed;
+ width: 100%;
+ bottom : 0px;
+ height : 60px;
+ text-align: center;
+ vertical-align: middle;
+ padding-top: 0.5 rem;
+ padding-bottom: 0.5 rem;
+ background-color: rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity));
+ color: #fff;
+}
+
+#start-tlg {
+ border: #79aec8;
+ border-radius: 12px;
+ font-size: 14px;
+ color: #fff;
+ text-transform: uppercase;
+ background-color: #79aec8;
+ letter-spacing: 0.5px;
+ padding: 8px;
+ -webkit-border-radius: 12px;
+ -moz-border-radius: 12px;
+ -ms-border-radius: 12px;
+ -o-border-radius: 12px;
+}
+
+#start-tlg:hover{
+ background-color: #417690;
+}
\ No newline at end of file
diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py
new file mode 100644
index 0000000000..f9e2fef73f
--- /dev/null
+++ b/decide/visualizer/telegramBot.py
@@ -0,0 +1,198 @@
+from django.conf import settings
+from voting import models
+from store import models as stmodels
+from telegram import InputMediaPhoto, Bot
+from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackQueryHandler
+from telegram.inline.inlinekeyboardbutton import InlineKeyboardButton
+from telegram.inline.inlinekeyboardmarkup import InlineKeyboardMarkup
+import logging, os, sys
+from .website_scrapping import get_graphs
+from .models import TelegramBot
+from threading import Thread
+
+
+#auth and front-end for '@VotitosBot'
+UPDATER = Updater(os.environ.get('TELEGRAM_TOKEN'),
+ use_context=True)
+
+BOT=Bot(token=os.environ.get('TELEGRAM_TOKEN'))
+
+#configures and activate '@VotitosBot' to receive any messages from users
+def init_bot():
+
+ #logging
+ logging.basicConfig(level=logging.ERROR,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+ setup_commands(UPDATER)
+ updates_setting()
+ #starts the bot
+ UPDATER.start_polling()
+
+#configures commands and handlers for the bot
+def setup_commands(votitos):
+
+ dp=votitos.dispatcher
+ dp.add_handler(CommandHandler('start', start))
+ dp.add_handler(CommandHandler('relaunch', relaunch, Filters.user(user_id=1931864468)))
+ dp.add_handler(CommandHandler('stop', stop, Filters.user(user_id=1931864468)))
+ dp.add_handler(CommandHandler('results', show_results))
+ dp.add_handler(CommandHandler('details', show_details))
+ dp.add_handler(CommandHandler('auto', change_auto_status))
+ dp.add_handler(CommandHandler('help', help))
+ dp.add_handler(MessageHandler(Filters.command, unknown_command))
+ dp.add_handler(CallbackQueryHandler(results_query_handler, pattern="^[1-9][0-9]*$"))
+ dp.add_handler(CallbackQueryHandler(details_query_handler, pattern="^d[1-9][0-9]*$"))
+ dp.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)"))
+
+#set bot configuration not to reply to old messages
+def updates_setting():
+ updates=BOT.get_updates()
+ if updates:
+ last_update=updates[-1].update_id
+ BOT.get_updates(offset=last_update+1)
+
+#gives the users a warming welcome
+def start(update, context):
+ name=update.message.from_user.first_name
+ id=update.message.chat.id
+ context.bot.send_message(chat_id=id, text="Hola {}, a las buenas tardes. ¿En qué puedo ayudarte?".format(name))
+ TelegramBot.objects.get_or_create(user_id=id)
+ help(update)
+
+# relaunch the bot and also the whole project (limited to admin)
+def relaunch(update, context):
+ Thread(target=stop_restart).start()
+
+# aux for relaunch
+def stop_restart():
+ UPDATER.stop()
+ os.execl(sys.executable, sys.executable, *sys.argv)
+
+#shut down the bot
+def stop(update, context):
+ UPDATER.stop()
+
+#list of available commands
+def help(update):
+
+ update.message.reply_text("""Esta es mi lista de comandos:
+ /start - Inicia la interacción conmigo
+ /results - Muestra los resultados de las votaciones cerradas
+ /details - Proporciona detalles de todas las votaciones
+ /auto - Permite activar o desactivar las notificaciones automáticas para nuevas votaciones
+ """)
+
+#replies to invalid command inputs
+def unknown_command(update, context):
+ update.message.reply_text("Lo siento, no sé qué es '%s'. Revisa que has escrito bien el comando o bien revisa mi lista de comandos, puedes hacerlo con\n/help" % update.message.text)
+
+#allows you to select a closed voting and show its results
+def show_results(update, context):
+ update.message.reply_text("Aquí tienes la lista de votaciones finalizadas.")
+ finished_votings=models.Voting.objects.exclude(start_date__isnull=True).exclude(end_date__isnull=True)
+ keyboard_buttons=[]
+ for v in finished_votings:
+ keyboard_buttons.append(InlineKeyboardButton(text=str(v.name), callback_data=str(v.id)))
+ keyboard=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2))
+ context.bot.send_message(chat_id=update.message.chat.id, text= "Elige por favor:", reply_markup=keyboard)
+
+#handler for '/results' command
+def results_query_handler(update, context):
+
+ query=update.callback_query
+ query.answer("¡A la orden!")
+ results_graph(query.data, update.callback_query.message.chat_id, context)
+
+#allows you to select an active or closed voting and show its details
+def show_details(update, context):
+ update.message.reply_text("Selecciona la votación de la que desea ver sus detalles")
+ votings=models.Voting.objects.exclude(start_date__isnull=True)
+ keyboard_buttons=[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)) for v in votings]
+ keyboard=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2))
+ context.bot.send_message(chat_id=update.message.chat.id, text="Seleccione una por favor:", reply_markup=keyboard)
+
+#constructs menu for inline buttons
+def build_keyboard_menu(buttons, n_cols):
+ return [buttons[b:(b + n_cols)] for b in range(0, len(buttons), n_cols)]
+
+#handler for '/details' command
+def details_query_handler(update, context):
+
+ query=update.callback_query
+ query.answer("¡A la orden!")
+ vot_id=query.data[1]
+ voting=models.Voting.objects.exclude(start_date__isnull=True).get(id=vot_id)
+ msg=aux_message_builder(voting)
+ context.bot.send_message(chat_id=query.message.chat_id,text=msg, parse_mode="HTML")
+
+#opt-in and opt-out for auto notifications
+def change_auto_status(update, context):
+ id=update.message.chat.id
+ status_user=TelegramBot.objects.get(user_id=id)
+ if status_user.auto_msg:
+ choose_msg="Actualmente las notificaciones automáticas se encuentran activadas.\n¿Desea desactivarlas?"
+ keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="False")],
+ [InlineKeyboardButton(text="No", callback_data="True")]]
+ else:
+ choose_msg="Actualmente las notificaciones automáticas se encuentran desactivadas.\n¿Desea activarlas?"
+ keyboard_buttons=[[InlineKeyboardButton(text="Sí", callback_data="True")],
+ [InlineKeyboardButton(text="No", callback_data="False")]]
+ keyboard=InlineKeyboardMarkup(keyboard_buttons)
+ context.bot.send_message(chat_id=id, text=choose_msg, reply_markup=keyboard)
+
+#handler for '/auto' command
+def auto_query_handler(update, context):
+ query=update.callback_query
+ u_id=update.callback_query.message.chat_id
+ msg_id=update.callback_query.message.message_id
+ for id in range(msg_id-2, msg_id+1):
+ context.bot.delete_message(chat_id=u_id, message_id=id)
+ TelegramBot.objects.filter(user_id=u_id).update(auto_msg=query.data)
+ query.answer("¡Listo! He actualizado tus preferencias")
+
+# ===================
+# AUXILIARY METHODS
+# ===================
+
+#auxiliary message to print details from votings
+def aux_message_builder(voting):
+
+ options=list(voting.question.options.values_list('option', flat=True))
+ tally=stmodels.Vote.objects.filter(voting_id=voting.id).values('voter_id').distinct().count() #gets unique votes for a voting
+ start_d=voting.start_date.strftime('%d-%m-%Y %H:%M:%S')+"\n"
+ end_d="Por decidir\n"
+
+ if voting.end_date is not None:
+ end_d=voting.end_date.strftime('%d-%m-%Y %H:%M:%S')+"\n"
+ elif tally is None:
+ tally="Desconocido por el momento"
+
+ opt_msg=""
+ for i,o in enumerate(options,1):
+ opt_msg+=" " + str(i)+". " + o+"\n"
+
+ msg="{}\n\nDescripción: {}\nPregunta: {}\n".format(str(voting.name).upper(), voting.desc, str(voting.question))
+ msg+="Opciones:\n{}\n".format(opt_msg)
+ msg+="Fecha de incio: {}Fecha de finalización: {}Conteo actual: {}".format(start_d, end_d, tally)
+
+ return msg
+
+#extracts graph's images from website selected voting and sends them to the user
+def results_graph(id, chat_identifier, context):
+ url=settings.VISUALIZER_VIEW+ str(id)
+ images=get_graphs(url)
+ if images:
+ media_group=[InputMediaPhoto(media=i) for i in images]
+ context.bot.sendMediaGroup(chat_id=chat_identifier, media=media_group)
+ else:
+ context.bot.send_message(chat_id=chat_identifier,
+ text="Upss! Parece que aún no hay ningún gráfico asociado a esta votación.\nInténtalo de nuevo en otro momento.")
+
+#sends notifications when a new voting is created
+def auto_notifications(voting):
+ users_id_enabled=list(TelegramBot.objects.values_list('user_id', flat=True).exclude(auto_msg=False))
+ msg=aux_message_builder(voting)
+ for id in users_id_enabled:
+ BOT.send_message(chat_id=id, text=msg, parse_mode="HTML")
+
+
diff --git a/decide/visualizer/templates/visualizer/telegram_admin.html b/decide/visualizer/templates/visualizer/telegram_admin.html
new file mode 100644
index 0000000000..661aa1b85e
--- /dev/null
+++ b/decide/visualizer/templates/visualizer/telegram_admin.html
@@ -0,0 +1,13 @@
+{% extends 'admin/change_list.html' %}
+{% load i18n static %}
+
+{% block extrahead %}
+
+{% endblock %}
+
+{% block content %}
+
+ Start Telegram Bot
+