GitLab's annual major release is around the corner. Along with a lot of new and exciting features, there will be a few breaking changes. Learn more here.

Commit f823e534 authored by Leonardo Alexandre Ferreira Leite's avatar Leonardo Alexandre Ferreira Leite
Browse files

Merge branch 'era-issue-525' into 'master'

#WIP Configuração de era #525

See merge request !545
parents dcdf6613 53c0d6a9
Pipeline #310210030 passed with stages
in 11 minutes and 16 seconds
......@@ -29,7 +29,7 @@ import logging
import numpy
from . import pca
import copy
# import time # timetrack
from datetime import date
logger = logging.getLogger("radar")
......@@ -48,33 +48,38 @@ class AnalisadorTemporal:
A classe AnalisadorTemporal tem métodos para criar os objetos
AnalisadorPeriodo e fazer as análises.
Atributos:
data_inicio e data_fim -- strings no formato 'aaaa-mm-dd'.
analises_periodo -- lista de objetos da classe AnalisePeriodo
palavras_chave -- lista de strings para serem utilizadas na filtragem
de votações
votacoes -- lista de objetos do tipo Votacao para serem usados
na análise se não for especificado, procura votações na base de dados
de acordo data_inicio, data_fim e palavras_chave.
"""
def __init__(self, casa_legislativa, periodicidade,
palavras_chave=[], votacoes=[]):
def __init__(self, casa_legislativa, periodicidade, palavras_chave=[],
ano_ini_era=None, ano_fim_era=None):
"""
Argumentos:
casa_legislativa -- objeto do tipo CasaLegislativa
periodicidade -- string dentre as opçoes de models.PERIODOS
palavras_chave -- lista de strings para a filtragem de votações
ano_ini_era e ano_fim_era -- inteiros delimitando a era a se
analisar (intervalo fechado)
"""
self.casa_legislativa = casa_legislativa
recuperador_votacoes = utils.PeriodosRetriever(
self.casa_legislativa, periodicidade)
self.periodos = recuperador_votacoes.get_periodos()
self.periodos = self._get_periodos(casa_legislativa, periodicidade,
ano_ini_era, ano_fim_era)
self.ini = self.periodos[0].ini
self.fim = self.periodos[len(self.periodos) - 1].fim
self.periodicidade = periodicidade
self.analises_periodo = []
self.palavras_chave = palavras_chave
self.votacoes = []
self.total_votacoes = 0
self.json = ""
self.chefes_executivos = []
def _get_periodos(self, casa_legislativa, periodicidade,
ano_ini_era, ano_fim_era):
data_ini_era = date(ano_ini_era, 1, 1) if ano_ini_era else None
data_fim_era = date(ano_fim_era, 12, 31) if ano_fim_era else None
periodos_retriever = utils.PeriodosRetriever(
casa_legislativa, periodicidade, data_ini_era, data_fim_era)
return periodos_retriever.get_periodos()
def get_analise_temporal(self):
"""Retorna instância de AnaliseTemporal"""
if not self.analises_periodo:
......@@ -83,7 +88,6 @@ class AnalisadorTemporal:
analise_temporal.casa_legislativa = self.casa_legislativa
analise_temporal.periodicidade = self.periodicidade
analise_temporal.analises_periodo = self.analises_periodo
analise_temporal.votacoes = self.votacoes
analise_temporal.chefes_executivos = self.chefes_executivos
analise_temporal.total_votacoes = self.total_votacoes
analise_temporal.palavras_chaves = self.palavras_chave
......@@ -97,8 +101,7 @@ class AnalisadorTemporal:
logger.info("Analisando periodo %s a %s." %
(str(periodo.ini), str(periodo.fim)))
analisadorPeriodo = AnalisadorPeriodo(self.casa_legislativa,
periodo, self.votacoes,
self.palavras_chave)
periodo, self.palavras_chave)
if analisadorPeriodo.votacoes:
logger.info("O periodo possui %d votações." %
len(analisadorPeriodo.votacoes))
......@@ -122,24 +125,19 @@ class AnalisadorTemporal:
votacoes_filtradas = []
for periodo in self.periodos:
analisadorPeriodo = AnalisadorPeriodo(self.casa_legislativa,
periodo, self.votacoes,
self.palavras_chave)
periodo, self.palavras_chave)
votacoes_filtradas.extend(analisadorPeriodo._inicializa_votacoes())
return votacoes_filtradas
class AnalisadorPeriodo:
def __init__(self, casa_legislativa, periodo,
votacoes=[], palavras_chave=[]):
def __init__(self, casa_legislativa, periodo, palavras_chave=[]):
"""Argumentos:
casa_legislativa -- objeto do tipo CasaLegislativa;
somente votações desta casa serão analisados.
periodo -- objeto do tipo PeriodoCasaLegislativa;
sem periodo, a análise é feita sobre todas as votações.
votacoes -- lista de objetos do tipo Votacao para serem usados na
análise se não for especificado, procura votações na base de dados
de acordo data_inicio, data_fim e palavras_chave.
palavras_chave -- lista de strings para serem usadas na filtragem
das votações
"""
......@@ -149,10 +147,8 @@ class AnalisadorPeriodo:
self.fim = periodo.fim if periodo is not None else None
self.partidos = self.casa_legislativa.partidos()
self.parlamentares = self.casa_legislativa.parlamentares()
self.votacoes = votacoes
self.palavras_chave = palavras_chave
if not self.votacoes:
self._inicializa_votacoes()
self._inicializa_votacoes()
self.num_votacoes = len(self.votacoes)
self.analise_ja_feita = False # quando a analise for feita, vale True.
......
......@@ -49,3 +49,39 @@ class AnalisePeriodo:
# partido.nome => lista de parlamentares do partido
# (independente de periodo).
self.parlamentares_por_partido = {}
class AnaliseAvancadaForm:
PROBLEMA_INICIO_ERA_POSTERIOR_AO_FIM_ERA = 'Início da era analisada não pode ser posterior ao fim da era'
PROBLEMA_ERA_CDEP_MUITO_GRANDE = 'Era não pode ser maior que 10 anos para a Câmara dos Deputados'
ERA_PRECISO_DOS_DOIS_EXTREMOS = 'Início e fim de era devem ser informados conjuntamente'
LIMITE_TAMANHO_ERA_CDEP = 10
def __init__(self, values, casa_legislativa):
self.ano_ini_era = values.get('ano_ini_era')
self.ano_fim_era = values.get('ano_fim_era')
self.casa_legislativa = casa_legislativa
"""Retorna uma lista de problemas (cada problema é uma string)"""
def problemas(self):
problemas = []
if not self.ano_ini_era and not self.ano_fim_era:
return problemas
if not self.ano_ini_era or not self.ano_fim_era:
problemas.append(AnaliseAvancadaForm.ERA_PRECISO_DOS_DOIS_EXTREMOS)
return problemas
if self.ano_ini_era and self.ano_fim_era:
if self.ano_ini_era > self.ano_fim_era:
problemas.append(AnaliseAvancadaForm.PROBLEMA_INICIO_ERA_POSTERIOR_AO_FIM_ERA)
if self.casa_legislativa.nome_curto == 'cdep':
intervalo = int(self.ano_fim_era) - int(self.ano_ini_era)
if intervalo > AnaliseAvancadaForm.LIMITE_TAMANHO_ERA_CDEP:
problemas.append(AnaliseAvancadaForm.PROBLEMA_ERA_CDEP_MUITO_GRANDE)
return problemas
......@@ -2,3 +2,4 @@ from .tests_analise import *
from .tests_grafico import *
from .tests_filtro import *
from .tests_genero import *
from .tests_models import *
from django.test import TestCase
from analises.models import AnaliseAvancadaForm
from modelagem.models import CasaLegislativa
class AnaliseAvancadaFormTest(TestCase):
def test_form_sem_problemas(self):
casa = self._casa('cdep')
# como datas vêm direto do form da view, os anos são string (não int)
params = {'ano_ini_era': '2009', 'ano_fim_era': '2010'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertFalse(problemas)
def test_form_sem_params_sem_problema(self):
casa = self._casa('cdep')
params = {}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertFalse(problemas)
def test_form_com_problema_ano_ini_maior_que_ano_fim(self):
casa = self._casa('cdep')
params = {'ano_ini_era': '2020', 'ano_fim_era': '2010'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertTrue(problemas)
self.assertEquals(problemas[0], AnaliseAvancadaForm.PROBLEMA_INICIO_ERA_POSTERIOR_AO_FIM_ERA)
def test_form_com_era_maior_que_dez_anos_para_cdep(self):
casa = self._casa('cdep')
params = {'ano_ini_era': '2000', 'ano_fim_era': '2020'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertTrue(problemas)
self.assertEquals(problemas[0], AnaliseAvancadaForm.PROBLEMA_ERA_CDEP_MUITO_GRANDE)
def test_form_com_era_maior_que_dez_anos_para_outra_casa_qualquer(self):
casa = self._casa('outra_casa_qualquer')
params = {'ano_ini_era': '2000', 'ano_fim_era': '2020'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertFalse(problemas)
def test_form_apenas_ini_era_informado(self):
casa = self._casa('cdep')
params = {'ano_ini_era': '2000'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertTrue(problemas)
self.assertEquals(problemas[0], AnaliseAvancadaForm.ERA_PRECISO_DOS_DOIS_EXTREMOS)
def test_form_apenas_fim_era_informado(self):
casa = self._casa('cdep')
params = {'ano_fim_era': '2000'}
form = AnaliseAvancadaForm(params, casa)
problemas = form.problemas()
self.assertTrue(problemas)
self.assertEquals(problemas[0], AnaliseAvancadaForm.ERA_PRECISO_DOS_DOIS_EXTREMOS)
def _casa(self, nome_curto):
casa = CasaLegislativa()
casa.nome_curto = nome_curto
return casa
......@@ -26,6 +26,6 @@ class JsonAnaliseViewTest(TestCase):
"""Test json_analise view."""
# Test made without 'palavra-chave' since we need to rely on
# elasticsearch for that
response = self.client.get('/radar/json/conv/BIENIO/')
response = self.client.get('/radar/json/conv?periodicidade=BIENIO')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/json')
......@@ -16,6 +16,7 @@
# along with Radar Parlamentar. If not, see <http://www.gnu.org/licenses/>.
import sys
from datetime import date
from django.contrib.admin.views.decorators import staff_member_required
from django.conf import settings
from django.core.cache import cache
......@@ -23,6 +24,8 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from modelagem import models
from modelagem.utils import StringUtils
from modelagem.utils import MandatoLists
from analises.models import AnaliseAvancadaForm
from .grafico import JsonAnaliseGenerator
from .analise import AnalisadorTemporal
import logging
......@@ -35,15 +38,29 @@ def analises(request):
def analise(request, nome_curto_casa_legislativa):
''' Retorna a lista de partidos para montar a legenda do gráfico'''
partidos = models.Partido.objects.order_by('numero').all()
casa_legislativa = get_object_or_404(
models.CasaLegislativa, nome_curto=nome_curto_casa_legislativa)
ano_ini_era = request.GET.get('ano_ini_era')
ano_fim_era = request.GET.get('ano_fim_era')
periodicidade = request.GET.get('periodicidade', models.BIENIO)
palavras_chave = request.GET.get('palavras_chave', '')
nome_parlamentar = request.GET.get('nome_parlamentar', '')
# lista de partidos para montar a legenda do gráfico
partidos = models.Partido.objects.order_by('numero').all()
casa_legislativa = get_object_or_404(
models.CasaLegislativa, nome_curto=nome_curto_casa_legislativa)
form = AnaliseAvancadaForm(request.GET, casa_legislativa)
problemas_form_analisa_avancada = form.problemas()
num_votacao = casa_legislativa.num_votacao()
mandatos = MandatoLists()
possiveis_inicios = mandatos.anos_iniciais_de_mandatos(casa_legislativa)
possiveis_fins = list(map(lambda ano: ano+3, possiveis_inicios))
possiveis_inicios = list(map(lambda ano: str(ano), possiveis_inicios))
possiveis_fins = list(map(lambda ano: str(ano), possiveis_fins))
possiveis_inicios.insert(0, "")
possiveis_fins.insert(0, "")
return render(request, 'analise.html',
{'casa_legislativa': casa_legislativa,
......@@ -51,15 +68,29 @@ def analise(request, nome_curto_casa_legislativa):
'num_votacao': num_votacao,
'periodicidade': periodicidade,
'palavras_chave': palavras_chave,
'nome_parlamentar': nome_parlamentar})
'ano_ini_era': ano_ini_era,
'ano_fim_era': ano_fim_era,
'nome_parlamentar': nome_parlamentar,
'possiveis_inicios': possiveis_inicios,
'possiveis_fins': possiveis_fins,
'problemas_form_analisa_avancada': problemas_form_analisa_avancada})
def json_analise(request, nome_curto_casa_legislativa,
periodicidade, palavras_chave=""):
def json_analise(request, nome_curto_casa_legislativa):
"""Retorna o JSON com as coordenadas do gráfico PCA"""
ano_ini_era = _to_int(request.GET.get('ano_ini_era'))
ano_fim_era = _to_int(request.GET.get('ano_fim_era'))
periodicidade = request.GET.get('periodicidade')
palavras_chave = request.GET.get('palavras_chave')
cache_key = f"json_analise_{nome_curto_casa_legislativa}_{periodicidade}"
if palavras_chave:
cache_key = f"{cache_key}_{palavras_chave}"
if ano_ini_era:
cache_key = f"{cache_key}_{ano_ini_era}"
if ano_fim_era:
cache_key = f"{cache_key}_{ano_fim_era}"
log.info("Recuperando cache para: %s", cache_key)
response = cache.get(cache_key)
if not response:
......@@ -69,7 +100,8 @@ def json_analise(request, nome_curto_casa_legislativa,
lista_de_palavras_chave = \
StringUtils.transforma_texto_em_lista_de_string(palavras_chave)
analisador = AnalisadorTemporal(
casa_legislativa, periodicidade, lista_de_palavras_chave)
casa_legislativa, periodicidade, lista_de_palavras_chave,
ano_ini_era, ano_fim_era)
analise_temporal = analisador.get_analise_temporal()
gen = JsonAnaliseGenerator(analise_temporal)
response = gen.get_json()
......@@ -80,6 +112,13 @@ def json_analise(request, nome_curto_casa_legislativa,
return HttpResponse(response, content_type='application/json')
def _to_int(str):
try:
return int(str)
except (ValueError, TypeError):
return None
def lista_de_votacoes_filtradas(request,
nome_curto_casa_legislativa,
periodicidade=models.BIENIO,
......
......@@ -23,7 +23,7 @@ import modelagem.models
import modelagem.utils
from util_test import flush_db
from django.utils.dateparse import parse_datetime
from modelagem.models import MUNICIPAL, FEDERAL, ESTADUAL, BIENIO
from modelagem.models import MUNICIPAL, FEDERAL, ESTADUAL, BIENIO, Votacao
import logging
logger = logging.getLogger("radar")
......@@ -254,6 +254,20 @@ class PeriodosRetrieverTest(TestCase):
resultado = retriever._data_inicio_prox_periodo(data_inicio_periodo)
self.assertEqual(resultado, datetime.date(1993, 1, 1))
def test_filtra_por_data_da_ultima_votacao(self):
data_primeira_votacao = Votacao.objects.filter(
proposicao__casa_legislativa__nome_curto='conv').order_by('data')[0].data
data_ultima_votacao = Votacao.objects.filter(
proposicao__casa_legislativa__nome_curto='conv').order_by('-data')[0].data
um_ano_antes_da_ultima_votacao = data_ultima_votacao.replace(
year=data_ultima_votacao.year-1)
retriever = modelagem.utils.PeriodosRetriever(
self.conv, modelagem.models.ANO,
data_da_primeira_votacao=data_primeira_votacao,
data_da_ultima_votacao=um_ano_antes_da_ultima_votacao)
data_fim_ultimo_periodo = retriever.get_periodos()[-1].fim
self.assertTrue(data_fim_ultimo_periodo < data_ultima_votacao)
class StringUtilsTest(TestCase):
......
......@@ -53,6 +53,12 @@ class MandatoLists:
y += 4
return mandatos
def anos_iniciais_de_mandatos(self, casa_legislativa):
data_inicial = Votacao.objects.filter(proposicao__casa_legislativa=casa_legislativa).earliest('data').data
hoje = datetime.date.today()
datas_iniciais = self.get_mandatos(casa_legislativa.esfera, data_inicial, hoje)
return list(map(lambda d: d.year, datas_iniciais))
class PeriodosRetriever:
......@@ -86,6 +92,10 @@ class PeriodosRetriever:
numero_minimo_de_votacoes=1):
self.casa_legislativa = casa_legislativa
self.periodicidade = periodicidade
if (data_da_primeira_votacao and not data_da_ultima_votacao) or \
(data_da_ultima_votacao and not data_da_primeira_votacao):
raise ValueError("Ou informa data_da_primeira_votacao e "
"data_da_ultima_votacao, ou não informa nenhuma")
self.data_da_primeira_votacao = data_da_primeira_votacao
self.data_da_ultima_votacao = data_da_ultima_votacao
self.numero_minimo_de_votacoes = numero_minimo_de_votacoes
......@@ -149,7 +159,7 @@ class PeriodosRetriever:
esfera, self.data_da_primeira_votacao, self.data_da_ultima_votacao)
i = 0
while i < len(mandatos) and \
mandatos[i] < self.data_da_primeira_votacao:
mandatos[i] <= self.data_da_primeira_votacao:
ano_inicial = mandatos[i].year
i += 1
inicio_primeiro_periodo = datetime.date(
......
from django.test import TestCase
from modelagem.models import Parlamentar, Proposicao, Votacao, \
PeriodoCasaLegislativa, Voto
from modelagem import utils
from modelagem.models import ANO, BIENIO, QUADRIENIO
from analises.analise import AnalisadorPeriodo
from plenaria import ordenacao
from modelagem import models
from datetime import date
from importadores import conv
from operator import itemgetter
class OrdenacaoTest(TestCase):
......@@ -44,6 +38,6 @@ class OrdenacaoTest(TestCase):
def test_def_ordenar_votantes(self):
proposicao = models.Proposicao.objects.first()
resultado = ordenacao.ordenar_votantes(proposicao)
parlamentar = Parlamentar.objects.filter(nome='Pierre')[0]
parlamentar = models.Parlamentar.objects.filter(nome='Pierre')[0]
primeiro = (parlamentar.partido, 1)
self.assertEqual(resultado[parlamentar], primeiro)
......@@ -31,15 +31,25 @@ Plot = (function ($) {
var parlamentar_pesquisado = "";
// Function to load the data and draw the chart
function initialize(nome_curto_casa_legislativa, periodicidade, palavras_chave, nome_parlamentar) {
function initialize(nome_curto_casa_legislativa, periodicidade, palavras_chave, nome_parlamentar,
ano_ini_era, ano_fim_era) {
parlamentar_pesquisado = nome_parlamentar.toUpperCase(); // Issue#272
if (palavras_chave == "") {
d3.json("/radar/json/" + nome_curto_casa_legislativa + "/" + periodicidade, plot_data);
} else {
d3.json("/radar/json/" + nome_curto_casa_legislativa + "/" + periodicidade + "/" + palavras_chave, plot_data);
url = "/radar/json/" + nome_curto_casa_legislativa + "?"
if (periodicidade != "") {
url = url + "periodicidade=" + periodicidade + "&"
}
if (palavras_chave != "") {
url = url + "palavras_chave=" + palavras_chave + "&"
}
if (ano_ini_era != "") {
url = url + "ano_ini_era=" + ano_ini_era + "&"
}
if (ano_fim_era != "") {
url = url + "ano_fim_era=" + ano_fim_era + "&"
}
d3.json(url, plot_data);
//para testes com arquivo hardcoded
// d3.json("/static/files/test_files/partidos.json", plot_data);
......
......@@ -66,17 +66,45 @@
<p><label for="palavras_chave">Filtrar votações por temas:</label>
Separe as palavras-chaves por vírgula.<br>
<input type="text" id="palavras_chave" name ="palavras_chave" size="30" placeholder="saúde, educação">
<input type="text" id="palavras_chave" name ="palavras_chave" size="30" placeholder="ex: saúde, educação">
</p>
<!-- REVIEW: Campo referente a issue 272 "Encontrar Parlamentar"
Mais informações podem ser encontradas em https://gitlab.com/radar-parlamentar/radar/issues/272
Descomente para dar continuidade a feature -->
<label style="margin-top: 20px" for="encontrar_parlamentar">Encontrar Parlamentar: </label>
<input type="text" id="nome_parlamentar" onkeyup="salvaValor(this)" name ="nome_parlamentar" size="30" placeholder="Ricardo Teixeira"><br>
<p><label style="margin-top: 20px" for="nome_parlamentar">Encontrar Parlamentar: </label>
<input type="text" id="nome_parlamentar" onkeyup="salvaValor(this)" name ="nome_parlamentar" size="30" placeholder="ex: Ricardo Teixeira"><br>
</p>
<p><div style="overflow: hidden;">
<label>Era analisada</label>
<div style="float: left;">
<label style="margin-top: 5px" for="ano_ini_era">Início: </label>
<select id="ano_ini_era" name="ano_ini_era">
{% for year in possiveis_inicios %}
<option value="{{ year }}" {% if year == ano_ini_era %}selected="selected"{% endif %}>{{ year }}</option>
{% endfor %}
</select>
</div>
<div style="margin-left: 100px;">
<label style="margin-top: 5px" for="ano_fim_era">Fim: </label>
<select id="ano_fim_era" name="ano_fim_era">
{% for year in possiveis_fins %}
<option value="{{ year }}" {% if year == ano_fim_era %}selected="selected"{% endif %}>{{ year }}</option>
{% endfor %}
</select>
</div>
</div>
</p>
<p>
{% for problema in problemas_form_analisa_avancada %}
<!-- Tentei usar um estilo definido em CSS, mas não consegui =/ -->
<span style="color:red;">{{problema}}</span><br/>
{% endfor %}
</p>
<input type="submit" value="Buscar">
<input type="submit" value="Analisar">
</form>
......@@ -122,7 +150,9 @@
</ul>
</aside>
<div style="width:700px" id="graficopca">
{% if not problemas_form_analisa_avancada %}
<figure id="animacao"><center><img id="loading" src="/static/files/images/spinner.gif"></img></center></figure>
{% endif %}
</div>
<aside id="como-funciona" style="margin: 0 90px 10px 20px; padding: 10px; background-color: rgba(220,220,220,0.7); -webkit-border-radius: 20px; -moz-border-radius: 20px; border-radius: 20px;">
<h4>Entenda o gráfico</h4>
......@@ -205,9 +235,14 @@
</script>
{% endcompress %}
{% if problemas_form_analisa_avancada %}
<script>mostrarBuscaAvancada();</script>
{% endif %}
{% endblock extrajsend %}
{% block extrascriptsend %}
{% if not problemas_form_analisa_avancada %}
var campo_select = document.getElementById("periodicidade");
for(var i=0; i < campo_select.options.length; i++) {
if(campo_select.options[i].value == '{{periodicidade}}') {
......@@ -227,7 +262,8 @@
menuActive("analises");
$(document).ready(function() {
Plot.initialize('{{casa_legislativa.nome_curto}}', '{{periodicidade}}', '{{palavras_chave}}','{{nome_parlamentar}}')
Plot.initialize('{{casa_legislativa.nome_curto}}', '{{periodicidade}}', '{{palavras_chave}}', '{{nome_parlamentar}}', '{{ano_ini_era}}', '{{ano_fim_era}}')
});
{% endif %}
{% endblock extrascriptsend %}
......@@ -36,10 +36,7 @@ genero_patterns = [
radar_patterns = [
path("<nome_curto_casa_legislativa>/", analises_views.analise),
path("json/<nome_curto_casa_legislativa>/<periodicidade>/",
analises_views.json_analise),
path("json/<nome_curto_casa_legislativa>/<periodicidade>/<palavras_chave>/",
analises_views.json_analise),
path("json/<nome_curto_casa_legislativa>", analises_views.json_analise),
path("clear_cache", analises_views.clear_cache)
]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment