diff --git a/.gitignore b/.gitignore index b823e313b4..9b32acb911 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,5 @@ ENV/ .mypy_cache/ .vagrant + +requirements.txt diff --git a/Procfile b/Procfile new file mode 100644 index 0000000000..813249a6c6 --- /dev/null +++ b/Procfile @@ -0,0 +1,4 @@ +% prepara el repositorio para su despliegue. +release: sh -c 'cd decide && python manage.py migrate' +% especifica el comando para lanzar Decide +web: sh -c 'cd decide && gunicorn decide.wsgi --log-file -' \ No newline at end of file diff --git a/README.md b/README.md index 83d0a57e27..240a10a63e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -[![Build Status](https://travis-ci.com/wadobo/decide.svg?branch=master)](https://travis-ci.com/wadobo/decide) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/94a85eaa0e974c71af6899ea3b0d27e0)](https://www.codacy.com/app/Wadobo/decide?utm_source=github.com&utm_medium=referral&utm_content=wadobo/decide&utm_campaign=Badge_Grade) [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/94a85eaa0e974c71af6899ea3b0d27e0)](https://www.codacy.com/app/Wadobo/decide?utm_source=github.com&utm_medium=referral&utm_content=wadobo/decide&utm_campaign=Badge_Coverage) +[![Python application](https://github.com/Penyagolosa-3/Decide/actions/workflows/django.yml/badge.svg?branch=develop)](https://github.com/Penyagolosa-3/Decide/actions/workflows/django.yml) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/06c453c2aba8409d8c5fbf354704fd65)](https://www.codacy.com/gh/Penyagolosa-3/Decide/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Penyagolosa-3/Decide&utm_campaign=Badge_Grade) +[![Codacy Badge](https://app.codacy.com/project/badge/Coverage/06c453c2aba8409d8c5fbf354704fd65)](https://www.codacy.com/gh/Penyagolosa-3/Decide/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Penyagolosa-3/Decide&utm_campaign=Badge_Coverage) + +[![Python application](https://github.com/Decide-part-Penyagolosa/Decide/actions/workflows/django.yml/badge.svg?branch=deploy)](https://github.com/Decide-part-Penyagolosa/Decide/actions/workflows/django.yml) Plataforma voto electrónico educativa ===================================== diff --git a/decide/authentication/models.py b/decide/authentication/models.py index 71a8362390..0f02227440 100644 --- a/decide/authentication/models.py +++ b/decide/authentication/models.py @@ -1,3 +1 @@ -from django.db import models - -# Create your models here. +from django.db import models \ No newline at end of file diff --git a/decide/authentication/templates/login.html b/decide/authentication/templates/login.html new file mode 100644 index 0000000000..4609a2d5b5 --- /dev/null +++ b/decide/authentication/templates/login.html @@ -0,0 +1,10 @@ +{% load socialaccount %} +{% providers_media_js %} + + + + + +{% if user.is_authenticated %} +estoy autenticado +{% endif %} \ No newline at end of file diff --git a/decide/authentication/urls.py b/decide/authentication/urls.py index d05bfed6fc..c70bcd5175 100644 --- a/decide/authentication/urls.py +++ b/decide/authentication/urls.py @@ -1,12 +1,15 @@ from django.urls import include, path from rest_framework.authtoken.views import obtain_auth_token +from django.views.generic import TemplateView +from django.contrib import admin from .views import GetUserView, LogoutView, RegisterView - urlpatterns = [ path('login/', obtain_auth_token), path('logout/', LogoutView.as_view()), path('getuser/', GetUserView.as_view()), path('register/', RegisterView.as_view()), + path('signin/',TemplateView.as_view(template_name="login.html")), + path('accounts/',include('allauth.urls')), ] diff --git a/decide/base/tests.py b/decide/base/tests.py index 907964aa95..4949c15104 100644 --- a/decide/base/tests.py +++ b/decide/base/tests.py @@ -16,7 +16,7 @@ def setUp(self): user_noadmin.set_password('qwerty') user_noadmin.save() - user_admin = User(username='admin', is_staff=True) + user_admin = User(username='admin', is_staff=True, is_superuser=True) user_admin.set_password('qwerty') user_admin.save() diff --git a/decide/booth/migrations/0001_initial.py b/decide/booth/migrations/0001_initial.py new file mode 100644 index 0000000000..6da7dd5d9e --- /dev/null +++ b/decide/booth/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 2.0 on 2021-12-17 23:56 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('voting', '0003_auto_20180605_0842'), + ] + + operations = [ + migrations.CreateModel( + name='VotingCount', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('option', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='voting.QuestionOption')), + ('voting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='voting.Voting')), + ], + ), + ] diff --git a/decide/booth/migrations/0002_votingcount_created_at.py b/decide/booth/migrations/0002_votingcount_created_at.py new file mode 100644 index 0000000000..0ee06389a4 --- /dev/null +++ b/decide/booth/migrations/0002_votingcount_created_at.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0 on 2021-12-20 16:50 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('booth', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='votingcount', + name='created_at', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/decide/booth/models.py b/decide/booth/models.py index 71a8362390..ca787c7340 100644 --- a/decide/booth/models.py +++ b/decide/booth/models.py @@ -1,3 +1,11 @@ from django.db import models +from django.utils import timezone + +from voting.models import Voting, QuestionOption # Create your models here. + +class VotingCount(models.Model): + voting = models.ForeignKey(Voting, on_delete=models.CASCADE) + option = models.ForeignKey(QuestionOption, on_delete=models.CASCADE) + created_at = models.DateTimeField(default=timezone.now) \ No newline at end of file diff --git a/decide/booth/serializers.py b/decide/booth/serializers.py new file mode 100644 index 0000000000..75fb047db3 --- /dev/null +++ b/decide/booth/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from .models import VotingCount + + +class VotingCountSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = VotingCount + fields = ('id', 'voting_id', 'option_id', 'created_at') diff --git a/decide/booth/templates/booth/booth.html b/decide/booth/templates/booth/booth.html index 9b8706593f..66afbbc56e 100644 --- a/decide/booth/templates/booth/booth.html +++ b/decide/booth/templates/booth/booth.html @@ -29,6 +29,7 @@

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

+ [[ voting.question.desc ]] + + {% extends "base.html" %} {% load i18n static %} @@ -20,9 +28,42 @@

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

Votación no comenzada

-

Votación en curso

+
+ Votación en curso: Información en vivo de votos realizados +
    +
  • [[ opt.option ]]: [[ opt.votingCount ]]
  • +
+ + Votos totales: [[ totalVotes ]] + Censo: [[ census ]] + Restante: [[ census - totalVotes ]] + + + + + + + + + + + + + + + + + + + + + + + +
OpciónPorcentajeVotos
[[opt.option]][[ (((opt.votingCount ?? 0) * 100)/census) ]]%[[opt.votingCount ?? 0]]
Votos realizados: [[ totalVotes ]]Censo: [[ census ]]Votos restantes: [[ census - totalVotes ]]
+
-

Resultados:

+

Resultadoss:

@@ -52,15 +93,304 @@

Resultados:

+ + + + +
+ +
+ + + {% if voting.end_date == null %} + + {% endif %} {% endblock %} diff --git a/decide/visualizer/tests.py b/decide/visualizer/tests.py index 7ce503c2dd..ba46731ebd 100644 --- a/decide/visualizer/tests.py +++ b/decide/visualizer/tests.py @@ -1,3 +1,160 @@ +from voting.tests import VotingTestCase from django.test import TestCase +from django.contrib.staticfiles.testing import StaticLiveServerTestCase -# Create your tests here. +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys + +from base.tests import BaseTestCase +import time + +class AdminTestCase(StaticLiveServerTestCase): + + + def setUp(self): + #Load base test functionality for decide + self.base = BaseTestCase() + self.base.setUp() + + options = webdriver.ChromeOptions() + options.headless = True + self.driver = webdriver.Chrome() + + super().setUp() + + def test_visualizer(self): + self.driver.get(self.live_server_url+'/admin/') + self.driver.find_element_by_id('id_username').send_keys("admin") + self.driver.find_element_by_id('id_password').send_keys("qwerty",Keys.ENTER) + time.sleep(2) + print(self.driver.current_url) + #In case of a correct loging, a element with id 'user-tools' is shown in the upper right part + self.assertTrue(len(self.driver.find_elements_by_id('user-tools'))==1) + + #Vamos a crear la votación + self.driver.find_element(by=By.LINK_TEXT, value="Votings").click() + time.sleep(1) + self.driver.find_element(by=By.XPATH, value="/html/body/div/div[3]/div/ul/li/a").click() + self.driver.find_element_by_id('id_name').send_keys("Votacion de prueba") + self.driver.find_element_by_id('id_desc').send_keys("Vamos a probar si funcionan los tests") + time.sleep(1) + #Almaceno la ventana de la votacion + window_before = self.driver.window_handles[0] + #Añadimos las opciones + self.driver.find_element_by_id('add_id_question').click() + #Almaceno la ventana de las opciones + window_after = self.driver.window_handles[1] + time.sleep(1) + #Cambio de ventana + self.driver.switch_to_window(window_after) + self.driver.find_element_by_id('id_desc').send_keys("¿Funcionan las pruebas de decide?") + time.sleep(1) + self.driver.find_element_by_id('id_options-0-number').send_keys("1") + self.driver.find_element_by_id('id_options-0-option').send_keys("Si") + self.driver.find_element_by_id('id_options-1-number').send_keys("2") + self.driver.find_element_by_id('id_options-1-option').send_keys("No") + self.driver.find_element_by_id('id_options-2-number').send_keys("3") + self.driver.find_element_by_id('id_options-2-option').send_keys("Casi") + self.driver.find_element(by=By.XPATH, value="/html/body/div/div[1]/div/form/div/div[2]/input").click() + time.sleep(1) + #Volvemos a la ventana de las votaciones + self.driver.switch_to_window(window_before) + window_before = self.driver.window_handles[0] + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/form/div/fieldset/div[4]/div/div[1]/a/img').click() + window_after = self.driver.window_handles[1] + self.driver.switch_to_window(window_after) + self.driver.find_element_by_id('id_name').send_keys("http://localhost:8000") + self.driver.find_element_by_id('id_url').send_keys("http://localhost:8000") + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[1]/div/form/div/div/input').click() + self.driver.switch_to_window(window_before) + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/form/div/div/input[1]').click() + time.sleep(1) + self.assertTrue(len(self.driver.find_elements_by_xpath('//*[@id="result_list"]/tbody/tr'))==1) + time.sleep(2) + + #Iniciamos la votación + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/div/form/div[2]/table/tbody/tr[1]/td[1]/input').click() + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/div/form/div[1]/label/select/option[3]').click() + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/div/form/div[1]/button').click() + time.sleep(5) + + #Añadimos usuario + self.driver.find_element_by_link_text('Home').click() + self.driver.find_element_by_link_text('Users').click() + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/ul/li/a').click() + time.sleep(1) + self.driver.find_element_by_id('id_username').send_keys('User1') + self.driver.find_element_by_id('id_password1').send_keys('contraseña1') + self.driver.find_element_by_id('id_password2').send_keys('contraseña1') + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/form/div/div/input[1]').click() + time.sleep(1) + self.driver.find_element_by_link_text('Home').click() + self.driver.find_element_by_link_text('Users').click() + time.sleep(3) + + self.assertTrue(len(self.driver.find_elements_by_xpath('//*[@id="result_list"]/tbody/tr'))==3) + time.sleep(2) + + #Añadimos censo + self.driver.find_element_by_link_text('Home').click() + self.driver.find_element_by_link_text('Censuss').click() + time.sleep(1) + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/ul/li/a').click() + time.sleep(1) + self.driver.find_element_by_id('id_voting_id').send_keys("1") + self.driver.find_element_by_id('id_voter_id').send_keys("2") + self.driver.find_element_by_xpath('/html/body/div/div[3]/div/form/div/div/input[1]').click() + time.sleep(2) + self.assertTrue(len(self.driver.find_elements_by_xpath('//*[@id="result_list"]/tbody/tr'))==1) + + #Se realiza la votacion + #self.driver.get(self.live_server_url+'/booth/1/') + #self.driver.find_element_by_id('username').send_keys("admin") + #self.driver.find_element_by_id('password').send_keys("qwerty", Keys.ENTER) + #time.sleep(1) + #self.driver.find_element_by_id('q1').click() + + #self.driver.find_element_by_xpath('/html/body/div/div/div/button').click() + #time.sleep(2) + + + def tearDown(self): + super().tearDown() + self.driver.quit() + + self.base.tearDown() + + +class VisualizerTestCase(VotingTestCase): + + def setUp(self): + super().setUp() + + def tearDown(self): + super().tearDown() + + def voteTallied(self): + voting = self.create_voting() + self.login() + for action in ['start','stop', 'tally']: + data = {'action': action} + response = self.client.put('/voting/{}/'.format(voting.pk), data, format='json') + return voting.pk + + def test_recibir_votos(self): + votingpk = self.voteTallied() + + data = {'update_id': 339892899, 'message': {'message_id': 285, 'from': {'id': 2004953283, 'is_bot': False, 'first_name': 'Lui', 'language_code': 'es'}, 'chat': {'id': 0, 'first_name': 'Lui', 'type': 'private'}, 'date': 1640018166, 'text': '/start', 'entities': [{'offset': 0, 'length': 6, 'type': 'bot_command'}]}} + response = self.client.put('/webhooks', data, format='json') + self.assertEqual(response.status_code, 301) + data2 = {'update_id': 339892900, 'message': {'message_id': 287, 'from': {'id': 2004953283, 'is_bot': False, 'first_name': 'Lui', 'language_code': 'es'}, 'chat': {'id': 0, 'first_name': 'Lui', 'type': 'private'}, 'date': 1640018174, 'text': '/visualizer {}'.format(votingpk), 'entities': [{'offset': 0, 'length': 11, 'type': 'bot_command'}]}} + response = self.client.post('/webhooks', data2, format='json') + self.assertEqual(response.status_code, 301) diff --git a/decide/visualizer/views.py b/decide/visualizer/views.py index 8fea64ecb2..3992bc8ad3 100644 --- a/decide/visualizer/views.py +++ b/decide/visualizer/views.py @@ -1,11 +1,30 @@ import json from django.views.generic import TemplateView from django.conf import settings -from django.http import Http404 - +from django.http import Http404, JsonResponse from base import mods +import os +import requests +from django.views import View +TELEGRAM_URL = "https://api.telegram.org/bot" +TELEGRAM_BOT_TOKEN = '5024206285:AAHIIblX89BBpbQrL5Pu2kA8CkGZ0qLIXqc' +# +# Bot de telegram en localhost. +# ¡Atención! +# Al completar este tutorial vincularás el bot de Decide a tu localhost y se quitará de heroku. +# +# Requisitos: +# 1.- Instalar ngrok +# 2.- Tener el servidor corriendo en el puerto 8000 +# Uso: +# 1.- Utilizar el comando "$ ngrok http 8000" en la terminal +# 2.- Apuntar la dirección https creada asociada al puerto 8000 +# 3.- Añadir esa dirección a la siguiente url y acceder a ella: +# https://api.telegram.org/bot5024206285:AAHIIblX89BBpbQrL5Pu2kA8CkGZ0qLIXqc/setWebhook?url=/webhooks/ +# 4.- Hablarle al bot de telegram @DecidePenyagolosaBot +# class VisualizerView(TemplateView): template_name = 'visualizer/visualizer.html' @@ -20,3 +39,61 @@ def get_context_data(self, **kwargs): raise Http404 return context + +class TelegramBot(View): + def post(self, request, *args, **kwargs): + t_data = json.loads(request.body) + try: + t_message = t_data["message"] + t_chat = t_message["chat"] + text = t_message["text"].strip().lower() + except Exception as e: + return JsonResponse({"ok": "POST request processed"}) + + command = text.lstrip("/").split()[0] + if command=="start": + rtext = "Usa el comando /visualizer - para consultar una votación" + self.send_message(rtext, t_chat["id"]) + if command=="visualizer": + try: + votingid = text.lstrip("/").split()[1] + + except Exception as e: + return JsonResponse({"ok": "POST request processed"}) + + if votingid.isnumeric(): + rtext = self.returndb(votingid) + else: + rtext = "Por favor introduzca un número valido" + self.send_message(rtext, t_chat["id"]) + + + + return JsonResponse({"ok": "POST request processed"}) + + @staticmethod + def send_message(message, chat_id): + data = { + "chat_id": chat_id, + "text": message, + "parse_mode": "Markdown", + } + response = requests.post( + "{}{}/sendMessage".format(TELEGRAM_URL,TELEGRAM_BOT_TOKEN), data=data + ) + + @staticmethod + def returndb(voting_id): + try: + r = mods.get('voting', params={'id': voting_id}) + voting = json.dumps(r[0]) + except: + return "Voto no finalizado" + voting = json.loads(voting) + text=' La votación "{}" ha tenido los siguientes resultados:\n\n'.format(voting["name"]) + if voting["postproc"] == None: + return ' La votación "{}" no ha hecho el tally.'.format(voting["name"]) + else: + for option in voting["postproc"]: + text+= "La opción {} ha recibido {} votos\n".format(option["option"],option["postproc"]) + return text \ No newline at end of file diff --git a/runtime.txt b/runtime.txt new file mode 100644 index 0000000000..4a9e57338b --- /dev/null +++ b/runtime.txt @@ -0,0 +1 @@ +python-3.7.12 \ No newline at end of file diff --git a/tests/selenium/login_failure.py b/tests/selenium/login_failure.py new file mode 100644 index 0000000000..dc49f7cf36 --- /dev/null +++ b/tests/selenium/login_failure.py @@ -0,0 +1,14 @@ +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys + +options = webdriver.ChromeOptions() +options.headless = True +driver = webdriver.Chrome(options=options) +driver.get("http://localhost:8000/admin/") +driver.find_element_by_id('id_username').send_keys("kwertyx") +driver.find_element_by_id('id_password').send_keys(".qwertyx.",Keys.ENTER) +print('Title: %s' % driver.title) +driver.quit() diff --git a/tests/selenium/login_success.py b/tests/selenium/login_success.py new file mode 100644 index 0000000000..50d45b1640 --- /dev/null +++ b/tests/selenium/login_success.py @@ -0,0 +1,14 @@ +from selenium import webdriver +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys + +options = webdriver.ChromeOptions() +options.headless = True +driver = webdriver.Chrome(options=options) +driver.get("http://localhost:8000/admin/") +driver.find_element_by_id('id_username').send_keys("kwertyx") +driver.find_element_by_id('id_password').send_keys(".Asdasdasd123.",Keys.ENTER) +print('Title: %s' % driver.title) +driver.quit()