diff --git a/docker-compose.yml b/docker-compose.yml index edc04b07e..e2dfb6824 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ sapldb: ports: - "5432:5432" sapl: - image: interlegis/sapl:3.1.152 + image: interlegis/sapl:3.1.153 restart: always environment: ADMIN_PASSWORD: interlegis diff --git a/docs/solr.rst b/docs/solr.rst index 4c82ab68d..ae8bbdb63 100644 --- a/docs/solr.rst +++ b/docs/solr.rst @@ -1,27 +1,55 @@ -**ESTAS INSTRUÇÕES ESTÃO DEFASADAS. EM BREVE IREMOS DISPONIBILIZAR UM TUTORIAL MAIS ATUALIZADO DE COMO INTEGRAR O SOLR AO SAPL** - ================================ Instruções para instalar o Solr ================================ -Solr é a ferramenta utilizada pelo SAPL 3.1 para indexar documentos para que possa ser feita -a Pesquisa Textual. +Solr é uma plataforma open source de indexação e busca textual utilizada pelo SAPL 3.1 para indexar documentos (normas jurídicas, matérias legislativas e documentos acessórios). + +Observação: Se a execução do SAPL for mediante containers Docker então use o arquivo *docker-compose.yml* disponível em +*https://github.com/interlegis/sapl/blob/3.1.x/solr/docker-compose.yml* (verifique os mapeamentos de volume estão corretos, a verso do SAPL referenciada no arquivo docker-compose.yml, e realize o backup de seu BD **antes** de qualquer tentativa de substituição do arquivo *docker-compose.yml* em uso corrente); + +1) Faça o download da distribuição *binária* do Apache Solr do site oficial do projeto **http://lucene.apache.org/solr** + + + As instalações Solr suportadas até o momento vão da 7.4 à 8; + + +2) Descompacte o arquivo em uma pasta do diretório (referenciada neste tutorial como $SOLR_HOME) + + +3) Inicie o Solr com o comando: + + **$SOLR_HOME/bin/solr start -c** + + +4) Por meio do browser, acesse a URL **http://localhost:8983** (ou informe o endereço da máquina onde o Solr foi instalado) + +5) Pare o servidor do SAPL; + +6) Edite o arquivo .env adicionando as seguintes linhas: + + USE_SOLR = True + + + SOLR_COLLECTION = sapl + + + SOLR_URL = http://localhost:8983 + -Adicione ao arquivo ``.env`` o seguinte atributo: + (o valor do campo SOLR_URL deve corresponder à URL acessada no item 3) -``SOLR_URL = 'http://127.0.0.1:8983/solr'`` +7) Entre no diretório raiz do SAPL e digite o comando: **python3 solr_api.py -c sapl -u http://localhost:8983`** -Dentro do diretório principal siga os seguintes passos:: + (a URL informada acima deve ser a mesma dos itens 3 e 6) - curl -LO https://archive.apache.org/dist/lucene/solr/4.10.2/solr-4.10.2.tgz - tar xvzf solr-4.10.2.tgz - cd solr-4.10.2 - cd example - java -jar start.jar - ./manage.py build_solr_schema --filename solr-4.10.2/example/solr/collection1/conf/schema.xml +8) Enquanto o Solr realiza a indexação da base de dados do SAPL, inicie em uma outra tela o SAPL; +9) Após realizados os passos com sucesso, nas telas de busca de Matéria Legislativa e Normas deverá aparecer um botão +de 'Busca Textual' próximo ao botão de busca tradicional. -Após isso, deve-se parar o servidor do Solr e restartar com ``java -jar start.jar`` +**Observações:** +* Para parar o Solr execute o comando **$SOLR_HOME/bin/solr stop** -**OBS: Toda vez que o código da pesquisa textual for modificado, os comandos de build_solr_schema e start.jar devem ser rodados, nessa mesma ordem.** +* Para reindexar os dados do SAPL execute o comando **python3 manage.py rebuild_index** (isso irá apagar todos os dados +do Solr e indexar tudo novamente). diff --git a/sapl/api/views.py b/sapl/api/views.py index 2eedb9464..8222e8227 100644 --- a/sapl/api/views.py +++ b/sapl/api/views.py @@ -27,7 +27,7 @@ from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\ from sapl.parlamentares.models import Parlamentar from sapl.protocoloadm.models import DocumentoAdministrativo,\ DocumentoAcessorioAdministrativo, TramitacaoAdministrativo -from sapl.sessao.models import SessaoPlenaria +from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria @@ -498,3 +498,17 @@ class _SessaoPlenariaViewSet: serializer = ChoiceSerializer(years, many=True) return Response(serializer.data) + + @action(detail=True) + def expedientes(self, request, *args, **kwargs): + + sessao = self.get_object() + + page = self.paginate_queryset(sessao.expedientesessao_set.all()) + if page is not None: + serializer = SaplApiViewSetConstrutor.get_class_for_model( + ExpedienteSessao).serializer_class(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(page, many=True) + return Response(serializer.data) diff --git a/sapl/comissoes/views.py b/sapl/comissoes/views.py index 7b8a6fd1d..1db8d23df 100644 --- a/sapl/comissoes/views.py +++ b/sapl/comissoes/views.py @@ -197,7 +197,8 @@ class ReuniaoCrud(MasterDetailCrud): public = [RP_LIST, RP_DETAIL, ] class BaseMixin(MasterDetailCrud.BaseMixin): - list_field_names = ['data', 'nome', 'tema'] + list_field_names = ['data', 'nome', 'tema', 'upload_ata'] + ordering = '-data' class ListView(MasterDetailCrud.ListView): logger = logging.getLogger(__name__) diff --git a/sapl/materia/views.py b/sapl/materia/views.py index d191a7568..18c4cdb99 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -14,7 +14,7 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError from django.core.urlresolvers import reverse from django.db.models import Max, Q from django.http import HttpResponse, JsonResponse @@ -1945,6 +1945,7 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): filterset_class = AcessorioEmLoteFilterSet template_name = 'materia/em_lote/acessorio.html' permission_required = ('materia.add_documentoacessorio',) + logger = logging.getLogger(__name__) def get_context_data(self, **kwargs): context = super(DocumentoAcessorioEmLoteView, @@ -1966,6 +1967,7 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): return context def post(self, request, *args, **kwargs): + username = request.user.username marcadas = request.POST.getlist('materia_id') if len(marcadas) == 0: @@ -1981,14 +1983,21 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): msg = _('Autor tem que ter menos do que 50 caracteres.') messages.add_message(request, messages.ERROR, msg) return self.get(request, self.kwargs) - - tmp_name = os.path.join(tempfile.gettempdir(), request.FILES['arquivo'].name) + + tmp_name = os.path.join(MEDIA_ROOT, request.FILES['arquivo'].name) with open(tmp_name, 'wb') as destination: for chunk in request.FILES['arquivo'].chunks(): destination.write(chunk) + try: + doc_data = tz.localize(datetime.strptime( + request.POST['data'], "%d/%m/%Y")) + except Exception as e: + msg = _('Formato da data incorreto. O formato deve ser da forma dd/mm/aaaa.') + messages.add_message(request, messages.ERROR, msg) + self.logger.error("User={}. {}. Data inserida: {}".format(username, str(msg), request.POST['data'])) + os.remove(tmp_name) + return self.get(request, self.kwargs) - doc_data = tz.localize(datetime.strptime( - request.POST['data'], "%d/%m/%Y")) for materia_id in marcadas: doc = DocumentoAcessorio() doc.materia_id = materia_id @@ -1997,6 +2006,18 @@ class DocumentoAcessorioEmLoteView(PermissionRequiredMixin, FilterView): doc.data = doc_data doc.autor = request.POST['autor'] doc.ementa = request.POST['ementa'] + doc.arquivo.name = tmp_name + try: + doc.clean_fields() + except ValidationError as e: + for m in [ '%s: %s' % (DocumentoAcessorio()._meta.get_field(k).verbose_name, ''.join(v)) + for k,v in e.message_dict.items() ]: + # Insere as mensagens de erro no formato: + # 'verbose_name do nome do campo': 'mensagem de erro' + messages.add_message(request, messages.ERROR, m) + self.logger.error("User={}. {}. Nome do arquivo: {}.".format(username, str(msg), request.FILES['arquivo'].name)) + os.remove(tmp_name) + return self.get(request, self.kwargs) doc.save() diretorio = os.path.join(MEDIA_ROOT, 'sapl/public/documentoacessorio', diff --git a/sapl/norma/forms.py b/sapl/norma/forms.py index 12eee547f..5a9e1b3f6 100644 --- a/sapl/norma/forms.py +++ b/sapl/norma/forms.py @@ -298,11 +298,10 @@ class AnexoNormaJuridicaForm(FileFieldCheckMixin, ModelForm): def save(self, commit=False): anexo = self.instance anexo.ano = self.cleaned_data['norma'].ano - anexo = super(AnexoNormaJuridicaForm, self).save(commit=True) anexo.norma = self.cleaned_data['norma'] anexo.assunto_anexo = self.cleaned_data['assunto_anexo'] anexo.anexo_arquivo = self.cleaned_data['anexo_arquivo'] - anexo.save() + anexo = super(AnexoNormaJuridicaForm, self).save(commit=True) return anexo diff --git a/sapl/norma/models.py b/sapl/norma/models.py index f7fcf17e9..5a5557e5b 100644 --- a/sapl/norma/models.py +++ b/sapl/norma/models.py @@ -351,3 +351,20 @@ class AnexoNormaJuridica(models.Model): def __str__(self): return _('Anexo: %(anexo)s da norma %(norma)s') % { 'anexo': self.anexo_arquivo, 'norma': self.norma} + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + if not self.pk and self.anexo_arquivo: + anexo_arquivo = self.anexo_arquivo + self.anexo_arquivo = None + models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) + self.anexo_arquivo = anexo_arquivo + + return models.Model.save(self, force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields) \ No newline at end of file diff --git a/sapl/parlamentares/forms.py b/sapl/parlamentares/forms.py index e1e773d9b..f52cea871 100755 --- a/sapl/parlamentares/forms.py +++ b/sapl/parlamentares/forms.py @@ -236,18 +236,6 @@ class ParlamentarFilterSet(django_filters.FilterSet): class ParlamentarCreateForm(ParlamentarForm): - legislatura = forms.ModelChoiceField( - label=_('Legislatura'), - required=True, - queryset=Legislatura.objects.all().order_by('-data_inicio'), - empty_label='----------', - ) - - data_expedicao_diploma = forms.DateField( - label=_('Expedição do Diploma'), - required=True, - ) - class Meta(ParlamentarForm.Meta): widgets = { 'fotografia': forms.ClearableFileInput(), @@ -258,13 +246,6 @@ class ParlamentarCreateForm(ParlamentarForm): @transaction.atomic def save(self, commit=True): parlamentar = super(ParlamentarCreateForm, self).save(commit) - legislatura = self.cleaned_data['legislatura'] - Mandato.objects.create( - parlamentar=parlamentar, - legislatura=legislatura, - data_inicio_mandato=legislatura.data_inicio, - data_fim_mandato=legislatura.data_fim, - data_expedicao_diploma=self.cleaned_data['data_expedicao_diploma']) content_type = ContentType.objects.get_for_model(Parlamentar) object_id = parlamentar.pk tipo = TipoAutor.objects.get(content_type=content_type) diff --git a/sapl/parlamentares/tests/test_parlamentares.py b/sapl/parlamentares/tests/test_parlamentares.py index e7b885871..e5f607a09 100644 --- a/sapl/parlamentares/tests/test_parlamentares.py +++ b/sapl/parlamentares/tests/test_parlamentares.py @@ -11,7 +11,6 @@ from sapl.parlamentares.models import (Dependente, Filiacao, Legislatura, @pytest.mark.django_db(transaction=False) def test_cadastro_parlamentar(admin_client): - legislatura = mommy.make(Legislatura) url = reverse('sapl.parlamentares:parlamentar_create') response = admin_client.get(url) @@ -20,21 +19,13 @@ def test_cadastro_parlamentar(admin_client): response = admin_client.post(url, {'nome_completo': 'Teresa Barbosa', 'nome_parlamentar': 'Terezinha', 'sexo': 'F', - 'ativo': 'True', - 'legislatura': legislatura.id, - 'data_expedicao_diploma': '2001-01-01'}, + 'ativo': 'True'}, follow=True) [parlamentar] = Parlamentar.objects.all() assert parlamentar.nome_parlamentar == 'Terezinha' assert parlamentar.sexo == 'F' assert parlamentar.ativo is True - # o primeiro mandato é criado - [mandato] = Mandato.objects.all() - assert mandato.parlamentar == parlamentar - assert str(mandato.data_expedicao_diploma) == '2001-01-01' - assert mandato.legislatura == legislatura - assert mandato.data_fim_mandato == legislatura.data_fim @pytest.mark.django_db(transaction=False) @@ -42,9 +33,7 @@ def test_incluir_parlamentar_errors(admin_client): url = reverse('sapl.parlamentares:parlamentar_create') response = admin_client.post(url) erros_esperados = {campo: ['Este campo é obrigatório.'] - for campo in ['legislatura', - 'data_expedicao_diploma', - 'nome_parlamentar', + for campo in ['nome_parlamentar', 'nome_completo', 'sexo', ]} diff --git a/sapl/sessao/models.py b/sapl/sessao/models.py index 7267e78fc..725898b57 100644 --- a/sapl/sessao/models.py +++ b/sapl/sessao/models.py @@ -290,8 +290,11 @@ class TipoExpediente(models.Model): @reversion.register() class ExpedienteSessao(models.Model): # ExpedienteSessaoPlenaria - sessao_plenaria = models.ForeignKey(SessaoPlenaria, - on_delete=models.CASCADE) + sessao_plenaria = models.ForeignKey( + SessaoPlenaria, + on_delete=models.CASCADE, + related_name='expedientesessao_set' + ) tipo = models.ForeignKey(TipoExpediente, on_delete=models.PROTECT) conteudo = models.TextField( blank=True, verbose_name=_('Conteúdo do expediente')) @@ -379,7 +382,7 @@ class OradorExpediente(AbstractOrador): # OradoresExpediente @reversion.register() -class OradorOrdemDia(AbstractOrador): # OradoresOrdemDia +class OradorOrdemDia(AbstractOrador): # OradoresOrdemDia class Meta: verbose_name = _('Orador da Ordem do Dia') @@ -584,10 +587,14 @@ class ResumoOrdenacao(models.Model): oitavo = models.CharField(max_length=30) nono = models.CharField(max_length=30) decimo = models.CharField(max_length=30) - decimo_primeiro = models.CharField(max_length=30,default="Ocorrências da Sessão") - decimo_segundo = models.CharField(max_length=30, default="Votos Nominais Mat Expediente") - decimo_terceiro = models.CharField(max_length=30, default="Votos Nominais Mat Ordem Dia") - decimo_quarto = models.CharField(max_length=30, default="Oradores da Ordem do Dia") + decimo_primeiro = models.CharField( + max_length=30, default="Ocorrências da Sessão") + decimo_segundo = models.CharField( + max_length=30, default="Votos Nominais Mat Expediente") + decimo_terceiro = models.CharField( + max_length=30, default="Votos Nominais Mat Ordem Dia") + decimo_quarto = models.CharField( + max_length=30, default="Oradores da Ordem do Dia") class Meta: verbose_name = _('Ordenação do Resumo de uma Sessão') @@ -596,6 +603,7 @@ class ResumoOrdenacao(models.Model): def __str__(self): return 'Ordenação do Resumo de uma Sessão' + @reversion.register() class TipoRetiradaPauta(models.Model): descricao = models.CharField(max_length=150, verbose_name=_('Descrição')) @@ -687,6 +695,7 @@ class JustificativaAusencia(models.Model): using=using, update_fields=update_fields) + class RetiradaPauta(models.Model): materia = models.ForeignKey(MateriaLegislativa, on_delete=models.CASCADE, diff --git a/sapl/settings.py b/sapl/settings.py index 057e8ec75..af1b8d697 100644 --- a/sapl/settings.py +++ b/sapl/settings.py @@ -41,7 +41,7 @@ ALLOWED_HOSTS = ['*'] LOGIN_REDIRECT_URL = '/' LOGIN_URL = '/login/?next=' -SAPL_VERSION = '3.1.152' +SAPL_VERSION = '3.1.153' if DEBUG: EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/sapl/templates/base.html b/sapl/templates/base.html index 3048f83b2..b19ec673e 100644 --- a/sapl/templates/base.html +++ b/sapl/templates/base.html @@ -179,7 +179,7 @@ Desenvolvido pelo Interlegis em software livre e aberto. - Release: 3.1.152 + Release: 3.1.153
Identificação Básica: {% for b in basica %} - {{b}} ; + {{b}} + {% if not forloop.last %} ; {% endif %} {% endfor %}
\ No newline at end of file diff --git a/sapl/templates/sessao/blocos_ata/lista_presenca.html b/sapl/templates/sessao/blocos_ata/lista_presenca.html index 329fde406..1efc2c8ea 100644 --- a/sapl/templates/sessao/blocos_ata/lista_presenca.html +++ b/sapl/templates/sessao/blocos_ata/lista_presenca.html @@ -5,7 +5,8 @@ {% if presenca_sessao %} Lista de Presença na Sessão: {% for p in presenca_sessao %} - {{p.nome_completo}} / {{ p|filiacao_data_filter:object.data_inicio }} ; + {{p.nome_completo}} / {{ p|filiacao_data_filter:object.data_inicio }} + {% if not forloop.last %} ; {% endif %} {% endfor %} {% endif %} @@ -13,7 +14,8 @@ {% if justificativa_ausencia %} Justificativas de Ausências na Sessão: {% for j in justificativa_ausencia %} - {{j.parlamentar.nome_completo}} / {{ j.tipo_ausencia }} ; + {{j.parlamentar.nome_completo}} / {{ j.tipo_ausencia }} + {% if not forloop.last %} ; {% endif %} {% endfor %} {% endif %} diff --git a/sapl/templates/sessao/blocos_ata/lista_presenca_ordem_dia.html b/sapl/templates/sessao/blocos_ata/lista_presenca_ordem_dia.html index d6c8014ca..855d1be6f 100644 --- a/sapl/templates/sessao/blocos_ata/lista_presenca_ordem_dia.html +++ b/sapl/templates/sessao/blocos_ata/lista_presenca_ordem_dia.html @@ -5,7 +5,8 @@ {% if presenca_ordem %} Lista de Presença na Ordem do Dia: {% for p in presenca_ordem %} - {{p.nome_completo}} / {{ p|filiacao_data_filter:object.data_inicio }} ; + {{p.nome_completo}} / {{ p|filiacao_data_filter:object.data_inicio }} + {% if not forloop.last %} ; {% endif %} {% endfor %} {% endif %} diff --git a/sapl/templates/sessao/blocos_ata/materias_expediente.html b/sapl/templates/sessao/blocos_ata/materias_expediente.html index 7613448fc..3ccc3fb84 100644 --- a/sapl/templates/sessao/blocos_ata/materias_expediente.html +++ b/sapl/templates/sessao/blocos_ata/materias_expediente.html @@ -1,27 +1,36 @@ \ No newline at end of file diff --git a/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html b/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html index d894e8a06..a167e01a1 100644 --- a/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html +++ b/sapl/templates/sessao/blocos_ata/materias_ordem_dia.html @@ -2,29 +2,32 @@Matérias da Ordem do Dia: {% for m in materias_ordem %} - {{m.numero}} - {{m.titulo}} - Descrição: {{m.ementa|safe}} - Autor{{ m.autor|length|pluralize:"es" }}: {{ m.autor|join:', ' }} + {{m.numero}} - {{m.titulo}}, + {{m.ementa|safe}} + Autor{{ m.autor|length|pluralize:"es" }}: {{ m.autor|join:', ' }}, {% if m.numero_protocolo %} - Número de Protocolo: {{ m.numero_protocolo }} + Número de Protocolo: {{ m.numero_protocolo }}, {% endif %} {% if m.numero_processo %} - Processo:{{ m.numero_processo }} + Processo: {{ m.numero_processo }}, {% endif %} {%if m.turno %} - Turno: {{m.turno}} + Turno: {{m.turno}}, {%endif %} - Tipo: {{m.tipo_votacao}} - Sim:{{m.voto_sim}} - Não:{{m.voto_nao}} - Abstenções:{{m.voto_abstencoes}} - Resultado: {{m.resultado}} {{m.resultado_observacao}} + {%if m.tipo_votacao %} + Tipo: {{m.tipo_votacao}}, + Sim: {{ m.voto_sim }}, + Não: {{ m.voto_nao }}, + Abstenções: {{m.voto_abstencoes}}, + {% endif %} + Resultado: {{m.resultado}} + {% if m.resultado_observacao %} - {{m.resultado_observacao}} {% endif %} {% if m.voto_nominal%} Votos Nominais : {% for voto in m.voto_nominal %} - / {{voto.0}} - {{voto.1}} - {% endfor %}; - {% endif %} - {% endfor %} -
+ {{voto.0}} - {{voto.1}} + {% if not forloop.last %} ; {% endif %} + {% endfor %} + {% endif %} + {% endfor %} \ No newline at end of file diff --git a/sapl/templates/sessao/blocos_ata/mesa_diretora.html b/sapl/templates/sessao/blocos_ata/mesa_diretora.html index fb27b0fcc..2d2de86d7 100644 --- a/sapl/templates/sessao/blocos_ata/mesa_diretora.html +++ b/sapl/templates/sessao/blocos_ata/mesa_diretora.html @@ -4,7 +4,8 @@ Mesa Diretora: {% for m in mesa %} {{m.cargo}}: - {{m.parlamentar.nome_completo}} / {{ m.parlamentar.filiacao_atual }} ; + {{m.parlamentar.nome_completo}} / {{ m.parlamentar.filiacao_atual }} + {% if not forloop.last %} ; {% endif %} {% endfor %} {% endif %} diff --git a/sapl/templates/sessao/blocos_ata/oradores_expediente.html b/sapl/templates/sessao/blocos_ata/oradores_expediente.html index bc39db224..f85b27895 100644 --- a/sapl/templates/sessao/blocos_ata/oradores_expediente.html +++ b/sapl/templates/sessao/blocos_ata/oradores_expediente.html @@ -3,10 +3,9 @@ {% if oradores %} Oradores do Expediente: {% for o in oradores %} -