From 4fd58cad96eee4871df8982c533d9cb4b77f256e Mon Sep 17 00:00:00 2001 From: alvechdel Date: Wed, 29 Dec 2021 18:56:36 +0100 Subject: [PATCH 1/4] feature-telegramBot-006 Revert settings to use Telegram Bot locally, add scripts to retrieve graphs from visualizer view and model to store them and update requirements --- decide/decide/settings.py | 5 +- decide/visualizer/models.py | 8 ++- .../visualizer/static/scripts/get_graphs.js | 48 +++++++++++++ decide/visualizer/static/visualizer.css | 4 +- decide/visualizer/telegramBot.py | 68 +++++++++++++------ .../templates/visualizer/telegram_admin.html | 3 +- .../templates/visualizer/visualizer.html | 8 +++ decide/visualizer/urls.py | 5 +- decide/visualizer/views.py | 33 +++++++-- decide/visualizer/website_scrapping.py | 17 ----- decide/voting/admin.py | 12 +++- requirements.txt | 3 +- 12 files changed, 157 insertions(+), 57 deletions(-) create mode 100644 decide/visualizer/static/scripts/get_graphs.js delete mode 100644 decide/visualizer/website_scrapping.py diff --git a/decide/decide/settings.py b/decide/decide/settings.py index 005681e78d..556afb4783 100644 --- a/decide/decide/settings.py +++ b/decide/decide/settings.py @@ -22,6 +22,9 @@ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '^##ydkswfu0+=ofw0l#$kv^8n)0$i(qd&d&ol#p9!b$8*5%j1+' +#will change on production +TELEGRAM_TOKEN = '2111051748:AAH1R736I0_HsZEW6_22Tf0r-OqihtF5x88' + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -167,7 +170,7 @@ STATIC_URL = '/static/' -#temporary link to visualizer page for bots (until hosted) +#temporary link to visualizer page for bots 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 diff --git a/decide/visualizer/models.py b/decide/visualizer/models.py index 1c622f6e5a..f958d68d1b 100644 --- a/decide/visualizer/models.py +++ b/decide/visualizer/models.py @@ -8,4 +8,10 @@ def __str__(self): return '{}'.format(self.auto_msg) class Meta: - verbose_name = 'Telegram user' \ No newline at end of file + verbose_name = 'Telegram user' + + + +class Graphs(models.Model): + voting_id=models.BigIntegerField(unique=True) + graphs_url=models.TextField() \ No newline at end of file diff --git a/decide/visualizer/static/scripts/get_graphs.js b/decide/visualizer/static/scripts/get_graphs.js new file mode 100644 index 0000000000..22cb70706e --- /dev/null +++ b/decide/visualizer/static/scripts/get_graphs.js @@ -0,0 +1,48 @@ +//ajax function to send graphs to backend +$(document).ready(function(){ + if (document.body.contains(document.getElementsByTagName("canvas")[0])) { + $('canvas:nth-of-type(2)').addClass(function(){ + const csrf_cookie=getCookie('csrftoken'); + $.ajax({ + url: "graphs/", + type: "POST", + data:{ + graphs:graphs_images() + }, + beforeSend: function(xhr) { + xhr.setRequestHeader("X-CSRFToken", csrf_cookie) + }, + failure: function(data){ + console.log(error) + } + }); + }); + } +}); + +//retrieve canvas graphs as base64 encoded image +function graphs_images(){ + var graphs=document.getElementsByClassName("chartjs-render-monitor"); + var images=[]; + for (var i=0; i< graphs.length; i++){ + images.push(graphs[i].toDataURL()); + } + return images +} + +//function to retrieve csrf_cookie, copied from Django Docs +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} diff --git a/decide/visualizer/static/visualizer.css b/decide/visualizer/static/visualizer.css index 135d61f328..6418444437 100644 --- a/decide/visualizer/static/visualizer.css +++ b/decide/visualizer/static/visualizer.css @@ -11,7 +11,7 @@ color: #fff; } -#start-tlg { +#start_tlg { border: #79aec8; border-radius: 12px; font-size: 14px; @@ -26,6 +26,6 @@ -o-border-radius: 12px; } -#start-tlg:hover{ +#start_tlg:hover{ background-color: #417690; } \ No newline at end of file diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index f9e2fef73f..2976a74c11 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -5,24 +5,21 @@ 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 +import os, sys, base64 +from .models import TelegramBot, Graphs from threading import Thread +from selenium import webdriver #auth and front-end for '@VotitosBot' -UPDATER = Updater(os.environ.get('TELEGRAM_TOKEN'), +UPDATER = Updater(settings.TELEGRAM_TOKEN, use_context=True) -BOT=Bot(token=os.environ.get('TELEGRAM_TOKEN')) +BOT=Bot(token=settings.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 @@ -149,16 +146,20 @@ def auto_query_handler(update, context): 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 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 + options=list(voting.question.options.values_list('option', flat=True)) + if stmodels.Vote.objects.filter(voting_id=voting.id).exists(): + tally=stmodels.Vote.objects.filter(voting_id=voting.id).values('voter_id').distinct().count() #gets unique votes for a voting + else: + tally=0 start_d=voting.start_date.strftime('%d-%m-%Y %H:%M:%S')+"\n" end_d="Por decidir\n" @@ -177,17 +178,42 @@ def aux_message_builder(voting): 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) + open_graphs_generator_view(id) + if Graphs.objects.filter(voting_id=id).exists(): + graphs_base64=Graphs.objects.filter(voting_id=id).values('graphs_url') + try: + base64_url_list=eval(graphs_base64[0]['graphs_url']) + b64_images=[] + media_group=[] + for i in range(0,len(base64_url_list)): + b64_images.append(base64_url_list[i].split(",")[1]) + path="graph_"+str(id)+"_"+str(i)+".png" + with open(path,"wb") as f: + f.write(base64.b64decode(b64_images[i])) + media_group.append(InputMediaPhoto(media=open(path, 'rb'))) + os.remove(path) + context.bot.sendMediaGroup(chat_id=chat_identifier, media=media_group) + except: + context.bot.send_message(chat_id=chat_identifier, + text="Vaya...no hay gráficas disponibles para mostrar.\nInténtalo de nuevo más tarde.") + 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.") - + text="Upss! Parece que aún no hay ninguna gráfica asociada a esta votación.\nInténtalo de nuevo en otro momento.") + + +#uses selenium to call view which generates voting graphs +def open_graphs_generator_view(id): + options = webdriver.ChromeOptions() + options.add_argument("--headless") + driver=webdriver.Chrome(options=options) + driver.get('http://127.0.0.1:8000/visualizer/' + str(id)) #VISUALIZER_VIEW will be taken from setting in production + + + #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)) diff --git a/decide/visualizer/templates/visualizer/telegram_admin.html b/decide/visualizer/templates/visualizer/telegram_admin.html index 661aa1b85e..c1760c0ab4 100644 --- a/decide/visualizer/templates/visualizer/telegram_admin.html +++ b/decide/visualizer/templates/visualizer/telegram_admin.html @@ -7,7 +7,8 @@ {% block content %}
- Start Telegram Bot + Start Telegram Bot

{{ block.super }} + {% endblock %} \ No newline at end of file diff --git a/decide/visualizer/templates/visualizer/visualizer.html b/decide/visualizer/templates/visualizer/visualizer.html index 6ded3da310..4ee86900f9 100644 --- a/decide/visualizer/templates/visualizer/visualizer.html +++ b/decide/visualizer/templates/visualizer/visualizer.html @@ -11,6 +11,7 @@ {% endblock %} {% block content %} + {% csrf_token %}
@@ -60,12 +61,17 @@

Resultados:

{% block extrabody %} + + + + + + {% endblock %} \ No newline at end of file diff --git a/decide/visualizer/urls.py b/decide/visualizer/urls.py index 1b58b8b407..f2f07810bd 100644 --- a/decide/visualizer/urls.py +++ b/decide/visualizer/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import VisualizerView, initialize +from .views import VisualizerView, initialize, graphs_requests urlpatterns = [ path('/', VisualizerView.as_view()), - path('startTelegram/', initialize, name="start_telegram") + path('startTelegram/', initialize, name="start_telegram"), + path('/graphs/', graphs_requests), ] diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 7734fd2f14..84dbc7b3ec 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -1,17 +1,20 @@ -import json -from django.http.response import HttpResponse, HttpResponseRedirect +from django.http.response import HttpResponse, HttpResponseRedirect, JsonResponse from django.views.generic import TemplateView from django.conf import settings from django.http import Http404 -import ast from base import mods from collections import OrderedDict + from .telegramBot import init_bot +import json +from .models import Graphs +TELEGRAM_BOT_STATUS=False class VisualizerView(TemplateView): - template_name = 'visualizer/visualizer' + template_name = 'visualizer/visualizer.html' + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) vid = kwargs.get('voting_id', 0) @@ -27,10 +30,26 @@ def get_context_data(self, **kwargs): raise Http404 return context - + + def initialize(request): #call to initalize telegram bot - init_bot() + global TELEGRAM_BOT_STATUS + if not TELEGRAM_BOT_STATUS: + init_bot() + TELEGRAM_BOT_STATUS= True return HttpResponseRedirect(request.META.get('HTTP_REFERER')) - + +def graphs_requests(request, voting_id): + if request.method == 'POST': + data=request.POST.getlist('graphs[]') + if Graphs.objects.filter(voting_id=voting_id).exists(): + Graphs.objects.filter(voting_id=voting_id).update(graphs_url=data) + else: + Graphs.objects.create(voting_id=voting_id, graphs_url=data) + return HttpResponse() + + if request.method == 'GET': + data=list(Graphs.objects.filter(voting_id=voting_id).values('voting_id', 'graphs_url')) + return HttpResponse(json.dumps(data), content_type="application/json") diff --git a/decide/visualizer/website_scrapping.py b/decide/visualizer/website_scrapping.py deleted file mode 100644 index 4fb5f62c7c..0000000000 --- a/decide/visualizer/website_scrapping.py +++ /dev/null @@ -1,17 +0,0 @@ -from bs4 import BeautifulSoup as bs -import urllib.request as request -from urllib.parse import urlparse - -#returns images of a voting -def get_graphs(link): - file=request.urlopen(link) - s=bs(file, "lxml") - images=s.find_all("img") - urls=[] - for img in images: - img_url=img.attrs.get("src") - if not img_url: - continue - if len(urls) == 0: - urls=False - return urls \ No newline at end of file diff --git a/decide/voting/admin.py b/decide/voting/admin.py index e94f9cfc38..11cfdf0012 100644 --- a/decide/voting/admin.py +++ b/decide/voting/admin.py @@ -4,8 +4,9 @@ from .models import QuestionOption from .models import Question from .models import Voting -from visualizer.telegramBot import auto_notifications from .filters import StartedFilter +from visualizer.telegramBot import auto_notifications +from visualizer.views import TELEGRAM_BOT_STATUS def start(modeladmin, request, queryset): @@ -13,14 +14,14 @@ def start(modeladmin, request, queryset): v.create_pubkey() v.start_date = timezone.now() v.save() - #for users who have auto notifications enabled - auto_notifications(v) + send_notifications(v) def stop(ModelAdmin, request, queryset): for v in queryset.all(): v.end_date = timezone.now() v.save() + send_notifications(v) def tally(ModelAdmin, request, queryset): @@ -28,6 +29,11 @@ def tally(ModelAdmin, request, queryset): token = request.session.get('auth-token', '') v.tally_votes(token) +#for users who have auto notifications enabled +def send_notifications(v): + global TELEGRAM_BOT_STATUS + if TELEGRAM_BOT_STATUS: + auto_notifications(v) class QuestionOptionInline(admin.TabularInline): model = QuestionOption diff --git a/requirements.txt b/requirements.txt index 074faa31fc..1ea45b13ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,4 @@ pymongo==3.12.1 six==1.16.0 sqlparse==0.2.4 python-telegram-bot==13.9.0 -bs4 - +selenium==3.9.0 From d3812906eb4b03c3c64b22768f7007c69eae5cf6 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Fri, 31 Dec 2021 03:05:46 +0100 Subject: [PATCH 2/4] feature-telegramBot-007 Update graphs script to fix unwanted behaviour, refactor bot commands to add voting type's selection menu and fix auto notification when votings are started or closed --- .../visualizer/static/scripts/get_graphs.js | 3 +- decide/visualizer/telegramBot.py | 141 +++++++++++++----- decide/visualizer/views.py | 2 +- decide/voting/admin.py | 6 +- 4 files changed, 105 insertions(+), 47 deletions(-) diff --git a/decide/visualizer/static/scripts/get_graphs.js b/decide/visualizer/static/scripts/get_graphs.js index 22cb70706e..492a8c5c69 100644 --- a/decide/visualizer/static/scripts/get_graphs.js +++ b/decide/visualizer/static/scripts/get_graphs.js @@ -1,6 +1,7 @@ //ajax function to send graphs to backend $(document).ready(function(){ - if (document.body.contains(document.getElementsByTagName("canvas")[0])) { + var canvas_elements=document.getElementsByClassName("chartjs-render-monitor") + if (canvas_elements.length==2) { $('canvas:nth-of-type(2)').addClass(function(){ const csrf_cookie=getCookie('csrftoken'); $.ajax({ diff --git a/decide/visualizer/telegramBot.py b/decide/visualizer/telegramBot.py index b0b542d507..a390b42e53 100644 --- a/decide/visualizer/telegramBot.py +++ b/decide/visualizer/telegramBot.py @@ -9,6 +9,7 @@ from .models import TelegramBot, Graphs from threading import Thread from selenium import webdriver +from functools import partial #This is token is temporary and won't be the same in final version, so security won't be compromised @@ -31,15 +32,16 @@ def setup_commands(votitos): dp=votitos.dispatcher dp.add_handler(CommandHandler('start', start)) + dp.add_handler(CommandHandler('help', help)) 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('results', partial(voting_selection_menu, command_name='results'))) + dp.add_handler(CommandHandler('details', partial(voting_selection_menu, command_name='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(voting_selection_query_handler, pattern="^v_[a-zA-Z]+_[a-zA-Z]+$")) 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(details_query_handler, pattern="^d[1-9][0-9]*_[a-zA-Z]+$")) dp.add_handler(CallbackQueryHandler(auto_query_handler, pattern="(^True$|^False$)")) #set bot configuration not to reply to old messages @@ -55,7 +57,7 @@ def start(update, context): 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) + help(update, context) # relaunch the bot and also the whole project (limited to admin) def relaunch(update, context): @@ -71,7 +73,7 @@ def stop(update, context): UPDATER.stop() #list of available commands -def help(update): +def help(update, context): update.message.reply_text("""Esta es mi lista de comandos: /start - Inicia la interacción conmigo @@ -85,42 +87,49 @@ 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) +def show_results(update, context, chat_identifier, vot_type): + votings=get_voting_objects(vot_type) + finished_votings=votings.exclude(start_date__isnull=True).exclude(end_date__isnull=True) + if finished_votings is not None: + 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=chat_identifier, text= "Aquí tienes la lista de votaciones finalizadas. Elige por favor:", reply_markup=keyboard) + else: + context.bot.send_message(chat_id=chat_identifier, text= "Vaya...no hay ninguna votación de este tipo que haya finalizado.\nInténtalo de nuevo en otro momemnto") #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) + results_graph(query.data, 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)] - +def show_details(update, context, chat_identifier,vot_type): + + votings=get_voting_objects(vot_type) + started_votings=votings.exclude(start_date__isnull=True) + type=vot_type.split("_")[1] + if started_votings is not None: + keyboard_buttons=[InlineKeyboardButton(text=str(v.name), callback_data="d"+str(v.id)+"_"+type) for v in started_votings] + keyboard=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2)) + context.bot.send_message(chat_id=chat_identifier, text="Selecciona la votación de la que desea ver sus detalles", reply_markup=keyboard) + else: + context.bot.send_message(chat_id=chat_identifier, text= "Vaya...no hay ninguna votación de este tipo ahora mismo.\nInténtalo de nuevo en otro momemnto") + #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) + response_array=query.data.split("_") + vot_id=response_array[0][1] + vot_type=response_array[1] + + voting=get_voting_objects(vot_type).exclude(start_date__isnull=True).get(id=vot_id) + v_type=translate_type(vot_type) + msg=aux_message_builder(voting,v_type) context.bot.send_message(chat_id=query.message.chat_id,text=msg, parse_mode="HTML") #opt-in and opt-out for auto notifications @@ -129,21 +138,22 @@ def change_auto_status(update, context): 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")]] + keyboard_buttons=build_keyboard_menu([InlineKeyboardButton(text="Sí", callback_data="False"), + InlineKeyboardButton(text="No", callback_data="True")], 2) + 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_buttons=build_keyboard_menu([InlineKeyboardButton(text="Sí", callback_data="True"), + InlineKeyboardButton(text="No", callback_data="False")], 2) 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): + u_id=query.message.chat_id + msg_id=query.message.message_id + for id in range(msg_id, 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") @@ -153,12 +163,61 @@ def auto_query_handler(update, context): # AUXILIARY METHODS # ===================== +#auxiliary keyboard to select voting type and get command name +def voting_selection_menu(update, context, command_name): + keyboard_buttons = [InlineKeyboardButton('Votación simple', callback_data='v_simple_'+command_name), + InlineKeyboardButton('Votación binaria', callback_data='v_binary_'+command_name), + InlineKeyboardButton('Votación múltiple', callback_data='v_multiple_'+command_name), + InlineKeyboardButton('Votación por puntuación', callback_data='v_score_'+command_name)] + context.bot.send_message(chat_id=update.message.chat.id, text="Elige el tipo de votación:", reply_markup=InlineKeyboardMarkup(build_keyboard_menu(keyboard_buttons,2))) + + +#handler for voting selection +def voting_selection_query_handler(update, context): + query=update.callback_query + vot_type_command=query.data + if 'details' in vot_type_command: + show_details(update, context, query.message.chat_id, vot_type_command) + elif 'results' in vot_type_command: + show_results(update, context, query.message.chat_id, vot_type_command) + query.answer() + +#gets voting type objects +def get_voting_objects(vot_type): + res=None + if 'simple' in vot_type: + res=models.Voting.objects + elif 'binary' in vot_type: + res=models.BinaryVoting.objects + elif 'multiple' in vot_type: + res=models.MultipleVoting.objects + elif 'score' in vot_type: + res=models.ScoreVoting.objects + return res + +#translate vot_type var to actual voting type name +def translate_type(vot_type): + res=None + if 'simple' in vot_type: + res=('V', 'Voting') + elif 'binary' in vot_type: + res=('BV', 'BinaryVoting') + elif 'multiple' in vot_type: + res=('MV', 'MultipleVoting') + elif 'score' in vot_type: + res=('SV', 'ScoreVoting') + return res + +#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)] + #auxiliary message to print details from votings -def aux_message_builder(voting): +def aux_message_builder(voting, vot_type): options=list(voting.question.options.values_list('option', flat=True)) - if stmodels.Vote.objects.filter(voting_id=voting.id).exists(): - tally=stmodels.Vote.objects.filter(voting_id=voting.id).values('voter_id').distinct().count() #gets unique votes for a voting + if stmodels.Vote.objects.filter(voting_id=voting.id, type=vot_type).exists(): + tally=stmodels.Vote.objects.filter(voting_id=voting.id, type=vot_type).values('voter_id').distinct().count() else: tally=0 start_d=voting.start_date.strftime('%d-%m-%Y %H:%M:%S')+"\n" @@ -218,7 +277,7 @@ def open_graphs_generator_view(id): #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) + msg=aux_message_builder(voting, voting.type) for id in users_id_enabled: BOT.send_message(chat_id=id, text=msg, parse_mode="HTML") diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 84dbc7b3ec..2f87d18342 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -37,7 +37,7 @@ def initialize(request): global TELEGRAM_BOT_STATUS if not TELEGRAM_BOT_STATUS: init_bot() - TELEGRAM_BOT_STATUS= True + TELEGRAM_BOT_STATUS=True return HttpResponseRedirect(request.META.get('HTTP_REFERER')) def graphs_requests(request, voting_id): diff --git a/decide/voting/admin.py b/decide/voting/admin.py index c32df17348..978e519a95 100644 --- a/decide/voting/admin.py +++ b/decide/voting/admin.py @@ -6,8 +6,7 @@ from .models import Voting, BinaryVoting, ScoreVoting from .filters import StartedFilter from visualizer.telegramBot import auto_notifications -from visualizer.views import TELEGRAM_BOT_STATUS - +import visualizer.views def start(modeladmin, request, queryset): for v in queryset.all(): @@ -31,8 +30,7 @@ def tally(ModelAdmin, request, queryset): #for users who have auto notifications enabled def send_notifications(v): - global TELEGRAM_BOT_STATUS - if TELEGRAM_BOT_STATUS: + if visualizer.views.TELEGRAM_BOT_STATUS: auto_notifications(v) class QuestionOptionInline(admin.TabularInline): From 02030225f5bc807d3ff1f6bd224644e990339a06 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Mon, 3 Jan 2022 00:41:32 +0100 Subject: [PATCH 3/4] feature-telegramBot-008 Change timezome to Madrid in Django Server, add urls to all voting type graphs, add script for graphs to all html voting related views and add margin to graphs canvas --- decide/decide/settings.py | 4 ++-- decide/visualizer/models.py | 8 +++++-- .../visualizer/static/scripts/get_graphs.js | 11 +++++---- decide/visualizer/static/visualizer.css | 5 +++- .../templates/visualizer/telegram_admin.html | 2 +- .../templates/visualizer/visualizer.html | 1 + .../visualizer/visualizer_binary.html | 17 ++++++++++---- .../visualizer/visualizer_multiple.html | 16 ++++++++++--- .../visualizer/visualizer_scoring.html | 23 +++++++++++++------ decide/visualizer/urls.py | 7 ++++-- decide/visualizer/views.py | 16 +++++++------ decide/voting/views.py | 4 ++-- 12 files changed, 79 insertions(+), 35 deletions(-) diff --git a/decide/decide/settings.py b/decide/decide/settings.py index 556afb4783..db1f621df5 100644 --- a/decide/decide/settings.py +++ b/decide/decide/settings.py @@ -154,7 +154,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Europe/Madrid' USE_I18N = True @@ -194,4 +194,4 @@ vars()[k] = v -INSTALLED_APPS = INSTALLED_APPS + MODULES +INSTALLED_APPS = INSTALLED_APPS + MODULES \ No newline at end of file diff --git a/decide/visualizer/models.py b/decide/visualizer/models.py index f958d68d1b..392b583b01 100644 --- a/decide/visualizer/models.py +++ b/decide/visualizer/models.py @@ -13,5 +13,9 @@ class Meta: class Graphs(models.Model): - voting_id=models.BigIntegerField(unique=True) - graphs_url=models.TextField() \ No newline at end of file + voting_id=models.BigIntegerField() + voting_type=models.CharField(max_length=30, default='V') + graphs_url=models.TextField(null=True, blank=True) + + class Meta: + unique_together = ('voting_id', 'voting_type',) \ No newline at end of file diff --git a/decide/visualizer/static/scripts/get_graphs.js b/decide/visualizer/static/scripts/get_graphs.js index 492a8c5c69..b9881afb85 100644 --- a/decide/visualizer/static/scripts/get_graphs.js +++ b/decide/visualizer/static/scripts/get_graphs.js @@ -1,14 +1,17 @@ //ajax function to send graphs to backend -$(document).ready(function(){ +$(document).ready(async function(){ + await new Promise(r => setTimeout(r, 1500)); var canvas_elements=document.getElementsByClassName("chartjs-render-monitor") if (canvas_elements.length==2) { - $('canvas:nth-of-type(2)').addClass(function(){ + $('#bar-chart').addClass(function(){ const csrf_cookie=getCookie('csrftoken'); $.ajax({ url: "graphs/", type: "POST", + dataType:"json", data:{ - graphs:graphs_images() + type:$('#vot_type').val(), + graphs:graphs_images(), }, beforeSend: function(xhr) { xhr.setRequestHeader("X-CSRFToken", csrf_cookie) @@ -46,4 +49,4 @@ function getCookie(name) { } } return cookieValue; -} +} \ No newline at end of file diff --git a/decide/visualizer/static/visualizer.css b/decide/visualizer/static/visualizer.css index 6418444437..afffc46dbe 100644 --- a/decide/visualizer/static/visualizer.css +++ b/decide/visualizer/static/visualizer.css @@ -2,11 +2,11 @@ position: fixed; width: 100%; bottom : 0px; + margin-top: 10px; 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; } @@ -28,4 +28,7 @@ #start_tlg:hover{ background-color: #417690; +} +.chartjs-render-monitor{ + margin: 77px 10px; } \ No newline at end of file diff --git a/decide/visualizer/templates/visualizer/telegram_admin.html b/decide/visualizer/templates/visualizer/telegram_admin.html index c1760c0ab4..ba17088d08 100644 --- a/decide/visualizer/templates/visualizer/telegram_admin.html +++ b/decide/visualizer/templates/visualizer/telegram_admin.html @@ -7,7 +7,7 @@ {% block content %}
- Start Telegram Bot + Start Telegram Bot

{{ block.super }} diff --git a/decide/visualizer/templates/visualizer/visualizer.html b/decide/visualizer/templates/visualizer/visualizer.html index 5e73db477e..1aa9681c70 100644 --- a/decide/visualizer/templates/visualizer/visualizer.html +++ b/decide/visualizer/templates/visualizer/visualizer.html @@ -19,6 +19,7 @@
+

[[ voting[0]["id"] ]] - [[ voting[0]["name"] ]]

Votación no comenzada

diff --git a/decide/visualizer/templates/visualizer/visualizer_binary.html b/decide/visualizer/templates/visualizer/visualizer_binary.html index c01846cde5..be5309ec1d 100644 --- a/decide/visualizer/templates/visualizer/visualizer_binary.html +++ b/decide/visualizer/templates/visualizer/visualizer_binary.html @@ -7,10 +7,12 @@ + {% endblock %} {% block content %} + {% csrf_token %}
@@ -18,6 +20,7 @@
+

[[ voting.id ]] - [[ voting.name ]]

Votación no comenzada

@@ -47,13 +50,20 @@

Resultados:

- - +
+ +
{% endblock %} {% block extrabody %} + + + @@ -146,7 +156,6 @@

Resultados:

- + {% endblock %} \ No newline at end of file diff --git a/decide/visualizer/templates/visualizer/visualizer_multiple.html b/decide/visualizer/templates/visualizer/visualizer_multiple.html index a05f0b329d..9ceeba3f33 100644 --- a/decide/visualizer/templates/visualizer/visualizer_multiple.html +++ b/decide/visualizer/templates/visualizer/visualizer_multiple.html @@ -7,10 +7,12 @@ + {% endblock %} {% block content %} + {% csrf_token %}
@@ -18,6 +20,7 @@
+

