Skip to content

Commit

Permalink
Merge pull request #12 from wadobo/master
Browse files Browse the repository at this point in the history
Decide 2019/2020 merge
  • Loading branch information
benavidescuevas authored Sep 19, 2019
2 parents 213dc89 + 2b873b4 commit fc64ea5
Show file tree
Hide file tree
Showing 41 changed files with 1,147 additions and 171 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,5 @@ ENV/

# mypy
.mypy_cache/

.vagrant
21 changes: 21 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
dist: xenial

services:
- postgresql
addons:
postgresql: "9.4"
before_script:
- psql -U postgres -c "create user decide password 'decide'"
- psql -U postgres -c "create database test_decide owner decide"
- psql -U postgres -c "ALTER USER decide CREATEDB"
language: python
python:
- "3.6"
install:
- pip install -r requirements.txt
- pip install codacy-coverage
script:
- cd decide
- coverage run --branch --source=. ./manage.py test --keepdb --with-xunit
- coverage xml
- python-codacy-coverage -r coverage.xml
196 changes: 196 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
[![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)

Plataforma voto electrónico educativa
=====================================

Expand All @@ -19,6 +21,33 @@ entre ellos. Para conseguir esto, los subsistemas se conectarán entre si median

Este proyecto Django estará dividido en apps (subsistemas y proyecto base), donde cualquier app podrá ser reemplazada individualmente.

Gateway
-------

Para ofrecer un punto de entrada conocido para todos los subsistemas
existe el llamado **gateway** que no es más que una ruta disponible
que redirigirá todas las peticiones al subsistema correspondiente, de
tal forma que cualquier cliente que use la API no tiene por qué saber
en qué servidor está desplegado cada subsistema.

La ruta se compone de:

http://DOMINIO/gateway/SUBSISTEMA/RUTA/EN/EL/SUBSISTEMA

Por ejemplo para acceder al subsistema de autenticación y hacer la petición
al endpoint de /authentication/login/ deberíamos hacer la petición a la
siguiente ruta:

http://DOMINIO/gateway/authentication/login/

Otro ejemplo sería para obtener una votación por id:

http://DOMINIO/gateway/voting/?id=1

A nivel interno, el módulo `mods` ofrece esta funcionalidad, pero el
gateway es útil para hacer uso desde peticiones de cliente, por ejemplo
en el javascript de la cabina de votación o la visualización de resultados,
y también para módulos externos que no sean aplicaciones django.

Configurar y ejecutar el proyecto
---------------------------------
Expand Down Expand Up @@ -94,3 +123,170 @@ Lanzar tests:
Lanzar una consola SQL:

$ docker exec -ti decide_db ash -c "su - postgres -c 'psql postgres'"

Ejecutar con vagrant + ansible
------------------------------

Existe una configuración de vagrant que crea una máquina virtual con todo
lo necesario instalado y listo para funcionar. La configuración está en
vagrant/Vagrantfile y por defecto utiliza Virtualbox, por lo que para
que esto funcione debes tener instalado en tu sistema vagrant y Virtualbox.

Crear la máquina virtual con vagrant:

$ cd vagrant
$ vagrant up

Una vez creada podremos acceder a la web, con el usuario admin/admin:

http://localhost:8080/admin

Acceder por ssh a la máquina:

$ vagrant ssh

Esto nos dará una consola con el usuario vagrant, que tiene permisos de
sudo, por lo que podremos acceder al usuario administrador con:

$ sudo su

Parar la máquina virtual:

$ vagrant stop

Una vez parada la máquina podemos volver a lanzarla con `vagrant up`.

Eliminar la máquina virtual:

$ vagrant destroy

Ansible
-------

El provisionamiento de la aplicación con vagrant está hecho con Ansible,
algo que nos permite utilizarlo de forma independiente para provisionar
una instalación de Decide en uno o varios servidores remotos con un
simple comando.

$ cd vagrant
$ ansible-playbook -i inventory playbook.yml

Para que esto funcione debes definir un fichero [inventory](https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html)
con los servidores destino.

Los scripts de ansible están divididos en varios ficheros .yml donde
se definen las diferentes tareas, por lo que es posible lanzar partes
independientes:

* packages.yml, dependencias del sistema
* user.yml, creación de usuario decide
* python.yml, git clone del repositorio e instalación de dependencias python en virtualenv
* files.yml, ficheros de configuración, systemd, nginx y local\_settings.py
* database.yml, creación de usuario y base de datos postgres
* django.yml, comandos django básicos y creación de usuario admin
* services.yml, reinicio de servicios, decide, nginx y postgres

Por ejemplo este comando sólo reinicia los servicios en el servidor:

$ ansible-playbook -i inventory -t services

El provisionamiento de ansible está diseñado para funcionar con **ubuntu/bionic64**,
para funcionar con otras distribuciones es posible que haga falta modificar
el fichero packages.yml.

Versionado
----------

El versionado de API está hecho utilizando Django Rest Framework, y la forma
elegida para este versionado es mediante [parámetros de búsqueda](https://www.django-rest-framework.org/api-guide/versioning/#queryparameterversioning),
podemos cambiarlo a parámetros en la URL o en el nombre del HOST, hay diferentes
tipos de versionado disponibles en Django Rest Framework, podemos verlos
[aqui](https://www.django-rest-framework.org/api-guide/versioning/#versioning).

Nosotros hemos escogido el de por parámetros por ser el más sencillo, y hemos
creado un ejemplo para que veamos su uso, podemos verlo en voting/views.py

Si nosotros queremos que la salida que nos da la llamada a la API /voting/, sea
diferente en la versión 2, solo tenemos que comprobar en la versión nos está
llegando, y hacer lo que queramos, por ejemplo:


```
def get(self, request, *args, **kwargs):
version = request.version # Con request.version obtenemos la versión
if version not in settings.ALLOWED_VERSIONS: # Versiones permitidas
version = settings.DEFAULT_VERSION # Si no existe: versión por defecto
# En el caso de usar la versión 2, usamos un serializador diferente
if version == 'v2':
self.serializer_class = SimpleVotingSerializer
return super().get(request, *args, **kwargs)
```

Para llamar a las diferentes versiones, haremos lo siguiente:

* /voting/?version=v1
* /voting/?version=v2


Test de estrés con Locust
-------------------------

Antes de empezar, comentaré para que sirven las pruebas de estrés. A veces necesitamos soportar que
nuestra aplicación ofrezca una cantidad de peticiones por segundo, porque habrá mucha gente entrando
a la misma vez, y ante este estrés, tenemos que comprobar como se comporta nuestra aplicación.

No es lo mismo que cuando la estresemos nos de un error 500 a que nos devuelva la petición de otro
usuario. Con estos test conseguiremos comprobar cual es ese comportamiento, y quizás mejorar la
velocidad de las peticiones para permitir más peticiones por segundo.

Para ejecutar los test de estrés utilizando locust, necesitaremos tener instalado locust:

$ pip install locust

Una vez instalado, necesitaremos tener un fichero locustfile.py donde tengamos la configuración de
lo que vamos a ejecutar. En nuestro caso, tenemos hecho dos ejemplos:

1. Visualizer: entra en el visualizador de una votación para ver cuantas peticiones puede aguantar.

Para ejecutar el test de Visualizer, tenemos que tener en cuenta que entra en la votación 1, por lo
que necesitaremos tenerla creada para que funcione correctamente, una vez hecho esto, podemos
comenzar a probar con el siguiente comando (dentro de la carpeta loadtest):

$ locust Visualizer

Esto abrirá un servidor que podremos ver en el navegador, el mismo comando nos dirá el puerto.
Cuando se abra, nos preguntará cuantos usuarios queremos que hagan peticiones a la vez, y como
queremos que vaya creciendo hasta llegar a ese número. Por ejemplo, si ponemos 100 y 5, estaremos
creando 5 nuevos usuarios cada segundo hasta llegar a 100.

2. Voters: utilizaremos usuarios previamente creados, y haremos una secuencia de peticiones: login,
getuser y store. Sería lo que realizaría un usuario cuando va a votar, por lo que con este ejemplo
estaremos comprobando cuantas votaciones podemos hacer.


Para ejecutar el test de Voter, necesitaremos realizar varios preparos. Necesitaremos la votación 1
abierta, y necesitaremos crear una serie de usuarios en el censo de esta votación, para que cuando
hagamos el test, estos usuario puedan autenticarse y votar correctamente. Para facilitar esta
tarea, hemos creado el script de python gen_census.py, en el cual creamos los usuarios que
tenemos dentro del fichero voters.json y los añadimos al censo utilizando la librería requests.
Para que este script funcione, necesitaremos tener instalado request:

$ pip install requests

Una vez instalado, ejecutamos el script:

$ python gen_census.py

Tras esto, ya podremos comenzar el test de estrés de votantes:

$ locust Voters

Importante mirar bien el fichero locustfile.py, donde existen algunas configuraciones que podremos
cambiar, dependiendo del HOST donde queramos hacer las pruebas y del id de la votación.

A tener en cuenta:

* En un servidor local, con un postgres que por defecto nos viene limitado a 100 usuarios
concurrentes, cuando pongamos más de 100, lo normal es que empiecen a fallar muchas peticiones.
* Si hacemos las pruebas en local, donde tenemos activado el modo debug de Django, lo normal es que
las peticiones tarden algo más y consigamos menos RPS (Peticiones por segundo).
49 changes: 49 additions & 0 deletions decide/authentication/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def setUp(self):
u.set_password('123')
u.save()

u2 = User(username='admin')
u2.set_password('admin')
u2.is_superuser = True
u2.save()

def tearDown(self):
self.client = None

Expand Down Expand Up @@ -79,3 +84,47 @@ def test_logout(self):
self.assertEqual(response.status_code, 200)

self.assertEqual(Token.objects.filter(user__username='voter1').count(), 0)

def test_register_bad_permissions(self):
data = {'username': 'voter1', 'password': '123'}
response = self.client.post('/authentication/login/', data, format='json')
self.assertEqual(response.status_code, 200)
token = response.json()

token.update({'username': 'user1'})
response = self.client.post('/authentication/register/', token, format='json')
self.assertEqual(response.status_code, 401)

def test_register_bad_request(self):
data = {'username': 'admin', 'password': 'admin'}
response = self.client.post('/authentication/login/', data, format='json')
self.assertEqual(response.status_code, 200)
token = response.json()

token.update({'username': 'user1'})
response = self.client.post('/authentication/register/', token, format='json')
self.assertEqual(response.status_code, 400)

def test_register_user_already_exist(self):
data = {'username': 'admin', 'password': 'admin'}
response = self.client.post('/authentication/login/', data, format='json')
self.assertEqual(response.status_code, 200)
token = response.json()

token.update(data)
response = self.client.post('/authentication/register/', token, format='json')
self.assertEqual(response.status_code, 400)

def test_register(self):
data = {'username': 'admin', 'password': 'admin'}
response = self.client.post('/authentication/login/', data, format='json')
self.assertEqual(response.status_code, 200)
token = response.json()

token.update({'username': 'user1', 'password': 'pwd1'})
response = self.client.post('/authentication/register/', token, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(
sorted(list(response.json().keys())),
['token', 'user_pk']
)
3 changes: 2 additions & 1 deletion decide/authentication/urls.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from django.urls import include, path
from rest_framework.authtoken.views import obtain_auth_token

from .views import GetUserView, LogoutView
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()),
]
29 changes: 29 additions & 0 deletions decide/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
from rest_framework.response import Response
from rest_framework.status import (
HTTP_201_CREATED,
HTTP_400_BAD_REQUEST,
HTTP_401_UNAUTHORIZED
)
from rest_framework.views import APIView
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import User
from django.db import IntegrityError
from django.shortcuts import get_object_or_404
from django.core.exceptions import ObjectDoesNotExist

Expand All @@ -24,3 +31,25 @@ def post(self, request):
pass

return Response({})


class RegisterView(APIView):
def post(self, request):
key = request.data.get('token', '')
tk = get_object_or_404(Token, key=key)
if not tk.user.is_superuser:
return Response({}, status=HTTP_401_UNAUTHORIZED)

username = request.data.get('username', '')
pwd = request.data.get('password', '')
if not username or not pwd:
return Response({}, status=HTTP_400_BAD_REQUEST)

try:
user = User(username=username)
user.set_password(pwd)
user.save()
token, _ = Token.objects.get_or_create(user=user)
except IntegrityError:
return Response({}, status=HTTP_400_BAD_REQUEST)
return Response({'user_pk': user.pk, 'token': token.key}, HTTP_201_CREATED)
4 changes: 4 additions & 0 deletions decide/booth/static/booth/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.voting {
margin-top: 20px;
margin-left: 40px;
}
Loading

0 comments on commit fc64ea5

Please sign in to comment.