[[ voting.id ]] - [[ voting.name ]]

Votación no comenzada

@@ -47,13 +50,21 @@

Resultados:

- +
+ +
{% endblock %} {% block extrabody %} + + + @@ -146,7 +157,6 @@

Resultados:

- + {% endblock %} \ No newline at end of file diff --git a/decide/visualizer/templates/visualizer/visualizer_scoring.html b/decide/visualizer/templates/visualizer/visualizer_scoring.html index c53b3dabda..48600613cb 100644 --- a/decide/visualizer/templates/visualizer/visualizer_scoring.html +++ b/decide/visualizer/templates/visualizer/visualizer_scoring.html @@ -7,10 +7,12 @@ + {% endblock %} {% block content %} + {% csrf_token %}
@@ -18,6 +20,7 @@
+

[[ voting.id ]] - [[ voting.name ]]

Votación no comenzada

@@ -42,18 +45,25 @@

Resultados:

Descargar datos en CSV - + +
-
- - +
+ +
{% endblock %} {% block extrabody %} + + + @@ -146,7 +156,6 @@

Resultados:

- - + + {% endblock %} \ No newline at end of file diff --git a/decide/visualizer/urls.py b/decide/visualizer/urls.py index d1515a1e52..4621a7212d 100644 --- a/decide/visualizer/urls.py +++ b/decide/visualizer/urls.py @@ -13,6 +13,9 @@ path('multipleVoting//', VisualizerViewMultiple.as_view()), path('votes/multipleVoting//', VotesMultiple_csv.as_view()), path('startTelegram/', initialize, name="start_telegram"), - path('/graphs/', graphs_requests) + path('/graphs/', graphs_requests), + path('binaryVoting//graphs/', graphs_requests), + path('scoringVoting//graphs/', graphs_requests), + path('multipleVoting//graphs/', graphs_requests), -] +] \ No newline at end of file diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 1beff28c41..599a5010af 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -1,4 +1,4 @@ -from django.http.response import HttpResponse, HttpResponseRedirect, JsonResponse +from django.http.response import HttpResponse, HttpResponseRedirect from django.views.generic import TemplateView from django.conf import settings from django.http import Http404 @@ -161,13 +161,15 @@ def initialize(request): def graphs_requests(request, voting_id): if request.method == 'POST': - data=request.POST.getlist('graphs[]') - if Graphs.objects.filter(voting_id=voting_id).exists(): - Graphs.objects.filter(voting_id=voting_id).update(graphs_url=data) + vot_type=request.POST.get('type') + urls=request.POST.getlist('graphs[]') + if Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).exists(): + Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).update(voting_type=vot_type, graphs_url=urls) else: - Graphs.objects.create(voting_id=voting_id, graphs_url=data) + Graphs.objects.create(voting_id=voting_id, voting_type=vot_type, graphs_url=urls) return HttpResponse() - if request.method == 'GET': - data=list(Graphs.objects.filter(voting_id=voting_id).values('voting_id', 'graphs_url')) + if request.method == 'GET': + vot_type=request.GET.get('type') + data=list(Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).values('voting_id', 'voting_type','graphs_url')) return HttpResponse(json.dumps(data), content_type="application/json") \ No newline at end of file diff --git a/decide/voting/views.py b/decide/voting/views.py index a174bab130..e583977430 100644 --- a/decide/voting/views.py +++ b/decide/voting/views.py @@ -6,8 +6,8 @@ from rest_framework import generics, status from rest_framework.response import Response -from .models import BinaryQuestion, BinaryQuestionOption, BinaryVoting, MultipleQuestion, MultipleQuestionOption, MultipleVoting, Question, QuestionOption, ScoreVoting, Voting -from .serializers import BinaryVotingSerializer, MultipleVotingSerializer, SimpleBinaryVotingSerializer, SimpleMultipleVotingSerializer, SimpleVotingSerializer, VotingSerializer +from .models import BinaryQuestion, BinaryQuestionOption, BinaryVoting, MultipleQuestion, MultipleQuestionOption, MultipleVoting, Question, QuestionOption, ScoreQuestion, ScoreQuestionOption, ScoreVoting, Voting +from .serializers import BinaryVotingSerializer, MultipleVotingSerializer, SimpleBinaryVotingSerializer, SimpleMultipleVotingSerializer, SimpleVotingSerializer, VotingSerializer, ScoreVotingSerializer from base.perms import UserIsStaff from base.models import Auth From 99279241700bd4d494318906c39c5e9350787924 Mon Sep 17 00:00:00 2001 From: alvechdel Date: Mon, 3 Jan 2022 15:28:10 +0100 Subject: [PATCH 4/4] feature-telegramBot-009 Add final fix to visualizer view regarding Django .update() and .save() behaviour and fix bug in request GET --- decide/visualizer/views.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 599a5010af..f82fc0c17a 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -150,26 +150,40 @@ def get(self,request,voting_id,*args,**kwargs): for vote in voting.postproc: csv_file.writerow([vote["option"], vote["postproc"], vote["votes"]]) return res - + def initialize(request): #call to initalize telegram bot global TELEGRAM_BOT_STATUS if not TELEGRAM_BOT_STATUS: init_bot() TELEGRAM_BOT_STATUS=True - return HttpResponseRedirect(request.META.get('HTTP_REFERER')) + return HttpResponseRedirect(request.META.get('HTTP_REFERER')) def graphs_requests(request, voting_id): if request.method == 'POST': vot_type=request.POST.get('type') urls=request.POST.getlist('graphs[]') if Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).exists(): - Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).update(voting_type=vot_type, graphs_url=urls) + to_update=Graphs.objects.get(voting_id=voting_id, voting_type=vot_type) + to_update.voting_type=vot_type + to_update.graphs_url=urls + to_update.save() else: Graphs.objects.create(voting_id=voting_id, voting_type=vot_type, graphs_url=urls) return HttpResponse() if request.method == 'GET': - vot_type=request.GET.get('type') - data=list(Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).values('voting_id', 'voting_type','graphs_url')) - return HttpResponse(json.dumps(data), content_type="application/json") \ No newline at end of file + vot_type=translate_type(request.path_info) + data=list(Graphs.objects.filter(voting_id=voting_id, voting_type=vot_type).values('voting_id', 'voting_type','graphs_url')) + return HttpResponse(json.dumps(data), content_type="application/json") + +#translate path to vot_type +def translate_type(path_url): + vot_type='V' + if 'binaryVoting' in path_url: + vot_type='BV' + elif 'multiple' in path_url: + vot_type='MV' + elif 'score' in path_url: + vot_type='SV' + return vot_type \ No newline at end of file